diff --git a/aqt/__init__.py b/aqt/__init__.py index 408c9ae81..cf71c8978 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -1,7 +1,7 @@ # Copyright: Damien Elmes # 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__": diff --git a/aqt/importing.py b/aqt/importing.py index a63ffc878..e85abd445 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -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 diff --git a/aqt/main.py b/aqt/main.py index f6696ecb7..5567acce2 100644 --- a/aqt/main.py +++ b/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) diff --git a/aqt/profiles.py b/aqt/profiles.py index 2e3659b8b..a7e206001 100644 --- a/aqt/profiles.py +++ b/aqt/profiles.py @@ -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 ###################################################################### diff --git a/aqt/progress.py b/aqt/progress.py index 0f3d1b0cf..c37696fbd 100644 --- a/aqt/progress.py +++ b/aqt/progress.py @@ -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 diff --git a/aqt/qt.py b/aqt/qt.py index 0025f61e6..0a4ac4891 100644 --- a/aqt/qt.py +++ b/aqt/qt.py @@ -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(): diff --git a/aqt/utils.py b/aqt/utils.py index e7a45db4f..c624019fa 100644 --- a/aqt/utils.py +++ b/aqt/utils.py @@ -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