NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

bugReporting.py (53876B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\services\bugReporting.py
      2 import carbonversion
      3 import json
      4 import os
      5 import platform
      6 import subprocess
      7 import utillib
      8 import yaml
      9 import zipfile
     10 import time
     11 from carbon.common.script.sys import basesession
     12 from carbon.common.script.sys.service import Service
     13 from carbon.common.script.sys.serviceConst import ROLE_GML
     14 from carbon.common.script.util import timerstuff
     15 from carbon.common.script.util.format import FmtDate
     16 from carbonui.control.combo import Combo
     17 from carbonui.control.singlelineedits.singleLineEditText import SingleLineEditText
     18 from carbonui.primitives.containerAutoSize import ContainerAutoSize
     19 from carbonui.primitives.frame import Frame
     20 from carbonui.primitives.layoutGrid import LayoutGrid
     21 from carbonui.util.various_unsorted import GetWindowAbove
     22 from eve.common.script.sys import eveCfg
     23 from eveexceptions import UserError
     24 from globalConfig.getFunctions import GetBugReportServer
     25 import blue
     26 import carbonversion
     27 import uthread
     28 import log
     29 import trinity
     30 import gatekeeper
     31 import webbrowser
     32 from localization import GetByLabel, GetByMessageID
     33 from eveexceptions.exceptionEater import ExceptionEater
     34 from carbon.common.lib.serverInfo import GetServerInfo
     35 from omega.client.clone_grade_svc import get_clone_grade_svc
     36 from player_messaging.client.ui_message import prompt_player_fsd_dialog
     37 from shipfitting.exportFittingUtil import GetFittingEFTString
     38 from carbonui import uiconst
     39 from carbonui.uicore import uicore
     40 from carbonui.primitives.container import Container
     41 from carbonui.primitives.fill import Fill
     42 from carbonui.primitives.sprite import Sprite
     43 from carbonui.primitives.desktop import UIRoot
     44 from carbonui.button.group import ButtonGroup
     45 from carbonui.control.button import Button
     46 from eve.client.script.ui.control.eveEditPlainText import EditPlainText
     47 from eve.client.script.ui.control.eveLabel import EveLabelMedium, EveLabelSmall, Label
     48 from eve.client.script.ui.control.eveLoadingWheel import LoadingWheel
     49 from carbonui.control.window import Window
     50 from eve.client.script.ui.control.infoIcon import MoreInfoIcon
     51 from eve.common.lib import appConst as const
     52 from bugreporter import BugReporter, SendAttachmentError
     53 from eveprefs import prefs, boot
     54 LEFT_ARROW = 'res:/UI/Texture/BugReport_Arrow_Left.png'
     55 RIGHT_ARROW = 'res:/UI/Texture/BugReport_Arrow_Right.png'
     56 COLOR_GREEN = (0.0,
     57  1.0,
     58  0.0,
     59  1.0)
     60 COLOR_RED = (1.0,
     61  0.0,
     62  0.0,
     63  1.0)
     64 COLOR_BLUE = (0.0,
     65  0.0,
     66  1.0,
     67  1.0)
     68 ARROW_HEIGHT = 28
     69 BASEFONTSIZE = 18
     70 NUMFONTSIZE = 26
     71 BUGREPORT_SERVER = 'https://ebr-service.ccpeveweb.com/v1'
     72 BUG_REPORTS_URL = 'https://community.eveonline.com/support/bug-reports/my-bug-reports/'
     73 BUG_GUIDE_URL = 'https://community.eveonline.com/support/test-servers/bug-reporting/'
     74 SCREENSHOT_NAME = 'igbr_%s.jpg'
     75 NUM_SCREENSHOTS = 3
     76 SAVE_NAME = 'igbr_save_%s.yaml'
     77 
     78 def HIWORD(f):
     79     return (f & 4294901760L) >> 16
     80 
     81 
     82 def LOWORD(f):
     83     return f & 65535
     84 
     85 
     86 def HIPART(f):
     87     return (f & 18446744069414584320L) >> 32
     88 
     89 
     90 def LOPART(f):
     91     return f & 4294967295L
     92 
     93 
     94 def DecodeDriverVersion(version):
     95     try:
     96         highpart = HIPART(version)
     97         lowpart = LOPART(version)
     98         product = HIWORD(highpart)
     99         version = LOWORD(highpart)
    100         subVersion = HIWORD(lowpart)
    101         build = LOWORD(lowpart)
    102         return '%s.%s.%s.%s' % (product,
    103          version,
    104          subVersion,
    105          build)
    106     except:
    107         return ''
    108 
    109 
    110 def AbsPath(path):
    111     return blue.paths.ResolvePathForWriting(path)
    112 
    113 
    114 class BugReportingService(Service):
    115     __guid__ = 'svc.bugReporting'
    116     __startupdependencies__ = ['machoNet']
    117 
    118     def __init__(self):
    119         super(BugReportingService, self).__init__()
    120         self.dxDiagFileName = None
    121         self.categories = None
    122         self.startStages = None
    123         self.fixInStages = None
    124         self.assignees = None
    125         self.priorities = None
    126         self.severities = None
    127         self.serverUrl = ''
    128         self.windowsUserName = ''
    129         self.charName = ''
    130         self.userName = ''
    131         self.bugReportServer = None
    132         self.screenshots = []
    133         self.wnd = None
    134         self.categories = None
    135 
    136     def StartCreateBugReport(self):
    137         self.OpenWindow()
    138 
    139     def OpenWindow(self):
    140         self.Init()
    141         self.wnd = BugReportingWindow.Open()
    142 
    143     def Init(self):
    144         self.screenshots = []
    145         self.bugReportServer = GetBugReportServer(self.machoNet, BUGREPORT_SERVER)
    146         self.bugReporter = BugReporter(self.bugReportServer, os.path.join(blue.paths.ResolvePath('cache:'), 'IGBR'))
    147         self.LogNotice('Opening bug reporting', self.userName, self.charName)
    148         if session.charid:
    149             self.charName = cfg.eveowners.Get(session.charid).name
    150         self.userName = settings.public.ui.Get('username', '')
    151         try:
    152             self.windowsUserName = os.environ.get('USERNAME')
    153         except:
    154             self.LogError('Unable to get windows username from environment')
    155 
    156         self.collectedData = None
    157         self.collectedFiles = None
    158         self.outputFolder = ''
    159         self.settingsFolder = 'settings:/../../'
    160         self.outputFolder = self.settingsFolder + u'IGBR/'
    161         try:
    162             os.mkdir(AbsPath(self.outputFolder))
    163         except:
    164             pass
    165 
    166         if self.dxDiagFileName is None:
    167             self.dxDiagFileName = AbsPath(self.outputFolder + 'dxdiag.txt')
    168             try:
    169                 os.remove(self.dxDiagFileName)
    170             except OSError:
    171                 pass
    172 
    173             try:
    174                 self.LogInfo('Getting DXDiag info...')
    175                 subprocess.Popen(['dxdiag.exe', '/t', self.dxDiagFileName])
    176             except:
    177                 self.dxDiagFileName = None
    178 
    179     def SaveToDisk(self, data):
    180         data['screenshots'] = self.screenshots
    181         f = open(AbsPath(self.outputFolder + self.GetSaveName()), 'w')
    182         yaml.safe_dump(data, f)
    183         f.close()
    184 
    185     def GetSaveName(self):
    186         name = SAVE_NAME
    187         name = name % 'br'
    188         return name
    189 
    190     def LoadFromDisk(self):
    191         path = AbsPath(self.outputFolder + self.GetSaveName())
    192         self.LogInfo('Loading data from disk', path)
    193         try:
    194             f = open(path, 'r')
    195         except:
    196             raise UserError('CustomInfo', {'info': 'No saved data found.'})
    197 
    198         data = yaml.safe_load(f)
    199         f.close()
    200         self.screenshots = data.get('screenshots', [])
    201         return data
    202 
    203     def CollectData(self):
    204         serverInfo = GetServerInfo()
    205         serverVersion, serverBuild = sm.RemoteSvc('cache').GetServerVersionAndBuild()
    206         collectedData = {'title': None,
    207          'description': None,
    208          'reproSteps': None,
    209          'session': str(session),
    210          'clientDateTime': FmtDate(blue.os.GetWallclockTime()),
    211          'clientVersion': boot.keyval['version'].split('=', 1)[1],
    212          'clientBuild': boot.build,
    213          'clientBranch': boot.branch,
    214          'serverName': serverInfo.name,
    215          'serverVersion': serverVersion,
    216          'serverBuild': serverBuild,
    217          'serverEspUrl': serverInfo.espUrl,
    218          'memoryFree': blue.sysinfo.GetMemory().availablePhysical / 1024}
    219         bitCount = blue.sysinfo.cpu.bitCount
    220         adapters = trinity.adapters
    221         ident = adapters.GetAdapterInfo(trinity.mainWindow.GetWindowState().adapter)
    222         driverVersion = DecodeDriverVersion(ident.driverVersion)
    223         computerInfo = {'memoryPhysical': blue.sysinfo.GetMemory().totalPhysical / 1024,
    224          'cpuArchitecture': blue.sysinfo.cpu.architecture,
    225          'cpuIdentifier': blue.sysinfo.cpu.identifier,
    226          'cpuLevel': blue.sysinfo.cpu.family,
    227          'cpuRevision': blue.sysinfo.cpu.revision,
    228          'cpuCount': blue.sysinfo.cpu.logicalCpuCount,
    229          'cpuMHz': blue.sysinfo.cpu.frequency,
    230          'cpuBitCount': bitCount,
    231          'osMajorVersion': blue.sysinfo.os.majorVersion,
    232          'osMinorVersion': blue.sysinfo.os.minorVersion,
    233          'osBuild': blue.sysinfo.os.buildNumber,
    234          'osPatch': blue.sysinfo.os.patch,
    235          'osPlatform': 2,
    236          'videoCardAdapter': ident.description.encode('ascii', 'ignore'),
    237          'videoCardIdentifier': ident.deviceIdentifier[1:-1],
    238          'videoDriverVersion': driverVersion,
    239          'clientBitCount': blue.sysinfo.processBitCount}
    240         try:
    241             driverInfo = ident.GetDriverInfo()
    242             computerInfo['videoDriverVersion'] = driverInfo.driverVersionString
    243             computerInfo['videoDriverDate'] = driverInfo.driverDate
    244             computerInfo['videoDriverVendor'] = driverInfo.driverVendor
    245             computerInfo['videoIsOptimus'] = 'Yes' if driverInfo.isOptimus else 'No'
    246             computerInfo['videoIsAmdDynamicSwitchable'] = 'Yes' if driverInfo.isAmdDynamicSwitchable else 'No'
    247         except RuntimeError:
    248             pass
    249 
    250         collectedData.update({'computerInfo': computerInfo})
    251         collectedFiles = self.GetAttachments()
    252         self.collectedData = utillib.KeyVal(data=collectedData, files=collectedFiles)
    253         return self.collectedData
    254 
    255     def GetScreenshotPath(self, n):
    256         if n is not None and n < len(self.screenshots):
    257             fileName = self.screenshots[n]
    258         else:
    259             fileName = self.outputFolder + SCREENSHOT_NAME % blue.os.GetTime()
    260         return fileName
    261 
    262     def GetScreenshot(self, n = None):
    263         if n > NUM_SCREENSHOTS - 1 or n is None and len(self.screenshots) >= NUM_SCREENSHOTS:
    264             raise UserError('CustomInfo', {'info': 'You have reached the maximum number of screenshots. Please delete a screenshot before grabbing abother one.'})
    265         fileName = self.GetScreenshotPath(n)
    266         self.LogInfo('Getting screenshot number', n, 'to', fileName, '...')
    267         self.wnd.HideAll()
    268         blue.pyos.synchro.SleepWallclock(300)
    269         trinity.SaveRenderTarget(AbsPath(fileName))
    270         if n is not None and n < len(self.screenshots):
    271             self.screenshots[n] = fileName
    272         else:
    273             n = len(self.screenshots)
    274             self.screenshots.append(fileName)
    275         self.LogInfo('Screenshot has been captured. I now have', len(self.screenshots), 'screenshots')
    276         self.wnd.Show()
    277         self.wnd.DoEditScreenshot(n)
    278         self.wnd.UpdateScreenshotButtons()
    279 
    280     def DeleteScreenshot(self, n):
    281         fileName = self.screenshots[n]
    282         del self.screenshots[n]
    283         try:
    284             os.remove(fileName)
    285         except OSError:
    286             pass
    287 
    288         self.wnd.UpdateScreenshotButtons()
    289 
    290     def GetAttachments(self):
    291         self.LogInfo('Getting Attachments...')
    292         files = []
    293         screenShots = []
    294         ret = []
    295         try:
    296             data = sm.GetService('monitor').GetInMemoryLogs()
    297             logs = ('logs.txt', data)
    298             files.append(logs)
    299             if len(data) > 250:
    300                 ret += [logs]
    301         except:
    302             log.LogException('Error getting logs for bug reporting. Skipping.')
    303 
    304         for i, fileName in enumerate(self.screenshots):
    305             try:
    306                 with open(AbsPath(fileName), 'rb') as ssfile:
    307                     data = ssfile.read()
    308             except IOError:
    309                 data = ''
    310 
    311             f = (SCREENSHOT_NAME % i, data)
    312             screenShots.append(f)
    313 
    314         self._AppendProcessHealth(files)
    315         self._AppendMethodCalls(files)
    316         self._AppendOutstandingCalls(files)
    317         self._AppendShipFitting(files)
    318         self._AppendLatestCrashes(files)
    319         self._AppendPrefsIni(files)
    320         self._AppendSettings(files)
    321         self._AppendDxdiag(files)
    322         self._AppendPDMData(files)
    323         self._AppendCarbonInfo(files)
    324         ZIP_FILENAME = 'igbr.zip'
    325         zipName = AbsPath(self.outputFolder + ZIP_FILENAME)
    326         with zipfile.ZipFile(zipName, 'w', zipfile.ZIP_DEFLATED) as zipDataFile:
    327             for fileName, data in files:
    328                 if not fileName.lower().endswith('.jpg'):
    329                     zipDataFile.writestr(fileName, data)
    330 
    331         zipData = open(zipName, 'rb').read()
    332         self.LogInfo('Done Getting Attachments!')
    333         ret += screenShots
    334         ret += [(ZIP_FILENAME, zipData)]
    335         return ret
    336 
    337     def _AppendProcessHealth(self, files):
    338         try:
    339             processHealthSvc = sm.GetService('processHealth')
    340             data = processHealthSvc.GetAllLogs()
    341             files.append(('processHealth.txt', data))
    342         except:
    343             log.LogException('Error getting processhealth data for bug reporting. Skipping.')
    344 
    345     def _AppendMethodCalls(self, files):
    346         try:
    347             data = sm.GetService('monitor').GetMethodCalls()
    348             files.append(('methodcalls.txt', data))
    349         except:
    350             log.LogException('Error getting method call timers for bug reporting. Skipping.')
    351 
    352     def _AppendOutstandingCalls(self, files):
    353         try:
    354             data = '\n'.join(('%s - %s' % (FmtDate(t), n) for n, t in basesession.outstandingCallTimers))
    355             files.append(('outstandingcalls.txt', data))
    356         except Exception:
    357             log.LogException('Failed to get outstanding calls')
    358 
    359     def _AppendShipFitting(self, files):
    360         try:
    361             fittingSvc = sm.GetService('fittingSvc')
    362             fitting = utillib.KeyVal()
    363             fitting.shipTypeID, fitting.fitData, _ = fittingSvc.GetFittingDictForActiveShip()
    364             fitting.name = cfg.evelocations.Get(eveCfg.GetActiveShip()).locationName
    365             data = GetFittingEFTString(fitting).encode('utf-8')
    366             files.append(('fitting.txt', data))
    367         except Exception:
    368             log.LogException('Failed to get ship fitting')
    369 
    370     def _AppendLatestCrashes(self, files):
    371         try:
    372             if platform.system() == 'Darwin':
    373                 crash_folder = os.path.join(blue.sysinfo.GetUserApplicationDataDirectory(), 'CCP', 'EVE', 'completed')
    374             else:
    375                 crash_folder = os.path.join(blue.sysinfo.GetUserApplicationDataDirectory(), 'CCP', 'EVE', 'reports')
    376             crashes = [ (os.path.splitext(i)[0], os.path.getmtime(os.path.join(crash_folder, i))) for i in os.listdir(crash_folder) if i.lower().endswith('.dmp') ]
    377         except Exception:
    378             return
    379 
    380         crashes.sort(key=lambda x: x[1], reverse=True)
    381         data = '\n'.join(('%s - %s' % (time.strftime('%Y-%m-%d %H:%M', time.gmtime(date)), crash) for crash, date in crashes[:10]))
    382         files.append(('lastcrashes.txt', data))
    383 
    384     def _AppendPrefsIni(self, files):
    385         try:
    386             f = open(prefs.ini.filename, 'rb')
    387             data = f.read()
    388             f.close()
    389             files.append((os.path.basename(prefs.ini.filename), data))
    390         except:
    391             log.LogException('Error getting prefs for bug reporting. Skipping.')
    392 
    393     def _AppendSettings(self, files):
    394         allSettings = {}
    395         try:
    396             for settingsType in ('public', 'user', 'char'):
    397                 allSettings[settingsType] = getattr(settings, settingsType).datastore
    398 
    399             data = yaml.dump(allSettings)
    400             files.append(('settings.yaml', data))
    401         except:
    402             log.LogException('Error getting settings for bug reporting. Skipping.')
    403 
    404     def _AppendDxdiag(self, files):
    405         try:
    406             if self.dxDiagFileName:
    407                 i = 0
    408                 data = ''
    409                 while i < 30:
    410                     try:
    411                         f = open(self.dxDiagFileName, 'r')
    412                         data = f.read()
    413                         f.close()
    414                         if len(data) == 0:
    415                             raise IOError
    416                         break
    417                     except IOError:
    418                         i += 1
    419                         blue.pyos.synchro.SleepWallclock(1000)
    420 
    421                 else:
    422                     self.LogError('Failed to get DXDiag file after 30 seconds', self.dxDiagFileName)
    423                     self.dxDiagFileName = None
    424 
    425                 files.append(('dxdiag.txt', data))
    426         except:
    427             log.LogException('Error getting DxDiag for bug reporting. Skipping.')
    428 
    429     def _AppendPDMData(self, files):
    430         try:
    431             files.append(('PDMData.txt', blue.sysinfo.GetPDMData().encode('utf-8')))
    432         except:
    433             log.LogException('Error getting PDM data for bug reporting. Skipping.')
    434 
    435     def _AppendCarbonInfo(self, files):
    436         try:
    437             data = json.dumps(carbonversion.get_carbon_version().get_json_data())
    438             files.append(('carbon.json', data))
    439         except:
    440             log.LogException('Error getting carbon info for bug reporting. Skipping.')
    441 
    442     def GetLabelsForIssue(self, categoryID):
    443         ret = []
    444         categoryName = self.GetCategoryName(categoryID).lower()
    445         language = {'DE': 'German',
    446          'EN': None,
    447          'ES': 'Spanish',
    448          'FR': 'French',
    449          'JA': 'Japanese',
    450          'KO': 'Korean',
    451          'RU': 'Russian',
    452          'ZH': 'Chinese'}.get(session.languageID, None)
    453         if language is not None:
    454             ret.append(language)
    455         if 'graphic' in categoryName:
    456             ret.append(trinity.platform)
    457         if blue.sysinfo.isWine:
    458             if blue.sysinfo.wineHostOs.startswith('Darwin'):
    459                 platformLabel = 'PlatformMacWine'
    460             else:
    461                 platformLabel = 'PlatformLinuxWine'
    462             ret.append(blue.sysinfo.wineVersion)
    463         elif platform.system() == 'Darwin':
    464             platformLabel = 'PlatformMac'
    465         elif platform.system() == 'Windows':
    466             platformLabel = 'PlatformWin'
    467             if blue.sysinfo.os.majorVersion == 10 and blue.sysinfo.os.buildNumber >= 22000:
    468                 ret.append('Windows11')
    469         else:
    470             platformLabel = 'PlatformUnknown'
    471         ret.append(platformLabel)
    472         if blue.sysinfo.computerName == 'GEFORCE-NOW':
    473             ret.append('GeforceNow')
    474         if blue.sysinfo.isRosetta:
    475             ret.append('Rosetta')
    476         ret.append('IsOmega' if get_clone_grade_svc().is_omega_with_grace_period() else 'IsAlpha')
    477         return ret
    478 
    479     def SendDefectOrBugReport(self, data):
    480         ret = self.CollectData()
    481         computer = self._GetComputerInfo(ret, ret.data['computerInfo'])
    482         sessionInfo = self._GetSessionInfo(self._GetEspUrl(ret))
    483         data['ServerName'] = ret.data['serverName']
    484         try:
    485             data['Title'] = data['Title'].encode('UTF-8')
    486             data['Description'] = data['Description'].encode('UTF-8')
    487             data['ReproductionSteps'] = data['ReproductionSteps'].encode('UTF-8')
    488         except Exception:
    489             self.LogError('For some reason I do not particularly care about failing to encode title, description or reproductionSteps')
    490 
    491         category = data['Category']
    492         labels = self.GetLabelsForIssue(category)
    493         return self.bugReporter.SendBugReport(category, data['Title'], data['Description'], data['ReproductionSteps'], sessionInfo, computer, session.userid, labels, ret.data['serverName'], GetServerVersion(ret.data['serverBuild'], ret.data['serverVersion']), ret.files)
    494 
    495     def _GetComputerInfo(self, ret, computerInfo):
    496         computer = 'Trinity platform: %(triPlatform)s\r\nProcess: %(bits)s\r\nOS: %(os)s.%(osminor)s, build: %(build)s, %(servicepack)s\r\nVideo Card: %(videocard)s (Driver: %(driverversion)s, Released: %(driverdate)s)\r\nIs Optimus: %(isoptimus)s\r\nIs AMD Dynamic Switchable: %(isamddynamicswitchable)s\r\nCPU: %(cpu)s @ %(ghz).2f GHz (%(numcpu)s CPUs)\r\nMemory: %(mem)s MB (%(memfree)s MB available)' % {'triPlatform': trinity.platform,
    497          'os': computerInfo.get('osMajorVersion', '-'),
    498          'osminor': computerInfo.get('osMinorVersion', '-'),
    499          'build': computerInfo.get('osBuild', '-'),
    500          'servicepack': computerInfo.get('osPatch', '-'),
    501          'videocard': computerInfo.get('videoCardAdapter', '-'),
    502          'driverversion': computerInfo.get('videoDriverVersion', '-'),
    503          'driverdate': computerInfo.get('videoDriverDate', '-'),
    504          'drivervendor': computerInfo.get('videoDriverVendor', '-'),
    505          'isoptimus': computerInfo.get('videoIsOptimus', '-'),
    506          'isamddynamicswitchable': computerInfo.get('videoIsAmdDynamicSwitchable', '-'),
    507          'cpu': computerInfo.get('cpuIdentifier', '-'),
    508          'ghz': int(computerInfo.get('cpuMHz', 0)) / 1000.0,
    509          'numcpu': computerInfo.get('cpuCount', '-'),
    510          'mem': int(computerInfo.get('memoryPhysical', 0)) / 1024,
    511          'memfree': int(ret.data.get('memoryFree', 0)) / 1024,
    512          'bits': 'x%s' % computerInfo.get('clientBitCount', 32)}
    513         return computer
    514 
    515     def _GetSessionInfo(self, espUrl):
    516         sessionInfo = ['Character: {charname} ({charid})'.format(charname=self.charName, charid=session.charid), 'Solar System: {solarsystemname} ({solarsystemid})'.format(solarsystemname=GetByMessageID(cfg.evelocations.Get(session.solarsystemid2).locationNameID, languageID='en-us'), solarsystemid=session.solarsystemid2)]
    517         cohorts = []
    518         with ExceptionEater('Error getting cohorts on bugreport generation'):
    519             cohorts = gatekeeper.character.GetCohorts()
    520         if len(cohorts) > 0:
    521             sessionInfo.append('Cohort IDs: {cohorts}'.format(cohorts=', '.join(map(str, cohorts))))
    522         if len(espUrl) > 0:
    523             sessionInfo.append(espUrl)
    524         return '\n'.join(sessionInfo)
    525 
    526     def _GetEspUrl(self, ret):
    527         espUrl = ''
    528         if session.role & ROLE_GML > 0:
    529             espUrl = 'ESP: http://%(serverEspUrl)s %(serverName)s' % {'serverName': ret.data['serverName'],
    530              'serverEspUrl': ret.data['serverEspUrl']}
    531         return espUrl
    532 
    533     def GetCategories(self):
    534         if self.categories is None:
    535             self.categories = self.bugReporter.GetCategories()
    536         return self.categories[:]
    537 
    538     def GetCategoryName(self, categoryID):
    539         for name, _categoryID in self.GetCategories():
    540             if _categoryID == categoryID:
    541                 return name
    542 
    543 
    544 class BugReportingWindow(Window):
    545     __guid__ = 'form.BugReportingWindow'
    546     default_windowID = 'BugReportingWindow'
    547     default_iconNum = 'res:/UI/Texture/WindowIcons/repairshop.png'
    548     default_height = 700
    549     default_minSize = (820, default_height)
    550     default_captionLabelPath = 'UI/BugReporting/ReportBug'
    551 
    552     def ApplyAttributes(self, attributes):
    553         self.screenshotWindow = None
    554         super(BugReportingWindow, self).ApplyAttributes(attributes)
    555         self.RegisterPositionTraceKeyEvents()
    556         self.bottomCont = ContainerAutoSize(parent=self.content, align=uiconst.TOBOTTOM, padding=const.defaultPadding)
    557         self.ConstructTopCont()
    558         self.ConstructBottomCont()
    559         self.ConstructMainCont()
    560         self.service = sm.GetService('bugReporting')
    561         self.service.Init()
    562         self.loadingWheel = LoadingWheel(parent=self.sr.maincontainer, align=uiconst.CENTER, width=64, height=64)
    563         self.content.opacity = 0.0
    564         uthread.new(self.FinishSettingUp)
    565 
    566     def FinishSettingUp(self):
    567         if not self.service.bugReporter.IsBugsServiceOnline():
    568             prompt_player_fsd_dialog('BugReportServerNotReachable')
    569             self.Close()
    570             return
    571         if self.destroyed:
    572             return
    573         self.loadingWheel.Close()
    574         self.UpdateScreenshotButtons()
    575         self.PopulateCategoryCombo()
    576         self.content.opacity = 1.0
    577 
    578     def PopulateCategoryCombo(self):
    579         categories = self.service.GetCategories()
    580         initialOption = GetByLabel('UI/BugReporting/SelectCategory')
    581         categories.insert(0, [initialOption, ''])
    582         self.categoryCombo.LoadOptions(categories)
    583 
    584     def HideAll(self):
    585         self.Hide()
    586         if self.screenshotWindow:
    587             self.screenshotWindow.Close()
    588 
    589     def GetScreenshot(self, n):
    590         self.service.GetScreenshot(n)
    591 
    592     def ConstructMainCont(self):
    593         self.mainCont = Container(name='mainCont', parent=self.content, align=uiconst.TOALL)
    594         descriptionCont = Container(name='descriptionCont', parent=self.mainCont, align=uiconst.TOTOP_PROP, height=0.5, padBottom=8)
    595         reproStepsCont = Container(name='reproStepsCont', parent=self.mainCont, align=uiconst.TOALL, padTop=8)
    596         EveLabelMedium(parent=descriptionCont, align=uiconst.TOTOP, padBottom=4, text=GetByLabel('UI/BugReporting/Description'))
    597         self.descriptionEdit = EditPlainText(name='descriptionEdit', parent=descriptionCont, align=uiconst.TOALL, hintText=GetByLabel('UI/BugReporting/DescriptionHintText'), setvalue='', maxLength=2900)
    598         EveLabelMedium(parent=reproStepsCont, align=uiconst.TOTOP, padBottom=4, text=GetByLabel('UI/BugReporting/ReproductionSteps'))
    599         self.reproStepsEdit = EditPlainText(name='reproStepsEdit', parent=reproStepsCont, align=uiconst.TOALL, hintText=GetByLabel('UI/BugReporting/ReproductionStepsHintText'), setvalue='', maxLength=2900)
    600 
    601     def ConstructTopCont(self):
    602         top_cont = ContainerAutoSize(name='topCont', parent=self.content, align=uiconst.TOTOP, alignMode=uiconst.TOPLEFT, padBottom=16)
    603         left_grid = LayoutGrid(parent=top_cont, align=uiconst.TOPLEFT, columns=2, cellSpacing=(8, 8))
    604         self.titleEdit = SingleLineEditText(name='titleEdit', parent=left_grid, align=uiconst.TOPLEFT, width=300, maxLength=250, hintText=GetByLabel('UI/BugReporting/Summary'), hint=GetByLabel('UI/BugReporting/SummaryHint'))
    605         self.categoryCombo = Combo(name='categoryCombo', parent=left_grid, width=0, options=[])
    606         self.moreInfoIcon = MoreInfoIcon(name='moreInfo', parent=top_cont, align=uiconst.CENTERRIGHT)
    607         self.moreInfoIcon.LoadTooltipPanel = self.LoadInfoTooltipPanel
    608 
    609     def ConstructBottomCont(self):
    610         buttonCont = Container(name='buttonCont', parent=self.bottomCont, align=uiconst.TOTOP, height=Button.default_height, top=16)
    611         self.btns = ButtonGroup(parent=self.bottomCont, align=uiconst.TOTOP, btns=[(GetByLabel('UI/BugReporting/Send'),
    612           self.ClickSend,
    613           (),
    614           84)], line=1)
    615         self.editScreenshotButtons = []
    616         self.takeScreenshotButtons = []
    617         for i in xrange(NUM_SCREENSHOTS):
    618             text = GetByLabel('UI/BugReporting/EditScreenshot', screenshotNum=i + 1)
    619             c = Button(name='editscreenshot_%s' % i, parent=buttonCont, align=uiconst.TOLEFT, left=8 if i > 0 else 0, label=text, func=self.EditScreenshot)
    620             self.editScreenshotButtons.append(c)
    621 
    622         for i in xrange(NUM_SCREENSHOTS):
    623             text = GetByLabel('UI/BugReporting/NewScreenshot', screenshotNum=i + 1)
    624             c = Button(name='newscreenshot_%s' % i, parent=buttonCont, align=uiconst.TOLEFT, left=8 if i > 0 else 0, label=text, func=self.NewScreenshot, hint=GetByLabel('UI/BugReporting/NewScreenshotHint'))
    625             self.takeScreenshotButtons.append(c)
    626 
    627         self.saveToDiskButton = Button(label=GetByLabel('UI/BugReporting/Save'), parent=buttonCont, func=self.SaveToDisk, align=uiconst.TORIGHT, hint=GetByLabel('UI/BugReporting/SaveHint'))
    628         self.loadFromDiskButton = Button(parent=buttonCont, align=uiconst.TORIGHT, left=8, label=GetByLabel('UI/BugReporting/Load'), func=self.LoadFromDisk, hint=GetByLabel('UI/BugReporting/LoadHint'))
    629 
    630     def LoadInfoTooltipPanel(self, tooltipPanel, *args):
    631         tooltipText = GetByLabel('UI/BugReporting/InfoTooltipText', url=BUG_GUIDE_URL)
    632         tooltipPanel.state = uiconst.UI_NORMAL
    633         tooltipPanel.LoadGeneric1ColumnTemplate()
    634         tooltipPanel.AddLabelMedium(text=tooltipText, bold=True, state=uiconst.UI_NORMAL, wrapWidth=250)
    635 
    636     def UpdateScreenshotButtons(self):
    637         for i, c in enumerate(self.editScreenshotButtons):
    638             c.state = uiconst.UI_HIDDEN
    639 
    640         for i, c in enumerate(self.takeScreenshotButtons):
    641             c.state = uiconst.UI_HIDDEN
    642 
    643         i = -1
    644         numScreenshots = len(self.service.screenshots)
    645         for i in xrange(numScreenshots):
    646             self.editScreenshotButtons[i].state = uiconst.UI_NORMAL
    647 
    648         if i == -1:
    649             self.takeScreenshotButtons[0].state = uiconst.UI_NORMAL
    650         elif i < NUM_SCREENSHOTS - 1:
    651             self.takeScreenshotButtons[i + 1].state = uiconst.UI_NORMAL
    652 
    653     def EnableSendButton(self, enable):
    654         try:
    655             btn = self.btns.GetChild('SEND_Btn')
    656             if enable:
    657                 btn.Enable()
    658             else:
    659                 btn.Disable()
    660         except:
    661             pass
    662 
    663     def ClickSend(self, *args):
    664         uthread.new(self.DoClickSend)
    665 
    666     def FormatString(self, txt):
    667         txt = txt.replace('<br>', '\r\n')
    668         return txt
    669 
    670     def GetDataFromForm(self):
    671         title = self.titleEdit.GetValue().strip()
    672         description = self.descriptionEdit.GetValue(html=0).strip()
    673         reproSteps = self.reproStepsEdit.GetValue(html=0).strip()
    674         category = self.categoryCombo.GetValue()
    675         description = self.FormatString(description)
    676         reproSteps = self.FormatString(reproSteps)
    677         data = {'Title': title,
    678          'Description': description,
    679          'ReproductionSteps': reproSteps,
    680          'Category': category}
    681         return data
    682 
    683     def DoClickSend(self):
    684         self.EnableSendButton(False)
    685         data = self.GetDataFromForm()
    686         if '' in (data['Title'], data['Description'], data['ReproductionSteps']) or not data['Category']:
    687             self.EnableSendButton(True)
    688             prompt_player_fsd_dialog('CustomInfo', {'info': GetByLabel('UI/BugReporting/AllFields')})
    689             return
    690         data['ClientBuildNumber'] = boot.build
    691         if len(self.service.screenshots) == 0:
    692             ret = prompt_player_fsd_dialog('CustomQuestion', {'header': GetByLabel('UI/BugReporting/NoScreenshotsHeader'),
    693              'question': GetByLabel('UI/BugReporting/NoScreenshotsMessage')}, uiconst.YESNO)
    694             if ret == uiconst.ID_YES:
    695                 self.EnableSendButton(True)
    696                 uthread.new(self.GetScreenshot, None)
    697                 return
    698             if ret in (uiconst.ID_CANCEL, uiconst.ID_CLOSE):
    699                 self.EnableSendButton(True)
    700                 return
    701         result = False
    702         bugreportID = ''
    703         attachments = False
    704         self.loadingWheel = LoadingWheel(parent=self.content, align=uiconst.CENTER, width=64, height=64)
    705         self.mainCont.opacity = 0.3
    706         try:
    707             result = self.service.SendDefectOrBugReport(data)
    708         except SendAttachmentError:
    709             log.LogException()
    710             attachments = False
    711             result = True
    712         except Exception as e:
    713             log.LogException()
    714             self.EnableSendButton(True)
    715             self.mainCont.opacity = 1.0
    716             self.loadingWheel.Close()
    717             prompt_player_fsd_dialog('CustomInfo', {'info': GetByLabel('UI/BugReporting/SendError', errorMessage=e)})
    718         else:
    719             bugreportID = result['key']
    720             attachments = True
    721 
    722         if result:
    723             self.Close()
    724             self.MessageSuccess(attachments, bugreportID)
    725         else:
    726             self.EnableSendButton(True)
    727 
    728     def MessageSuccess(self, attachments, bugreportID):
    729         header = GetByLabel('UI/BugReporting/ReportSentHeader')
    730         if attachments:
    731             msg = GetByLabel('UI/BugReporting/ReportSent', bugreportID=bugreportID)
    732         else:
    733             msg = GetByLabel('UI/BugReporting/ReportSentAttFail')
    734         prompt_player_fsd_dialog('CustomWarning', {'header': header,
    735          'warning': msg})
    736 
    737     def OpenExternalBugs(self, *args):
    738         header = GetByLabel('UI/BugReporting/OpenBugsExternalHeader')
    739         question = GetByLabel('UI/BugReporting/OpenBugsExternal')
    740         if prompt_player_fsd_dialog('CustomQuestion', {'header': header,
    741          'question': question}, uiconst.YESNO, modal=False) == uiconst.ID_YES:
    742             webbrowser.open_new(BUG_REPORTS_URL)
    743 
    744     def NewScreenshot(self, button):
    745         n = int(button.name.split('_')[1])
    746         self.GetScreenshot(n)
    747 
    748     def EditScreenshot(self, button):
    749         n = int(button.name.split('_')[1])
    750         self.DoEditScreenshot(n)
    751 
    752     def DoEditScreenshot(self, n):
    753         self.screenshotWindow = ScreenshotEditingWnd.Open(n=n, service=self.service)
    754 
    755     def ClearLogs(self, *args):
    756         sm.GetService('monitor').ClearLogInMemory()
    757 
    758     def ViewLogs(self, *args):
    759         sm.GetService('monitor').ShowLogTab()
    760 
    761     def CopyException(self, *args):
    762         logs = blue.logInMemory.GetEntries()
    763         lastException = ''
    764         for l in logs:
    765             if 'EXCEPTION END' in l[4]:
    766                 lastException = l[4]
    767                 break
    768 
    769         if len(lastException):
    770             inf = 'Most recent exception has been copied to the clipboard'
    771             lastException = lastException.replace('\n', '\r\n')
    772             lastException = lastException.replace('<', '&lt;')
    773             blue.pyos.SetClipboardData(lastException)
    774         else:
    775             inf = 'No exceptions in the logs'
    776         prompt_player_fsd_dialog('CustomNotify', {'notify': inf})
    777 
    778     def SaveToDisk(self, *args):
    779         data = self.GetDataFromForm()
    780         self.service.SaveToDisk(data)
    781         prompt_player_fsd_dialog('CustomNotify', {'notify': GetByLabel('UI/BugReporting/SavedToDisk')})
    782 
    783     def LoadFromDisk(self, *args):
    784         ret = prompt_player_fsd_dialog('CustomQuestion', {'header': GetByLabel('UI/BugReporting/LoadFromDiskHeader'),
    785          'question': GetByLabel('UI/BugReporting/LoadFromDisk')}, uiconst.YESNO)
    786         if ret != uiconst.ID_YES:
    787             return
    788         data = self.service.LoadFromDisk()
    789         self.titleEdit.SetValue(data.get('Title', ''))
    790         self.descriptionEdit.SetValue(data.get('Description', ''))
    791         self.reproStepsEdit.SetValue(data.get('ReproductionSteps', ''))
    792         self.categoryCombo.SelectItemByValue(data.get('Category', None))
    793         self.UpdateScreenshotButtons()
    794 
    795     def RegisterPositionTraceKeyEvents(self):
    796         self.keyDownCookie = uicore.event.RegisterForTriuiEvents(uiconst.UI_KEYDOWN, self.OnGlobalKeyDownCallback)
    797 
    798     def OnGlobalKeyDownCallback(self, wnd, eventID, param):
    799         vkey, flag = param
    800         if self.destroyed:
    801             return False
    802         ctrl = uicore.uilib.Key(uiconst.VK_CONTROL)
    803         shift = uicore.uilib.Key(uiconst.VK_SHIFT)
    804         alt = uicore.uilib.Key(uiconst.VK_MENU)
    805         p = uicore.uilib.Key(uiconst.VK_P)
    806         if ctrl and alt and shift and p:
    807             uthread.new(self.GetScreenshot, None)
    808         return True
    809 
    810 
    811 class ScreenshotEditingWnd(Window):
    812     __guid__ = 'form.ScreenshotEditingWnd'
    813     default_windowID = 'ScreenshotEditingWnd'
    814 
    815     def ApplyAttributes(self, attributes):
    816         Window.ApplyAttributes(self, attributes)
    817         self.SetCaption(GetByLabel('UI/BugReporting/ScreenshotEditor'))
    818         self.MakeUnstackable()
    819         self.service = attributes['service']
    820         self.n = attributes['n']
    821         self.screenshotPath = sm.GetService('bugReporting').GetScreenshotPath(self.n)
    822         self.service.LogInfo('Screenshot editing window using screenshot from', self.screenshotPath)
    823         w = h = 0
    824         try:
    825             surface = trinity.Tr2HostBitmap()
    826             surface.CreateFromFile(self.screenshotPath)
    827             w, h = surface.width, surface.height
    828         except:
    829             log.LogException()
    830 
    831         self.screenshotWidth = w or uicore.desktop.width
    832         self.screenshotHeight = h or uicore.desktop.height
    833         self.screenShotScaling = None
    834         maxWidth = max(self.screenshotWidth / 2, 955)
    835         self.SetMinSize([maxWidth, self.screenshotHeight / 2])
    836         topCont = Container(name='topCont', parent=self.content, align=uiconst.TOTOP, height=30, padding=const.defaultPadding)
    837         self.layoutAreaParent = Container(parent=self.content, padding=10)
    838         self.layoutAreaParent._OnSizeChange_NoBlock = self.RecalcLayout
    839         self.layoutArea = Container(parent=self.layoutAreaParent, align=uiconst.CENTER, width=self.screenshotWidth / 4, height=self.screenshotHeight / 4)
    840         self.overlays = Container(name='overlays', parent=self.layoutArea, align=uiconst.TOALL, clipChildren=True)
    841         self.screenshotSprite = Sprite(name='screenshotSprite', parent=self.layoutArea, texturePath=self.screenshotPath, align=uiconst.TOALL, padding=0)
    842         self.screenshotSprite.ReloadTexture()
    843         Button(label=GetByLabel('UI/BugReporting/ScreenshotSave'), parent=topCont, func=self.SaveScreenshot, left=8, align=uiconst.CENTERRIGHT)
    844         fb = Button(label=GetByLabel('UI/BugReporting/ScreenshotAddFrame'), func=self.AddFrame, parent=topCont, left=8, align=uiconst.CENTERLEFT)
    845         cfb = Button(label=GetByLabel('UI/BugReporting/ScreenshotAddCropFrame'), func=self.AddCropFrame, parent=topCont, left=fb.left + fb.width + 8, align=uiconst.CENTERLEFT)
    846         la = Button(label=GetByLabel('UI/BugReporting/ScreenshotAddLeftArrow'), func=self.AddLeftArrowFrame, parent=topCont, left=cfb.left + cfb.width + 8, align=uiconst.CENTERLEFT)
    847         ra = Button(label=GetByLabel('UI/BugReporting/ScreenshotAddRightArrow'), func=self.AddRightArrowFrame, parent=topCont, left=la.left + la.width + 8, align=uiconst.CENTERLEFT)
    848         tc = Button(label=GetByLabel('UI/BugReporting/ScreenshotAddText'), func=self.AddTextFrame, parent=topCont, left=ra.left + ra.width + 8, align=uiconst.CENTERLEFT)
    849         cb = Button(label=GetByLabel('UI/BugReporting/ScreenshotButtonNew'), func=self.NewScreenshot, parent=topCont, left=tc.left + tc.width + 20, align=uiconst.CENTERLEFT)
    850         cb = Button(label=GetByLabel('UI/BugReporting/ScreenshotDelete'), func=self.DeleteScreenshot, parent=topCont, left=cb.left + cb.width + 8, align=uiconst.CENTERLEFT)
    851         self.UpdateLayoutArea()
    852 
    853     def UpdateLayoutArea(self):
    854         prevScaling = self.screenShotScaling
    855         areaWidth, areaHeight = self.layoutAreaParent.GetAbsoluteSize()
    856         xFitScale = areaWidth / float(self.screenshotWidth)
    857         yFitScale = areaHeight / float(self.screenshotHeight)
    858         self.screenShotScaling = scaling = min(xFitScale, yFitScale)
    859         self.layoutArea.width = int(self.screenshotWidth * scaling)
    860         self.layoutArea.height = int(self.screenshotHeight * scaling)
    861         if prevScaling and prevScaling != scaling:
    862             for overlay in self.overlays.children:
    863                 overlay.UpdateProportionalPosition(scaling)
    864 
    865     def NewScreenshot(self, *args):
    866         self.service.GetScreenshot(self.n)
    867 
    868     def DeleteScreenshot(self, *args):
    869         self.service.DeleteScreenshot(self.n)
    870         self.Close()
    871 
    872     def AddTextFrame(self, *args):
    873         initWidth = int(200 * self.screenShotScaling)
    874         initHeight = int(32 * self.screenShotScaling)
    875         initLeft = int(self.screenshotWidth * 0.5 * self.screenShotScaling) - initWidth / 2
    876         initTop = int(self.screenshotHeight * 0.5 * self.screenShotScaling) - initHeight / 2
    877         MoveableTextRect(name='Text', parent=self.overlays, pos=(initLeft,
    878          initTop,
    879          initWidth,
    880          initHeight), idx=0, showControls=True, scaling=self.screenShotScaling)
    881 
    882     def AddRightArrowFrame(self, *args):
    883         initWidth = int(ARROW_HEIGHT * 2 * self.screenShotScaling)
    884         initHeight = int(ARROW_HEIGHT * self.screenShotScaling)
    885         initLeft = int(self.screenshotWidth * 0.5 * self.screenShotScaling)
    886         initTop = int(self.screenshotHeight * 0.5 * self.screenShotScaling)
    887         container = MoveableRect(name='Arrow', parent=self.overlays, pos=(initLeft,
    888          initTop,
    889          initWidth,
    890          initHeight), showControls=False, idx=0)
    891         icon = Sprite(label='arrow', parent=container, texturePath=RIGHT_ARROW, align=uiconst.TOALL, state=uiconst.UI_DISABLED)
    892         container.AddColorObject(icon)
    893         container.ChangeColor(COLOR_GREEN)
    894 
    895     def AddLeftArrowFrame(self, *args):
    896         initWidth = int(ARROW_HEIGHT * 2 * self.screenShotScaling)
    897         initHeight = int(ARROW_HEIGHT * self.screenShotScaling)
    898         initLeft = int(self.screenshotWidth * 0.5 * self.screenShotScaling)
    899         initTop = int(self.screenshotHeight * 0.5 * self.screenShotScaling)
    900         container = MoveableRect(name='Arrow', parent=self.overlays, pos=(initLeft,
    901          initTop,
    902          initWidth,
    903          initHeight), showControls=False, idx=0)
    904         icon = Sprite(label='arrow', parent=container, texturePath=LEFT_ARROW, align=uiconst.TOALL, state=uiconst.UI_DISABLED)
    905         container.AddColorObject(icon)
    906         container.ChangeColor(COLOR_GREEN)
    907 
    908     def AddCropFrame(self, *args):
    909         for each in self.overlays.children[:]:
    910             if each.name == 'cropFrame':
    911                 each.Close()
    912 
    913         initLeft = int(self.screenshotWidth * 0.25 * self.screenShotScaling)
    914         initTop = int(self.screenshotHeight * 0.25 * self.screenShotScaling)
    915         initWidth = int(self.screenshotWidth * 0.5 * self.screenShotScaling)
    916         initHeight = int(self.screenshotHeight * 0.5 * self.screenShotScaling)
    917         MoveableRect(name='Crop', parent=self.overlays, pos=(initLeft,
    918          initTop,
    919          initWidth,
    920          initHeight), maskColor=(0, 0, 0, 0.75), showControls=True)
    921 
    922     def AddFrame(self, *args):
    923         initLeft = int((self.screenshotWidth - 200) * self.screenShotScaling) / 2
    924         initTop = int((self.screenshotHeight - 200) * self.screenShotScaling) / 2
    925         initWidth = int(200 * self.screenShotScaling)
    926         initHeight = int(200 * self.screenShotScaling)
    927         MoveableRect(parent=self.overlays, name='Frame', pos=(initLeft,
    928          initTop,
    929          initWidth,
    930          initHeight), showFrame=True, showControls=True, idx=0)
    931 
    932     def Clear(self, *args):
    933         self.overlays.Flush()
    934 
    935     def SaveScreenshot(self, *args):
    936         self.Hide()
    937         baseTexture = trinity.Tr2RenderTarget(self.screenshotWidth, self.screenshotHeight, 1, trinity.PIXEL_FORMAT.B8G8R8A8_UNORM)
    938         renderJob = trinity.CreateRenderJob()
    939         desktop = UIRoot(name='screenshot', width=self.screenshotWidth, height=self.screenshotHeight, renderTarget=baseTexture, renderJob=renderJob)
    940         cropProportion = None
    941         for each in self.overlays.children:
    942             if each.name == 'Crop':
    943                 cropProportion = each.proportionalPosition
    944 
    945         self.overlays.SetParent(desktop)
    946         self.screenshotSprite.SetParent(desktop)
    947         for overlay in self.overlays.children:
    948             overlay.UpdateProportionalPosition(1.0)
    949 
    950         desktop.UpdateAlignment()
    951         blue.pyos.synchro.Yield()
    952         renderJob.ScheduleOnce()
    953         renderJob.WaitForFinish()
    954         blue.pyos.synchro.Yield()
    955         path = AbsPath(self.screenshotPath)
    956         if cropProportion:
    957             cl, cr, ct, cb = cropProportion
    958             cr = min(1.0, cr)
    959             cb = min(1.0, cb)
    960             bmp = trinity.Tr2HostBitmap(baseTexture)
    961             bmp.Crop(int(cl * self.screenshotWidth), int(ct * self.screenshotHeight), int(cr * self.screenshotWidth), int(cb * self.screenshotHeight))
    962             bmp.Save(path)
    963         else:
    964             trinity.Tr2HostBitmap(baseTexture).Save(path)
    965         desktop.Close()
    966         self.CloseByUser()
    967 
    968     def OnScale_(self, *args):
    969         pass
    970 
    971     def RecalcLayout(self, *args):
    972         self.UpdateLayoutArea()
    973 
    974 
    975 class MoveableRect(Container):
    976     __guid__ = 'MoveableRect'
    977     default_align = uiconst.TOPLEFT
    978     default_state = uiconst.UI_NORMAL
    979     CONTROLS = (('topleft', uiconst.TOPLEFT),
    980      ('topright', uiconst.TOPRIGHT),
    981      ('bottomright', uiconst.BOTTOMRIGHT),
    982      ('bottomleft', uiconst.BOTTOMLEFT))
    983 
    984     def ApplyAttributes(self, attributes):
    985         Container.ApplyAttributes(self, attributes)
    986         self.modifyThread = None
    987         self.frame = None
    988         self.colorObjects = []
    989         self.color = COLOR_GREEN
    990         self.controls = Container(parent=self, state=uiconst.UI_PICKCHILDREN)
    991         if attributes.showControls:
    992             for cornerName, align in self.CONTROLS:
    993                 scalePoint = Fill(parent=self.controls, align=align, color=(1, 1, 1, 1), pos=(0, 0, 10, 10), state=uiconst.UI_NORMAL, name=cornerName)
    994                 scalePoint.OnMouseDown = (self.StartScaling, scalePoint)
    995                 scalePoint.OnMouseUp = (self.EndScaling, scalePoint)
    996                 self.AddColorObject(scalePoint)
    997 
    998         self.maskColor = attributes.maskColor
    999         if self.maskColor:
   1000             self.UpdateMask()
   1001         if attributes.showFrame:
   1002             self.frame = Frame(parent=self, frameConst=uiconst.FRAME_BORDER2_CORNER3, color=(1.0, 1.0, 1.0, 0.5))
   1003             self.AddColorObject(self.frame)
   1004         self.UpdateColor()
   1005         self.RegisterProportionalValues()
   1006 
   1007     def AddColorObject(self, obj):
   1008         self.colorObjects.append(obj)
   1009         self.UpdateColor()
   1010 
   1011     def StartScaling(self, obj, *args):
   1012         startProps = (obj.name,
   1013          uicore.uilib.x,
   1014          uicore.uilib.y,
   1015          self.left,
   1016          self.top,
   1017          self.width,
   1018          self.height)
   1019         self.modifyThread = timerstuff.AutoTimer(1, self.ModifyRect, startProps)
   1020 
   1021     def EndScaling(self, obj, *args):
   1022         self.modifyThread = None
   1023 
   1024     def OnMouseDown(self, *args):
   1025         startProps = ('move',
   1026          uicore.uilib.x,
   1027          uicore.uilib.y,
   1028          self.left,
   1029          self.top,
   1030          self.width,
   1031          self.height)
   1032         self.modifyThread = timerstuff.AutoTimer(1, self.ModifyRect, startProps)
   1033 
   1034     def OnMouseUp(self, *args):
   1035         self.modifyThread = None
   1036 
   1037     def ModifyRect(self, startProps, *args):
   1038         side, cursorX, cursorY, l, t, w, h = startProps
   1039         dx = uicore.uilib.x - cursorX
   1040         dy = uicore.uilib.y - cursorY
   1041         if side == 'move':
   1042             self.left = max(0, l + dx)
   1043             self.top = max(0, t + dy)
   1044         else:
   1045             if 'left' in side:
   1046                 self.width = max(16, w - dx)
   1047                 self.left = max(0, min(l + dx, l + w - 16))
   1048             else:
   1049                 self.width = max(16, w + dx)
   1050             if 'top' in side:
   1051                 self.height = max(16, h - dy)
   1052                 self.top = max(0, min(t + dy, t + h - 16))
   1053             else:
   1054                 self.height = max(16, h + dy)
   1055         if self.maskColor:
   1056             self.UpdateMask()
   1057         self.RegisterProportionalValues()
   1058 
   1059     def RegisterProportionalValues(self):
   1060         parWidth, parHeight = self.parent.GetAbsoluteSize()
   1061         pl = self.left / float(parWidth)
   1062         pr = (self.left + self.width) / float(parWidth)
   1063         pt = self.top / float(parHeight)
   1064         pb = (self.top + self.height) / float(parHeight)
   1065         self.proportionalPosition = (pl,
   1066          pr,
   1067          pt,
   1068          pb)
   1069 
   1070     def UpdateProportionalPosition(self, newScaling):
   1071         parWidth, parHeight = self.parent.GetAbsoluteSize()
   1072         pl, pr, pt, pb = self.proportionalPosition
   1073         self.left = int(pl * parWidth)
   1074         self.top = int(pt * parHeight)
   1075         self.width = int((pr - pl) * parWidth)
   1076         self.height = int((pb - pt) * parHeight)
   1077         if newScaling == 1.0 and getattr(self, 'numLabel', None):
   1078             self.numLabel.useSizeFromTexture = True
   1079             self.numLabel.Layout()
   1080             self.numLabel.top = -self.numLabel.textheight - 2
   1081         if self.maskColor:
   1082             self.UpdateMask()
   1083 
   1084     def UpdateMask(self):
   1085         maskFills = getattr(self, 'maskFills', None)
   1086         if not maskFills:
   1087             self.maskFills = []
   1088             for i in xrange(4):
   1089                 self.maskFills.append(Fill(parent=self, color=self.maskColor, align=uiconst.TOPLEFT))
   1090 
   1091         self.maskFills[0].top = -uicore.desktop.height
   1092         self.maskFills[0].height = uicore.desktop.height
   1093         self.maskFills[0].left = -uicore.desktop.width
   1094         self.maskFills[0].width = uicore.desktop.width * 2
   1095         self.maskFills[1].top = self.height
   1096         self.maskFills[1].height = uicore.desktop.height
   1097         self.maskFills[1].left = -uicore.desktop.width
   1098         self.maskFills[1].width = uicore.desktop.width * 2
   1099         self.maskFills[2].top = 0
   1100         self.maskFills[2].height = self.height
   1101         self.maskFills[2].left = -uicore.desktop.width
   1102         self.maskFills[2].width = uicore.desktop.width
   1103         self.maskFills[3].top = 0
   1104         self.maskFills[3].height = self.height
   1105         self.maskFills[3].left = self.width
   1106         self.maskFills[3].width = uicore.desktop.width
   1107 
   1108     def OnMouseEnter(self, *args):
   1109         self.ShowControls()
   1110         self.mouseOverThread = timerstuff.AutoTimer(1, self.CheckMouseOver)
   1111 
   1112     def CheckMouseOver(self, *args):
   1113         if self.destroyed:
   1114             self.mouseOverThread = None
   1115             return
   1116         mo = uicore.uilib.mouseOver
   1117         if mo is self or mo.IsUnder(self) or self.modifyThread:
   1118             return
   1119         self.mouseOverThread = None
   1120         self.HideControls()
   1121 
   1122     def ShowControls(self):
   1123         self.controls.Show()
   1124 
   1125     def HideControls(self):
   1126         self.controls.Hide()
   1127 
   1128     def Numberate(self, *args):
   1129         currentNumbers = []
   1130         for each in self.parent.children:
   1131             autoNumber = getattr(each, 'autoNumber', None)
   1132             if autoNumber is not None:
   1133                 currentNumbers.append(autoNumber)
   1134 
   1135         tryNum = 1
   1136         while tryNum in currentNumbers:
   1137             tryNum += 1
   1138 
   1139         self.AssignNumber(tryNum)
   1140 
   1141     def AssignNumber(self, number):
   1142         if getattr(self, 'numLabel', None) is None:
   1143             self.numLabel = Label(parent=self, fontsize=NUMFONTSIZE, align=uiconst.TOPRIGHT, bold=1)
   1144             self.numLabel.useSizeFromTexture = False
   1145             self.numLabel._OnSizeChange_NoBlock = self.OnNumberSizeChange
   1146         self.autoNumber = number
   1147         self.numLabel.text = unicode(number)
   1148         self.AddColorObject(self.numLabel)
   1149 
   1150     def OnNumberSizeChange(self, *args):
   1151         screenShotWindow = GetWindowAbove(self)
   1152         if screenShotWindow:
   1153             self.numLabel.displayWidth = self.numLabel.textwidth * screenShotWindow.screenShotScaling
   1154             self.numLabel.displayHeight = self.numLabel.textheight * screenShotWindow.screenShotScaling
   1155             self.numLabel.top = -self.ReverseScaleDpi((self.numLabel.textheight + 2) * screenShotWindow.screenShotScaling)
   1156 
   1157     def GetMenu(self, *args):
   1158         m = [(GetByLabel('UI/BugReporting/ScreenshotDeleteObject', object=self.name), self.Delete), (GetByLabel('UI/BugReporting/ScreenshotNumberate'), self.Numberate)]
   1159         if self.maskColor:
   1160             return m
   1161         m = m + [None,
   1162          (GetByLabel('UI/BugReporting/ScreenshotRed'), self.ChangeColor, ((1, 0, 0, 1),)),
   1163          (GetByLabel('UI/BugReporting/ScreenshotGreen'), self.ChangeColor, ((0, 1, 0, 1),)),
   1164          (GetByLabel('UI/BugReporting/ScreenshotBlue'), self.ChangeColor, ((0, 0, 1, 1),))]
   1165         return m
   1166 
   1167     def Delete(self, *args):
   1168         uthread.new(self.Close)
   1169 
   1170     def UpdateColor(self):
   1171         self.ChangeColor(self.color)
   1172 
   1173     def ChangeColor(self, color):
   1174         self.color = color
   1175         for each in self.colorObjects:
   1176             each.SetRGBA(*color)
   1177 
   1178 
   1179 class MoveableTextRect(MoveableRect):
   1180     __guid__ = 'MoveableTextRect'
   1181     DEFAULTTEXT = GetByLabel('UI/BugReporting/ScreenshotDefaultText')
   1182     CONTROLS = ()
   1183 
   1184     def ApplyAttributes(self, attributes):
   1185         MoveableRect.ApplyAttributes(self, attributes)
   1186         self.originalScaling = attributes.get('scaling', 1.0)
   1187         self.textEdit = None
   1188         self.underlay = Fill(parent=self)
   1189         self.sampleText = Label(parent=self, left=8, top=8, text=self.DEFAULTTEXT, fontsize=BASEFONTSIZE)
   1190         self.sampleText.useSizeFromTexture = False
   1191         self.sampleText._OnSizeChange_NoBlock = self.OnTextSizeChange
   1192         self.UpdateColor()
   1193         self.OnTextSizeChange()
   1194 
   1195     def OnTextSizeChange(self, *args):
   1196         screenShotWindow = GetWindowAbove(self)
   1197         if screenShotWindow:
   1198             self.sampleText.displayWidth = self.sampleText.textwidth * screenShotWindow.screenShotScaling
   1199             self.sampleText.displayHeight = self.sampleText.textheight * screenShotWindow.screenShotScaling
   1200             self.width = self.ReverseScaleDpi(self.sampleText.displayWidth) + 16
   1201             self.height = self.ReverseScaleDpi(self.sampleText.displayHeight) + 16
   1202 
   1203     def OnDblClick(self, *args):
   1204         self.sampleText.Hide()
   1205         setValue = ''
   1206         if self.sampleText.text != self.DEFAULTTEXT:
   1207             setValue = self.sampleText.text
   1208         self.width = 200
   1209         self.height = 100
   1210         self.textEdit = EditPlainText(parent=self, maxLength=1000, align=uiconst.TOALL, idx=0, pos=(0, 0, 0, 0))
   1211         self.textEdit.HideBackground()
   1212         self.textEdit.RemoveActiveFrame()
   1213         uicore.registry.SetFocus(self.textEdit)
   1214         self.textEdit.SetValue(setValue, cursorPos=len(setValue))
   1215         self.sr.cookie = uicore.event.RegisterForTriuiEvents(uiconst.UI_MOUSEDOWN, self.OnGlobalMouseDown)
   1216 
   1217     def OnGlobalMouseDown(self, *args):
   1218         mo = uicore.uilib.mouseOver
   1219         if mo is self or mo.IsUnder(self):
   1220             return True
   1221         if self.textEdit:
   1222             val = self.textEdit.GetValue()
   1223             self.sampleText.Show()
   1224             self.sampleText.text = val or self.DEFAULTTEXT
   1225             self.textEdit.Close()
   1226             self.textEdit = None
   1227         return False
   1228 
   1229     def ChangeColor(self, color):
   1230         MoveableRect.ChangeColor(self, color)
   1231         if getattr(self, 'sampleText', None):
   1232             self.sampleText.SetTextColor(color)
   1233             self.underlay.SetRGBA(*color)
   1234             self.underlay.opacity = 0.1
   1235 
   1236     def UpdateProportionalPosition(self, newScaling):
   1237         parWidth, parHeight = self.parent.GetAbsoluteSize()
   1238         pl, pr, pt, pb = self.proportionalPosition
   1239         self.left = int(pl * parWidth)
   1240         self.top = int(pt * parHeight)
   1241         self.sampleText.fontsize = int(BASEFONTSIZE * newScaling / self.originalScaling)
   1242         if newScaling == 1.0:
   1243             self.sampleText.useSizeFromTexture = True
   1244             self.sampleText.Layout()
   1245             self.width = self.sampleText.textwidth + 16
   1246             self.height = self.sampleText.textheight + 16
   1247 
   1248 
   1249 def GetServerVersion(serverBuild, serverVersion):
   1250     return '%s - %s.%s' % (boot.codename, serverVersion, serverBuild)