0afce5f19a
Turns out that WebKit does support UNC pathnames in the base tag, but it is picky about the leading number of slashes.
424 lines
12 KiB
Python
424 lines
12 KiB
Python
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
from aqt.qt import *
|
|
import re, os, sys, urllib, subprocess
|
|
import aqt
|
|
from anki.sound import stripSounds
|
|
from anki.utils import isWin, isMac, invalidFilename
|
|
|
|
def openHelp(section):
|
|
link = aqt.appHelpSite
|
|
if section:
|
|
link += "#%s" % section
|
|
openLink(link)
|
|
|
|
def openLink(link):
|
|
tooltip(_("Loading..."), period=1000)
|
|
QDesktopServices.openUrl(QUrl(link))
|
|
|
|
def showWarning(text, parent=None, help=""):
|
|
"Show a small warning with an OK button."
|
|
return showInfo(text, parent, help, "warning")
|
|
|
|
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=False, help="", type="info"):
|
|
"Show a small info window with an OK button."
|
|
if parent is False:
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
if type == "warning":
|
|
icon = QMessageBox.Warning
|
|
elif type == "critical":
|
|
icon = QMessageBox.Critical
|
|
else:
|
|
icon = QMessageBox.Information
|
|
mb = QMessageBox(parent)
|
|
mb.setText(text)
|
|
mb.setIcon(icon)
|
|
mb.setWindowModality(Qt.WindowModal)
|
|
b = mb.addButton(QMessageBox.Ok)
|
|
b.setDefault(True)
|
|
if help:
|
|
b = mb.addButton(QMessageBox.Help)
|
|
b.connect(b, SIGNAL("clicked()"), lambda: openHelp(help))
|
|
b.setAutoDefault(False)
|
|
return mb.exec_()
|
|
|
|
def showText(txt, parent=None, type="text", run=True):
|
|
if not parent:
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
diag = QDialog(parent)
|
|
diag.setWindowTitle("Anki")
|
|
layout = QVBoxLayout(diag)
|
|
diag.setLayout(layout)
|
|
text = QTextEdit()
|
|
text.setReadOnly(True)
|
|
if type == "text":
|
|
text.setPlainText(txt)
|
|
else:
|
|
text.setHtml(txt)
|
|
layout.addWidget(text)
|
|
box = QDialogButtonBox(QDialogButtonBox.Close)
|
|
layout.addWidget(box)
|
|
diag.connect(box, SIGNAL("rejected()"), diag, SLOT("reject()"))
|
|
diag.setMinimumHeight(400)
|
|
diag.setMinimumWidth(500)
|
|
if run:
|
|
diag.exec_()
|
|
else:
|
|
return diag, box
|
|
|
|
def askUser(text, parent=None, help="", defaultno=False, msgfunc=None):
|
|
"Show a yes/no question. Return true if yes."
|
|
if not parent:
|
|
parent = aqt.mw.app.activeWindow()
|
|
if not msgfunc:
|
|
msgfunc = QMessageBox.question
|
|
sb = QMessageBox.Yes | QMessageBox.No
|
|
if help:
|
|
sb |= QMessageBox.Help
|
|
while 1:
|
|
if defaultno:
|
|
default = QMessageBox.No
|
|
else:
|
|
default = QMessageBox.Yes
|
|
r = msgfunc(parent, "Anki", text, sb,
|
|
default)
|
|
if r == QMessageBox.Help:
|
|
|
|
openHelp(help)
|
|
else:
|
|
break
|
|
return r == QMessageBox.Yes
|
|
|
|
class ButtonedDialog(QMessageBox):
|
|
|
|
def __init__(self, text, buttons, parent=None, help=""):
|
|
QDialog.__init__(self, parent)
|
|
self.buttons = []
|
|
self.setWindowTitle("Anki")
|
|
self.help = help
|
|
self.setIcon(QMessageBox.Warning)
|
|
self.setText(text)
|
|
# v = QVBoxLayout()
|
|
# v.addWidget(QLabel(text))
|
|
# box = QDialogButtonBox()
|
|
# v.addWidget(box)
|
|
for b in buttons:
|
|
self.buttons.append(
|
|
self.addButton(b, QMessageBox.AcceptRole))
|
|
if help:
|
|
self.addButton(_("Help"), QMessageBox.HelpRole)
|
|
buttons.append(_("Help"))
|
|
#self.setLayout(v)
|
|
|
|
def run(self):
|
|
self.exec_()
|
|
but = self.clickedButton().text()
|
|
if but == "Help":
|
|
# FIXME stop dialog closing?
|
|
openHelp(self.help)
|
|
return self.clickedButton().text()
|
|
|
|
def setDefault(self, idx):
|
|
self.setDefaultButton(self.buttons[idx])
|
|
|
|
def askUserDialog(text, buttons, parent=None, help=""):
|
|
if not parent:
|
|
parent = aqt.mw
|
|
diag = ButtonedDialog(text, buttons, parent, help)
|
|
return diag
|
|
|
|
class GetTextDialog(QDialog):
|
|
|
|
def __init__(self, parent, question, help=None, edit=None, default=u"",
|
|
title="Anki"):
|
|
QDialog.__init__(self, parent)
|
|
self.setWindowTitle(title)
|
|
self.question = question
|
|
self.help = help
|
|
self.qlabel = QLabel(question)
|
|
self.setMinimumWidth(400)
|
|
v = QVBoxLayout()
|
|
v.addWidget(self.qlabel)
|
|
if not edit:
|
|
edit = QLineEdit()
|
|
self.l = edit
|
|
if default:
|
|
self.l.setText(default)
|
|
self.l.selectAll()
|
|
v.addWidget(self.l)
|
|
buts = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
|
if help:
|
|
buts |= QDialogButtonBox.Help
|
|
b = QDialogButtonBox(buts)
|
|
v.addWidget(b)
|
|
self.setLayout(v)
|
|
self.connect(b.button(QDialogButtonBox.Ok),
|
|
SIGNAL("clicked()"), self.accept)
|
|
self.connect(b.button(QDialogButtonBox.Cancel),
|
|
SIGNAL("clicked()"), self.reject)
|
|
if help:
|
|
self.connect(b.button(QDialogButtonBox.Help),
|
|
SIGNAL("clicked()"), self.helpRequested)
|
|
|
|
def accept(self):
|
|
return QDialog.accept(self)
|
|
|
|
def reject(self):
|
|
return QDialog.reject(self)
|
|
|
|
def helpRequested(self):
|
|
openHelp(self.help)
|
|
|
|
def getText(prompt, parent=None, help=None, edit=None, default=u"", title="Anki"):
|
|
if not parent:
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
d = GetTextDialog(parent, prompt, help=help, edit=edit,
|
|
default=default, title=title)
|
|
d.setWindowModality(Qt.WindowModal)
|
|
ret = d.exec_()
|
|
return (unicode(d.l.text()), ret)
|
|
|
|
def getOnlyText(*args, **kwargs):
|
|
(s, r) = getText(*args, **kwargs)
|
|
if r:
|
|
return s
|
|
else:
|
|
return u""
|
|
|
|
# fixme: these utilities could be combined into a single base class
|
|
def chooseList(prompt, choices, startrow=0, parent=None):
|
|
if not parent:
|
|
parent = aqt.mw.app.activeWindow()
|
|
d = QDialog(parent)
|
|
d.setWindowModality(Qt.WindowModal)
|
|
l = QVBoxLayout()
|
|
d.setLayout(l)
|
|
t = QLabel(prompt)
|
|
l.addWidget(t)
|
|
c = QListWidget()
|
|
c.addItems(choices)
|
|
c.setCurrentRow(startrow)
|
|
l.addWidget(c)
|
|
bb = QDialogButtonBox(QDialogButtonBox.Ok)
|
|
bb.connect(bb, SIGNAL("accepted()"), d, SLOT("accept()"))
|
|
l.addWidget(bb)
|
|
d.exec_()
|
|
return c.currentRow()
|
|
|
|
def getTag(parent, deck, question, tags="user", **kwargs):
|
|
from aqt.tagedit import TagEdit
|
|
te = TagEdit(parent)
|
|
te.setCol(deck)
|
|
ret = getText(question, parent, edit=te, **kwargs)
|
|
te.hideCompleter()
|
|
return ret
|
|
|
|
# File handling
|
|
######################################################################
|
|
|
|
def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
|
|
"Ask the user for a file."
|
|
assert not dir or not key
|
|
if not dir:
|
|
dirkey = key+"Directory"
|
|
dir = aqt.mw.pm.profile.get(dirkey, "")
|
|
else:
|
|
dirkey = None
|
|
d = QFileDialog(parent)
|
|
# fix #233 crash
|
|
if isMac:
|
|
d.setOptions(QFileDialog.DontUseNativeDialog)
|
|
d.setFileMode(QFileDialog.ExistingFile)
|
|
d.setDirectory(dir)
|
|
d.setWindowTitle(title)
|
|
d.setNameFilter(filter)
|
|
ret = []
|
|
def accept():
|
|
# work around an osx crash
|
|
aqt.mw.app.processEvents()
|
|
file = unicode(list(d.selectedFiles())[0])
|
|
if dirkey:
|
|
dir = os.path.dirname(file)
|
|
aqt.mw.pm.profile[dirkey] = dir
|
|
if cb:
|
|
cb(file)
|
|
ret.append(file)
|
|
d.connect(d, SIGNAL("accepted()"), accept)
|
|
d.exec_()
|
|
return ret and ret[0]
|
|
|
|
def getSaveFile(parent, title, dir, key, ext):
|
|
"Ask the user for a file to save. Use DIR as config variable."
|
|
dirkey = dir+"Directory"
|
|
file = unicode(QFileDialog.getSaveFileName(
|
|
parent, title, aqt.mw.pm.base, "{0} (*{1})".format(key, ext),
|
|
options=QFileDialog.DontConfirmOverwrite))
|
|
if file:
|
|
# add extension
|
|
if not file.lower().endswith(ext):
|
|
file += ext
|
|
# save new default
|
|
dir = os.path.dirname(file)
|
|
aqt.mw.pm.profile[dirkey] = dir
|
|
# check if it exists
|
|
if os.path.exists(file):
|
|
if not askUser(
|
|
_("This file exists. Are you sure you want to overwrite it?"),
|
|
parent):
|
|
return None
|
|
return file
|
|
|
|
def saveGeom(widget, key):
|
|
key += "Geom"
|
|
aqt.mw.pm.profile[key] = widget.saveGeometry()
|
|
|
|
def restoreGeom(widget, key, offset=None):
|
|
key += "Geom"
|
|
if aqt.mw.pm.profile.get(key):
|
|
widget.restoreGeometry(aqt.mw.pm.profile[key])
|
|
if isMac and offset:
|
|
from aqt.main import QtConfig as q
|
|
minor = (q.qt_version & 0x00ff00) >> 8
|
|
if minor > 6:
|
|
# bug in osx toolkit
|
|
s = widget.size()
|
|
widget.resize(s.width(), s.height()+offset*2)
|
|
|
|
def saveState(widget, key):
|
|
key += "State"
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
def restoreState(widget, key):
|
|
key += "State"
|
|
if aqt.mw.pm.profile.get(key):
|
|
widget.restoreState(aqt.mw.pm.profile[key])
|
|
|
|
def saveSplitter(widget, key):
|
|
key += "Splitter"
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
def restoreSplitter(widget, key):
|
|
key += "Splitter"
|
|
if aqt.mw.pm.profile.get(key):
|
|
widget.restoreState(aqt.mw.pm.profile[key])
|
|
|
|
def saveHeader(widget, key):
|
|
key += "Header"
|
|
aqt.mw.pm.profile[key] = widget.saveState()
|
|
|
|
def restoreHeader(widget, key):
|
|
key += "Header"
|
|
if aqt.mw.pm.profile.get(key):
|
|
widget.restoreState(aqt.mw.pm.profile[key])
|
|
|
|
def mungeQA(txt):
|
|
txt = stripSounds(txt)
|
|
# osx webkit doesn't understand font weight 600
|
|
txt = re.sub("font-weight: *600", "font-weight:bold", txt)
|
|
return txt
|
|
|
|
def applyStyles(widget):
|
|
p = os.path.join(aqt.mw.pm.base, "style.css")
|
|
if os.path.exists(p):
|
|
widget.setStyleSheet(open(p).read())
|
|
|
|
def getBase(col):
|
|
base = None
|
|
mdir = col.media.dir()
|
|
if isWin and not mdir.startswith("\\\\"):
|
|
prefix = u"file:///"
|
|
else:
|
|
prefix = u"file://"
|
|
mdir = mdir.replace("\\", "/")
|
|
base = prefix + unicode(
|
|
urllib.quote(mdir.encode("utf-8")),
|
|
"utf-8") + "/"
|
|
return '<base href="%s">' % base
|
|
|
|
def openFolder(path):
|
|
if isWin:
|
|
if isinstance(path, unicode):
|
|
path = path.encode(sys.getfilesystemencoding())
|
|
subprocess.Popen(["explorer", path])
|
|
else:
|
|
QDesktopServices.openUrl(QUrl("file://" + path))
|
|
|
|
def shortcut(key):
|
|
if isMac:
|
|
return re.sub("(?i)ctrl", "Command", key)
|
|
return key
|
|
|
|
def maybeHideClose(bbox):
|
|
if isMac:
|
|
b = bbox.button(QDialogButtonBox.Close)
|
|
if b:
|
|
bbox.removeButton(b)
|
|
|
|
def addCloseShortcut(widg):
|
|
if not isMac:
|
|
return
|
|
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
|
widg.connect(widg._closeShortcut, SIGNAL("activated()"),
|
|
widg, SLOT("reject()"))
|
|
|
|
# Tooltips
|
|
######################################################################
|
|
|
|
_tooltipTimer = None
|
|
_tooltipLabel = None
|
|
|
|
def tooltip(msg, period=3000, parent=None):
|
|
global _tooltipTimer, _tooltipLabel
|
|
class CustomLabel(QLabel):
|
|
def mousePressEvent(self, evt):
|
|
evt.accept()
|
|
self.hide()
|
|
closeTooltip()
|
|
aw = parent or aqt.mw.app.activeWindow() or aqt.mw
|
|
lab = CustomLabel("""\
|
|
<table cellpadding=10>
|
|
<tr>
|
|
<td><img src=":/icons/help-hint.png"></td>
|
|
<td>%s</td>
|
|
</tr>
|
|
</table>""" % msg, aw)
|
|
lab.setFrameStyle(QFrame.Panel)
|
|
lab.setLineWidth(2)
|
|
lab.setWindowFlags(Qt.ToolTip)
|
|
p = QPalette()
|
|
p.setColor(QPalette.Window, QColor("#feffc4"))
|
|
lab.setPalette(p)
|
|
lab.move(
|
|
aw.mapToGlobal(QPoint(0, -100 + aw.height())))
|
|
lab.show()
|
|
_tooltipTimer = aqt.mw.progress.timer(
|
|
period, closeTooltip, False)
|
|
_tooltipLabel = lab
|
|
|
|
def closeTooltip():
|
|
global _tooltipLabel, _tooltipTimer
|
|
if _tooltipLabel:
|
|
try:
|
|
_tooltipLabel.deleteLater()
|
|
except:
|
|
# already deleted as parent window closed
|
|
pass
|
|
_tooltipLabel = None
|
|
if _tooltipTimer:
|
|
_tooltipTimer.stop()
|
|
_tooltipTimer = None
|
|
|
|
# true if invalid; print warning
|
|
def checkInvalidFilename(str, dirsep=True):
|
|
bad = invalidFilename(str, dirsep)
|
|
if bad:
|
|
showWarning(_("The following character can not be used: %s") %
|
|
bad)
|
|
return True
|
|
return False
|