Module pluginhost
[hide private]
[frames] | no frames]

Source Code for Module pluginhost

  1  import sys 
  2  import os 
  3  import glob 
  4   
  5  import ts3lib 
  6  from _plugincmd import _PluginCommandHandler 
  7  import ts3defines 
  8  import pytson 
  9  import ts3client 
 10   
 11  import importlib 
 12  import traceback 
 13  import json 
 14   
 15  from configparser import ConfigParser 
 16  from pytsonui.console import PythonConsole 
 17  from pytsonui.config import ConfigurationDialog 
 18  from PythonQt.QtGui import QFont, QColor, QMessageBox, QTextBrowser 
 19  from PythonQt.QtCore import Qt, QUrl, QTimer, QTranslator, QCoreApplication 
 20  from PythonQt.QtNetwork import (QNetworkAccessManager, QNetworkRequest, 
 21                                  QNetworkReply) 
 22   
 23  from weakref import WeakValueDictionary 
 24   
 25   
 26  REL_URL = QUrl("https://api.github.com/repos/pathmann/pyTSon/releases/latest") 
27 28 29 -def logprint(msg, loglevel, channel):
30 err = ts3lib.logMessage(msg, loglevel, channel, 0) 31 if err != ts3defines.ERROR_ok: 32 print(msg)
33
34 35 -class PluginHost(pytson.Translatable):
36 defaultConfig = [("general", [("differentApi", "False"), 37 ("uninstallQuestion", "True"), 38 ("loadAllMenus", "True"), 39 ("language", "inherited"), 40 ("verbose", "False")]), 41 ("plugins", []), 42 ("console", [("backgroundColor", "#000000"), 43 ("textColor", "#FFFFFF"), 44 ("fontFamily", "Monospace"), 45 ("fontSize", "12"), ("tabcomplete", "True"), 46 ("spaces", "True"), ("tabwidth", "2"), 47 ("width", "800"), ("height", "600"), 48 ("startup", ""), 49 ("silentStartup", "False")])] 50 51 @classmethod
52 - def setupConfig(cls):
53 for (section, options) in cls.defaultConfig: 54 if not cls.cfg.has_section(section): 55 cls.cfg.add_section(section) 56 57 for (o, v) in options: 58 if not cls.cfg.has_option(section, o): 59 cls.cfg.set(section, o, v)
60 61 @classmethod
62 - def verboseLog(cls, text, channel):
63 if cls.cfg.getboolean("general", "verbose"): 64 logprint(text, ts3defines.LogLevel.LogLevel_INFO, channel)
65 66 @classmethod
67 - def init(cls):
68 pytson._setup() 69 70 cls.shell = None 71 cls.confdlg = None 72 73 cls.proxies = WeakValueDictionary() 74 75 cls.nwm = None 76 77 cls.modules = {} 78 79 cls.menus = {} 80 cls.hotkeys = {} 81 82 cls.translator = None 83 84 cls.cfg = ConfigParser() 85 cls.cfg.read(pytson.getConfigPath("pyTSon.conf")) 86 87 cls.setupConfig() 88 cls.setupTranslator() 89 90 cls.verboseLog(cls._tr("Starting up"), "pyTSon.PluginHost.init") 91 92 cls.registerCallbackProxy(_PluginCommandHandler) 93 94 cls.reload() 95 cls.start() 96 97 cls.verboseLog(cls._tr("Init success"), "pyTSon.PluginHost.init")
98 99 @classmethod
100 - def setupTranslator(cls):
101 lang = cls.cfg.get("general", "language") 102 if lang == "inherited": 103 ccfg = ts3client.Config() 104 q = ccfg.query("SELECT * FROM application WHERE key='Language'") 105 106 if q.next(): 107 lang = q.value("value") 108 else: 109 lang = "en_US" 110 logprint(cls._tr("Error querying language from client config"), 111 ts3defines.LogLevel.LogLevel_ERROR, 112 "pyTSon.PluginHost.setupTranslator") 113 del ccfg 114 115 if cls.translator: 116 if not QCoreApplication.removeTranslator(cls.translator): 117 logprint(cls._tr("Error removing translator"), 118 ts3defines.LogLevel.LogLevel_ERROR, 119 "pyTSon.PluginHost.setupTranslator") 120 cls.translator = None 121 122 p = pytson.getPluginPath("ressources", "i18n", "pyTSon-%s.qm" % lang) 123 124 if os.path.isfile(p): 125 cls.verboseLog(cls._tr("Using translator from {file}"). 126 format(file=p), "pyTSon.PluginHost.setupTranslator") 127 128 cls.translator = QTranslator() 129 if not cls.translator.load(p): 130 logprint("Error loading translator from %s" % p, 131 ts3defines.LogLevel.LogLevel_ERROR, 132 "pyTSon.PluginHost.setupTranslator") 133 cls.translator = None 134 return 135 136 if not QCoreApplication.installTranslator(cls.translator): 137 logprint("Error installing translator from %s" % p, 138 ts3defines.LogLevel.LogLevel_ERROR, 139 "pyTSon.PluginHost.setupTranslator") 140 cls.translator = None
141 142 @classmethod
143 - def startPlugin(cls, key):
144 cls.verboseLog(cls._tr("Starting plugin {name}").format(name=key), 145 "pyTSon.PluginHost.startPlugin") 146 try: 147 cls.active[key] = cls.plugins[key]() 148 cls.cfg.set("plugins", key, "True") 149 except: 150 logprint(cls._tr("Error starting python plugin {name}: {trace}"). 151 format(name=key, trace=traceback.format_exc()), 152 ts3defines.LogLevel.LogLevel_ERROR, 153 "pyTSon.PluginHost.start")
154 155 @classmethod
156 - def start(cls):
157 # start plugin if config says so, or if new plugin and 158 # requestAutoload is True 159 for key in cls.plugins: 160 load = False 161 if not cls.cfg.has_option("plugins", key): 162 if cls.plugins[key].requestAutoload: 163 load = True 164 elif cls.cfg.getboolean("plugins", key, fallback=False): 165 load = True 166 167 if load: 168 if (cls.plugins[key].apiVersion != 169 pytson.getCurrentApiVersion()): 170 if not cls.cfg.getboolean("general", "differentApi", 171 fallback=False): 172 continue 173 174 cls.startPlugin(key) 175 176 # restore reloaded menus 177 for globid, (p, locid) in cls.menus.items(): 178 if p in cls.active: 179 cls.menus[globid] = (cls.active[p], locid) 180 181 # restore reloaded hotkeys 182 for keyword, (p, lockey) in cls.hotkeys.items(): 183 if p in cls.active: 184 cls.hotkeys[keyword] = (cls.active[p], lockey)
185 186 @classmethod
187 - def shutdown(cls):
188 cls.verboseLog(cls._tr("Shutting down"), "pyTSon.PluginHost.shutdown") 189 190 if cls.shell: 191 cls.shell.delete() 192 cls.shell = None 193 if cls.confdlg: 194 cls.confdlg.delete() 195 cls.confdlg = None 196 197 if cls.nwm: 198 cls.nwm.delete() 199 cls.nwm = None 200 201 # store config 202 with open(pytson.getConfigPath("pyTSon.conf"), "w") as f: 203 cls.cfg.write(f) 204 205 # stop all plugins 206 for key, p in cls.active.items(): 207 try: 208 p.stop() 209 except: 210 print(cls._tr("Error stopping python plugin {name}: {trace}"). 211 format(name=key, trace=traceback.format_exc())) 212 213 cls.active = {} 214 215 # save local menu ids 216 for globid, (p, locid) in cls.menus.items(): 217 # previously reloaded? 218 if not type(p) is str: 219 cls.menus[globid] = (p.name, locid) 220 221 # save local hotkeys 222 for keyword, (p, lockey) in cls.hotkeys.items(): 223 if not type(p) is str: 224 cls.hotkeys[keyword] = (p.name, lockey) 225 226 if cls.translator: 227 if not QCoreApplication.removeTranslator(cls.translator): 228 logprint(cls._tr("Error removing translator"), 229 ts3defines.LogLevel.LogLevel_ERROR, 230 "pyTSon.PluginHost.shutdown")
231 232 @classmethod
233 - def activate(cls, pname):
234 cls.verboseLog(cls._tr("Activating plugin {name}").format(name=pname), 235 "pyTSon.PluginHost.activate") 236 if pname in cls.plugins: 237 try: 238 cls.active[pname] = cls.plugins[pname]() 239 cls.cfg.set("plugins", pname, "True") 240 241 for globid, (p, locid) in cls.menus.items(): 242 if type(p) is str and p == pname: 243 cls.menus[globid] = (cls.active[p], locid) 244 ts3lib.setPluginMenuEnabled(globid, True) 245 if hasattr(cls.active[pname], "menuCreated"): 246 cls.active[pname].menuCreated() 247 248 for keyword, (p, lockey) in cls.hotkeys.items(): 249 if type(p) is str and p == pname: 250 cls.hotkeys[keyword] = (cls.active[p], lockey) 251 252 return True 253 except: 254 logprint(cls._tr("Error starting python plugin {name}: " 255 "{trace}").format(name=pname, 256 trace=traceback.format_exc()), 257 ts3defines.LogLevel.LogLevel_ERROR, 258 "pyTSon.PluginHost.activate") 259 260 return False
261 262 @classmethod
263 - def deactivate(cls, pname):
264 cls.verboseLog(cls._tr("Deactivating plugin {name}"). 265 format(name=pname), "pyTSon.PluginHost.deactivate") 266 if pname in cls.active: 267 try: 268 # remove hotkeys 269 for key in cls.hotkeys: 270 if type(cls.hotkeys[key][0]) is not str: 271 if cls.hotkeys[key][0].name == pname: 272 cls.hotkeys[key] = (pname, cls.hotkeys[key][1]) 273 274 # remove menuItems 275 for key in cls.menus: 276 if type(cls.menus[key][0]) is not str: 277 if cls.menus[key][0].name == pname: 278 cls.menus[key] = (pname, cls.menus[key][1]) 279 ts3lib.setPluginMenuEnabled(key, False) 280 281 cls.active[pname].stop() 282 del cls.active[pname] 283 cls.cfg.set("plugins", pname, "False") 284 except: 285 logprint(cls._tr("Error stopping python plugin {name}: " 286 "{trace}").format(name=pname, 287 trace=traceback. 288 format_exc()), 289 ts3defines.LogLevel.LogLevel_ERROR, 290 "pyTSon.PluginHost.deactivate")
291 292 @classmethod
293 - def reload(cls):
294 cls.verboseLog(cls._tr("Reloading plugins"), 295 "pyTSon.PluginHost.reload") 296 # stop all running modules 297 for key, p in cls.active.items(): 298 try: 299 p.stop() 300 except: 301 logprint(cls._tr("Error stopping python plugin {name}: " 302 "{trace}").format(name=key, 303 trace=traceback. 304 format_exc()), 305 ts3defines.LogLevel.LogLevel_ERROR, 306 "pyTSon.PluginHost.reload") 307 308 cls.active = {} 309 cls.plugins = {} 310 311 # import all modules 312 spath = pytson.getPluginPath("scripts") 313 for d in glob.glob(os.path.join(spath, "*/")): 314 if not os.path.isdir(d): 315 continue 316 317 base = os.path.relpath(d, spath) 318 try: 319 if base in cls.modules: 320 cls.modules[base] = importlib.reload(cls.modules[base]) 321 else: 322 cls.modules[base] = importlib.__import__(base) 323 except: 324 logprint(cls._tr( 325 "Error loading python plugin from {path}: {trace}"). 326 format(path=d, trace=traceback.format_exc()), 327 ts3defines.LogLevel.LogLevel_ERROR, 328 "pyTSon.PluginHost.reload") 329 330 # save local menu ids 331 for globid, (p, locid) in cls.menus.items(): 332 # previously reloaded? 333 if not type(p) is str: 334 cls.menus[globid] = (p.name, locid) 335 336 # save local hotkeys 337 for keyword, (p, lockey) in cls.hotkeys.items(): 338 if not type(p) is str: 339 cls.hotkeys[keyword] = (p.name, lockey)
340 341 @classmethod
342 - def showScriptingConsole(cls):
343 if not cls.shell: 344 cls.verboseLog(cls._tr("Creating scripting console"), 345 "pyTSon.PluginHost.showScriptingConsole") 346 tabcomp = cls.cfg.getboolean("console", "tabcomplete") 347 spaces = cls.cfg.getboolean("console", "spaces") 348 tabwidth = cls.cfg.getint("console", "tabwidth") 349 font = QFont(cls.cfg.get("console", "fontFamily"), 350 cls.cfg.getint("console", "fontSize")) 351 bgcolor = QColor(cls.cfg.get("console", "backgroundColor")) 352 txtcolor = QColor(cls.cfg.get("console", "textColor")) 353 w = cls.cfg.getint("console", "width") 354 h = cls.cfg.getint("console", "height") 355 startup = cls.cfg.get("console", "startup") 356 silent = cls.cfg.getboolean("console", "silentStartup") 357 cls.shell = PythonConsole(tabcomp, spaces, tabwidth, font, bgcolor, 358 txtcolor, w, h, startup, silent) 359 cls.shell.connect("destroyed()", cls.scriptingConsoleDestroyed) 360 cls.shell.show()
361 362 @classmethod
364 cls.cfg.set("console", "width", str(cls.shell.width)) 365 cls.cfg.set("console", "height", str(cls.shell.height))
366 367 @classmethod
368 - def configure(cls, mainwindow=None):
369 if not cls.confdlg: 370 cls.verboseLog(cls._tr("Creating config dialog"), 371 "pyTSon.PluginHost.configure") 372 cls.confdlg = ConfigurationDialog(cls.cfg, cls, mainwindow) 373 374 cls.confdlg.show() 375 cls.confdlg.raise_() 376 cls.confdlg.activateWindow()
377 378 @classmethod
379 - def callMethod(cls, name, *args):
380 meth = getattr(PluginHost, name, None) 381 if meth: 382 return meth(*args) 383 384 return cls.invokePlugins(name, *args)
385 386 @classmethod
387 - def invokePlugins(cls, name, *args):
388 ret = [] 389 for key, p in cls.active.items(): 390 meth = getattr(p, name, None) 391 392 if meth: 393 try: 394 ret.append(meth(*args)) 395 except: 396 msg = cls._tr("Error calling method {methname} of plugin " 397 "{name}: {trace}").format( 398 methname=name, name=key, 399 trace=traceback.format_exc()) 400 print(msg) 401 if name != "onUserLoggingMessageEvent": 402 cls.verboseLog(msg, "pyTSon.PluginHost.invokePlugins") 403 404 # call callback proxies; they can't affect the return value 405 zombies = [] 406 for p in cls.proxies.values(): 407 if not p: 408 zombies.append(id(p)) 409 else: 410 meth = getattr(p, name, None) 411 412 if meth: 413 try: 414 ret.append(meth(*args)) 415 except: 416 msg = cls._tr("Error calling method {methname} in " 417 "callbackproxy: {trace}").format( 418 methname=name, 419 trace=traceback.format_exc()) 420 print(msg) 421 cls.verboseLog(msg, "pyTSon.PluginHost.invokePlugins") 422 423 for z in zombies: 424 del cls.proxies[z] 425 426 for r in ret: 427 if r: 428 return True 429 430 return False
431 432 @classmethod
433 - def registerCallbackProxy(cls, obj):
434 cls.verboseLog(cls._tr("Callbackproxy {name} registered"). 435 format(name=obj.__class__.__name__), 436 "pyTSon.PluginHost.registerCallbackProxy") 437 if not id(obj) in cls.proxies: 438 cls.proxies[id(obj)] = obj
439 440 @classmethod
441 - def unregisterCallbackProxy(cls, obj):
442 cls.verboseLog(cls._tr("Callbackproxy {name} unregistered"). 443 format(name=obj.__class__.__name__), 444 "pyTSon.PluginHost.unregisterCallbackProxy") 445 if obj in cls.proxies: 446 cls.proxies.remove(id(obj))
447 448 @classmethod
449 - def processCommand(cls, schid, command):
450 cls.verboseLog(cls._tr("Processing command {cmd}").format(cmd=command), 451 "pyTSon.PluginHost.processCommand") 452 tokens = command.split(' ') 453 454 if len(tokens) == 0 or tokens[0] == "": 455 return False 456 457 for key, p in cls.active.items(): 458 if p.commandKeyword == tokens[0]: 459 try: 460 return p.processCommand(schid, " ".join(tokens[1:])) 461 except: 462 logprint(cls._tr("Error calling processCommand of python " 463 "plugin {name}: {trace}").format( 464 name=p.name, 465 trace=traceback.format_exc()), 466 ts3defines.LogLevel.LogLevel_ERROR, 467 "pyTSon.PluginHost.processCommand") 468 469 return False
470 471 @classmethod
472 - def infoData(cls, schid, aid, atype):
473 ret = [] 474 for key, p in cls.active.items(): 475 if p.infoTitle is not None and hasattr(p, "infoData"): 476 try: 477 data = p.infoData(schid, aid, atype) 478 if data is not None: 479 if p.infoTitle != "": 480 ret.append(p.infoTitle) 481 ret += data 482 except: 483 logprint(cls._tr("Error calling infoData of python plugin " 484 "{name}: {trace}").format( 485 name=key, trace=traceback.format_exc()), 486 ts3defines.LogLevel.LogLevel_ERROR, 487 "pyTSon.PluginHost.infoData") 488 489 return ret
490 491 @classmethod
492 - def onPluginCommandEvent(cls, schid, pluginName, pluginCommand):
493 # pluginName is always 'pyTSon', so we ignore it completely 494 fire, sender, cmds = _PluginCommandHandler.handlePluginCommand(schid, 495 pluginCommand) 496 497 if fire: 498 for cmd in cmds: 499 cls.invokePlugins("onPluginCommandEvent", schid, sender, cmd)
500 501 @classmethod
502 - def parseUpdateReply(cls, repstr):
503 def platform_str(): 504 try: 505 import platform 506 except: 507 raise Exception("Error importing platform module") 508 509 if sys.platform == "linux": 510 if platform.architecture()[0] == "64bit": 511 return "linux_amd64" 512 else: 513 return "linux_x86" 514 elif sys.platform == "win32": 515 return "win%s" % platform.architecture()[0][:2] 516 else: 517 return "mac"
518 519 try: 520 obj = json.loads(repstr) 521 522 if obj["tag_name"] == "v%s" % pytson.getVersion(): 523 QMessageBox.information(None, cls._tr("pyTSon Update Check"), 524 cls._tr("You are running the latest " 525 "pyTSon release")) 526 else: 527 for a in obj["assets"]: 528 if a["name"] == "pyTSon_%s.ts3_plugin" % platform_str(): 529 msg = cls._tr("There is an update of pyTSon for your " 530 "platform. Get it from <a href='{url}'>" 531 "here</a>").format(url=obj["html_url"]) 532 QMessageBox.information(None, 533 cls._tr("pyTSon Update Check"), 534 msg) 535 return 536 537 QMessageBox.information(None, cls._tr("pyTSon Update Check"), 538 cls._tr("You are running the latest " 539 "pyTSon release (at least for " 540 "your platform)")) 541 except: 542 logprint(cls._tr("Error parsing reply from update check: {trace}"). 543 format(trace=traceback.format_exc()), 544 ts3defines.LogLevel.LogLevel_ERROR, 545 "pyTSon.PluginHost.parseUpdateReply")
546 547 @classmethod
548 - def updateCheckFinished(cls, reply):
549 if reply.error() == QNetworkReply.NoError: 550 cls.parseUpdateReply(reply.readAll().data().decode('utf-8')) 551 else: 552 logprint(cls._tr("Error checking for update: {errcode}").format( 553 errcode=reply.error()), 554 ts3defines.LogLevel.LogLevel_ERROR, 555 "pyTSon.PluginHost.updateCheckFinished") 556 557 reply.deleteLater() 558 cls.nwm.delete() 559 cls.nwm = None
560 561 @classmethod
562 - def updateCheck(cls):
563 if cls.nwm: 564 # there is a pending updatecheck 565 return 566 567 cls.nwm = QNetworkAccessManager() 568 cls.nwm.connect("finished(QNetworkReply*)", cls.updateCheckFinished) 569 cls.nwm.get(QNetworkRequest(REL_URL))
570 571 @classmethod
572 - def initMenus(cls):
573 cls.verboseLog(cls._tr("Initing menus"), "pyTSon.PluginHost.initMenus") 574 cls.menus = {} 575 ret = [(ts3defines.PluginMenuType.PLUGIN_MENU_TYPE_GLOBAL, 0, 576 cls._tr("Console"), os.path.join("ressources", "octicons", 577 "terminal.svg.png")), 578 (ts3defines.PluginMenuType.PLUGIN_MENU_TYPE_GLOBAL, 1, 579 cls._tr("Settings"), os.path.join("ressources", "octicons", 580 "settings.svg.png")), 581 (ts3defines.PluginMenuType.PLUGIN_MENU_TYPE_GLOBAL, 2, 582 cls._tr("Check for update"), os.path.join( 583 "ressources", "octicons", "cloud-download.svg.png")), 584 (ts3defines.PluginMenuType.PLUGIN_MENU_TYPE_GLOBAL, 3, 585 cls._tr("Changelog"), os.path.join("ressources", "octicons", 586 "book.svg.png"))] 587 nextid = len(ret) 588 589 loadall = cls.cfg.getboolean("general", "loadAllMenus") 590 menustates = [] 591 592 for key, p in sorted(cls.plugins.items()): 593 for (atype, locid, text, icon) in p.menuItems: 594 if p.name in cls.active: 595 cls.menus[nextid] = (cls.active[p.name], locid) 596 ret.append((atype, nextid, text, icon)) 597 menustates.append((nextid, True)) 598 elif loadall: 599 cls.menus[nextid] = (p.name, locid) 600 ret.append((atype, nextid, text, icon)) 601 # we have to remember the id, to disable it afterwards 602 menustates.append((nextid, False)) 603 604 nextid += 1 605 606 def deactivateMenus(): 607 for key, val in menustates: 608 ts3lib.setPluginMenuEnabled(key, val) 609 610 for key, p in cls.active.items(): 611 if hasattr(p, "menuCreated"): 612 p.menuCreated()
613 614 QTimer.singleShot(1000, deactivateMenus) 615 616 return ret 617 618 @classmethod
619 - def globalMenuID(cls, plugin, localid):
620 for key, (p, locid) in cls.menus.items(): 621 if p == plugin and locid == localid: 622 return key 623 624 return None
625 626 @classmethod
627 - def initHotkeys(cls):
628 cls.verboseLog(cls._tr("Initing hotkeys"), 629 "pyTSon.PluginHost.initHotkeys") 630 nextkey = 2 631 cls.hotkeys = {} 632 ret = [("0", cls._tr("Show the python scripting console")), 633 ("1", cls._tr("Show the pyTSon settings dialog"))] 634 635 for key, p in cls.active.items(): 636 for (lockey, description) in p.hotkeys: 637 ret.append((str(nextkey), description)) 638 cls.hotkeys[str(nextkey)] = (p, lockey) 639 nextkey += 1 640 641 return ret
642 643 @classmethod
644 - def onMenuItemEvent(cls, schid, atype, menuItemID, selectedItemID):
645 if menuItemID == 0: 646 cls.showScriptingConsole() 647 return 648 elif menuItemID == 1: 649 cls.configure() 650 return 651 elif menuItemID == 2: 652 cls.updateCheck() 653 return 654 elif menuItemID == 3: 655 cls.showChangelog() 656 return 657 658 if menuItemID in cls.menus: 659 (plugin, locid) = cls.menus[menuItemID] 660 if type(plugin) is not str: 661 # if plugin was reloaded, but menuItem does not exist anymore 662 try: 663 plugin.onMenuItemEvent(schid, atype, locid, selectedItemID) 664 except: 665 logprint(cls._tr("Error calling onMenuItemEvent of python " 666 "plugin {name}: {trace}").format( 667 name=plugin.name, 668 trace=traceback.format_exc()), 669 ts3defines.LogLevel.LogLevel_ERROR, 670 "pyTSon.PluginHost.onMenuItemEvent")
671 672 @classmethod
673 - def globalHotkeyKeyword(cls, plugin, localkeyword):
674 for key, (p, lockey) in cls.hotkeys.items(): 675 if p == plugin and lockey == localkeyword: 676 return key 677 678 return None
679 680 @classmethod
681 - def onHotkeyEvent(cls, keyword):
682 if keyword == "0": 683 cls.showScriptingConsole() 684 return 685 elif keyword == "1": 686 cls.configure() 687 return 688 689 if keyword in cls.hotkeys: 690 (plugin, lockey) = cls.hotkeys[keyword] 691 if type(plugin) is not str: 692 try: 693 plugin.onHotkeyEvent(lockey) 694 except: 695 logprint(cls._tr("Error calling onHotkeyEvent of python " 696 "plugin {name}: {trace}").format(name=plugin.name, 697 trace=traceback.format_exc()), 698 ts3defines.LogLevel.LogLevel_ERROR, 699 "pyTSon.PluginHost.onHotkeyEvent")
700 701 @classmethod
702 - def showChangelog(cls):
703 fname = pytson.getPluginPath("Changelog.html") 704 if not os.path.isfile(fname): 705 QMessageBox.critical(None, cls._tr("Error"), cls._tr("Can't find " 706 "Changelog")) 707 return 708 709 with open(fname, "r") as f: 710 # store it just to keep it in scope 711 cls.viewer = viewer = QTextBrowser() 712 viewer.setAttribute(Qt.WA_DeleteOnClose) 713 viewer.openExternalLinks = True 714 viewer.setHtml(f.read()) 715 716 viewer.show()
717