574 lines
20 KiB
Python
Executable File
574 lines
20 KiB
Python
Executable File
#!/usr/bin/python
|
|
'''
|
|
=========================================================================================================
|
|
T&T Software and Systems
|
|
Project: Renumbering tool
|
|
Module: Main module
|
|
Description: Tool to renumber ohoto and video replace VB one
|
|
|
|
|
|
Tiziano Trabattoni | 2016/12/10 | first version
|
|
Tiziano Trabattoni | 2022/07/31 | Porting su MacOS, PyQt5, python3
|
|
=========================================================================================================
|
|
|
|
Usefull link: http://www.freeformatter.com/json-formatter.html#ad-output
|
|
|
|
'''
|
|
|
|
from collections import OrderedDict
|
|
import sys
|
|
import os
|
|
import shutil
|
|
import time
|
|
import logging
|
|
import threading
|
|
from datetime import datetime
|
|
import tempfile
|
|
import json
|
|
|
|
''' PyQT4 imports'''
|
|
from PICT_numtool_ui import Ui_MainWindow
|
|
from PyQt6.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView as QabsW
|
|
from PyQt6 import QtGui
|
|
#from PyQt5.QtCore import *
|
|
from PyQt6 import QtCore
|
|
|
|
''' Module version '''
|
|
mod_ver = "v1.1"
|
|
mod_dver = "2022:07:31"
|
|
|
|
''' Logger Variables '''
|
|
LOG_FORMAT = '%(asctime)s|%(levelname)-7s|%(funcName)-15s|%(lineno)-5d: %(message)-50s'
|
|
LOG_TIME_FORMAT = '%m-%d %H:%M:%S'
|
|
|
|
PHOTO_filter = 'Images (*.jpg *.jpeg *.png *.tiff *.heic)'
|
|
MOVIE_filter = 'Movies (*.mov *.avi *.mp4 *.wmk)'
|
|
|
|
FILE_TREE_CNTL = [('Filename.', 300, QTreeView),
|
|
('FullNAme', 600, QtGui.QTextItem), ('Preview', 100, QLabel)]
|
|
|
|
YEARS_RANGE = [1970, 2040]
|
|
|
|
COL_WHITE = "QPushButton { color: white;}"
|
|
COL_RED = "QPushButton { color: red;}"
|
|
COL_GREEN = "QPushButton { color: green;}"
|
|
COL_YELLOW = "QPushButton { color: yellow;}"
|
|
COL_BLUE = "QPushButton { color: blue;}"
|
|
COL_BLACK = "QPushButton { color: black;}"
|
|
|
|
BCOL_WHITE = "background-color: white;"
|
|
BCOL_RED = "background-color: red;"
|
|
BCOL_GREEN = "background-color: green;"
|
|
BCOL_YELLOW = "background-color: yellow;"
|
|
BCOL_CYAN = "background-color: cyan;"
|
|
BCOL_BLACK = "background-color: black;"
|
|
|
|
''' Default instances '''
|
|
DEF_INSTANCE = 'select'
|
|
NON_FILE_CHARACTERS = '\|/*$?:;<>+"'''
|
|
|
|
''' Ordering dictionary '''
|
|
WorkData = {}
|
|
WorkDir = ''
|
|
TmpDir = ''
|
|
|
|
class PICTitem(QTreeWidgetItem):
|
|
'''
|
|
Custom QTreeWidgetItem with Widgets
|
|
'''
|
|
_basename = ""
|
|
_name = ""
|
|
|
|
@property
|
|
def basename(self):
|
|
return self._basename
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
def __init__(self, parent, basename, name):
|
|
'''
|
|
parent (QTreeWidget) : Item's QTreeWidget parent.
|
|
value (str) : Item's name. just an example.
|
|
'''
|
|
|
|
## Init super class ( QtGui.QTreeWidgetItem )
|
|
super(PICTitem, self).__init__(parent)
|
|
|
|
''' '''
|
|
self.setText(0, basename)
|
|
self.setTextAlignment(0, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
|
|
self.setText(1, name)
|
|
self.setTextAlignment(1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
|
|
self._name=name
|
|
self._basename=basename
|
|
|
|
def __repr__(self) -> str:
|
|
return str(self)
|
|
|
|
def __str__(self):
|
|
return f"<class '{self.__class__.__name__}'> Filename:{self.basename}"
|
|
|
|
|
|
''' ======================== MAIN WINDOW CLASS with all methods ================================ '''
|
|
class main(QMainWindow):
|
|
def __init__(self):
|
|
QMainWindow.__init__(self)
|
|
self.ui = Ui_MainWindow()
|
|
self.ui.setupUi(self)
|
|
|
|
''' create Progressbar element '''
|
|
self.pb = PB(self.ui.pb_Run,mi = 0, ma =100)
|
|
|
|
''' set default values '''
|
|
self.day = 0
|
|
self.month = 0
|
|
self.year = 1900
|
|
self.name = ''
|
|
self.fullName = ''
|
|
self.ineditname = False
|
|
self.filecriteria = PHOTO_filter
|
|
self.TmpDir = ''
|
|
self.WorkDir = ''
|
|
self.WorkData = {}
|
|
self.toRename = []
|
|
|
|
|
|
''' set the tree_OUT widget'''
|
|
self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL))
|
|
self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL))
|
|
self.ui.tree_OUT.setUniformRowHeights(True)
|
|
#self.ui.tree_OUT.setSelectionMode(QabsW.ExtendedSelection)
|
|
|
|
''' set all Methods '''
|
|
self.ui.btn_Renumber.setEnabled(False)
|
|
self.ui.btn_Renumber.clicked.connect(self.click_Renumber)
|
|
self.ui.btn_Open.clicked.connect(self.click_Open)
|
|
self.ui.btn_Exit.clicked.connect(self.click_Exit)
|
|
#self.ui.txt_Name.editingFinished.connect(self.edited_Name)
|
|
self.ui.rbtn_Photo.setChecked(True)
|
|
|
|
''' set all combo '''
|
|
for i in range(1,32):
|
|
self.ui.cmb_Day.addItem(str(i))
|
|
for i in range(1,13):
|
|
self.ui.cmb_Month.addItem(str(i))
|
|
for i in range(YEARS_RANGE[0],YEARS_RANGE[1]+1):
|
|
self.ui.cmb_Year.addItem(str(i))
|
|
|
|
''' set date and time '''
|
|
# print datetime.now().strftime('%d/%m/%Y')
|
|
self.day = int(datetime.now().strftime('%d'))
|
|
self.month = int(datetime.now().strftime('%m'))
|
|
self.year = int(datetime.now().strftime('%Y'))
|
|
self.ui.cmb_Day.setCurrentIndex(self.day - 1)
|
|
self.ui.cmb_Month.setCurrentIndex(self.month - 1)
|
|
self.ui.cmb_Year.setCurrentIndex(self.year - YEARS_RANGE[0])
|
|
self.ui.date_Day.setDate(datetime.now())
|
|
self.ui.txt_Name.textChanged.connect(self.changed_Name)
|
|
|
|
######## These are OLD PyQT4 signal sintax ###########
|
|
#''' set Object signals '''
|
|
#''' visualization selection via Radio Button '''
|
|
#self.ui.rbtn_Movies.connect(self.ui.rbtn_Movies, QtCore.SIGNAL("clicked()"),self.selected_rbtn_Movies)
|
|
#self.ui.rbtn_Photo.connect(self.ui.rbtn_Photo, QtCore.SIGNAL("clicked()"), self.selected_rbtn_Photos)
|
|
#self.ui.date_Day.connect(self.ui.date_Day, QtCore.SIGNAL("dateChanged(QDate)"), self.change_date)
|
|
#
|
|
#''' Signals '''
|
|
#self.ui.cmb_Day.connect(self.ui.cmb_Day, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Day)
|
|
#self.ui.cmb_Month.connect(self.ui.cmb_Month, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Month)
|
|
#self.ui.cmb_Year.connect(self.ui.cmb_Year, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Year)
|
|
## self.ui.txt_Name.connect(self.ui.txt_Name, QtCore.SIGNAL("insertPlainText (const Qstring&"), self.changed_Name)
|
|
|
|
''' set Object signals '''
|
|
''' visualization selection via Radio Button '''
|
|
self.ui.rbtn_Movies.clicked.connect(self.selected_rbtn_Movies)
|
|
self.ui.rbtn_Photo.clicked.connect(self.selected_rbtn_Photos)
|
|
self.ui.date_Day.dateChanged.connect(self.change_date)
|
|
self.ui.tree_OUT.itemSelectionChanged.connect(self.on_selectionChanged)
|
|
|
|
''' Signals '''
|
|
self.ui.cmb_Day.currentIndexChanged.connect(self.choose_Day)
|
|
self.ui.cmb_Month.currentIndexChanged.connect(self.choose_Month)
|
|
self.ui.cmb_Year.currentIndexChanged.connect(self.choose_Year)
|
|
|
|
''' other setting '''
|
|
self.ui.pb_Run.setMinimum(0)
|
|
self.ui.pb_Run.setMaximum(100)
|
|
self.ui.pb_Run.setVisible(False)
|
|
|
|
def click_Exit(self):
|
|
sys.exit(0)
|
|
return
|
|
|
|
def QaddFILEChild(self, parent, basename, name):
|
|
item = PICTitem(parent, basename, name)
|
|
return item
|
|
|
|
|
|
def on_selectionChanged(self):
|
|
selected = [ s.basename for s in self.ui.tree_OUT.selectedItems() ]
|
|
for f in self.WorkData:
|
|
if f in selected:
|
|
self.WorkData[f]['rename'] = True
|
|
else:
|
|
self.WorkData[f]['rename'] = False
|
|
return
|
|
|
|
|
|
def click_Open(self):
|
|
''' open source directory '''
|
|
files = QFileDialog.getOpenFileNames(self, "Open Directory", "" , self.filecriteria, None, QFileDialog.DontUseNativeDialog)
|
|
nf = len(files)
|
|
# SHIT now QfileDialog return a Tuple instead of list
|
|
# Has to be addressed as [0]
|
|
# because files[1] contains the Type: <class 'str'>, - Value >Images (*.jpg *.jpeg *.png *.tiff)
|
|
|
|
|
|
if len(files[0]) > 0:
|
|
''' clear and set the tree_OUT widget'''
|
|
self.ui.tree_OUT.clear()
|
|
self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL))
|
|
self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL))
|
|
self.ui.tree_OUT.setUniformRowHeights(True)
|
|
|
|
self.pb.start(steps=nf)
|
|
|
|
''' initialized work list dictionary '''
|
|
self.WorkData = {}
|
|
|
|
#LOGGER.debug("Type of files[] >%s<" % (str(type(files))))
|
|
#self.WorkDir = os.path.dirname(str(files[0]))
|
|
# SHIT now QfileDialog return a Tuple instead of list
|
|
# Has to be addressed as [0]
|
|
# because files[1] contains the Type: <class 'str'>, - Value >Images (*.jpg *.jpeg *.png *.tiff)
|
|
fileuno = files[0][0]
|
|
#LOGGER.debug("fileuno >%s<" % fileuno)
|
|
self.WorkDir = os.path.dirname(fileuno)
|
|
LOGGER.debug("Workdir >%s<" % self.WorkDir)
|
|
|
|
''' tempory dir must be on same disk otherwise is too slow'''
|
|
basedir = os.path.dirname(self.WorkDir)
|
|
self.TmpDir = basedir + '/PICTtool-tmp'
|
|
if not os.path.exists(self.TmpDir):
|
|
os.mkdir(self.TmpDir)
|
|
count = 1
|
|
|
|
#for fname in files[0]:
|
|
# LOGGER.debug("Type: %s, - Value >%s<" % (type(fname), fname))
|
|
|
|
''' for all selected files '''
|
|
for fname in files[0]:
|
|
self.pb.incr()
|
|
''' add an item to Qtree including resizing of pictures '''
|
|
self.add_Qtree_item(fname)
|
|
|
|
basename = os.path.basename(str(fname))
|
|
''' fill dictionary '''
|
|
self.WorkData.update({
|
|
basename : {
|
|
'itemNum' : str(count),
|
|
'fileExt' : self.get_extension(str(fname)),
|
|
'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()),
|
|
'rename' : True
|
|
},
|
|
})
|
|
count += 1
|
|
|
|
self.pb.end()
|
|
return
|
|
|
|
def click_Renumber(self):
|
|
|
|
''' renumber and create a new WorkData for next phases '''
|
|
newWorkData = {}
|
|
|
|
''' only if WorkData Contains elements '''
|
|
nf = len(self.WorkData)
|
|
if nf > 0:
|
|
self.pb.start(steps=nf * 2)
|
|
|
|
# WorkDataKL = self.WorkData.keys()
|
|
# WorkDataKL.sort()
|
|
WorkData_s = OrderedDict(self.WorkData)
|
|
#print(json.dumps(WorkData_s, indent=2))
|
|
#WorkData_s = {k: self.WorkData[k] for k in sorted(self.WorkData)}
|
|
|
|
#for fname in WorkData_s:
|
|
# LOGGER.debug("fname: >%s<" % fname)
|
|
|
|
''' for all files move to temp to avoid overlapping'''
|
|
for fname in WorkData_s:
|
|
if WorkData_s[fname]['rename']:
|
|
self.pb.incr()
|
|
try:
|
|
tmpfile = self.WorkData[fname]['tmpName']
|
|
origfile = self.WorkDir + '/' + fname
|
|
except:
|
|
LOGGER.error('Parsing file attribites [%s] ... Skipping ...' % (fname))
|
|
else:
|
|
try:
|
|
shutil.move(origfile, tmpfile)
|
|
#os.rename(origfile, tmpfile)
|
|
except:
|
|
LOGGER.error('Renaming file [%s] to: %s ... Skipping ...' % (origfile, tmpfile))
|
|
else:
|
|
# LOGGER.debug('Origfile [%s] ==> TmpFile [%s]' % (origfile, tmpfile))
|
|
pass
|
|
|
|
''' for all files move to current directory with new name'''
|
|
count = 1
|
|
for fname in WorkData_s:
|
|
if WorkData_s[fname]['rename']:
|
|
self.pb.incr()
|
|
try:
|
|
tmpfile = self.WorkData[fname]['tmpName']
|
|
extension = self.WorkData[fname]['fileExt']
|
|
count = int(self.WorkData[fname]['itemNum'])
|
|
except:
|
|
LOGGER.error('Parsing file attributes [%s] ... Skipping ...' % (fname))
|
|
else:
|
|
targetfile = self.WorkDir + '/' + self.fullName + '-{0:04d}'.format(count) + extension
|
|
targetfname = self.fullName + '-{0:04d}'.format(count) + extension
|
|
try:
|
|
shutil.move(tmpfile, targetfile)
|
|
#os.rename(tmpfile, targetfile)
|
|
except:
|
|
LOGGER.error('Renaming file [%s] to: %s ... Skipping ...' % (tmpfile , targetfile))
|
|
else:
|
|
#LOGGER.debug('TmpFile [%s] ==> TargetFIle [%s]' % (tmpfile, targetfile))
|
|
LOGGER.info('Renanme [%s] ==> [%s]' % (fname, targetfile))
|
|
pass
|
|
|
|
''' fill dictionary '''
|
|
newWorkData.update({
|
|
targetfname : {
|
|
'itemNum' : str(count),
|
|
'fileExt' : self.get_extension(str(targetfname)),
|
|
'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()),
|
|
'rename' : True
|
|
},
|
|
})
|
|
count += 1
|
|
|
|
self.pb.end()
|
|
|
|
''' also rename directory '''
|
|
basedir = os.path.dirname(self.WorkDir)
|
|
olddir = self.WorkDir
|
|
newdir = basedir + '/' + self.fullName
|
|
try:
|
|
shutil.move(olddir, newdir)
|
|
except:
|
|
LOGGER.error('Renanme of directory to [%s] failed ...Skipping...' % (basedir))
|
|
|
|
''' sel new Current working directory and Workdata to new dictionary '''
|
|
self.WorkDir = newdir
|
|
os.chdir(self.WorkDir)
|
|
self.WorkData = newWorkData
|
|
|
|
''' empties Qtree '''
|
|
self.ui.tree_OUT.clear()
|
|
self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL))
|
|
self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL))
|
|
self.ui.tree_OUT.setUniformRowHeights(True)
|
|
|
|
''' and refill it again '''
|
|
nf = len (self.WorkData)
|
|
# WorkDataKL = self.WorkData.keys()
|
|
# WorkDataKL.sort()
|
|
# WorkData_s = {k: self.WorkData[k] for k in sorted(self.WorkData)}
|
|
WorkData_s = OrderedDict(self.WorkData)
|
|
|
|
|
|
self.pb.start(steps=nf)
|
|
for fname in WorkData_s:
|
|
self.pb.incr()
|
|
self.add_Qtree_item(self.WorkDir + '/' + fname)
|
|
|
|
self.pb.end()
|
|
|
|
return
|
|
|
|
def add_Qtree_item(self, fname):
|
|
basefile = os.path.basename(str(fname))
|
|
|
|
if self.ui.chk_Preview.isChecked():
|
|
''' review is activated need to elaborate thumbnail '''
|
|
item = self.QaddFILEChild(self.ui.tree_OUT, basefile, str(fname))
|
|
try:
|
|
''' rezize file '''
|
|
img = QtGui.QImage(fname)
|
|
qim = img.scaled(64, 64, QtCore.Qt.KeepAspectRatioByExpanding, QtCore.Qt.SmoothTransformation)
|
|
pict = QPixmap.fromImage(qim)
|
|
except:
|
|
LOGGER.error('Unable to process image [%s] ... Skipping ...' % (fname))
|
|
label = QLabel()
|
|
label.setText("Preview Error")
|
|
else:
|
|
''' create a Label with picture as background '''
|
|
label = QLabel()
|
|
label.setPixmap(pict)
|
|
|
|
''' add column 2 a Label with picture in pixmap '''
|
|
item.treeWidget().setItemWidget(item, 2, label)
|
|
else:
|
|
item = self.QaddFILEChild(self.ui.tree_OUT, basefile, str(fname))
|
|
return item
|
|
|
|
def get_extension(self, fname):
|
|
basename = os.path.basename(fname) # os independent
|
|
ext = '.'.join(basename.split('.')[1:])
|
|
return '.' + ext if ext else None
|
|
|
|
def change_date(self):
|
|
self.day = self.ui.date_Day.date().day()
|
|
self.month = self.ui.date_Day.date().month()
|
|
self.year = self.ui.date_Day.date().year()
|
|
self.ui.cmb_Day.setCurrentIndex(self.day - 1)
|
|
self.ui.cmb_Month.setCurrentIndex(self.month - 1)
|
|
self.ui.cmb_Year.setCurrentIndex(self.year - YEARS_RANGE[0] )
|
|
|
|
''' set fullname '''
|
|
self.set_fullName()
|
|
|
|
return
|
|
|
|
def changed_Name(self):
|
|
if self.ineditname:
|
|
return
|
|
else:
|
|
tc = self.ui.txt_Name.textCursor()
|
|
name = str(self.ui.txt_Name.toPlainText())
|
|
filename = "".join(i for i in name if i not in NON_FILE_CHARACTERS)
|
|
self.ineditname = True
|
|
self.ui.txt_Name.setPlainText(filename)
|
|
self.ui.txt_Name.setTextCursor(tc)
|
|
self.ineditname = False
|
|
self.name = filename
|
|
self.set_fullName()
|
|
return
|
|
|
|
def selected_rbtn_Movies(self):
|
|
self.filecriteria = MOVIE_filter
|
|
return
|
|
|
|
def selected_rbtn_Photos(self):
|
|
self.filecriteria = PHOTO_filter
|
|
return
|
|
|
|
def choose_Day(self):
|
|
self.day = int(self.ui.cmb_Day.currentText())
|
|
self.set_fullName()
|
|
return
|
|
|
|
def choose_Month(self):
|
|
self.month = int(self.ui.cmb_Month.currentText())
|
|
self.set_fullName()
|
|
return
|
|
|
|
def choose_Year(self):
|
|
self.year = int(self.ui.cmb_Year.currentText())
|
|
self.set_fullName()
|
|
return
|
|
|
|
def set_qtree_headers(self, tree_ctnl):
|
|
tree_HEADERS = []
|
|
i = 0
|
|
for treeitem in tree_ctnl:
|
|
tree_HEADERS.append(treeitem[0])
|
|
self.ui.tree_OUT.setColumnWidth(i, treeitem[1])
|
|
i += 1
|
|
return tree_HEADERS
|
|
|
|
def set_fullName(self):
|
|
self.fullName = '{0:4d}{1:02d}{2:02d}-{3:s}'.format(self.year, self.month, self.day, self.name)
|
|
self.ui.txt_FullName.setText(self.fullName)
|
|
if len(self.fullName) > 9:
|
|
self.ui.btn_Renumber.setEnabled(True)
|
|
else:
|
|
self.ui.btn_Renumber.setEnabled(False)
|
|
|
|
def msg_print(self, msg):
|
|
self.ui.txt_Error.setText(str(msg))
|
|
self.repaint()
|
|
|
|
''' ========================= Other Functions ==================================================='''
|
|
''' progress bar class and methods '''
|
|
class PB():
|
|
def __init__(self, progressbar, mi = 0, ma = 100 ):
|
|
# super(PB, self).__init__(parent)
|
|
self.val = 0
|
|
self.min = mi
|
|
self.max = ma
|
|
self.steps = 10
|
|
self.step = (self.max - self.min)/self.steps
|
|
self.progressbar = progressbar
|
|
self.progressbar.setMinimum(self.min)
|
|
self.progressbar.setMaximum(self.max)
|
|
|
|
def start(self, steps=10):
|
|
self.steps = (steps if steps > 0 else 10)
|
|
self.progressbar.setVisible(True)
|
|
self.progressbar.setValue(0)
|
|
|
|
def end(self):
|
|
self.progressbar.setValue(self.max)
|
|
# start a timer to make progressbar invisible
|
|
threading.Timer(3,self.clear).start()
|
|
|
|
def clear(self):
|
|
self.progressbar.setVisible(False)
|
|
|
|
def upd(self, val):
|
|
self.val = val
|
|
self.progressbar.setValue(self.val)
|
|
|
|
def incr(self):
|
|
actv = self.progressbar.value()
|
|
newv = actv + self.step
|
|
if newv <= self.max:
|
|
self.progressbar.setValue(newv)
|
|
|
|
|
|
'''=========================================================================================================================='''
|
|
''' Main Program '''
|
|
if __name__ == '__main__':
|
|
|
|
global LOGGER
|
|
|
|
''' Enabling Logger
|
|
'''
|
|
prog_name = os.path.basename(sys.argv[0:][0])
|
|
LOGGER = logging.getLogger(__name__)
|
|
LOGGER.setLevel(logging.DEBUG)
|
|
LOGGER.propagate = False
|
|
logging.captureWarnings(True)
|
|
formatter = logging.Formatter(LOG_FORMAT, LOG_TIME_FORMAT)
|
|
|
|
''' File logging
|
|
'''
|
|
log_name = "LOG-" + prog_name + ".log"
|
|
fh = logging.FileHandler(log_name)
|
|
fh.setLevel(logging.DEBUG)
|
|
fh.setFormatter(formatter)
|
|
LOGGER.addHandler(fh)
|
|
|
|
''' Console logging
|
|
'''
|
|
cl = logging.StreamHandler(sys.stdout)
|
|
cl.setLevel(logging.DEBUG)
|
|
cl.setFormatter(formatter)
|
|
LOGGER.addHandler(cl)
|
|
|
|
time.sleep(1)
|
|
|
|
LOGGER.info("%s: Ver %s - Date: %s" % (prog_name, mod_ver, mod_dver))
|
|
|
|
''' Open window and give control to QTPY '''
|
|
app = QApplication(sys.argv)
|
|
window = main()
|
|
window.show()
|
|
sys.exit(app.exec_())
|