namadino/namadino.py

817 lines
29 KiB
Python
Raw Normal View History

2019-11-30 02:57:27 -06:00
from sys import argv
from os import mkdir, remove
from os.path import exists, join, dirname
from uuid import uuid4
from re import compile, findall
from tempfile import gettempdir
from webbrowser import open as wbopen
from PyQt5.QtGui import QIcon, QPalette, QColor
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QApplication, QWidget, QMainWindow, QLineEdit,
QVBoxLayout, QHBoxLayout, QSplitter, QAction, qApp, QLabel, QDesktopWidget,
QPlainTextEdit, QMenu, QMessageBox, QFileDialog, QColorDialog, QInputDialog,
QMessageBox,
)
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
from mistune import Markdown, Renderer, InlineLexer, InlineGrammar
from nacl.secret import SecretBox
from nacl.hash import generichash
from nacl.exceptions import CryptoError
from pickle import dump, load, dumps, loads
myPath = dirname(__file__)
DATADIR = join(myPath,"data")
TMPDIR = join(gettempdir(),"namadino_"+str(uuid4()))
mkdir(TMPDIR)
HTMLtemplate = ""
with open(join(DATADIR, "HTMLtemplate.html")) as f:
HTMLtemplate = f.read()
################################################################################
class Note(object):
def __init__(self, title="New note", markdown="Enter your notes here\n\n**Markdown** is supported. <Hover notes work like this>(Hello world)"):
super().__init__()
self.title = title
self.markdown = markdown
def toDict(self):
return self.__dict__.copy()
@classmethod
def fromDict(cls, state):
return cls(**state)
def setMarkdown(self, markdown):
self.markdown = markdown
def setTitle(self, title):
self.title = title
def copy(self):
return Note(self.title, self.markdown)
class Folder(object):
def __init__(self, title="New Folder"):
super().__init__()
self.title = title
self.folders = []
self.notes = []
def __getstate__(self):
return self.toDict()
def __setstate__(self, state):
self.__dict__.update( self.fromDict(state).__dict__ )
def toDict(self):
"""
Allow for folders to be pickled for saving
"""
state = {
"title": self.title,
"folders": [f.toDict() for f in self.folders],
"notes": [n.toDict() for n in self.notes]
}
return state
@classmethod
def fromDict(cls, state):
f = cls(state['title'])
f.folders = [Folder.fromDict(fstate) for fstate in state["folders"]]
f.notes = [Note.fromDict(nstate) for nstate in state["notes"]]
return f
def setTitle(self, title):
self.title = title
def addFolder(self, folder=None):
if not folder:
folder = Folder()
elif isinstance(folder, str):
folder = Folder(folder)
self.folders.append(folder)
return folder
def addNote(self, note=None):
if not note:
note = Note()
self.notes.append(note)
return note
def concatNotes(self):
out = []
for f in self.folders:
out += f.concatNotes()
for n in self.notes:
out.append(n)
return out
def deleteFolder(self, folder):
self.folders = [f for f in self.folders if f != folder]
def deleteNote(self, note):
self.notes = [n for n in self.notes if n != note]
def copy(self):
"""
Returns a copy of the folder, used for defaults
"""
out = Folder(self.title)
out.folders = [f.copy() for f in self.folders]
out.notes = [n.copy() for n in self.notes]
return out
################################################################################
hoverNoteRe = compile(r'<([^}]+?)>\(([^)]+?)\)')
class CustomRenderer(Renderer):
def hover_note(self, base, hover):
return '<a class="ht" href="" title="{1}">{0}</a>'.format(base, hover)
class CustomInlineGrammar(InlineGrammar):
hover_note = hoverNoteRe
class CustomInlineLexer(InlineLexer):
grammar_class = CustomInlineGrammar
def enable_hover_note(self):
self.rules.hoverNote = hoverNoteRe
self.default_rules.insert(0, 'hover_note')
def output_hover_note(self, m):
base = m.group(1)
hover = m.group(2)
return self.renderer.hover_note(base, hover)
renderer = CustomRenderer()
inline = CustomInlineLexer(renderer)
inline.enable_hover_note()
MD = Markdown(renderer, inline=inline)
################################################################################
DEFAULT_DATA = Folder("Untitled Project")
DEFAULT_DATA.addNote()
DEFAULT_DATA.notes[0].setTitle("Welcome")
DEFAULT_DATA.notes[0].setMarkdown(
"""Welcome to the **Na**meless **Ma**rkdown **Di**rectory-organized **No**tation
software, a.k.a. **Namadino** *(nah-mah-dee-no)*
Here you can organize your thoughts into folders and notes. This program was
originally written to aid in translating different media, however it can be
applied to nearly any study.
Notes support basic **Markdown** with one additional, custom pattern for "hover
text". Hover text allows you to put hidden definitions or solutions directly
above the relavent information when the note is rendered, like so: <日本>(にっぽん
| Japan). It also acts as a quick template for making Anki
flashcards. All instances of hover text in a project can be exported into a
`txt` file that can be imported into an Anki deck by using the `Export Project
To` menu at the top of the screen. The same can be done for all children in a
folder or all hover text in a single note by using the `Export To` menu in the
right-click menu.
Projects, folders, and notes can also be exported to an interactive HTML file.
These files can be shared easily and viewed with any web browser. Alternatively,
the HTML file can be opened directly in your browser.
"""
)
class GUI(QMainWindow):
def __init__(self):
super().__init__()
self.defaults()
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.render)
self.autosaveTime = 5*60*1000
self.autosavetimer = QTimer()
self.autosavetimer.timeout.connect(self.autoSave)
self.autosavetimer.start(self.autosaveTime)
self.loadSettings()
self.buildGUI()
def defaults(self):
self.data = DEFAULT_DATA.copy()
self.lastSave = Folder()
self.filepath = None
self.selectedNote = None
self.hashword = None
def buildGUI(self):
#self.setWindowIcon(QIcon(join(DATADIR,"d20_32x32.png")))
self.center()
self.statusBar().showMessage('Welcome to the thing with no name',3000)
self.setPalette(QPalette(QColor(self.settings["theme"])))
########################################################################
menubar = self.menuBar()
# File
fileMenu = menubar.addMenu('&File')
# File > New
new = QAction(QIcon.fromTheme("document-new"), "&New", self)
new.setShortcut("Ctrl+N")
new.setStatusTip("New notes")
new.triggered.connect(self.newProject)
fileMenu.addAction(new)
# File > Open
op = QAction(QIcon.fromTheme("document-open"), "&Open", self)
op.setShortcut("Ctrl+O")
op.setStatusTip("Open notes")
op.triggered.connect(self.openProject)
fileMenu.addAction(op)
# File > Save
sav = QAction(QIcon.fromTheme("document-save"), "&Save", self)
sav.setShortcut("Ctrl+S")
sav.setStatusTip("Save notes")
sav.triggered.connect(self.saveProject)
fileMenu.addAction(sav)
# File > Save As
sas = QAction(QIcon.fromTheme("document-save-as"), "Save &As", self)
sas.setShortcut("Ctrl+Shift+S")
sas.setStatusTip("Save notes")
sas.triggered.connect(self.saveProjectAs)
fileMenu.addAction(sas)
# File > View source code
fileMenu.addSeparator()
source = QAction(QIcon.fromTheme("applications-internet"),"View source code",self)
source.triggered.connect(self.openGitLab)
source.setStatusTip('Open source code in browser')
fileMenu.addAction(source)
fileMenu.addSeparator()
# File > Quit
exitAct = QAction(QIcon.fromTheme("application-exit"), '&Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(qApp.quit)
fileMenu.addAction(exitAct)
# Edit
edtMenu = menubar.addMenu('&Edit')
# Edit > Color
co = QAction(QIcon.fromTheme("applications-graphics"), "Theme", self)
co.setStatusTip("Pick a color to generate a theme from")
co.triggered.connect(self.setTheme)
edtMenu.addAction(co)
# Export
expMenu = menubar.addMenu('&Export Project To')
expMenu.REF = self.data
self.makeExportMenu(expMenu, expMenu)
########################################################################
self.main = QSplitter()
self.main.setChildrenCollapsible(False)
self.tree = QTreeWidget(self.main)
self.tree.setColumnCount(1)
self.tree.setHeaderHidden(True)
self.traverseTree()
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree.customContextMenuRequested.connect(self.treeContext)
self.tree.currentItemChanged.connect(self.updateSel)
self.tree.itemChanged.connect(self.endRename)
self.note = QPlainTextEdit(self.main)
self.note.textChanged.connect(self.noteUpdate)
self.note.setEnabled(False)
self.note.setPlaceholderText("Select a note...")
self.renderer = QWebEngineView(self.main)
self.getCSSInversion()
self.renderer.setZoomFactor(1.5)
self.renderer.settings().setAttribute(
QWebEngineSettings.FocusOnNavigationEnabled, False)
self.renderer.keyPressEvent = None
self.renderHTML("")
self.main.setStretchFactor(0,1)
self.main.setStretchFactor(1,1)
self.main.setStretchFactor(2,1)
self.main.restoreState(self.settings["frame"])
# There is no event for when the handles are released
# without keeping track of all of the handles' `mouseReleaseEvent`s
# So instead we watch the constant update event with a timer.
self.main.splitterMoved.connect(self.frameResizing)
self.main.timer = QTimer()
self.main.timer.setSingleShot(True)
self.main.timer.timeout.connect(self.frameResized)
# Finished loading GUI.
self.setCentralWidget(self.main)
self.show()
def center(self):
"""
Resizes the window to be 80% of the available space and centers it
"""
space = QDesktopWidget().availableGeometry()
self.resize(
space.width() * 0.80,
space.height() * 0.80
)
qr = self.frameGeometry()
qr.moveCenter(space.center())
self.move(qr.topLeft())
def checkUnsaved(self):
self.autosavetimer.stop()
if self.data.toDict() == DEFAULT_DATA.toDict():
return True
if self.data.toDict() != self.lastSave.toDict():
alert = QMessageBox.question(self, "Unsaved changes",
"There are unsaved changes. Save before closing?".format(self.data.title),
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, QMessageBox.No)
if alert == QMessageBox.Yes:
self.saveProject()
elif alert == QMessageBox.Cancel:
return False
return True
def closeEvent(self, event):
_ = self.checkUnsaved()
def newProject(self):
if self.checkUnsaved():
self.defaults()
self.traverseTree()
self.resetNoteField()
self.statusBar().showMessage("Blank project opened",3000)
self.autosavetimer.start(self.autosaveTime)
def openProject(self):
if self.checkUnsaved():
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filepath, OK = QFileDialog.getOpenFileName(self,
"Open...",
self.data.title+".notes",
"Notes Files (*.notes);;Encrypted Notes Files *.enotes)",
options=options)
if not OK:
self.autosavetimer.start(self.autosaveTime)
return None
if filepath[-6:] == "enotes":
if not self.encOpen(filepath):
self.autosavetimer.start(self.autosaveTime)
return self.showError("Error opening project")
else:
self.hashword = None
self.openAction(filepath)
self.filepath = filepath
self.lastSave = self.data.copy()
self.traverseTree()
self.resetNoteField()
self.statusBar().showMessage("Opened "+self.data.title,3000)
self.autosavetimer.start(self.autosaveTime)
def openAction(self, fp):
with open(fp, "rb") as f:
self.data = load(f)
def encOpen(self, fp):
pwd, OK = QInputDialog.getText(self,
"Input password",
"Type the password for this project",
QLineEdit.Password)
if not OK:
return
hashword = generichash(bytes(pwd, "utf-8"), digest_size=16)
return self.encOpenAction(fp, hashword)
def encOpenAction(self, fp, hashword):
box = SecretBox(hashword)
data = None
with open(fp, "rb") as f:
try:
data = loads(box.decrypt(load(f)))
except CryptoError:
return self.showError("Invalid password")
except Exception as e:
return self.showError("Unknown error: "+str(e))
self.hashword = hashword
self.data = data
return True
def saveProject(self):
if not self.filepath:
self.saveProjectAs()
return None
self.saveActionWrap()
def saveProjectAs(self):
self.autosavetimer.stop()
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filepath, OK = QFileDialog.getSaveFileName(self,
"Save as...",
self.data.title+".notes",
"Notes File (*.notes);;"+
"Encrypted Notes File (*.enotes)",
options=options)
if OK:
self.filepath = filepath
self.saveActionWrap(resetHash=True)
self.autosavetimer.start(self.autosaveTime)
def saveActionWrap(self, resetHash=False):
if self.filepath[-6:].lower() == "enotes":
if not self.encSave(resetHash):
return
else:
self.hashword = None
self.saveAction(self.filepath, self.data)
self.lastSave = self.data.copy()
windowTitle = self.data.title
if self.hashword:
windowTitle += " <Secure>"
self.setWindowTitle(windowTitle + " - Namadino")
self.statusBar().showMessage("Saved {0} to {1}".format(self.data.title, self.filepath),3000)
def autoSave(self):
if not self.filepath:
return
if self.hashword:
self.encSaveAction(self.filepath, self.hashword)
else:
self.saveAction(self.filepath, self.data)
self.statusBar().showMessage("Project autosaved", 3000)
def saveAction(self, fp, data, full=True):
with open(fp, "wb") as f:
dump(data, f)
def encSave(self, resetHash):
success = False
if resetHash:
success = self.encValidate(None)
else:
success = self.encValitade(self.hashword)
if success:
self.encSaveAction(self.filepath, self.hashword)
def encValidate(self, pwd1):
if not pwd1:
pwd1, OK = QInputDialog.getText(self,
"Input password",
"Type a password for this project",
QLineEdit.Password)
if not OK:
return self.showError("Save aborted")
pwd1 = generichash(bytes(pwd1, "utf-8"), digest_size=16)
pwd2, OK = QInputDialog.getText(self,
"Confirm password",
"Reype the password for this project",
QLineEdit.Password)
if not OK:
return self.showError("Save aborted")
pwd2 = generichash(bytes(pwd2, "utf-8"), digest_size=16)
if pwd1 != pwd2:
return self.showError("Passwords do not match")
self.hashword = pwd1
return True
def encSaveAction(self, fp, hashword):
box = SecretBox(hashword)
self.saveAction(fp, box.encrypt(dumps(self.data)))
return True
def showError(self, error):
QMessageBox(QMessageBox.Critical,
"Error",
error
).exec_()
return
def traverseTree(self, parent=None, folder=None):
"""
Parse the data and populate the tree
"""
if not parent:
self.tree.clear()
trunk = QTreeWidgetItem([self.data.title])
trunk.REF = self.data
if self.hashword:
trunk.setIcon(0, QIcon.fromTheme("security-high"))
else:
trunk.setIcon(0, QIcon.fromTheme("package-x-generic"))
self.tree.addTopLevelItem(self.traverseTree(trunk, self.data))
windowTitle = self.data.title
if self.hashword:
windowTitle += " <Secure>"
self.setWindowTitle(windowTitle + " - Namadino")
return
for f in folder.folders:
branch = QTreeWidgetItem([f.title])
branch.REF = f
branch.setIcon(0, QIcon.fromTheme("folder"))
parent.addChild(self.traverseTree(branch, f))
for n in folder.notes:
leaf = QTreeWidgetItem([n.title])
leaf.REF = n
leaf.setIcon(0, QIcon.fromTheme("text-x-generic"))
parent.addChild(leaf)
return parent
def noteUpdate(self):
"""
Called every time the text box changes
Starts timer to save markdown
"""
self.timer.start(1000)
def render(self):
"""
Saves note text to active Note object
"""
if self.selectedNote:
self.selectedNote.setMarkdown(self.note.toPlainText())
self.renderHTML(MD(self.note.toPlainText()))
def treeContext(self, point):
sel = self.tree.currentItem()
if sel:
popMenu = QMenu(self)
if isinstance(sel.REF, Folder):
popMenu.new = QMenu("New...")
popMenu.new.f = QAction("Folder")
popMenu.new.f.triggered.connect(self.newFolder)
popMenu.new.f.PARENT = sel
popMenu.new.addAction(popMenu.new.f)
popMenu.new.n = QAction("Note")
popMenu.new.n.triggered.connect(self.newNote)
popMenu.new.n.PARENT = sel
popMenu.new.addAction(popMenu.new.n)
popMenu.addMenu(popMenu.new)
popMenu.rename = QAction("Rename")
popMenu.rename.PARENT = sel
popMenu.rename.triggered.connect(self.renameThing)
popMenu.addAction(popMenu.rename)
popMenu.export = QMenu("Export to...")
self.makeExportMenu(popMenu.export, sel)
popMenu.addMenu(popMenu.export)
popMenu.addSeparator()
if sel.REF != self.data:
popMenu.delete = QAction("Delete")
popMenu.delete.PARENT = sel
popMenu.delete.triggered.connect(self.deleteThing)
popMenu.addAction(popMenu.delete)
popMenu.exec_(self.tree.mapToGlobal(point))
def makeExportMenu(self, menu, parent):
menu.anki = QAction(QIcon.fromTheme("text-x-generic"), "Anki Text")
menu.anki.triggered.connect(self.hoverTextAnkiExport)
menu.anki.setStatusTip("Export all hover text pairs to Anki-importable file")
menu.anki.PARENT = parent
menu.addAction(menu.anki)
menu.html = QAction(QIcon.fromTheme("text-html"), "HTML")
menu.html.triggered.connect(self.htmlExportFile)
menu.html.setStatusTip("Export to interactive HTML file")
menu.html.PARENT = parent
menu.addAction(menu.html)
menu.web = QAction(QIcon.fromTheme("applications-internet"),"Web Browser")
menu.web.triggered.connect(self.htmlExportWebpage)
menu.web.setStatusTip("Open render directly in your browser")
menu.web.PARENT = parent
menu.addAction(menu.web)
def updateSel(self, sel, old):
if not sel:
# This doesn't seem to be possible but I don't trust it
self.resetNoteField()
elif isinstance(sel.REF, Note):
self.note.setEnabled(True)
self.note.setPlainText(sel.REF.markdown)
self.selectedNote = sel.REF
self.timer.stop()
self.render()
else:
self.resetNoteField()
if self.tree.isPersistentEditorOpen(old,0):
self.endRename(old)
def renameThing(self, thing=None):
if not thing:
thing = self.sender().PARENT
self.tree.openPersistentEditor(thing,0)
self.tree.focusNextChild()
def endRename(self, thing):
self.tree.closePersistentEditor(thing)
new = thing.data(0,thing.type())
if new:
thing.REF.setTitle(new)
else:
thing.setText(0, thing.REF.title)
def resetNoteField(self):
self.selectedNote = None
self.note.setEnabled(False)
self.note.setPlainText("")
self.note.setPlaceholderText("Select a note...")
self.timer.stop()
self.render()
def newFolder(self):
parent = self.sender().PARENT
f = parent.REF.addFolder()
self.newFinish(parent, f, "folder")
def newNote(self):
parent = self.sender().PARENT
n = parent.REF.addNote()
self.newFinish(parent, n, "text-x-generic")
def newFinish(self, parent, item, icon):
# This is janky but...
oldKids = [c.REF for c in parent.takeChildren()]
self.traverseTree(parent=parent, folder=parent.REF)
newKids = parent.takeChildren()
parent.addChildren(newKids)
newKid = [k for k in newKids if k.REF not in oldKids][0]
if not parent.isExpanded():
parent.setExpanded(True)
self.renameThing(newKid)
def deleteThing(self):
thing = self.sender().PARENT
if isinstance(thing.REF,Note):
confirm = QMessageBox.question(self, "Confirm Deletion",
"Permanently delete {}?".format(thing.REF.title),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.Yes:
thing.parent().REF.deleteNote(thing.REF)
thing.parent().removeChild(thing)
else:
confirm = QMessageBox.question(self, "Confirm Deletion",
"Permanently delete {} and everything in it?".format(thing.REF.title),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.Yes:
thing.parent().REF.deleteFolder(thing.REF)
thing.parent().removeChild(thing)
def allowExport(self):
if self.hashword:
confirm = QMessageBox.question(self, "Allow cleartext export",
("This project is password protected. Exporting it will allow "
"its contents to be read in clear text. Proceed with export?"),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.No:
return False
if not encValidate(self.hashword):
return False
return True
def hoverTextAnkiExport(self):
if not self.allowExport():
return None
data = self.sender().PARENT.REF
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
fp, OK = QFileDialog.getSaveFileName(self,
"Export to...",
data.title+".txt","Anki Import Files (*.txt)",
options=options)
if not OK:
return None
out = ""
if isinstance(data, Folder):
notes = data.concatNotes()
else:
notes = [data]
for n in notes:
for front, back in findall(hoverNoteRe, n.markdown):
out += front.replace("\t"," ")+"\t"+back.replace("\t"," ")+"\n"
with open(fp,"w") as f:
f.write(out)
self.statusBar().showMessage("Exported {0} to {1}".format(self.data.title, fp))
def htmlExportWebpage(self):
if not self.allowExport():
return None
data = self.sender().PARENT.REF
tmp = join(TMPDIR, str(uuid4())+".html")
html = self.htmlExporter(data)
with open(tmp,"w") as f:
# format doesn't like the extra brackets from the CSS/JS
f.write(html)
wbopen(tmp)
def htmlExportFile(self):
if not self.allowExport():
return None
data = self.sender().PARENT.REF
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
fp, OK = QFileDialog.getSaveFileName(self,
"Export to...",
data.title+".html","HTML Files (*.html)",
options=options)
if not OK:
return None
html = self.htmlExporter(data)
with open(fp,"w") as f:
# format doesn't like the extra brackets from the CSS/JS
f.write(html)
self.statusBar().showMessage("Exported {0} to {1}".format(data.title, fp))
def htmlExporter(self, data):
out = "<head>\n <title>{0}</title>\n</head>\n<body><h1>{0}</h1>".format(data.title)
out = self.htmlExportRecurse(data, out)
out += "</body>"
return HTMLtemplate.replace("{}",out)
def htmlExportRecurse(self, data, out):
if isinstance(data, Folder):
for f in data.folders:
out += '<br/><button class="hid" onclick="toggle(this)">{}</button><div>'.format(f.title)
out = self.htmlExportRecurse(f, out)
out += "</div>"
for n in data.notes:
out += '<br/><button class="hid" onclick="toggle(this)">{}</button><div>'.format(n.title)
out += "<div>{}</div>".format(MD(n.markdown))
out += "</div>"
else:
out += '<br/><button class="hid" onclick="toggle(this)">{}</button><div>'.format(data.title)
out += "<div>{}</div>".format(MD(data.markdown))
out += "</div>"
return out
def setTheme(self):
col = QColorDialog.getColor()
if col.isValid():
p = QPalette(col)
self.setPalette(p)
self.getCSSInversion()
self.settings["theme"] = p.window().color().name()
self.renderHTML(self.renderer.HTML)
self.saveSettings()
def getCSSInversion(self):
"""
If the palette says the background should be black, invert colors
"""
self.renderer.CSS = (self.palette().base().color().name(),
self.palette().text().color().name())
def renderHTML(self, html):
self.renderer.HTML = html
self.renderer.setHtml(
"<style> body {{ background-color: {0}; color: {1};}}</style><body>".format(
*self.renderer.CSS
) + self.renderer.HTML + "</body>"
)
def defaultSettings(self):
self.settings = {
"theme": "#ffffff",
"frame": b'\x00\x00\x00\xff\x00\x00\x00\x01\x00\x00\x00\x03\x00\x01\x86\x9f\x00\x01\x86\x9f\x00\x01\x86\x9f\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00'
}
self.saveSettings()
def saveSettings(self):
with open(join(DATADIR, "preferences.pkl"),"wb") as f:
dump(self.settings, f)
def loadSettings(self):
prefs = join(DATADIR, "preferences.pkl")
if exists(prefs):
with open(prefs, "rb") as f:
self.settings = load(f)
else:
self.defaultSettings()
def frameResizing(self, ix, pos):
self.main.timer.start(3000)
def frameResized(self):
self.settings["frame"] = self.main.saveState()
self.saveSettings()
def openGitLab(self):
# Launch web browser pointed towards this script's source code.
wbopen("https://gitlab.com/WolfgangAxel/namadino")
if __name__ == '__main__':
app = 0
if QApplication.instance():
app = QApplication.instance()
else:
app = QApplication(argv)
G = GUI()
exit(app.exec_())