mirror of
https://github.com/Escartem/AnimeWwise.git
synced 2026-06-04 23:40:25 +08:00
584 lines
19 KiB
Python
584 lines
19 KiB
Python
import os
|
|
import sys
|
|
import json
|
|
import math
|
|
import time
|
|
import extract
|
|
import platform
|
|
import urllib
|
|
import webbrowser
|
|
from PyQt5 import uic
|
|
from requests import get
|
|
from PyQt5.QtGui import QTextCursor
|
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QMetaType, Qt
|
|
from PyQt5.QtWidgets import QDesktopWidget, QDialog, QMessageBox, QMainWindow, QApplication, QFileDialog, QHeaderView, QAbstractItemView, QTreeWidgetItem, QAction, QActionGroup
|
|
|
|
QMetaType.type("QTextCursor")
|
|
|
|
class TextEditStream(QObject):
|
|
append_text = pyqtSignal(str)
|
|
|
|
def __init__(self, text_edit):
|
|
super().__init__()
|
|
self.text_edit = text_edit
|
|
self.append_text.connect(self._append_text)
|
|
|
|
def write(self, text):
|
|
self.append_text.emit(text)
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def _append_text(self, text):
|
|
self.text_edit.moveCursor(QTextCursor.End)
|
|
self.text_edit.insertPlainText(text)
|
|
self.text_edit.moveCursor(QTextCursor.End)
|
|
|
|
class BackgroundWorker(QObject):
|
|
finished = pyqtSignal(dict)
|
|
progress = pyqtSignal(list)
|
|
|
|
def __init__(self, action, extract, data):
|
|
super().__init__()
|
|
self.action = action
|
|
self.extract = extract
|
|
|
|
if action == "load":
|
|
self.base = data["base"]
|
|
self.input = data["input"]
|
|
self.map = data["map"]
|
|
self.diff = data["diff"]
|
|
if action == "extract":
|
|
self.input = data["input"]
|
|
self.files = data["files"]
|
|
self.format = data["format"]
|
|
self.output = data["output"]
|
|
|
|
def run(self):
|
|
if self.action == "load":
|
|
print("Loading files and mapping if necessary...")
|
|
fileStructure = self.extract.load_folder(self.map, self.input, self.diff, self.base, progress=self.progress.emit)
|
|
if fileStructure is None:
|
|
self.finished.emit({"action": "error", "content": {"msg": "Nothing found !", "state": 1}})
|
|
print("Nothing found !")
|
|
return
|
|
print("Building file structure...")
|
|
self.finished.emit({"action": "load", "content": fileStructure})
|
|
if self.action == "extract":
|
|
if len(self.files) == 0:
|
|
self.finished.emit({"action": "error", "content": {"msg": "Nothing selected !", "state": 2}})
|
|
return
|
|
print(f"Extracting {len(self.files)} files...")
|
|
self.extract.extract_files(self.input, self.files, self.output, self.format, progress=self.progress.emit)
|
|
self.finished.emit({"action": "extract"})
|
|
|
|
class UpdaterWorker(QObject):
|
|
finished = pyqtSignal(bool)
|
|
progress = pyqtSignal(list)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def run(self):
|
|
try:
|
|
# know current and latest maps
|
|
self.progress.emit([0, "Fetching index..."])
|
|
ver = lambda s: int(s.replace(".", ""))
|
|
|
|
index = open("version.json", "r")
|
|
currentMaps = json.loads(index.read())
|
|
index.close()
|
|
|
|
latestMaps = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/version.json")
|
|
|
|
if latestMaps.status_code == 200:
|
|
latestMaps = json.loads(latestMaps.text)
|
|
|
|
# do each game
|
|
n_games = len(latestMaps["maps"])
|
|
game_size = 95 // n_games
|
|
for i in range(n_games):
|
|
current = currentMaps["maps"][i]
|
|
latest = latestMaps["maps"][i]
|
|
name = f"maps/{latest['name']}"
|
|
|
|
if (ver(current["version"]) < ver(latest["version"])) or not os.path.isfile(name):
|
|
self.progress.emit([5 + game_size * i, f'Updating {latest["game"]} to {latest["version"]}'])
|
|
|
|
url = f"https://raw.githubusercontent.com/Escartem/AnimeWwise/master/{name}"
|
|
urllib.request.urlretrieve(url, "maps/temp.map")
|
|
|
|
if os.path.isfile(name):
|
|
os.remove(name)
|
|
os.rename("maps/temp.map", name)
|
|
|
|
# update index
|
|
currentMaps["maps"][i]["version"] = latest["version"]
|
|
|
|
# save new index
|
|
index_sum = sum([ver(e["version"]) for e in currentMaps["maps"]])
|
|
|
|
with open("version.json", "r+") as f:
|
|
data = json.loads(f.read())
|
|
data["mapsVersion"] = index_sum
|
|
data["maps"] = currentMaps["maps"]
|
|
f.seek(0)
|
|
f.write(json.dumps(data, indent=4))
|
|
f.truncate()
|
|
f.close()
|
|
|
|
# done
|
|
self.progress.emit([100, "Update finished ! The program will start shortly..."])
|
|
except Exception as e:
|
|
# failure :(
|
|
self.progress.emit([100, f"Update failed ! The program will start shortly... | {e}"])
|
|
|
|
time.sleep(3)
|
|
self.finished.emit(True)
|
|
|
|
|
|
class Updater(QDialog):
|
|
def __init__(self):
|
|
super(Updater, self).__init__()
|
|
uic.loadUi("updater.ui", self)
|
|
self.setWindowFlag(Qt.WindowCloseButtonHint, False)
|
|
self.center()
|
|
self.update()
|
|
|
|
def center(self):
|
|
qr = self.frameGeometry()
|
|
cp = QDesktopWidget().availableGeometry().center()
|
|
qr.moveCenter(cp)
|
|
self.move(qr.topLeft())
|
|
|
|
def update(self):
|
|
self.backgroundThread = QThread()
|
|
self.backgroundWorker = UpdaterWorker()
|
|
self.backgroundWorker.moveToThread(self.backgroundThread)
|
|
self.backgroundThread.started.connect(self.backgroundWorker.run)
|
|
self.backgroundWorker.finished.connect(self.updateFinished)
|
|
self.backgroundWorker.finished.connect(self.backgroundThread.quit)
|
|
self.backgroundWorker.finished.connect(self.backgroundWorker.deleteLater)
|
|
self.backgroundThread.finished.connect(self.backgroundThread.deleteLater)
|
|
|
|
self.backgroundWorker.progress.connect(self.updateProgress)
|
|
self.backgroundThread.start()
|
|
|
|
@pyqtSlot(bool)
|
|
def updateFinished(self):
|
|
self.close()
|
|
|
|
@pyqtSlot(list)
|
|
def updateProgress(self, data):
|
|
self.progressBar.setValue(data[0])
|
|
if len(data) == 2:
|
|
self.status.setText(data[1])
|
|
|
|
class AnimeWwise(QMainWindow):
|
|
def __init__(self):
|
|
super(AnimeWwise, self).__init__()
|
|
uic.loadUi("gui.ui", self)
|
|
self.versions = self.getJson("version")
|
|
self.version = self.versions["version"]
|
|
self.maps = self.versions["maps"]
|
|
self.setWindowTitle(f'AnimeWwise | v{".".join(list(str(self.version)))}')
|
|
self.folders = {
|
|
"input": "",
|
|
"output": "",
|
|
"diff": ""
|
|
}
|
|
self.format = "wav"
|
|
self.fileStructure = {"folders": {}, "files": []}
|
|
self.setupActions()
|
|
sys.stdout = TextEditStream(self.console)
|
|
self.extract = extract.WwiseExtract()
|
|
self.checkUpdates()
|
|
self.totalProgress.setMaximum(10000)
|
|
self.fileProgress.setMaximum(10000)
|
|
|
|
# utils
|
|
self.selectFolder = lambda: QFileDialog.getExistingDirectory(self, "Select Folder")
|
|
|
|
def checkUpdates(self):
|
|
print("Checking for updates...")
|
|
try:
|
|
currentVersion = self.versions
|
|
latestVersionReq = get("https://raw.githubusercontent.com/Escartem/AnimeWwise/master/version.json")
|
|
|
|
if latestVersionReq.status_code == 200:
|
|
latestVersion = json.loads(latestVersionReq.text)
|
|
|
|
if currentVersion["version"] < latestVersion["version"]:
|
|
print("Update found !")
|
|
QMessageBox.information(None, "Info", "Newer version of the program is availble, please update it.", QMessageBox.Ok)
|
|
elif currentVersion["mapsVersion"] < latestVersion["mapsVersion"]:
|
|
print("Update found !")
|
|
QMessageBox.information(None, "Info", "Newer version of the mappings are availble, the program will update them now.", QMessageBox.Ok)
|
|
self.updaterWindow = Updater()
|
|
self.updaterWindow.exec_()
|
|
self.maps = latestVersion["maps"]
|
|
else:
|
|
print("No updates")
|
|
except:
|
|
print("Failed to check updates :(")
|
|
|
|
def getJson(self, path):
|
|
with open(f"{path}.json", "r") as f:
|
|
data = json.loads(f.read())
|
|
f.close()
|
|
|
|
return data
|
|
|
|
def setFolder(self, elem=None, folder=None):
|
|
path = self.selectFolder()
|
|
self.folders[folder] = path
|
|
if elem:
|
|
elem.setText(path)
|
|
|
|
def setupActions(self):
|
|
self.changeAltInput.clicked.connect(lambda: self.setFolder(self.altInputPath, "diff"))
|
|
|
|
self.pckLoadTypeCombo.addItems(["Folder", "File"])
|
|
self.pckLoadTypeCombo.currentIndexChanged.connect(self.loadTypeChange)
|
|
self.loadType = "folder"
|
|
|
|
self.assetMap.addItems(["No map", *[f'{e["game"]} - v{e["version"]}' for e in self.maps]])
|
|
|
|
self.setExtractionState(False)
|
|
|
|
self.updateTreeWidget(self.fileStructure)
|
|
|
|
self.loadFilesButton.clicked.connect(lambda: self.loadFiles())
|
|
|
|
self.actionReset.triggered.connect(lambda: self.resetApp())
|
|
self.actionExit.triggered.connect(lambda: self.close())
|
|
|
|
self.actionExpand_all.triggered.connect(lambda: self.treeWidget.expandAll())
|
|
self.actionCollapse_all.triggered.connect(lambda: self.treeWidget.collapseAll())
|
|
|
|
self.actionExtract_Selected.triggered.connect(lambda: self.extractItems(False))
|
|
self.actionExtract_All.triggered.connect(lambda: self.extractItems(True))
|
|
|
|
self.actionReport_a_bug.triggered.connect(lambda: self.openLink(0))
|
|
self.actionSource_code.triggered.connect(lambda: self.openLink(1))
|
|
self.actionDiscord.triggered.connect(lambda: self.openLink(2))
|
|
|
|
self.searchAsset.textChanged.connect(lambda: self.filterAsset())
|
|
|
|
# output format
|
|
formats = ["wem (fastest)", "wav (fast)", "mp3 (slow)", "ogg (slow)"]
|
|
action_group = QActionGroup(self)
|
|
action_group.setExclusive(True)
|
|
|
|
for index, item_name in enumerate(formats):
|
|
action = QAction(item_name, self)
|
|
action.setCheckable(True)
|
|
if index == 1:
|
|
action.setChecked(True)
|
|
self.menuOutput_format.addAction(action)
|
|
action_group.addAction(action)
|
|
|
|
action_group.triggered.connect(self.updateFormat)
|
|
|
|
# utils
|
|
def loadTypeChange(self, event):
|
|
if event == 0:
|
|
self.pckSubFold.setEnabled(True)
|
|
self.loadType = "folder"
|
|
elif event == 1:
|
|
self.pckSubFold.setEnabled(False)
|
|
self.loadType = "file"
|
|
|
|
def updateFormat(self, event):
|
|
text = event.text()
|
|
self.format = text.split(" ")[0]
|
|
|
|
def openLink(self, id):
|
|
urls = [
|
|
"https://github.com/Escartem/AnimeWwise/issues/new",
|
|
"https://github.com/Escartem/AnimeWwise",
|
|
"https://discord.gg/fzRdtVh"
|
|
]
|
|
|
|
webbrowser.open(urls[id])
|
|
|
|
def setExtractionState(self, state):
|
|
self.actionExtract_Selected.setEnabled(state)
|
|
self.actionExtract_All.setEnabled(state)
|
|
self.actionExpand_all.setEnabled(state)
|
|
self.actionCollapse_all.setEnabled(state)
|
|
|
|
def displaySize(self, size):
|
|
if size < 1024:
|
|
return f"{size} b"
|
|
elif size > 1024 and size < 1048576:
|
|
return f"{size//1024} KiB"
|
|
elif size > 1048576 and size < 1073741824:
|
|
return f"{size//1048576} MiB"
|
|
elif size > 1073741824:
|
|
return f"{size//1073741824} GiB"
|
|
|
|
# workers
|
|
@pyqtSlot(list)
|
|
def progressBarSlot(self, progress):
|
|
progress_value = math.ceil(progress[1]*100)
|
|
if progress[0] == "total":
|
|
self.totalProgress.setValue(progress_value)
|
|
self.totalProgress.setFormat("%.02f %%" % (progress_value / 100))
|
|
elif progress[0] == "file":
|
|
self.fileProgress.setValue(progress_value)
|
|
self.fileProgress.setFormat("%.02f %%" % (progress_value / 100))
|
|
|
|
@pyqtSlot(dict)
|
|
def handleFinished(self, data):
|
|
if data["action"] == "load":
|
|
self.fileStructure = data["content"]
|
|
self.updateTreeWidget(self.fileStructure)
|
|
self.loadFilesButton.setEnabled(True)
|
|
self.setExtractionState(True)
|
|
self.tabs.setCurrentIndex(1)
|
|
print("Done !")
|
|
if data["action"] == "error":
|
|
QMessageBox.warning(None, "Warning", data["content"]["msg"], QMessageBox.Ok)
|
|
state = data["content"]["state"]
|
|
if state == 1:
|
|
self.loadFilesButton.setEnabled(True)
|
|
if state == 2:
|
|
self.setExtractionState(True)
|
|
if data["action"] == "extract":
|
|
self.setExtractionState(True)
|
|
print("Finished extracting everything !")
|
|
|
|
if platform.system() == "Windows":
|
|
os.startfile(self.folders["output"])
|
|
|
|
# page 1 - config
|
|
def loadFiles(self):
|
|
if self.loadType == "folder":
|
|
self.setFolder(folder="input")
|
|
files = []
|
|
if self.folders["input"]:
|
|
if self.pckSubFold.isChecked():
|
|
files = [os.path.join(root, f) for root, dirs, files_in_dir in os.walk(self.folders["input"]) for f in files_in_dir if f.endswith(".pck") or f.endswith(".chk")]
|
|
else:
|
|
files = [os.path.join(self.folders["input"], f) for f in os.listdir(self.folders["input"]) if f.endswith(".pck") or f.endswith(".chk")]
|
|
elif self.loadType == "file":
|
|
path = QFileDialog.getOpenFileName(self, "Select audio file", "", "PCK Files (*.pck);; CHK Files (*.chk)", options=QFileDialog.Options())
|
|
self.folders["input"] = os.path.dirname(path[0])
|
|
files = [path[0]]
|
|
|
|
if len(files) == 0 or files[0] == "":
|
|
QMessageBox.warning(None, "Warning", "Nothing to load !", QMessageBox.Ok)
|
|
return
|
|
|
|
self.currentInput = self.folders["input"]
|
|
if not self.folders["input"]:
|
|
self.currentInput = os.path.dirname(path[0])
|
|
|
|
_map = self.assetMap.currentIndex()
|
|
if _map != 0:
|
|
_map = self.maps[_map-1]["name"]
|
|
else:
|
|
_map = None
|
|
|
|
self.resetTreeWidget()
|
|
self.loadFilesButton.setEnabled(False)
|
|
|
|
# why is all this required for threading damnit
|
|
self.backgroundThread = QThread()
|
|
self.backgroundWorker = BackgroundWorker("load", self.extract, {"base": self.folders["input"], "input": files, "map": _map, "diff": self.folders["diff"]})
|
|
self.backgroundWorker.moveToThread(self.backgroundThread)
|
|
self.backgroundThread.started.connect(self.backgroundWorker.run)
|
|
self.backgroundWorker.finished.connect(self.handleFinished)
|
|
self.backgroundWorker.finished.connect(self.backgroundThread.quit)
|
|
self.backgroundWorker.finished.connect(self.backgroundWorker.deleteLater)
|
|
self.backgroundThread.finished.connect(self.backgroundThread.deleteLater)
|
|
|
|
self.backgroundWorker.progress.connect(self.progressBarSlot)
|
|
self.backgroundThread.start()
|
|
|
|
# page 2 - browsing
|
|
def filterAsset(self):
|
|
search = self.searchAsset.text()
|
|
if search == "":
|
|
self.updateTreeWidget(self.fileStructure)
|
|
return
|
|
result = self.searchFiles(self.fileStructure, search)
|
|
self.updateTreeWidget(result)
|
|
|
|
def searchFiles(self, data, substring, current_path="", flatten=False):
|
|
result = {"folders": {}, "files": []}
|
|
|
|
result["files"] = [file for file in data.get("files", []) if substring in file[0]]
|
|
|
|
for folder_name, folder_data in data.get("folders", {}).items():
|
|
subfolder_result = self.searchFiles(folder_data, substring)
|
|
if subfolder_result["files"] or subfolder_result["folders"]:
|
|
result["folders"][folder_name] = subfolder_result
|
|
|
|
if flatten:
|
|
while result["files"] == []:
|
|
if len(result["folders"]) == 0:
|
|
break
|
|
result = list(result["folders"].values())[0]
|
|
|
|
return result
|
|
|
|
def resetTreeWidget(self):
|
|
self.treeWidget.clear()
|
|
self.fileStructure = {"folders": {}, "files": []}
|
|
self.audioInfoLabel.setText("Click on an audio file to get more infos !")
|
|
self.setExtractionState(False)
|
|
|
|
def updateTreeWidget(self, structure):
|
|
self.treeWidget.clear()
|
|
self.treeWidget.setColumnCount(4)
|
|
self.treeWidget.setHeaderLabels(["Name", "Duration", "Compressed Size", "Source", "Offset"])
|
|
|
|
self.addItems(None, structure)
|
|
|
|
self.treeWidget.expandAll()
|
|
|
|
self.treeWidget.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
|
self.treeWidget.header().setSectionResizeMode(1, QHeaderView.Stretch)
|
|
self.treeWidget.header().setSectionResizeMode(2, QHeaderView.Stretch)
|
|
self.treeWidget.header().setSectionResizeMode(3, QHeaderView.Stretch)
|
|
|
|
self.treeWidget.setHeaderHidden(False)
|
|
|
|
self.treeWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
self.treeWidget.setDragDropMode(QAbstractItemView.NoDragDrop)
|
|
self.treeWidget.itemClicked.connect(self.updateAudioPreview)
|
|
|
|
def computeFolderSize(self, folder):
|
|
total_size = 0
|
|
|
|
for file in folder.get("files", []):
|
|
total_size += file[1]["size"]
|
|
|
|
for subfolder_name, subfolder in folder.get("folders", {}).items():
|
|
subfolder_size = self.computeFolderSize(subfolder)
|
|
total_size += subfolder_size
|
|
|
|
return total_size
|
|
|
|
def updateAudioPreview(self, item, column):
|
|
file_data = self.searchFiles(self.fileStructure, item.text(0), flatten=True)
|
|
|
|
if file_data == {"folders": {}, "files": []}:
|
|
self.audioInfoLabel.setText("Click on an audio file to get more infos !")
|
|
return
|
|
|
|
meta = file_data["files"][0][1]["metadata"]
|
|
|
|
# show meta
|
|
text = f'Infos for {item.text(0)} => Channels : {meta["channels"]} | Sample rate : {meta["sampleRate"]} Hz | Bitrate : {meta["avgBitrate"]} kbps | Codec : {meta["codecDisplay"]} | Layout type : {meta["layoutType"]}'
|
|
self.audioInfoLabel.setText(text)
|
|
|
|
def addItems(self, parent, element):
|
|
for folder_name in sorted(element.get("folders", {}).keys()):
|
|
folder_content = element["folders"][folder_name]
|
|
folder_item = QTreeWidgetItem([folder_name, "", self.displaySize(self.computeFolderSize(folder_content)), "", ""])
|
|
folder_item.setFlags(folder_item.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
|
|
folder_item.setCheckState(0, Qt.Unchecked)
|
|
if parent is None:
|
|
self.treeWidget.addTopLevelItem(folder_item)
|
|
else:
|
|
parent.addChild(folder_item)
|
|
self.addItems(folder_item, folder_content)
|
|
|
|
for file in sorted(element.get("files", []), key=lambda x: x[0]):
|
|
file_meta = file[1]
|
|
file_item = QTreeWidgetItem([file[0], f'{round(file_meta["metadata"]["duration"], 1)} seconds', self.displaySize(file_meta["size"]), file_meta["source"], str(hex(file_meta["offset"]))])
|
|
file_item.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
|
|
file_item.setCheckState(0, Qt.Unchecked)
|
|
if parent is None:
|
|
self.treeWidget.addTopLevelItem(file_item)
|
|
else:
|
|
parent.addChild(file_item)
|
|
|
|
# page 3 - extraction
|
|
def extractItems(self, _all):
|
|
self.setFolder(folder="output")
|
|
|
|
# meta
|
|
self.meta_index = {}
|
|
stack = [self.fileStructure]
|
|
|
|
while stack:
|
|
node = stack.pop()
|
|
for f in node["files"]:
|
|
self.meta_index[f[0]] = f[1]
|
|
stack.extend(node["folders"].values())
|
|
###
|
|
|
|
checked_items = []
|
|
|
|
def check_items(item, _all):
|
|
if item.checkState(0) == Qt.Checked or _all:
|
|
if item.text(1) != "":
|
|
checked_items.append(self.getFileMeta(item))
|
|
for i in range(item.childCount()):
|
|
check_items(item.child(i), _all)
|
|
|
|
for i in range(self.treeWidget.topLevelItemCount()):
|
|
check_items(self.treeWidget.topLevelItem(i), _all)
|
|
|
|
self.setExtractionState(False)
|
|
|
|
# yet another block of threading bs
|
|
self.backgroundThread = QThread()
|
|
self.backgroundWorker = BackgroundWorker("extract", self.extract, {"input": self.currentInput, "files": checked_items, "format": self.format, "output": self.folders["output"]})
|
|
self.backgroundWorker.moveToThread(self.backgroundThread)
|
|
self.backgroundThread.started.connect(self.backgroundWorker.run)
|
|
self.backgroundWorker.finished.connect(self.handleFinished)
|
|
self.backgroundWorker.finished.connect(self.backgroundThread.quit)
|
|
self.backgroundWorker.finished.connect(self.backgroundWorker.deleteLater)
|
|
self.backgroundThread.finished.connect(self.backgroundThread.deleteLater)
|
|
|
|
self.backgroundWorker.progress.connect(self.progressBarSlot)
|
|
self.backgroundThread.start()
|
|
|
|
def getFileMeta(self, item):
|
|
path = []
|
|
current_item = item
|
|
|
|
while current_item is not None:
|
|
path.insert(0, current_item.text(0))
|
|
current_item = current_item.parent()
|
|
|
|
meta = self.meta_index.get(item.text(0))
|
|
|
|
return {
|
|
"name": item.text(0),
|
|
"path": path[:-1],
|
|
"source": meta["source"],
|
|
"offset": meta["offset"],
|
|
"size": meta["size"],
|
|
"original_name": meta["original_name"]
|
|
}
|
|
|
|
# misc
|
|
def resetApp(self):
|
|
self.resetTreeWidget()
|
|
self.extract.reset()
|
|
self.currentInput = None
|
|
self.setExtractionState(False)
|
|
self.tabs.setCurrentIndex(0)
|
|
self.totalProgress.setValue(0)
|
|
self.fileProgress.setValue(0)
|
|
print("Reset !")
|
|
|
|
def _appendText(self, text):
|
|
cursor = self.console.textCursor()
|
|
cursor.movePosition(cursor.End)
|
|
cursor.insertText(text)
|
|
self.console.setTextCursor(cursor)
|
|
self.console.ensureCursorVisible()
|
|
|
|
if __name__ == "__main__":
|
|
app = QApplication(sys.argv)
|
|
window = AnimeWwise()
|
|
window.show()
|
|
sys.exit(app.exec_())
|