|
# -*- coding: utf-8 -*- |
|
#coded using python 3.8 (under Windows 10) |
|
|
|
#ToDo: |
|
# * Allow scaling of the viewport for vector based roms.... |
|
# .... maybe via a percentage slider, with some arbitrary minimum, |
|
# .... or by an "InputText"-element for the reference axis pixel dimension .!?. |
|
# * SRC to PAR=1 Calculations -> utilize choosen reference axis, don't stick to |
|
# .... default one. &Xtra: avoid downscaling! |
|
|
|
import sys |
|
import os |
|
import re |
|
import textwrap |
|
from fractions import Fraction |
|
#import numpy as np //not needed so far |
|
#& there may be "better" ways/libraries for this use-case, but for now I stick to pandas... |
|
import pandas as pd |
|
import mpmath as mp |
|
import PySimpleGUI as sg |
|
import clipboard as clip |
|
#using mpmath - for mp.identify() and simply because I really, really hate the trouble with floats .... |
|
#setting mpmaths precision to 100 decimals |
|
mp.mp.dps=100 |
|
|
|
# ------------------ Setup GUI ------------------ |
|
def SetupUI(dbase, tx, ty): |
|
#dbase pandas dataframe |
|
#tx,ty width/height of the target frame |
|
|
|
ltxtrcol=44 #length for the textelements of the UIs right column |
|
|
|
sg.SetOptions(font=("Courier", 11)) |
|
sg.theme('Default1') |
|
|
|
#The padding had no concept to start with and was added along the way, it is a mess that could need some rework/streamlining ("recognizable concept")... |
|
# ... as is the whole sizing/padding in regards to fonts/OS used :/ |
|
tcol = [[sg.Text(text="*", key='-romdesc-', relief=sg.RELIEF_GROOVE,pad=(0, 0), size=(66, 2))]] |
|
|
|
lcol1 = [[sg.InputText(key='-ROM_IN-', enable_events=False, pad=((1, 9), (3, 5)), size=(15, 1))], #cause of using window.keyboard_events, there is no need for one here |
|
#populate a listbox with all the roms from the database and extend it to include a dummy NIL rom |
|
[sg.Listbox(values=dbase["ROM"].tolist()+["<unlisted rom>"], select_mode=sg.LISTBOX_SELECT_MODE_SINGLE , key='-LBOX-', enable_events=True, pad=((1, 1), 3), size=(15,8))]] |
|
|
|
lcol2 = [[sg.Button(button_text=" - Set Target - ", key="-TARGET-", pad=(5, 1), size=(16, 1))], |
|
[sg.Text(text=" nScale x ", pad=(5, 1)), sg.Combo(values=[0], default_value=0, key="-nScale-", readonly=True, enable_events=True, pad=((0,1), 1), size=(3, 1))]] |
|
|
|
rcol1 = [[sg.Text(text="*", key='-screen-', pad=(9, (0, 0)), size=(ltxtrcol, 1))], |
|
[sg.Text(text="*", key='-DAR_text-', pad=(9,(0, 2)), size=(ltxtrcol, 1))], |
|
[sg.Text(text="*", key='-SAR_xy-', pad=(9, (3, 1)), size=(ltxtrcol, 1))], |
|
[sg.Text(text="*", key='-SAR_ratio-', pad=(9,0), size=(ltxtrcol, 1))], |
|
[sg.Text(text="*",key='-SAR_val-', pad=(9,(0,2)),size=(ltxtrcol, 1))], |
|
[sg.Text(text="*", key='-PAR_ratio-', pad=(9,(2,0)), size=(ltxtrcol, 1))], |
|
[sg.Text(text="*",key='-PAR_val-', pad=(9,0), size=(ltxtrcol, 1))], |
|
[sg.Text(text="*", key='-SimpleSize-', pad=(9, (6, 1)), size=(ltxtrcol, 1))]] |
|
|
|
rcol2=[[sg.Text(text="Targeted Display/Frame: "+tx+" x "+ty, key="-TARGETED-", pad=((5, 0), (2,1)), size=(ltxtrcol-5, 1))], |
|
[sg.Text(text="Axis for integer scaling:", pad=((4, 0), 1), size=(25, 1)), |
|
sg.InputCombo(values=(' horizontal', ' vertical'), default_value=' vertical', key='-AXIS-', readonly=True, enable_events=True, pad=(2, 1), size=(12, 1))]] |
|
|
|
rcol2b=[[sg.Text(text=" ", pad=(0, 0), size=(1, 2)), sg.Button(button_text="copy\nCVP", key="-CVP-", button_color=('#000000','#c0c0c0'), disabled=True, pad=(0, (3,0)), size=(4, 2))]] |
|
|
|
mcol1 = [[sg.Column(lcol1, vertical_alignment='top'), sg.Column(rcol1, vertical_alignment='top')]] |
|
|
|
|
|
mcol2_addextra=False |
|
if dbase["emulation"][1]=="unknown": |
|
if dbase["graphic"][1]=="unknown": |
|
#oldest xml variant I encountered, sound should be present |
|
DRVTTip=" Driver (Sound) Status " |
|
DRVStatus=0 |
|
else: |
|
DRVTTip=" Driver Status " |
|
DRVStatus=1 |
|
mcol2_addextra=True |
|
else: |
|
DRVTTip=" Driver (Emulation) Status " |
|
if dbase["graphic"][1]!="unknown" or dbase["sound"][1]!="unknown": |
|
mcol2_addextra=True |
|
DRVStatus=2 |
|
else: |
|
DRVStatus=3 |
|
|
|
|
|
if mcol2_addextra: |
|
mcol2 = [[sg.Text(text="Category :", font=("Courier", 10), pad=((5, 0),(3, 1)), size=(11, 1)), sg.Text(text="NIL", key='-CAT-', font=("Courier", 10), pad=((0, 6), (3, 1)), size=(61, 1))], |
|
[sg.Text(text="Sampleset :", font=("Courier", 10), pad=((5, 0),1), size=(11, 1)), sg.Text(text="-", key='-SAMPLE-', font=("Courier", 10), pad=((0, 1), 1), size=(18, 1)), |
|
sg.Text(text="CHD... :", font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="------", key='-CHD-', tooltip="-", font=("Courier", 10), pad=(0, 1), size=(34, 1))], |
|
[sg.Text(text="Clone of :", font=("Courier", 10), pad=((5, 0),(1, 2)), size=(11, 1)), sg.Text(text="-", key='-CLONE-', font=("Courier", 10), pad=((0, 1), (1, 2)), size=(18, 1)), |
|
sg.Text(text="Driver :", tooltip=DRVTTip, font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="- (-)", key='-DRIVER-', tooltip=DRVTTip, font=("Courier", 10), pad=(0, 1), size=(34, 1))], |
|
[sg.Text(text=" Graphic :", font=("Courier", 10), pad=((5, 0),(1, 2)), size=(11, 1)), sg.Text(text="-", key='-GFX-', font=("Courier", 10), pad=((0, 1), (1, 2)), size=(18, 1)), |
|
sg.Text(text="Sound :", font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="-", key='-SND-', font=("Courier", 10), pad=(0, 1), size=(18, 1))]] |
|
else: |
|
mcol2 = [[sg.Text(text="Category :", font=("Courier", 10), pad=((5, 0),(3, 1)), size=(11, 1)), sg.Text(text="NIL", key='-CAT-', font=("Courier", 10), pad=((0, 6), (3, 1)), size=(61, 1))], |
|
[sg.Text(text="Sampleset :", font=("Courier", 10), pad=((5, 0),1), size=(11, 1)), sg.Text(text="-", key='-SAMPLE-', font=("Courier", 10), pad=((0, 1), 1), size=(18, 1)), |
|
sg.Text(text="CHD... :", font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="- (-)", key='-CHD-', tooltip="-", font=("Courier", 10), pad=(0, 1), size=(34, 1))], |
|
[sg.Text(text="Clone of :", font=("Courier", 10), pad=((5, 0),(1, 2)), size=(11, 1)), sg.Text(text="-", key='-CLONE-', font=("Courier", 10), pad=((0, 1), (1, 2)), size=(18, 1)), |
|
sg.Text(text="Driver :", tooltip=DRVTTip, font=("Courier", 10), pad=((5, 0),1), size=(8, 1)), sg.Text(text="- (-)", key='-DRIVER-', tooltip=DRVTTip, font=("Courier", 10), pad=(0, 1), size=(34, 1))]] |
|
|
|
lcol3 = [[sg.Text(text="*", key="-CALC_XY-", pad=(5, 3), size=(32, 1))], |
|
[sg.Text(text="*", key="-SCALE_X-", pad=(5, 0), size=(32, 1))], |
|
[sg.Text(text="*", key="-SCALE_Y-", pad=(5, (1, 3)), size=(32, 1))]] |
|
|
|
rcol3 = [[sg.Text(text="*", key="-DCALC_XY-", pad=(0, 3), size=(32, 1))], |
|
[sg.Text(text="*", key="-DSCALE_X-", pad=(0, 0), size=(32, 1))], |
|
[sg.Text(text="*", key="-DSCALE_Y-", pad=(0, (1, 3)), size=(32, 1))]] |
|
|
|
layout = [[sg.Column(tcol, vertical_alignment='top')], |
|
[sg.Frame('', mcol1, relief=sg.RELIEF_RIDGE, vertical_alignment='top', pad=(5, (1, 1)))], |
|
[sg.Frame('', mcol2, key='-MCOL2-', relief=sg.RELIEF_GROOVE, vertical_alignment='top', pad=(5, (1, 3)), metadata=DRVStatus)], |
|
[sg.Column(lcol2, vertical_alignment='top',), sg.Column(rcol2, vertical_alignment='top'), sg.Column(rcol2b, vertical_alignment='top')], |
|
[sg.Column(lcol3, vertical_alignment='top'), sg.Column(rcol3, vertical_alignment='top')]] |
|
window = sg.Window('VPCalc', layout, return_keyboard_events=True, finalize=True) |
|
window.TKroot.focus_force() #If the datfile was choosen via the file requester, out window won't have the focus... which is a PITA IMHO, so force it... |
|
#As setting the "default_values" inside above declaration of the listbox element will not scroll to our NIL element, we are updating the listbox here... |
|
window['-LBOX-'].update(scroll_to_index=len(dbase), set_to_index=len(dbase)) |
|
return window |
|
|
|
# -------------- Queries / Calculations ------------------ |
|
def GetParts(numIN): |
|
# * Determining numenator / denumenator from number |
|
# 1st check if it ain't an int |
|
if mp.isint(numIN): |
|
numParts=[str(int(numIN)), "1"] |
|
else: |
|
mpId=mp.identify(numIN) |
|
#**print("Get Parts for: "+mp.nstr(numIN, 21)) |
|
#type of mpId may vary |
|
if mpId: |
|
if mpId.count("/")==1: |
|
numParts=mpId.strip("()").split("/") |
|
else: |
|
print("mp.identify couldn't find a simple fraction") |
|
numParts=["-1", "-1"] |
|
else: |
|
print("mp.identify returned 'None'") |
|
#numParts=["-1", "-1"] |
|
print("trying Fraction(s) instead...") |
|
# if App breaks here, or UI starts to look ugly... coding time again :/ |
|
Parts=Fraction(float(numIN)).limit_denominator() |
|
numParts=[str(Parts.numerator),str(Parts.denominator)] |
|
#**print("return: ", numParts) |
|
return (numParts) |
|
|
|
def QueryRes(xx, yy): |
|
#validate values for target resolution on arbitary limits (besides being even) |
|
#still "valid" results may be problematic for our scaling/fit calculations :/ |
|
#ToDo - check at least for standart DARs, don't allow arbitary ones... |
|
# Downscaling may still be problematic though.... |
|
if (xx==-1) or (yy==-1): |
|
sg.popup("Can't accept empty fields!") |
|
return False |
|
elif (xx<240) or (yy<240): |
|
sg.popup("Resolution should at least be: 240 x 240!") |
|
return False |
|
elif xx%2!=0 or yy%2!=0: |
|
sg.popup("Only even numbers are acceptable!") |
|
return False |
|
return True |
|
|
|
def getEven(this): |
|
#check if the integer part of this is an even number, if not round to nearest even (So far I don't see any benefit of allowing |
|
# uneven pixelsizes for the rounding of fractional pixeldimensions (the error for the DAR is, in both cases, negligible and |
|
# even numbers are far easier to handle (Example: 224*4/4=298.667, nearest INT 299, nearest even INT 298)) |
|
# |
|
#get nearest int |
|
that=mp.nint(this) |
|
mZero=mp.mpf("0.0") |
|
if mp.fmod(that, "2.0")!=mZero: |
|
if int(this)%2!=0: |
|
that=mp.nint(this+mp.mpf("0.5")) |
|
else: |
|
that=mp.mpf(int(this)) |
|
return that |
|
|
|
def QueryDownscale(DAR, sx, sy, framexy, scaleV): |
|
#!!!!We are only checking if the reference axis and the scaled to DAR minimal other axis is fitting our targetframe |
|
#!!!We are not aborting the calculations if that scaled minimal of the other axis is smaller then it is within the source |
|
frameH=mp.mpf(framexy[0]) |
|
frameV=mp.mpf(framexy[1]) |
|
print("Check Downscale:", sx,"x", sy,"vs.", frameH, "x", frameV) |
|
if scaleV: |
|
print("Query reference: vertical axis ") |
|
if sy>frameV: |
|
print("!!!Target height < source_y") |
|
return True |
|
if frameH<getEven(sy*DAR): |
|
print("!!!Target width < scaled width") |
|
return True |
|
if sx>frameH: |
|
print("***Source_x > scaled width") |
|
else: |
|
print("Query reference: horizontal axis ") |
|
if sx>frameH: |
|
print("!!!Target width < source_x") |
|
return True |
|
if frameV<getEven(sx*(mp.mpf("1.0")/DAR)): |
|
print("!!!Target height < scaled height") |
|
return True |
|
if sy>frameV: |
|
print("***Source_y > scaled height") |
|
return False |
|
|
|
def CalcVectorfit(DAR, sx, sy, framexy): |
|
frameH=mp.mpf(framexy[0]) |
|
frameV=mp.mpf(framexy[1]) |
|
|
|
#downscaling ain't a problem here, reference axis may be(?)... |
|
width=getEven(frameV*DAR) |
|
height=frameV |
|
if width>frameH: |
|
#width based on vertical axis exceeds the target frame_width |
|
width=frameV |
|
height=getEven(frameH*(mp.mpf("1.0")/DAR)) |
|
if height>frameV: |
|
print("Houston, we have a problem! ... in CalcFit for Vectors") |
|
txt_Frame="Calc x Error" |
|
else: |
|
txt_Frame=str(int(width))+" x "+str(int(height)) |
|
scalex=1 #-1 |
|
scaley=1 #-1 |
|
else: |
|
txt_Frame=str(int(width))+" x "+str(int(height)) |
|
scalex=1 #-1 |
|
scaley=1 #-1 |
|
return (txt_Frame, scalex, scaley) |
|
|
|
def CalcFit(DAR, sx, sy, framexy, scaleV): |
|
frameH=mp.mpf(framexy[0]) |
|
frameV=mp.mpf(framexy[1]) |
|
if scaleV: |
|
#Vertical Axis is ref |
|
print("n-scaling: vertical axis ") |
|
max_n=mp.floor(frameV/sy) |
|
height=sy*max_n |
|
width=getEven(height*DAR) |
|
while width>frameH: |
|
max_n-=1 #Shouldn't become 0 (QueryDownscale verified n=1 case) |
|
height=sy*max_n |
|
width=getEven(height*DAR) |
|
scaley=max_n |
|
scalex=width/sx |
|
else: |
|
#Horizontal Axis is ref |
|
print("n-scaling: horizontal axis ") |
|
max_n=mp.floor(frameH/sx) |
|
width=sx*max_n |
|
height=getEven(width*(mp.mpf("1.0")/DAR)) |
|
while height>frameV: |
|
max_n-=1 |
|
width=sx*max_n |
|
height=getEven(width*(mp.mpf("1.0")/DAR)) |
|
scalex=max_n |
|
scaley=height/sy |
|
txt_Frame=str(int(width))+" x "+str(int(height)) |
|
return (txt_Frame, scalex, scaley) |
|
|
|
def CalcMax(DAR, sx, sy, framexy): |
|
#Ok, I bet there is a slick and neat way to deal with that (for all possible target res), |
|
#for now we are doing it step by step and add/change if needed.... |
|
tx=mp.mpf(framexy[0]) |
|
ty=mp.mpf(framexy[1]) |
|
height=ty |
|
width=getEven(ty*DAR ) |
|
if width>tx: |
|
#calculated width exceeds display resolution |
|
width=tx |
|
height=getEven(tx*(mp.mpf("1.0")/DAR)) |
|
if height>ty: |
|
#is this possible???? [late night/early morning question] |
|
print("SCRIPT-ERROR: uncaptured CalcMax case") |
|
return ("Calc x Max Error", "-1.0","-1.0") |
|
txt_Frame=str(int(width))+" x "+str(int(height)) |
|
if sx==-1: #check for vector screen (could have well been sy to query upon) |
|
scalex=1 #-1 |
|
scaley=1 #-1 |
|
else: |
|
scalex=width/sx |
|
scaley=height/sy |
|
return (txt_Frame, scalex, scaley) |
|
|
|
def CalcFrame(DAR, axis, nscale): |
|
#nScale the reference axis and calculate the othe one from DAR |
|
RefAx=axis*nscale |
|
OtherAx=getEven(RefAx*DAR) |
|
return (str(int(RefAx)), str(int(OtherAx))) |
|
|
|
# ----------------- UI Updates ------------------ |
|
# Display for invalid rom names |
|
def BlankInfo(window): |
|
#No valid 'rom' set, so we need to blank our displayed information |
|
window['-romdesc-'].update(value="NIL - the missing rom") |
|
window['-CAT-'].update(value="NIL") |
|
#window['-cloneof-'].update(value="Clone of: lostrom") |
|
window['-CLONE-'].update(value="-") |
|
window['-CHD-'].update(value="-----") |
|
window['-SAMPLE-'].update(value="-") |
|
window['-screen-'].update(value="Screen: none [none]") |
|
window['-DAR_text-'].update(value="DAR = # / # = #.#") |
|
window['-SAR_xy-'].update(value="Source: ... x ... px") |
|
window['-SAR_ratio-'].update(value="SAR = # / #") |
|
window['-SAR_val-'].update(value=" = #.#") |
|
window['-PAR_ratio-'].update(value="PAR = # / #") |
|
window['-PAR_val-'].update(value=" = #.#") |
|
window['-SimpleSize-'].update(value="SRC to 1/1 PAR: ... x ...") |
|
window['-CALC_XY-'].update(value="nScaled_Frame: ... x ...") |
|
window['-SCALE_X-'].update(value="Scale: (Source X) x #.#") |
|
window['-SCALE_Y-'].update(value="Scale: (Source Y) x #.#") |
|
window['-DCALC_XY-'].update(value="max_Frame: ... x ...") |
|
window['-DSCALE_X-'].update(value="Scale: (Source X) x #.#") |
|
window['-DSCALE_Y-'].update(value="Scale: (Source Y) x #.#") |
|
if window['-MCOL2-'].metadata==1: |
|
window['-DRIVER-'].update(value="-") |
|
window['-GFX-'].update(value="-") |
|
window['-SND-'].update(value="-") |
|
else: |
|
window['-DRIVER-'].update(value="- (-)") |
|
if window['-MCOL2-'].metadata==2: |
|
window['-GFX-'].update(value="-") |
|
window['-SND-'].update(value="-") |
|
window['-nScale-'].update(value=0, values=[0]) |
|
|
|
def UpdateCalc(window, dbase, rIdx, ndigits, ltxt, VPxy, scaleV): |
|
#Calculate our (n)scaled Viewport |
|
print("Update calculations for:", dbase["ROM"][rIdx]) |
|
|
|
sx=mp.mpf(dbase["sx"][rIdx]) |
|
sy=mp.mpf(dbase["sy"][rIdx]) |
|
|
|
if dbase["H|V"][rIdx]=="H": |
|
DAR=mp.fraction(4,3) |
|
else: |
|
DAR=mp.fraction(3,4) |
|
|
|
if dbase["Screen"][rIdx]=="vector": |
|
txt_Calc=CalcVectorfit(DAR, sx, sy, VPxy) |
|
elif QueryDownscale(DAR, sx, sy, VPxy, scaleV): |
|
#integer downscaling of the reference axis ain't possible |
|
txt_Calc=["Calc x Error (Downscaling)", "#.#","#.#"] |
|
else: #Screentype is Raster and simple n=1 scaling of the reference axis is valid |
|
txt_Calc=CalcFit(DAR, sx, sy, VPxy, scaleV) |
|
txt_DCalc=CalcMax(DAR, sx, sy, VPxy) |
|
|
|
window['-CALC_XY-'].update(value="nScaled_Frame: "+txt_Calc[0]) |
|
window['-SCALE_X-'].update(value="Scale: (Source X) x "+mp.nstr(txt_Calc[1], ndigits)) |
|
window['-SCALE_Y-'].update(value="Scale: (Source Y) x "+mp.nstr(txt_Calc[2], ndigits)) |
|
window['-DCALC_XY-'].update(value="max_Frame: "+txt_DCalc[0]) |
|
window['-DSCALE_X-'].update(value="Scale: (Source X) x "+mp.nstr(txt_DCalc[1], ndigits)) |
|
window['-DSCALE_Y-'].update(value="Scale: (Source Y) x "+mp.nstr(txt_DCalc[2], ndigits)) |
|
|
|
return txt_Calc |
|
|
|
def setInfo_nDis(window, dbase, rIdx): |
|
BlankInfo(window) |
|
#20201126 Update the UI for multidisplay Item |
|
# Lines below are modified copyNpastes form UpdateInfo |
|
#20201205 Excluded Screen!=Raster|Vector from the calculation |
|
if dbase["multidisplay"][rIdx]=="False": |
|
window['-screen-'].update(value="Screen: "+dbase["Screen"][rIdx]) |
|
txt_SARxy="Source: "+dbase["sx"][rIdx]+" x "+dbase["sy"][rIdx]+" (?Pixel?)" |
|
window['-SAR_xy-'].update(value=txt_SARxy) |
|
else: |
|
window['-screen-'].update(value="Screen: multiple Displays") |
|
cloneof=dbase["cloneof"][rIdx] |
|
if cloneof=="none": |
|
cloneof="-" |
|
sampleset=dbase["sampleof"][rIdx] |
|
if sampleset=="none": |
|
sampleset="-" |
|
CHD=dbase["chd"][rIdx] |
|
if CHD=="none": |
|
CHD="-" |
|
#CHD textelement is of size 36 (see SetupUI) |
|
CHDtooltip=CHD |
|
if len(CHD)>36: |
|
CHD=CHD[0:32]+"..." |
|
driver=dbase["driver"][rIdx] |
|
if window['-MCOL2-'].metadata==0: |
|
driver+=" ("+dbase["sound"][rIdx]+")" |
|
elif window['-MCOL2-'].metadata in (2, 3): |
|
driver+=" ("+dbase["emulation"][rIdx]+")" |
|
category=dbase["Category"][rIdx] |
|
#... Do we got subtypes? |
|
if (dbase["Subtype"][rIdx]!="<_generic_>"): |
|
category+=" - "+str(dbase["Subtype"][rIdx]) |
|
#dtype was set to str in reading the database, so no boolean compare, but to string instead |
|
if dbase["Mature"][rIdx]=="True": |
|
category+=" *MATURE*" |
|
window['-romdesc-'].update(value="\n".join(textwrap.wrap(dbase["Desc"][rIdx], width=64))) |
|
window['-CLONE-'].update(value=cloneof) |
|
window['-CAT-'].update(value=category) |
|
window['-SAMPLE-'].update(value=sampleset) |
|
window['-CHD-'].update(value=CHD) |
|
window['-CHD-'].SetTooltip(CHDtooltip) |
|
window['-DRIVER-'].update(value=driver) |
|
if window['-MCOL2-'].metadata in (1, 2): |
|
window['-GFX-'].update(dbase["graphic"][rIdx]) |
|
window['-SND-'].update(dbase["sound"][rIdx]) |
|
|
|
# Display the Information for an actual rom |
|
def UpdateInfo(window, dbase, rIdx, ndigits, ltxt, VPxy, DRVStatus): # UpdateInfo returns scaleV |
|
#variables: window element, rom database, roms Listindex, significant digits to print, maximal length for the output string |
|
#**print("UpdateInfo() invoked. Digits to print: "+str(ndigits)) |
|
#sane ltxt value? |
|
if ltxt<14: #assume space for at least ndigits of 6 |
|
ltxt=14 |
|
elif ltxt>103: # totally arbitrary |
|
ltxt=103 |
|
#limit for ndigits: ltxt - 7 >> rowtitle (6 chars) + radix point (1 char) |
|
# and (assuming that the integer part of the fraction ain't >1 digit, ndigits should at least be 2 |
|
if ndigits < 2: |
|
ndigits=2 |
|
elif ndigits > (ltxt-7): |
|
ndigits=ltxt-7 |
|
|
|
#Display the current roms full description (human readable title) |
|
window['-romdesc-'].update(value="\n".join(textwrap.wrap(dbase["Desc"][rIdx], width=64))) |
|
#Build informal strings and update the UI |
|
txt_screen="Screen: " |
|
txt_DAR="DAR = " |
|
txt_SARratio="SAR = " |
|
txt_PARratio="PAR = " |
|
txt_SARval=" = " |
|
txt_PARval=" = " |
|
txt_SimpleSize="SRC to 1/1 PAR: " |
|
sx=dbase["sx"][rIdx] |
|
sy=dbase["sy"][rIdx] |
|
simple_x=mp.mpf(sx) |
|
simple_y=mp.mpf(sy) |
|
cloneof=dbase["cloneof"][rIdx] |
|
if cloneof=="none": |
|
cloneof="-" |
|
sampleset=dbase["sampleof"][rIdx] |
|
if sampleset=="none": |
|
sampleset="-" |
|
CHD=dbase["chd"][rIdx] |
|
if CHD=="none": |
|
CHD="-" |
|
#CHD textelement is of size 36 (see SetupUI) |
|
CHDtooltip=CHD |
|
if len(CHD)>36: |
|
CHD=CHD[0:32]+"..." |
|
driver=dbase["driver"][rIdx] |
|
if window['-MCOL2-'].metadata==0: |
|
driver+=" ("+dbase["sound"][rIdx]+")" |
|
elif window['-MCOL2-'].metadata in (2, 3): |
|
driver+=" ("+dbase["emulation"][rIdx]+")" |
|
category=dbase["Category"][rIdx] |
|
#... Do we got subtypes? |
|
if (dbase["Subtype"][rIdx]!="<_generic_>"): |
|
category+=" - "+str(dbase["Subtype"][rIdx]) |
|
#dtype was set to str in reading the database, so no boolean compare, but to string instead |
|
if dbase["Mature"][rIdx]=="True": |
|
category+=" *MATURE*" |
|
#Simple Setup of the Basic Information |
|
txt_SARxy="Source: "+sx+" x "+sy+" (Pixel)" |
|
#For the displayed DAR Information we don't need any variable, as we are either using 4:3 or 3:4, depending on the orientation of the game [H/V] |
|
#though it is still needed later to calculate the PAR |
|
if dbase["H|V"][rIdx]=="H": |
|
txt_angle=" [Horizontal]" |
|
txt_DAR+=" 4 / 3 = 1.333333333..." |
|
DAR=mp.fraction(4, 3) |
|
window["-AXIS-"].update(value=" vertical") |
|
scaleV=True |
|
else: |
|
txt_angle=" [Vertical]" |
|
txt_DAR+=" 3 / 4 = 0.75" |
|
DAR=mp.fraction(3, 4) |
|
window["-AXIS-"].update(value=" horizontal") |
|
scaleV=False |
|
if dbase["Screen"][rIdx]=="vector": |
|
txt_screen+="Vector"+txt_angle |
|
txt_SARxy="" |
|
#thats it, no further info for vector based games needed |
|
else: #Screentype is Raster (?) and simple n=1 scaling of the reference axis is valid |
|
txt_screen+="Raster"+txt_angle |
|
SAR=simple_x/simple_y |
|
SARparts=GetParts(SAR) |
|
#https://en.wikipedia.org/wiki/Pixel_aspect_ratio |
|
PAR=DAR/SAR |
|
PARparts=GetParts(PAR) |
|
#(for now no len check/restriction for the fractionals) |
|
#But make sure we got valid ones |
|
if "-1" in SARparts: |
|
txt_SARratio+="couldn't compute a fraction for it" |
|
else: |
|
txt_SARratio+=" / ".join(SARparts) |
|
if "-1" in PARparts: |
|
txt_PARratio+="couldn't compute a fraction for it" |
|
else: |
|
txt_PARratio+=" / ".join(PARparts) |
|
txt_SARval+=mp.nstr(SAR, ndigits) |
|
txt_PARval+=mp.nstr(PAR, ndigits) |
|
#Simple Resize of the SAR to 1:1 PAR |
|
if txt_angle==" [Horizontal]": |
|
simple_x=simple_y*DAR |
|
#**("Simple: ",mp.nstr(simple_x,6)," x ",mp.nstr(simple_y,6)," AR=", (simple_x/simple_y)) |
|
simple_x=getEven(simple_x) |
|
print("Simple: ",mp.nstr(simple_x,6)," x ",mp.nstr(simple_y,6)," AR=", mp.nstr((simple_x/simple_y), 8)) |
|
else: |
|
simple_y=simple_x*(mp.mpf("1.0")/DAR) |
|
#**print("Simple: ",simple_x," x ",mp.nstr(simple_y,6)," AR=", (simple_x/simple_y)) |
|
simple_y=getEven(simple_y) |
|
print("Simple: ",simple_x," x ",mp.nstr(simple_y,6)," AR=", mp.nstr((simple_x/simple_y), 8)) |
|
txt_SimpleSize+=(str(int(simple_x))+" x "+str(int(simple_y))) |
|
#finally update the UI |
|
window['-screen-'].update(value=txt_screen) |
|
window['-DAR_text-'].update(value=txt_DAR) |
|
window['-SAR_xy-'].update(value=txt_SARxy) |
|
window['-SAR_ratio-'].update(value=txt_SARratio) |
|
window['-SAR_val-'].update(value=txt_SARval) |
|
window['-PAR_ratio-'].update(value=txt_PARratio) |
|
window['-PAR_val-'].update(value=txt_PARval) |
|
window['-SimpleSize-'].update(value=txt_SimpleSize) |
|
window['-CLONE-'].update(value=cloneof) |
|
window['-CAT-'].update(value=category) |
|
window['-SAMPLE-'].update(value=sampleset) |
|
window['-CHD-'].update(value=CHD) |
|
window['-CHD-'].SetTooltip(CHDtooltip) |
|
window['-DRIVER-'].update(value=driver) |
|
if window['-MCOL2-'].metadata in (1, 2): |
|
window['-GFX-'].update(dbase["graphic"][rIdx]) |
|
window['-SND-'].update(dbase["sound"][rIdx]) |
|
return scaleV |
|
|
|
#Set new values for the targeted resolution |
|
def SetTarget(tx, ty): |
|
layout=[[sg.Text(text="Set Target Frame")], |
|
[sg.Text(text="width = "), sg.InputText(key="-IN_TX-", enable_events=True, size=(8, 1))], |
|
[sg.Text(text="height = "), sg.InputText(key="-IN_TY-", enable_events=True, size=(8, 1))], |
|
[sg.Button(button_text="SET", key="-TSet-"), sg.Button(button_text="Cancel", key="-TCancel-")]] |
|
|
|
window=sg.Window("VPC: Set Target Size", layout, disable_minimize=True, modal=True, finalize=True) |
|
while True: |
|
event, values = window.read() |
|
#**print("SetTargetEvents>> ", event, values) |
|
if (event == sg.WIN_CLOSED) or (event== "-TCancel-"): |
|
break |
|
#Allow only numbers as input, restrict numer of digits to 5 |
|
if event == '-IN_TX-': |
|
if values['-IN_TX-'] and values['-IN_TX-'][-1] not in ('0123456789'): |
|
window['-IN_TX-'].update(values['-IN_TX-'][:-1]) |
|
elif len(values['-IN_TX-'])>5: |
|
window['-IN_TX-'].update(values['-IN_TX-'][:-1]) |
|
elif event == '-IN_TY-': |
|
if values['-IN_TY-'] and values['-IN_TY-'][-1] not in ('0123456789'): |
|
window['-IN_TY-'].update(values['-IN_TY-'][:-1]) |
|
elif len(values['-IN_TY-'])>5: |
|
window['-IN_TY-'].update(values['-IN_TY-'][:-1]) |
|
elif event=="-TSet-": |
|
txt=window['-IN_TX-'].Get() |
|
if len(txt)>0: |
|
rx=int(txt) |
|
else: |
|
rx=-1 |
|
txt=window['-IN_TY-'].Get() |
|
if len(txt)>0: |
|
ry=int(txt) |
|
else: |
|
ry=-1 |
|
#Return new values only if acceptable, otherwise stay open |
|
if QueryRes(rx, ry): |
|
tx=rx |
|
ty=ry |
|
break |
|
window.make_modal() |
|
|
|
window.close() |
|
return (str(tx), str(ty)) |
|
|
|
def UpdateScale(window, dbase, nscale, scaleV, Idx, ndigits): |
|
sx=mp.mpf(dbase["sx"][Idx]) |
|
sy=mp.mpf(dbase["sy"][Idx]) |
|
DAR=mp.fraction("4","3") #For original reference axis |
|
if scaleV: |
|
#inverted DAR for changed reference axis |
|
if dbase["H|V"][Idx]=="V": |
|
DAR=mp.fraction("3","4") |
|
VP=CalcFrame(DAR, sy, nscale) |
|
txtFrame=VP[1]+" x "+VP[0] |
|
scaleY=mp.mpf(str(nscale)) |
|
scaleX=mp.mpf(VP[1])/sx |
|
else: |
|
#inverted DAR for changed reference axis |
|
if dbase["H|V"][Idx]=="H": |
|
DAR=mp.fraction("3","4") |
|
VP=CalcFrame(DAR, sx, nscale) |
|
txtFrame=VP[0]+" x "+VP[1] |
|
scaleX=mp.mpf(str(nscale)) |
|
scaleY=mp.mpf(VP[1])/sy |
|
window['-CALC_XY-'].update(value="nScaled_Frame: "+txtFrame) |
|
window['-SCALE_X-'].update(value="Scale: (Source X) x "+mp.nstr(scaleX, ndigits)) |
|
window['-SCALE_Y-'].update(value="Scale: (Source Y) x "+mp.nstr(scaleY, ndigits)) |
|
return txtFrame |
|
|
|
# ------------------ |
|
# -- MAIN APP -- |
|
# ------------------ |
|
def VPCalc(database, targetxy=["1600", "1200"]): |
|
#Setting up some base variables |
|
dbItems = len(database["ROM"]) |
|
LBoxIdx = dbItems #selected item in listbox |
|
srcVP="1600 x 1200" #Viewport Dimensions (for Copy 2 Clipboard Info) |
|
|
|
#Setup the UI (ltxtrcol shouldn't be lower then 42) |
|
ltxtrcol = 44 #Size for the textfields within the right column |
|
window = SetupUI(database,targetxy[0],targetxy[1]) |
|
BlankInfo(window) |
|
DRVStatus=window['-MCOL2-'].metadata |
|
|
|
kip_prev=window['-ROM_IN-'].Get() #set initial kip_prev outside of the loop |
|
sigDigits=34 #number of digits to print |
|
scaleV=True |
|
|
|
# ------------- Start Event Loop ------------- |
|
while True: |
|
event, values = window.read() |
|
print("Event read:", event, values) |
|
|
|
# reset some variables |
|
updateINFO=True #Update Rom Information? |
|
updateCALC=False #Update our Calculations? |
|
updateSCALE=False #Update nScale Calculation? |
|
kip=window['-ROM_IN-'].Get() |
|
lkip=len(kip) |
|
#print("prev_kip:"+str(kip_prev)+", kip: "+str(kip)) |
|
#Check kip for valid characters (*), undo unwanted changes |
|
# * >> so far: alphanumerical + underscore |
|
if not bool(re.match("^[A-Za-z0-9_]*$", kip)): |
|
print("Removed Invalid Character from kip") |
|
kip=kip_prev |
|
window['-ROM_IN-'].update(kip) |
|
|
|
#window closed >> terminate app? [20201206 needed to split this up, because the following check would fail if the winow was closed via the UI] |
|
if (event == sg.WIN_CLOSED): |
|
break |
|
#After finding out, that running the script under ubuntu (20.04) does not only mess up with the layout, |
|
#but also that the keycodes (numerical part) are not identical, we are going to ommit them: |
|
if ":" in event: |
|
orgEvent=event |
|
event="!"+event.split(":")[0] |
|
print("Modified",orgEvent," event to:", event) |
|
|
|
#ESC pressed? >> terminate app? |
|
if event=="!Escape": |
|
break |
|
|
|
# CURSOR UP pressed |
|
if event=='!Up': |
|
#lets move one item up within the listbox |
|
if LBoxIdx>1: |
|
LBoxIdx-=1 |
|
#update elements |
|
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
# CURSOR DOWN pressed |
|
elif event=="!Down": |
|
#lets move one item down within the listbox |
|
#should we allow or disallow moving to the NIL item???? -> for now, allow it |
|
if LBoxIdx<dbItems: |
|
LBoxIdx+=1 |
|
#update elements |
|
if LBoxIdx==dbItems: |
|
window['-ROM_IN-'].update("") |
|
else: |
|
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
elif event=="!Home": |
|
LBoxIdx=1 |
|
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
elif event=="!End": |
|
LBoxIdx=dbItems-1 |
|
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
elif event=="!Prior": |
|
LBoxIdx-=7 |
|
if LBoxIdx<1: |
|
LBoxIdx=1 |
|
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
elif event=="!Next": |
|
LBoxIdx+=7 |
|
if LBoxIdx>dbItems: |
|
LBoxIdx=dbItems |
|
window['-ROM_IN-'].update(window['-LBOX-'].GetListValues()[LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
# UI EVENT Listbox |
|
elif event == '-LBOX-': |
|
LBoxIdx=window[event].GetIndexes()[0] |
|
#**print("-LBOX- >>",LBoxIdx, values[event][0]) |
|
#Update Inputfield and return focus to it |
|
if LBoxIdx<dbItems: |
|
kip=values[event][0] |
|
else: |
|
kip="" |
|
window['-ROM_IN-'].update(kip) |
|
window['-ROM_IN-'].SetFocus() |
|
# UI Event Copy Config/Viewport Info to Clipboard |
|
elif event == '-CVP-': |
|
print(srcVP) |
|
VPaxis=srcVP.strip().split(" x ") |
|
# Lets build the VP String |
|
VPtxt="#VPC-Viewport for rom: "+database["ROM"][LBoxIdx]+"\n" |
|
VPtxt+="# 20 = Config, 21 = 1:1 PAR, 22 = Core Provided, 23 = Custom Viewport\naspect_ratio_index = \"23\"\n\n" |
|
VPtxt+="# these two define the pixel size of the emulated screen\n" |
|
VPtxt+="custom_viewport_width = \""+VPaxis[0]+"\"\n" |
|
VPtxt+="custom_viewport_height = \""+VPaxis[1]+"\"\n\n" |
|
VPtxt+="# the following two decide how far from the left and top the game screen is shown\n" |
|
#so far I am not seeing any case where the division by 2 may result in a non int (as the target Dimensons and calculated VPxy results are bound to be even) |
|
VPtxt+="# ("+targetxy[0]+" - "+VPaxis[0]+") / 2 =\n" |
|
VPtxt+="custom_viewport_x = \""+str(int((int(targetxy[0])-int(VPaxis[0]))/2))+"\"\n" |
|
VPtxt+="# ("+targetxy[1]+" - "+VPaxis[1]+") / 2 =\n" |
|
VPtxt+="custom_viewport_y = \""+str(int((int(targetxy[1])-int(VPaxis[1]))/2))+"\"\n" |
|
# and copy it to the clipboard |
|
clip.copy(VPtxt) |
|
updateINFO=False |
|
# UI EVENT set Axis for scaling |
|
elif event == '-AXIS-': |
|
print("changed axis for scaling?") |
|
if (values['-AXIS-']==" vertical" and scaleV==False) or (values["-AXIS-"]==' horizontal' and scaleV==True): |
|
print("axis changed!") |
|
scaleV=not scaleV |
|
if LBoxIdx<dbItems: |
|
updateCALC=True |
|
else: |
|
print("not changed!") |
|
updateINFO=False |
|
window['-ROM_IN-'].SetFocus() |
|
# UI EVENT SetTarget |
|
elif event=="-TARGET-": |
|
targetxy=SetTarget(targetxy[0], targetxy[1]) |
|
window['-TARGETED-'].update("Targeted Display/Frame: "+targetxy[0]+" x "+targetxy[1]) |
|
#ToDo -> seperate updates, here we just need the CalcFit Values... |
|
elif event=="-nScale-": |
|
if database["Screen"][LBoxIdx]!="vector": |
|
nscaleis=int(values['-nScale-']) |
|
window['-nScale-'].update(value=nscaleis) |
|
updateINFO=False |
|
updateSCALE=True |
|
else: |
|
updateINFO=False |
|
window['-ROM_IN-'].SetFocus() |
|
# UI EVENT InputText (simulated) |
|
elif kip_prev!=kip: |
|
#romname changed in input field ?? |
|
#Find 1st matching romname for the text typed so far |
|
LBoxIdx =next((i for i, test in enumerate(database["ROM"]) if kip in test[0:lkip]), -1) |
|
if LBoxIdx ==-1 or kip=='': |
|
#print(dbItems,": Not in List!") |
|
LBoxIdx=dbItems |
|
else: |
|
print("-ROM_IN- >>",LBoxIdx,": "+database["ROM"][LBoxIdx]) |
|
window['-LBOX-'].update(scroll_to_index=LBoxIdx, set_to_index=LBoxIdx) |
|
else: |
|
updateINFO=False |
|
|
|
kip_prev=kip #save current kip value for the next Event |
|
|
|
# Updating rom information if needed |
|
if updateINFO: |
|
#do we need dummy values, because our target is NIL? |
|
if LBoxIdx==dbItems: |
|
BlankInfo(window) |
|
window['-CVP-'].update(disabled=True) |
|
#no, so lets update the fields with some real information! |
|
else: |
|
if database["Screen"][LBoxIdx]!="raster": |
|
window['-nScale-'].update(disabled=True) |
|
window['-AXIS-'].update(disabled=True) |
|
else: |
|
window['-nScale-'].update(disabled=False) |
|
window['-AXIS-'].update(disabled=False) |
|
if database["multidisplay"][LBoxIdx]=="False": |
|
#enable copyVPC |
|
window['-CVP-'].update(disabled=False) |
|
scaleV=UpdateInfo(window, database, LBoxIdx, sigDigits, ltxtrcol, targetxy, DRVStatus) |
|
updateCALC=True |
|
else: |
|
#disable copyVPC |
|
window['-CVP-'].update(disabled=True) |
|
setInfo_nDis(window, database, LBoxIdx) |
|
if updateCALC: |
|
calcScale=UpdateCalc(window, database, LBoxIdx, 8, ltxtrcol, targetxy, scaleV) |
|
srcVP=calcScale[0] |
|
if database["Screen"][LBoxIdx]=="vector": |
|
nscaleis=0 |
|
window['-nScale-'].update(value=nscaleis, values=[0]) |
|
elif database["Screen"][LBoxIdx]=="raster": |
|
if scaleV: |
|
nscaleis=int(calcScale[2]) |
|
window['-nScale-'].update(value=nscaleis, values=list(range(nscaleis, 0, -1))) |
|
else: |
|
nscaleis=int(calcScale[1]) |
|
window['-nScale-'].update(value=nscaleis, values=list(range(nscaleis, 0, -1))) |
|
else: |
|
print("Screen!=Raster|Vector") |
|
window['-CVP-'].update(disabled=True) |
|
setInfo_nDis(window, database, LBoxIdx) |
|
if updateSCALE: |
|
if database["multidisplay"][LBoxIdx]=="False": |
|
srcVP=UpdateScale(window, database, nscaleis, scaleV, LBoxIdx, 8) |
|
else: |
|
print("updateSCALE called for multidisplay item!") #Debug Info.... |
|
|
|
#send an empty line to the terminal |
|
print() |
|
|
|
#Terminate Application |
|
window.close() |
|
|
|
def VPC_StartUp(infile): |
|
#ToDo: (Optional) exclusion of all roms from the database where VPC won't calculate a viewport (wrong screentype, multidisplay, etc.) |
|
print("Reading information from "+infile) |
|
DBase = pd.read_csv(infile,sep="\t", dtype=str, na_filter=False) #dtype=str means columns!=BOOL |
|
DBase.sort_values(by=["ROM"], inplace=True, ignore_index=True) |
|
#ToDo: Check if all needed "columns" (headers) are present |
|
#.... |
|
AllValid=True |
|
n=2 #line 1: headers |
|
for item in DBase["ROM"]: |
|
if not bool(re.match("^[A-Za-z0-9_]*$", item)): |
|
print("Rom Entry (line",n, "):", item, " contains unsupported characters!") |
|
AllValid=False |
|
n+=1 |
|
if AllValid==True: |
|
VPCalc(DBase) |
|
else: |
|
print("Aborted, due to encountered errors within:", infile) |
|
|
|
# ------------- |
|
# -- MAIN -- |
|
# ------------- |
|
#ToDo Filecheck... |
|
#read the database into a pandas dataframe, using 1st row as the column headers (default), and as we are using mpmath: read all columns/values as strings |
|
#ToDo check database rom names for "invalid" characters - and notify the user about it (either extend the check for typed characters inside the VPCalc |
|
#loop, or remove the item from the database (within the sourcefile or from our read in values) ..... |
|
# |
|
#Execute only if not imported, after all & how unlikely it is, I or someone else may import this to run some kind of batch processing over a datfile... |
|
if __name__=="__main__": |
|
infile = sys.argv[1] if len(sys.argv) > 1 else sg.popup_get_file('Choose VPC_ReferenceDat') |
|
#Test if file exists |
|
if os.path.isfile(infile): |
|
VPC_StartUp(infile) |
|
else: |
|
print("Cannot find "+infile+"!") |
|
|
|
|
|
|