mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
importing from the command line
- a file path provided on the command line now will now trigger the import process - if anki was already running on win/lin, send the import to that process - if no profile was open, wait until profile opened before importing - don't do that for password-protected profiles, as it could be used abusively in a school environment - drop the old pid-based instance check
This commit is contained in:
parent
f7b41a11f6
commit
bc758220a7
7 changed files with 139 additions and 61 deletions
|
@ -1,7 +1,7 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import os, sys, __builtin__
|
||||
import os, sys, optparse, atexit, __builtin__
|
||||
from aqt.qt import *
|
||||
import locale, gettext
|
||||
import anki.lang
|
||||
|
@ -103,13 +103,78 @@ def setupLang(pm, app, force=None):
|
|||
|
||||
class AnkiApp(QApplication):
|
||||
|
||||
# Single instance support on Win32/Linux
|
||||
##################################################
|
||||
|
||||
KEY = "anki"
|
||||
TMOUT = 5000
|
||||
|
||||
def __init__(self, argv):
|
||||
QApplication.__init__(self, argv)
|
||||
self._argv = argv
|
||||
self._shmem = QSharedMemory(self.KEY)
|
||||
self.alreadyRunning = self._shmem.attach()
|
||||
|
||||
def secondInstance(self):
|
||||
if not self.alreadyRunning:
|
||||
# use a 1 byte shared memory instance to signal we exist
|
||||
if not self._shmem.create(1):
|
||||
raise Exception("shared memory not supported")
|
||||
atexit.register(self._shmem.detach)
|
||||
# and a named pipe/unix domain socket for ipc
|
||||
QLocalServer.removeServer(self.KEY)
|
||||
self._srv = QLocalServer(self)
|
||||
self.connect(self._srv, SIGNAL("newConnection()"), self.onRecv)
|
||||
self._srv.listen(self.KEY)
|
||||
# if we were given a file on startup, send import it
|
||||
else:
|
||||
# we accept only one command line argument. if it's missing, send
|
||||
# a blank screen to just raise the existing window
|
||||
opts, args = parseArgs(self._argv)
|
||||
buf = "raise"
|
||||
if args and args[0]:
|
||||
buf = os.path.abspath(args[0])
|
||||
self.sendMsg(buf)
|
||||
return True
|
||||
|
||||
def sendMsg(self, txt):
|
||||
sock = QLocalSocket(self)
|
||||
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
|
||||
if not sock.waitForConnected(self.TMOUT):
|
||||
raise Exception("existing instance not responding")
|
||||
sock.write(txt)
|
||||
if not sock.waitForBytesWritten(self.TMOUT):
|
||||
raise Exception("existing instance not emptying")
|
||||
sock.disconnectFromServer()
|
||||
|
||||
def onRecv(self):
|
||||
sock = self._srv.nextPendingConnection()
|
||||
if not sock.waitForReadyRead(self.TMOUT):
|
||||
sys.stderr.write(sock.errorString())
|
||||
return
|
||||
buf = sock.readAll()
|
||||
self.emit(SIGNAL("appMsg"), unicode(buf))
|
||||
sock.disconnectFromServer()
|
||||
|
||||
# OS X file/url handler
|
||||
##################################################
|
||||
|
||||
def event(self, evt):
|
||||
from anki.hooks import runHook
|
||||
if evt.type() == QEvent.FileOpen:
|
||||
runHook("macLoadEvent", unicode(evt.file()))
|
||||
self.emit(SIGNAL("appMsg"), evt.file() or "raise")
|
||||
return True
|
||||
return QApplication.event(self, evt)
|
||||
|
||||
def parseArgs(argv):
|
||||
"Returns (opts, args)."
|
||||
parser = optparse.OptionParser()
|
||||
parser.usage = "%prog [OPTIONS] [file to import]"
|
||||
parser.add_option("-b", "--base", help="path to base folder")
|
||||
parser.add_option("-p", "--profile", help="profile name to load")
|
||||
parser.add_option("-l", "--lang", help="interface language (en, de, etc)")
|
||||
return parser.parse_args(argv[1:])
|
||||
|
||||
def run():
|
||||
global mw
|
||||
from anki.utils import isWin, isMac
|
||||
|
@ -122,15 +187,12 @@ def run():
|
|||
# create the app
|
||||
app = AnkiApp(sys.argv)
|
||||
QCoreApplication.setApplicationName("Anki")
|
||||
if app.secondInstance():
|
||||
# we've signaled the primary instance, so we should close
|
||||
return
|
||||
|
||||
# parse args
|
||||
import optparse
|
||||
parser = optparse.OptionParser()
|
||||
parser.usage = "%prog [OPTIONS]"
|
||||
parser.add_option("-b", "--base", help="path to base folder")
|
||||
parser.add_option("-p", "--profile", help="profile name to load")
|
||||
parser.add_option("-l", "--lang", help="interface language (en, de, etc)")
|
||||
(opts, args) = parser.parse_args(sys.argv[1:])
|
||||
opts, args = parseArgs(sys.argv)
|
||||
opts.base = unicode(opts.base or "", sys.getfilesystemencoding())
|
||||
opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding())
|
||||
|
||||
|
@ -142,12 +204,11 @@ def run():
|
|||
setupLang(pm, app, opts.lang)
|
||||
|
||||
# remaining pm init
|
||||
pm.checkPid()
|
||||
pm.ensureProfile()
|
||||
|
||||
# load the main window
|
||||
import aqt.main
|
||||
mw = aqt.main.AnkiQt(app, pm)
|
||||
mw = aqt.main.AnkiQt(app, pm, args)
|
||||
app.exec_()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -249,6 +249,9 @@ def onImport(mw):
|
|||
if not file:
|
||||
return
|
||||
file = unicode(file)
|
||||
importFile(mw, file)
|
||||
|
||||
def importFile(mw, file):
|
||||
ext = os.path.splitext(file)[1]
|
||||
importer = None
|
||||
done = False
|
||||
|
|
72
aqt/main.py
72
aqt/main.py
|
@ -21,8 +21,9 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
|
|||
tooltip, openHelp, openLink
|
||||
|
||||
class AnkiQt(QMainWindow):
|
||||
def __init__(self, app, profileManager):
|
||||
def __init__(self, app, profileManager, args):
|
||||
QMainWindow.__init__(self)
|
||||
self.state = "startup"
|
||||
aqt.mw = self
|
||||
self.app = app
|
||||
self.pm = profileManager
|
||||
|
@ -43,21 +44,23 @@ class AnkiQt(QMainWindow):
|
|||
except:
|
||||
showInfo(_("Error during startup:\n%s") % traceback.format_exc())
|
||||
sys.exit(1)
|
||||
# were we given a file to import?
|
||||
if args and args[0]:
|
||||
self.onAppMsg(args[0])
|
||||
# Load profile in a timer so we can let the window finish init and not
|
||||
# close on profile load error.
|
||||
self.progress.timer(10, self.setupProfile, False)
|
||||
|
||||
def setupUI(self):
|
||||
self.col = None
|
||||
self.state = "overview"
|
||||
self.hideSchemaMsg = False
|
||||
self.setupAppMsg()
|
||||
self.setupKeys()
|
||||
self.setupThreads()
|
||||
self.setupFonts()
|
||||
self.setupMainWindow()
|
||||
self.setupSystemSpecific()
|
||||
self.setupStyle()
|
||||
self.setupProxy()
|
||||
self.setupMenus()
|
||||
self.setupProgress()
|
||||
self.setupErrorHandler()
|
||||
|
@ -75,6 +78,7 @@ class AnkiQt(QMainWindow):
|
|||
##########################################################################
|
||||
|
||||
def setupProfile(self):
|
||||
self.pendingImport = None
|
||||
# profile not provided on command line?
|
||||
if not self.pm.name:
|
||||
# if there's a single profile, load it automatically
|
||||
|
@ -91,6 +95,7 @@ class AnkiQt(QMainWindow):
|
|||
self.loadProfile()
|
||||
|
||||
def showProfileManager(self):
|
||||
self.state = "profileManager"
|
||||
d = self.profileDiag = QDialog()
|
||||
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
|
||||
f.setupUi(d)
|
||||
|
@ -210,6 +215,15 @@ Are you sure?""")):
|
|||
self.raise_()
|
||||
# maybe sync (will load DB)
|
||||
self.onSync(auto=True)
|
||||
# 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:
|
||||
import aqt.importing
|
||||
aqt.importing.importFile(self, self.pendingImport)
|
||||
self.pendingImport = None
|
||||
runHook("profileLoaded")
|
||||
|
||||
def unloadProfile(self, browser=True):
|
||||
|
@ -352,9 +366,13 @@ Are you sure?""")):
|
|||
"Signal queue needs to be rebuilt when edits are finished or by user."
|
||||
self.autosave()
|
||||
self.resetModal = modal
|
||||
if self.state in ("overview", "review", "deckBrowser"):
|
||||
if self.interactiveState():
|
||||
self.moveToState("resetRequired")
|
||||
|
||||
def interactiveState(self):
|
||||
"True if not in profile manager, syncing, etc."
|
||||
return self.state in ("overview", "review", "deckBrowser")
|
||||
|
||||
def maybeReset(self):
|
||||
self.autosave()
|
||||
if self.state == "resetRequired":
|
||||
|
@ -995,12 +1013,8 @@ will be lost. Continue?"""))
|
|||
ws.setFontSize(QWebSettings.DefaultFontSize, self.fontHeight)
|
||||
|
||||
def setupSystemSpecific(self):
|
||||
# use system font for webviews
|
||||
# mac tweaks
|
||||
addHook("macLoadEvent", self.onMacLoad)
|
||||
if isMac:
|
||||
qt_mac_set_menubar_icons(False)
|
||||
#self.setUnifiedTitleAndToolBarOnMac(self.pm.profile['showToolbar'])
|
||||
# mac users expect a minimize option
|
||||
self.minimizeShortcut = QShortcut("Ctrl+M", self)
|
||||
self.connect(self.minimizeShortcut, SIGNAL("activated()"),
|
||||
|
@ -1025,12 +1039,40 @@ will be lost. Continue?"""))
|
|||
def onMacMinimize(self):
|
||||
self.setWindowState(self.windowState() | Qt.WindowMinimized)
|
||||
|
||||
def onMacLoad(self, fname):
|
||||
self.loadDeck(fname)
|
||||
|
||||
# Proxy support
|
||||
# Single instance support
|
||||
##########################################################################
|
||||
|
||||
def setupProxy(self):
|
||||
return
|
||||
# need to bundle socksipy and install a default socket handler
|
||||
def setupAppMsg(self):
|
||||
self.connect(self.app, SIGNAL("appMsg"), self.onAppMsg)
|
||||
|
||||
def onAppMsg(self, buf):
|
||||
if self.state == "startup":
|
||||
# try again in a second
|
||||
return self.progress.timer(1000, lambda: self.onAppMsg(buf), False)
|
||||
elif self.state == "profileManager":
|
||||
self.pendingImport = buf
|
||||
return showInfo(_("Deck will be imported when a profile is opened."))
|
||||
if not self.interactiveState() or self.progress.busy():
|
||||
# we can't raise the main window while in profile dialog, syncing, etc
|
||||
if buf != "raise":
|
||||
showInfo(_("""\
|
||||
Please ensure a profile is open and Anki is not busy, then try again."""),
|
||||
parent=None)
|
||||
return
|
||||
# raise window
|
||||
if isWin:
|
||||
# on windows we can raise the window by minimizing and restoring
|
||||
self.showMinimized()
|
||||
self.setWindowState(Qt.WindowActive)
|
||||
self.showNormal()
|
||||
else:
|
||||
# on osx we can raise the window. on unity the icon in the tray will just flash.
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
if buf == "raise":
|
||||
return
|
||||
# import
|
||||
if not os.path.exists(buf):
|
||||
return showInfo(_("Provided file does not exist."))
|
||||
import aqt.importing
|
||||
aqt.importing.importFile(self, buf)
|
||||
|
|
|
@ -82,39 +82,6 @@ Anki can't write to the harddisk. Please see the \
|
|||
documentation for information on using a flash drive.""")
|
||||
raise
|
||||
|
||||
# Pid checking
|
||||
######################################################################
|
||||
|
||||
def checkPid(self):
|
||||
p = os.path.join(self.base, "pid")
|
||||
# check if an existing instance is running
|
||||
if os.path.exists(p):
|
||||
pid = int(open(p).read())
|
||||
exists = False
|
||||
if isWin:
|
||||
# no posix on windows, sigh
|
||||
from win32process import EnumProcesses as enum
|
||||
if pid in enum():
|
||||
exists = True
|
||||
else:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
exists = True
|
||||
except OSError:
|
||||
pass
|
||||
if exists:
|
||||
QMessageBox.warning(
|
||||
None, "Error", _("""\
|
||||
Anki is already running. Please close the existing copy or restart your \
|
||||
computer."""))
|
||||
raise Exception("Already running")
|
||||
# write out pid to the file
|
||||
open(p, "w").write(str(os.getpid()))
|
||||
# add handler to cleanup on exit
|
||||
def cleanup():
|
||||
os.unlink(p)
|
||||
atexit.register(cleanup)
|
||||
|
||||
# Profile load/save
|
||||
######################################################################
|
||||
|
||||
|
|
|
@ -155,3 +155,7 @@ Your pysqlite2 is too old. Anki will appear frozen during long operations."""
|
|||
def _unsetBusy(self):
|
||||
self._disabled = False
|
||||
self.app.restoreOverrideCursor()
|
||||
|
||||
def busy(self):
|
||||
"True if processing."
|
||||
return self._levels
|
||||
|
|
|
@ -10,6 +10,7 @@ sip.setapi('QUrl', 2)
|
|||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
||||
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
|
||||
from PyQt4 import pyqtconfig
|
||||
|
||||
def debug():
|
||||
|
|
|
@ -25,9 +25,9 @@ def showCritical(text, parent=None, help=""):
|
|||
"Show a small critical error with an OK button."
|
||||
return showInfo(text, parent, help, "critical")
|
||||
|
||||
def showInfo(text, parent=None, help="", type="info"):
|
||||
def showInfo(text, parent=False, help="", type="info"):
|
||||
"Show a small info window with an OK button."
|
||||
if not parent:
|
||||
if parent is False:
|
||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||
if type == "warning":
|
||||
icon = QMessageBox.Warning
|
||||
|
|
Loading…
Reference in a new issue