2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2015-09-28 15:09:30 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
|
|
|
from aqt.qt import *
|
2016-05-12 06:45:35 +02:00
|
|
|
import re, os, sys, urllib.request, urllib.parse, urllib.error, subprocess
|
2012-12-21 08:51:59 +01:00
|
|
|
import aqt
|
2013-02-20 07:12:07 +01:00
|
|
|
from anki.sound import stripSounds
|
2018-10-28 02:41:43 +01:00
|
|
|
from anki.utils import isWin, isMac, invalidFilename, noBundledLibs
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def openHelp(section):
|
|
|
|
link = aqt.appHelpSite
|
|
|
|
if section:
|
|
|
|
link += "#%s" % section
|
|
|
|
openLink(link)
|
|
|
|
|
|
|
|
def openLink(link):
|
|
|
|
tooltip(_("Loading..."), period=1000)
|
2017-06-27 04:04:42 +02:00
|
|
|
with noBundledLibs():
|
|
|
|
QDesktopServices.openUrl(QUrl(link))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-02-26 00:36:02 +01:00
|
|
|
def showWarning(text, parent=None, help="", title="Anki", textFormat=None):
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small warning with an OK button."
|
2019-02-26 00:36:02 +01:00
|
|
|
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-02-26 00:36:02 +01:00
|
|
|
def showCritical(text, parent=None, help="", title="Anki", textFormat=None):
|
2012-12-21 08:51:59 +01:00
|
|
|
"Show a small critical error with an OK button."
|
2019-02-26 00:36:02 +01:00
|
|
|
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-02-26 00:36:02 +01:00
|
|
|
def showInfo(text, parent=False, help="", type="info", title="Anki", textFormat=None):
|
2012-12-21 08:51:59 +01:00
|
|
|
"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)
|
2019-02-26 00:36:02 +01:00
|
|
|
if textFormat == "plain":
|
|
|
|
mb.setTextFormat(Qt.PlainText)
|
|
|
|
elif textFormat == "rich":
|
|
|
|
mb.setTextFormat(Qt.RichText)
|
|
|
|
else:
|
|
|
|
raise Exception("unexpected textFormat type")
|
2012-12-21 08:51:59 +01:00
|
|
|
mb.setText(text)
|
|
|
|
mb.setIcon(icon)
|
2016-04-30 07:44:41 +02:00
|
|
|
mb.setWindowTitle(title)
|
2012-12-21 08:51:59 +01:00
|
|
|
b = mb.addButton(QMessageBox.Ok)
|
|
|
|
b.setDefault(True)
|
|
|
|
if help:
|
|
|
|
b = mb.addButton(QMessageBox.Help)
|
2016-05-31 10:51:40 +02:00
|
|
|
b.clicked.connect(lambda: openHelp(help))
|
2012-12-21 08:51:59 +01:00
|
|
|
b.setAutoDefault(False)
|
|
|
|
return mb.exec_()
|
|
|
|
|
2016-04-30 07:44:41 +02:00
|
|
|
def showText(txt, parent=None, type="text", run=True, geomKey=None, \
|
2019-02-16 23:05:06 +01:00
|
|
|
minWidth=500, minHeight=400, title="Anki", copyBtn=False):
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
diag = QDialog(parent)
|
2016-04-30 07:44:41 +02:00
|
|
|
diag.setWindowTitle(title)
|
2012-12-21 08:51:59 +01:00
|
|
|
layout = QVBoxLayout(diag)
|
|
|
|
diag.setLayout(layout)
|
2017-09-06 08:40:35 +02:00
|
|
|
text = QTextBrowser()
|
|
|
|
text.setOpenExternalLinks(True)
|
2012-12-21 08:51:59 +01:00
|
|
|
if type == "text":
|
|
|
|
text.setPlainText(txt)
|
|
|
|
else:
|
|
|
|
text.setHtml(txt)
|
|
|
|
layout.addWidget(text)
|
|
|
|
box = QDialogButtonBox(QDialogButtonBox.Close)
|
|
|
|
layout.addWidget(box)
|
2019-02-16 23:05:06 +01:00
|
|
|
if copyBtn:
|
|
|
|
def onCopy():
|
|
|
|
QApplication.clipboard().setText(text.toPlainText())
|
|
|
|
btn = QPushButton(_("Copy to Clipboard"))
|
|
|
|
btn.clicked.connect(onCopy)
|
|
|
|
box.addButton(btn, QDialogButtonBox.ActionRole)
|
2014-06-18 20:47:45 +02:00
|
|
|
def onReject():
|
|
|
|
if geomKey:
|
|
|
|
saveGeom(diag, geomKey)
|
|
|
|
QDialog.reject(diag)
|
2016-05-31 10:51:40 +02:00
|
|
|
box.rejected.connect(onReject)
|
2017-04-26 22:25:16 +02:00
|
|
|
def onFinish():
|
|
|
|
if geomKey:
|
|
|
|
saveGeom(diag, geomKey)
|
2017-05-03 11:22:01 +02:00
|
|
|
box.accepted.connect(onFinish)
|
2016-04-30 07:44:41 +02:00
|
|
|
diag.setMinimumHeight(minHeight)
|
|
|
|
diag.setMinimumWidth(minWidth)
|
2014-06-18 20:47:45 +02:00
|
|
|
if geomKey:
|
|
|
|
restoreGeom(diag, geomKey)
|
2012-12-21 08:51:59 +01:00
|
|
|
if run:
|
|
|
|
diag.exec_()
|
|
|
|
else:
|
|
|
|
return diag, box
|
|
|
|
|
2016-04-30 07:44:41 +02:00
|
|
|
def askUser(text, parent=None, help="", defaultno=False, msgfunc=None, \
|
|
|
|
title="Anki"):
|
2012-12-21 08:51:59 +01:00
|
|
|
"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
|
2016-04-30 07:44:41 +02:00
|
|
|
r = msgfunc(parent, title, text, sb, default)
|
2012-12-21 08:51:59 +01:00
|
|
|
if r == QMessageBox.Help:
|
|
|
|
|
|
|
|
openHelp(help)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
return r == QMessageBox.Yes
|
|
|
|
|
2016-05-01 05:32:17 +02:00
|
|
|
class ButtonedDialog(QMessageBox):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2016-05-01 05:32:17 +02:00
|
|
|
def __init__(self, text, buttons, parent=None, help="", title="Anki"):
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, parent)
|
|
|
|
self.buttons = []
|
2016-04-30 07:44:41 +02:00
|
|
|
self.setWindowTitle(title)
|
2012-12-21 08:51:59 +01:00
|
|
|
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)
|
2017-07-31 07:48:34 +02:00
|
|
|
txt = self.clickedButton().text()
|
|
|
|
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
|
|
|
return txt.replace("&", "")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def setDefault(self, idx):
|
|
|
|
self.setDefaultButton(self.buttons[idx])
|
|
|
|
|
2016-04-30 07:44:41 +02:00
|
|
|
def askUserDialog(text, buttons, parent=None, help="", title="Anki"):
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw
|
2016-04-30 07:44:41 +02:00
|
|
|
diag = ButtonedDialog(text, buttons, parent, help, title=title)
|
2012-12-21 08:51:59 +01:00
|
|
|
return diag
|
|
|
|
|
|
|
|
class GetTextDialog(QDialog):
|
|
|
|
|
2016-05-12 06:45:35 +02:00
|
|
|
def __init__(self, parent, question, help=None, edit=None, default="", \
|
2016-04-30 07:44:41 +02:00
|
|
|
title="Anki", minWidth=400):
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, parent)
|
|
|
|
self.setWindowTitle(title)
|
|
|
|
self.question = question
|
|
|
|
self.help = help
|
|
|
|
self.qlabel = QLabel(question)
|
2016-04-30 07:44:41 +02:00
|
|
|
self.setMinimumWidth(minWidth)
|
2012-12-21 08:51:59 +01:00
|
|
|
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)
|
2016-05-31 10:51:40 +02:00
|
|
|
b.button(QDialogButtonBox.Ok).clicked.connect(self.accept)
|
|
|
|
b.button(QDialogButtonBox.Cancel).clicked.connect(self.reject)
|
2012-12-21 08:51:59 +01:00
|
|
|
if help:
|
2016-05-31 10:51:40 +02:00
|
|
|
b.button(QDialogButtonBox.Help).clicked.connect(self.helpRequested)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
return QDialog.accept(self)
|
|
|
|
|
|
|
|
def reject(self):
|
|
|
|
return QDialog.reject(self)
|
|
|
|
|
|
|
|
def helpRequested(self):
|
|
|
|
openHelp(self.help)
|
|
|
|
|
2017-05-03 10:55:24 +02:00
|
|
|
def getText(prompt, parent=None, help=None, edit=None, default="",
|
|
|
|
title="Anki", geomKey=None, **kwargs):
|
2012-12-21 08:51:59 +01:00
|
|
|
if not parent:
|
|
|
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
|
|
|
d = GetTextDialog(parent, prompt, help=help, edit=edit,
|
2017-05-03 10:55:24 +02:00
|
|
|
default=default, title=title, **kwargs)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.setWindowModality(Qt.WindowModal)
|
2017-05-03 10:55:24 +02:00
|
|
|
if geomKey:
|
|
|
|
restoreGeom(d, geomKey)
|
2012-12-21 08:51:59 +01:00
|
|
|
ret = d.exec_()
|
2017-05-03 10:55:24 +02:00
|
|
|
if geomKey and ret:
|
|
|
|
saveGeom(d, geomKey)
|
2016-05-12 06:45:35 +02:00
|
|
|
return (str(d.l.text()), ret)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def getOnlyText(*args, **kwargs):
|
|
|
|
(s, r) = getText(*args, **kwargs)
|
|
|
|
if r:
|
|
|
|
return s
|
|
|
|
else:
|
2016-05-12 06:45:35 +02:00
|
|
|
return ""
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# 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)
|
2016-05-31 10:51:40 +02:00
|
|
|
bb.accepted.connect(d.accept)
|
2012-12-21 08:51:59 +01:00
|
|
|
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)
|
2017-05-03 10:55:24 +02:00
|
|
|
ret = getText(question, parent, edit=te,
|
|
|
|
geomKey='getTag', **kwargs)
|
2012-12-21 08:51:59 +01:00
|
|
|
te.hideCompleter()
|
|
|
|
return ret
|
|
|
|
|
|
|
|
# File handling
|
|
|
|
######################################################################
|
|
|
|
|
2019-02-18 07:10:43 +01:00
|
|
|
def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
2012-12-21 08:51:59 +01:00
|
|
|
"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)
|
2019-02-18 07:10:43 +01:00
|
|
|
mode = QFileDialog.ExistingFiles if multi else QFileDialog.ExistingFile
|
|
|
|
d.setFileMode(mode)
|
2016-03-21 00:57:45 +01:00
|
|
|
if os.path.exists(dir):
|
|
|
|
d.setDirectory(dir)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.setWindowTitle(title)
|
|
|
|
d.setNameFilter(filter)
|
|
|
|
ret = []
|
|
|
|
def accept():
|
2019-02-18 07:10:43 +01:00
|
|
|
files = list(d.selectedFiles())
|
2012-12-21 08:51:59 +01:00
|
|
|
if dirkey:
|
2019-02-18 07:10:43 +01:00
|
|
|
dir = os.path.dirname(files[0])
|
2012-12-21 08:51:59 +01:00
|
|
|
aqt.mw.pm.profile[dirkey] = dir
|
2019-02-18 07:10:43 +01:00
|
|
|
result = files if multi else files[0]
|
2012-12-21 08:51:59 +01:00
|
|
|
if cb:
|
2019-02-18 07:10:43 +01:00
|
|
|
cb(result)
|
|
|
|
ret.append(result)
|
2016-05-31 10:51:40 +02:00
|
|
|
d.accepted.connect(accept)
|
2018-07-23 05:57:17 +02:00
|
|
|
if key:
|
|
|
|
restoreState(d, key)
|
2012-12-21 08:51:59 +01:00
|
|
|
d.exec_()
|
2018-07-23 05:57:17 +02:00
|
|
|
if key:
|
|
|
|
saveState(d, key)
|
2012-12-21 08:51:59 +01:00
|
|
|
return ret and ret[0]
|
|
|
|
|
2013-05-21 04:53:02 +02:00
|
|
|
def getSaveFile(parent, title, dir_description, key, ext, fname=None):
|
2013-05-18 18:24:53 +02:00
|
|
|
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
2013-05-21 04:53:02 +02:00
|
|
|
variable. The file dialog will default to open with FNAME."""
|
|
|
|
config_key = dir_description + 'Directory'
|
2017-09-10 08:42:29 +02:00
|
|
|
|
2017-09-10 09:01:52 +02:00
|
|
|
defaultPath = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
|
|
|
|
base = aqt.mw.pm.profile.get(config_key, defaultPath)
|
2013-05-21 04:53:02 +02:00
|
|
|
path = os.path.join(base, fname)
|
2016-05-31 10:51:40 +02:00
|
|
|
file = QFileDialog.getSaveFileName(
|
2016-05-12 06:45:35 +02:00
|
|
|
parent, title, path, "{0} (*{1})".format(key, ext),
|
2016-05-31 10:51:40 +02:00
|
|
|
options=QFileDialog.DontConfirmOverwrite)[0]
|
2012-12-21 08:51:59 +01:00
|
|
|
if file:
|
|
|
|
# add extension
|
|
|
|
if not file.lower().endswith(ext):
|
|
|
|
file += ext
|
|
|
|
# save new default
|
|
|
|
dir = os.path.dirname(file)
|
2013-05-18 18:24:53 +02:00
|
|
|
aqt.mw.pm.profile[config_key] = dir
|
2012-12-21 08:51:59 +01:00
|
|
|
# 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"
|
2018-08-08 04:30:58 +02:00
|
|
|
if isMac and widget.windowState() & Qt.WindowFullScreen:
|
|
|
|
geom = None
|
|
|
|
else:
|
|
|
|
geom = widget.saveGeometry()
|
|
|
|
aqt.mw.pm.profile[key] = geom
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2014-06-18 20:47:45 +02:00
|
|
|
def restoreGeom(widget, key, offset=None, adjustSize=False):
|
2012-12-21 08:51:59 +01:00
|
|
|
key += "Geom"
|
|
|
|
if aqt.mw.pm.profile.get(key):
|
|
|
|
widget.restoreGeometry(aqt.mw.pm.profile[key])
|
|
|
|
if isMac and offset:
|
2013-05-17 08:17:04 +02:00
|
|
|
if qtminor > 6:
|
2012-12-21 08:51:59 +01:00
|
|
|
# bug in osx toolkit
|
|
|
|
s = widget.size()
|
|
|
|
widget.resize(s.width(), s.height()+offset*2)
|
2019-02-11 22:49:35 +01:00
|
|
|
ensureWidgetInScreenBoundaries(widget)
|
2014-06-18 20:47:45 +02:00
|
|
|
else:
|
|
|
|
if adjustSize:
|
|
|
|
widget.adjustSize()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-02-11 22:49:35 +01:00
|
|
|
def ensureWidgetInScreenBoundaries(widget):
|
|
|
|
handle = widget.window().windowHandle()
|
|
|
|
if not handle:
|
|
|
|
# window has not yet been shown, retry later
|
|
|
|
aqt.mw.progress.timer(50, lambda: ensureWidgetInScreenBoundaries(widget), False)
|
|
|
|
return
|
|
|
|
|
2019-02-14 04:47:44 +01:00
|
|
|
# ensure widget is smaller than screen bounds
|
2019-02-11 22:49:35 +01:00
|
|
|
geom = handle.screen().availableGeometry()
|
2019-02-14 04:47:44 +01:00
|
|
|
wsize = widget.size()
|
|
|
|
cappedWidth = min(geom.width(), wsize.width())
|
|
|
|
cappedHeight = min(geom.height(), wsize.height())
|
|
|
|
if cappedWidth > wsize.width() or cappedHeight > wsize.height():
|
|
|
|
widget.resize(QSize(cappedWidth, cappedHeight))
|
|
|
|
|
|
|
|
# ensure widget is inside top left
|
|
|
|
wpos = widget.pos()
|
|
|
|
x = max(geom.x(), wpos.x())
|
|
|
|
y = max(geom.y(), wpos.y())
|
|
|
|
# and bottom right
|
|
|
|
x = min(x, geom.width()+geom.x()-cappedWidth)
|
|
|
|
y = min(y, geom.height()+geom.y()-cappedHeight)
|
|
|
|
if x != wpos.x() or y != wpos.y():
|
2019-02-11 22:49:35 +01:00
|
|
|
widget.move(x, y)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
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])
|
|
|
|
|
2013-06-10 08:28:34 +02:00
|
|
|
def mungeQA(col, txt):
|
|
|
|
txt = col.media.escapeImages(txt)
|
2012-12-21 08:51:59 +01:00
|
|
|
txt = stripSounds(txt)
|
|
|
|
return txt
|
|
|
|
|
|
|
|
def openFolder(path):
|
|
|
|
if isWin:
|
2016-12-17 04:23:05 +01:00
|
|
|
subprocess.Popen(["explorer", "file://"+path])
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2017-06-27 04:04:42 +02:00
|
|
|
with noBundledLibs():
|
|
|
|
QDesktopServices.openUrl(QUrl("file://" + path))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2012-12-22 01:11:29 +01:00
|
|
|
def addCloseShortcut(widg):
|
|
|
|
if not isMac:
|
|
|
|
return
|
|
|
|
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
2016-05-31 10:51:40 +02:00
|
|
|
widg._closeShortcut.activated.connect(widg.reject)
|
2012-12-22 01:11:29 +01:00
|
|
|
|
2015-09-28 15:09:30 +02:00
|
|
|
def downArrow():
|
|
|
|
if isWin:
|
2016-05-12 06:45:35 +02:00
|
|
|
return "▼"
|
2015-09-28 15:09:30 +02:00
|
|
|
# windows 10 is lacking the smaller arrow on English installs
|
2016-05-12 06:45:35 +02:00
|
|
|
return "▾"
|
2015-09-28 15:09:30 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Tooltips
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
_tooltipTimer = None
|
|
|
|
_tooltipLabel = None
|
|
|
|
|
|
|
|
def tooltip(msg, period=3000, parent=None):
|
|
|
|
global _tooltipTimer, _tooltipLabel
|
|
|
|
class CustomLabel(QLabel):
|
2017-08-25 04:14:59 +02:00
|
|
|
silentlyClose = True
|
2012-12-21 08:51:59 +01:00
|
|
|
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>%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"))
|
2014-06-02 20:02:26 +02:00
|
|
|
p.setColor(QPalette.WindowText, QColor("#000000"))
|
2012-12-21 08:51:59 +01:00
|
|
|
lab.setPalette(p)
|
|
|
|
lab.move(
|
|
|
|
aw.mapToGlobal(QPoint(0, -100 + aw.height())))
|
|
|
|
lab.show()
|
|
|
|
_tooltipTimer = aqt.mw.progress.timer(
|
2019-02-07 04:59:24 +01:00
|
|
|
period, closeTooltip, False, requiresCollection=False)
|
2012-12-21 08:51:59 +01:00
|
|
|
_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
|
2013-02-20 07:12:07 +01:00
|
|
|
|
|
|
|
# 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
|
2017-08-15 10:41:36 +02:00
|
|
|
|
|
|
|
# Menus
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
class MenuList:
|
|
|
|
def __init__(self):
|
|
|
|
self.children = []
|
|
|
|
|
|
|
|
def addItem(self, title, func):
|
|
|
|
item = MenuItem(title, func)
|
|
|
|
self.children.append(item)
|
|
|
|
return item
|
|
|
|
|
|
|
|
def addSeparator(self):
|
|
|
|
self.children.append(None)
|
|
|
|
|
|
|
|
def addMenu(self, title):
|
|
|
|
submenu = SubMenu(title)
|
|
|
|
self.children.append(submenu)
|
|
|
|
return submenu
|
|
|
|
|
|
|
|
def addChild(self, child):
|
|
|
|
self.children.append(child)
|
|
|
|
|
|
|
|
def renderTo(self, qmenu):
|
|
|
|
for child in self.children:
|
|
|
|
if child is None:
|
|
|
|
qmenu.addSeparator()
|
|
|
|
elif isinstance(child, QAction):
|
|
|
|
qmenu.addAction(child)
|
|
|
|
else:
|
|
|
|
child.renderTo(qmenu)
|
|
|
|
|
|
|
|
def popupOver(self, widget):
|
|
|
|
qmenu = QMenu()
|
|
|
|
self.renderTo(qmenu)
|
|
|
|
qmenu.exec_(widget.mapToGlobal(QPoint(0,0)))
|
|
|
|
|
|
|
|
# Chunking
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
chunkSize = 30
|
|
|
|
|
|
|
|
def chunked(self):
|
|
|
|
if len(self.children) <= self.chunkSize:
|
|
|
|
return self
|
|
|
|
|
|
|
|
newList = MenuList()
|
|
|
|
oldItems = self.children[:]
|
|
|
|
while oldItems:
|
|
|
|
chunk = oldItems[:self.chunkSize]
|
|
|
|
del oldItems[:self.chunkSize]
|
|
|
|
label = self._chunkLabel(chunk)
|
|
|
|
menu = newList.addMenu(label)
|
|
|
|
menu.children = chunk
|
|
|
|
return newList
|
|
|
|
|
|
|
|
def _chunkLabel(self, items):
|
|
|
|
start = items[0].title
|
|
|
|
end = items[-1].title
|
|
|
|
prefix = os.path.commonprefix([start.upper(), end.upper()])
|
|
|
|
n = len(prefix)+1
|
2018-01-16 07:07:30 +01:00
|
|
|
return "{}-{}".format(start[:n].upper(), end[:n].upper())
|
2017-08-15 10:41:36 +02:00
|
|
|
|
|
|
|
class SubMenu(MenuList):
|
|
|
|
def __init__(self, title):
|
|
|
|
super().__init__()
|
|
|
|
self.title = title
|
|
|
|
|
|
|
|
def renderTo(self, menu):
|
|
|
|
submenu = menu.addMenu(self.title)
|
|
|
|
super().renderTo(submenu)
|
|
|
|
|
|
|
|
class MenuItem:
|
|
|
|
def __init__(self, title, func):
|
|
|
|
self.title = title
|
|
|
|
self.func = func
|
|
|
|
|
|
|
|
def renderTo(self, qmenu):
|
|
|
|
a = qmenu.addAction(self.title)
|
|
|
|
a.triggered.connect(self.func)
|
|
|
|
|
2019-02-05 05:37:07 +01:00
|
|
|
def qtMenuShortcutWorkaround(qmenu):
|
|
|
|
if qtminor < 10:
|
|
|
|
return
|
|
|
|
for act in qmenu.actions():
|
|
|
|
act.setShortcutVisibleInContextMenu(True)
|
|
|
|
|
2018-12-13 11:59:06 +01:00
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def versionWithBuild():
|
|
|
|
from aqt import appVersion
|
|
|
|
try:
|
|
|
|
from aqt.buildhash import build
|
|
|
|
except:
|
|
|
|
build = "dev"
|
|
|
|
return "%s (%s)" % (appVersion, build)
|
2018-12-18 10:29:34 +01:00
|
|
|
|
2019-02-24 14:50:39 +01:00
|
|
|
def supportText():
|
|
|
|
import platform
|
|
|
|
from aqt import mw
|
|
|
|
from aqt.utils import versionWithBuild
|
|
|
|
|
|
|
|
if isWin:
|
|
|
|
platname = "Windows " + platform.win32_ver()[0]
|
|
|
|
elif isMac:
|
|
|
|
platname = "Mac " + platform.mac_ver()[0]
|
|
|
|
else:
|
|
|
|
platname = "Linux"
|
|
|
|
|
|
|
|
def schedVer():
|
|
|
|
try:
|
|
|
|
return mw.col.schedVer()
|
|
|
|
except:
|
|
|
|
return "?"
|
|
|
|
|
|
|
|
return """\
|
|
|
|
Anki {} Python {} Qt {} PyQt {}
|
|
|
|
Platform: {}
|
|
|
|
Flags: frz={} ao={} sv={}
|
|
|
|
""".format(versionWithBuild(), platform.python_version(),
|
|
|
|
QT_VERSION_STR, PYQT_VERSION_STR, platname,
|
|
|
|
getattr(sys, "frozen", False),
|
|
|
|
mw.addonManager.dirty, schedVer())
|
|
|
|
|
2018-12-18 10:29:34 +01:00
|
|
|
######################################################################
|
|
|
|
|
|
|
|
# adapted from version detection in qutebrowser
|
|
|
|
def opengl_vendor():
|
|
|
|
old_context = QOpenGLContext.currentContext()
|
|
|
|
old_surface = None if old_context is None else old_context.surface()
|
|
|
|
|
|
|
|
surface = QOffscreenSurface()
|
|
|
|
surface.create()
|
|
|
|
|
|
|
|
ctx = QOpenGLContext()
|
|
|
|
ok = ctx.create()
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
|
|
|
|
ok = ctx.makeCurrent(surface)
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
if ctx.isOpenGLES():
|
|
|
|
# Can't use versionFunctions there
|
|
|
|
return None
|
|
|
|
|
|
|
|
vp = QOpenGLVersionProfile()
|
|
|
|
vp.setVersion(2, 0)
|
|
|
|
|
|
|
|
try:
|
|
|
|
vf = ctx.versionFunctions(vp)
|
|
|
|
except ImportError as e:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if vf is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return vf.glGetString(vf.GL_VENDOR)
|
|
|
|
finally:
|
|
|
|
ctx.doneCurrent()
|
|
|
|
if old_context and old_surface:
|
|
|
|
old_context.makeCurrent(old_surface)
|
|
|
|
|
|
|
|
def gfxDriverIsBroken():
|
|
|
|
driver = opengl_vendor()
|
|
|
|
return driver == "nouveau"
|