From bd26c9a38ac56537d6dd0658fe521571a2d9fe13 Mon Sep 17 00:00:00 2001 From: Taka Date: Tue, 25 Apr 2017 06:35:46 +0000 Subject: [PATCH] Added a startup form that lets the user initialize/load a database. --- _freeze.py | 23 +- asl_cards/__init__.py | 0 asl_cards/__main__.py | 10 +- asl_cards/db.py | 10 +- asl_cards/parse.py | 31 +- asl_cards/tests/test_real_data.py | 5 +- constants.py | 0 main.py | 26 +- main_window.py | 76 ++++- resources/analyze.png | Bin 0 -> 2263 bytes resources/dir_dialog.png | Bin 0 -> 3926 bytes resources/file_dialog.png | Bin 0 -> 1401 bytes resources/load_db.png | Bin 0 -> 3151 bytes resources/open_file.png | Bin 0 -> 4112 bytes resources/stop.png | Bin 0 -> 3359 bytes startup_widget.py | 232 ++++++++++++++ ui/add_card_dialog.ui | 14 +- ui/startup_widget.ui | 513 ++++++++++++++++++++++++++++++ 18 files changed, 876 insertions(+), 64 deletions(-) create mode 100644 asl_cards/__init__.py mode change 100755 => 100644 constants.py mode change 100755 => 100644 main_window.py create mode 100755 resources/analyze.png create mode 100755 resources/dir_dialog.png create mode 100755 resources/file_dialog.png create mode 100755 resources/load_db.png create mode 100755 resources/open_file.png create mode 100755 resources/stop.png create mode 100644 startup_widget.py create mode 100644 ui/startup_widget.ui diff --git a/_freeze.py b/_freeze.py index 1b2d77c..5607fe0 100644 --- a/_freeze.py +++ b/_freeze.py @@ -2,18 +2,31 @@ # FIXME: Get py2exe working for Windows builds (since it produces a single EXE). import sys +import os +import glob from cx_Freeze import setup , Executable from constants import * +base_dir = os.path.split( os.path.abspath(__file__) )[ 0 ] +os.chdir( base_dir ) + +import asl_cards + # --------------------------------------------------------------------- +def get_extra_files( fspec ) : + """Locate extra files to include in the release.""" + fnames = glob.glob( fspec ) + return zip( fnames , fnames ) + # initialize -extra_files = [ - "resources/app.ico" -] +extra_files = [] +extra_files.extend( get_extra_files( "resources/*.ico" ) ) +extra_files.extend( get_extra_files( "resources/*.png" ) ) +extra_files.extend( get_extra_files( "ui/*ui" ) ) build_options = { - "packages": [ "os" ] , + "packages": [ "os" , "sqlalchemy" ] , "excludes": [ "tkinter" ] , "include_files": extra_files , } @@ -30,7 +43,7 @@ setup( version = APP_VERSION , description = APP_DESCRIPTION , options = { - APP_NAME: build_options + "build_exe": build_options } , executables = [ target ] ) diff --git a/asl_cards/__init__.py b/asl_cards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/asl_cards/__main__.py b/asl_cards/__main__.py index b405ea3..2c11346 100755 --- a/asl_cards/__main__.py +++ b/asl_cards/__main__.py @@ -5,8 +5,9 @@ import sys import os import getopt -from parse import PdfParser -import db +sys.path.append( ".." ) # fudge! need this to allow a script to run within a package :-/ +from asl_cards.parse import PdfParser +from asl_cards import db # --------------------------------------------------------------------- @@ -45,9 +46,6 @@ def main( args ) : raise RuntimeError( "Unknown argument: {}".format( opt ) ) if not db_fname : raise RuntimeError( "No database was specified." ) - # initialize - db.open_database( db_fname ) - # do the requested processing pdf_parser = PdfParser( progress_callback if log_progress else None ) if parse_targets : @@ -56,8 +54,10 @@ def main( args ) : cards.extend ( pdf_parser.parse( pt , max_pages=max_pages , images=extract_images ) ) + db.open_database( db_fname , True ) db.add_cards( cards ) elif dump : + db.open_database( db_fname , False ) db.dump_database() else : raise RuntimeError( "No action." ) diff --git a/asl_cards/db.py b/asl_cards/db.py index c702b21..8dd2a8a 100644 --- a/asl_cards/db.py +++ b/asl_cards/db.py @@ -60,11 +60,17 @@ class AslCardImage( DbBase , DbBaseMixin ) : # --------------------------------------------------------------------- -def open_database( fname ) : +def open_database( fname , create ) : """Open the database.""" # open the database is_new = not os.path.isfile( fname ) + if create : + if not is_new : + raise Exception( "File exists: {}".format( fname ) ) + else : + if is_new : + raise Exception( "Can't find file: {}".format( fname ) ) conn_string = "sqlite:///{}".format( fname ) global db_engine db_engine = create_engine( conn_string , convert_unicode=True ) @@ -76,7 +82,7 @@ def open_database( fname ) : db_session.execute( "PRAGMA foreign_keys = on" ) # nb: foreign keys are disabled by default in SQLite # check if we are creating a new database - if is_new : + if create : # yup - make it so DbBase.metadata.create_all( db_engine ) diff --git a/asl_cards/parse.py b/asl_cards/parse.py index f8ed6fe..43de795 100644 --- a/asl_cards/parse.py +++ b/asl_cards/parse.py @@ -14,15 +14,17 @@ from pdfminer.pdfpage import PDFPage import ghostscript from PIL import Image , ImageChops -from db import AslCard , AslCardImage +from asl_cards.db import AslCard , AslCardImage # --------------------------------------------------------------------- class PdfParser: - def __init__( self , progress=None ) : + def __init__( self , progress=None , progress2=None ) : # initialize - self.progress = progress + self.progress = progress # nb: for tracking file progress + self.progress2 = progress2 # nb: for tracking page progress within a file + self.cancelling = False def parse( self , target , max_pages=-1 , images=True ) : """Extract the cards from a PDF file.""" @@ -38,6 +40,7 @@ class PdfParser: # parse each file cards = [] for fname in fnames : + if self.cancelling : raise RuntimeError("Cancelled.") cards.extend( self._do_parse_file( fname , max_pages , images ) ) return cards @@ -49,10 +52,11 @@ class PdfParser: interp = PDFPageInterpreter( rmgr , dev ) cards = [] with open(fname,"rb") as fp : - self._progress( 0 , "Loading file: {}".format( fname ) ) + self._progress( 0 , "Analyzing {}...".format( os.path.split(fname)[1] ) ) pages = list( PDFPage.get_pages( fp ) ) for page_no,page in enumerate(pages) : - self._progress( float(page_no)/len(pages) , "Extracting card info from page {}...".format( 1+page_no ) ) + if self.cancelling : raise RuntimeError("Cancelled.") + self._progress2( float(page_no) / len(pages) ) page_cards = self._parse_page( cards , interp , page_no , page ) cards.extend( page_cards ) if max_pages > 0 and 1+page_no >= max_pages : @@ -64,6 +68,7 @@ class PdfParser: if len(cards) != len(card_images) : raise RuntimeError( "Found {} cards, {} card images.".format( len(cards) , len(card_images) ) ) for i in range(0,len(cards)) : + if self.cancelling : raise RuntimeError("Cancelled.") cards[i].card_image = AslCardImage( image_data=card_images[i] ) return cards @@ -75,12 +80,14 @@ class PdfParser: # locate the info box for each card (in the top-left corner) info_boxes = [] for item in lt_page : + if self.cancelling : raise RuntimeError("Cancelled.") if type(item) is not LTTextBoxHorizontal : continue item_text = item.get_text().strip() if item_text.startswith( ("Vehicle","Ordnance") ) : info_boxes.append( [item] ) # get the details from each info box for item in lt_page : + if self.cancelling : raise RuntimeError("Cancelled.") if type(item) is not LTTextBoxHorizontal : continue # check if the next item could be part of an info box - it must be within the left/right boundary # of the first item (within a certain tolerance), and below it (but not too far) @@ -93,7 +100,6 @@ class PdfParser: # generate an AslCard from each info box for info_box in info_boxes : card = self._make_asl_card( lt_page , info_box ) - self._progress( None , "Found card: {}".format( card ) ) cards.append( card ) return cards @@ -132,7 +138,7 @@ class PdfParser: # FIXME! clean up left-over temp files before we start args = [ s.encode(locale.getpreferredencoding()) for s in args ] # FIXME! stop GhostScript from issuing warnings (stdout). - self._progress( 0 , "Extracting images..." ) + self._progress( 0 , "Extracting images from {}...".format( os.path.split(fname)[1] ) ) ghostscript.Ghostscript( *args ) # figure out how many files were created (so we can show progress) npages = 0 @@ -144,8 +150,9 @@ class PdfParser: # extract the cards from each page card_images = [] for page_no in range(0,npages) : + if self.cancelling : raise RuntimeError("Cancelled.") # open the next page image - self._progress( float(page_no)/npages , "Extracting card images from page {}...".format( 1+page_no ) ) + self._progress2( float(page_no) / npages ) fname = fname_template % (1+page_no) img = Image.open( fname ) img_width , img_height = img.size @@ -196,10 +203,14 @@ class PdfParser: os.unlink( fname ) return buf , rgn.size - def _progress( self , progress , msg ) : + def _progress( self , pval , msg ) : """Call the progress callback.""" if self.progress : - self.progress( progress , msg ) + self.progress( pval , msg ) + def _progress2( self , pval ) : + """Call the progress callback.""" + if self.progress2 : + self.progress2( pval ) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/asl_cards/tests/test_real_data.py b/asl_cards/tests/test_real_data.py index 9a8cb58..b981670 100755 --- a/asl_cards/tests/test_real_data.py +++ b/asl_cards/tests/test_real_data.py @@ -5,8 +5,9 @@ import os import unittest base_dir = os.path.split( __file__ )[ 0 ] -sys.path.append( os.path.join( base_dir , ".." ) ) -from parse import PdfParser , AslCard + +sys.path.append( ".." ) # fudge! need this to allow a script to run within a package :-/ +from asl_cards.parse import PdfParser , AslCard # --------------------------------------------------------------------- diff --git a/constants.py b/constants.py old mode 100755 new mode 100644 diff --git a/main.py b/main.py index fac4884..ae85f0d 100755 --- a/main.py +++ b/main.py @@ -9,7 +9,6 @@ from PyQt5.QtWidgets import QApplication from constants import * import globals -import asl_cards.db as db # --------------------------------------------------------------------- @@ -41,41 +40,36 @@ def do_main( args ) : raise RuntimeError( "Unknown argument: {}".format( opt ) ) if not settings_fname : # try to locate the settings file - settings_fname = os.path.join( globals.base_dir , globals.app_name+".ini" ) + fname = globals.app_name+".ini" if sys.platform == "win32" else "."+globals.app_name + settings_fname = os.path.join( globals.base_dir , fname ) if not os.path.isfile( settings_fname ) : - settings_fname = os.path.split(settings_fname)[ 1 ] - if sys.platform != "win32" : - settings_fname = "." + settings_fname - settings_fname = os.path.join( QDir.homePath() , settings_fname ) + settings_fname = os.path.join( QDir.homePath() , fname ) if not db_fname : - # try to locate the database + # use the default location db_fname = os.path.join( globals.base_dir , globals.app_name+".db" ) - if not os.path.isfile( db_fname ) : - raise RuntimeError( "Can't find database: {}".format( db_fname ) ) # load our settings globals.app_settings = QSettings( settings_fname , QSettings.IniFormat ) fname = os.path.join( os.path.split(settings_fname)[0] , "debug.ini" ) globals.debug_settings = QSettings( fname , QSettings.IniFormat ) - # open the database - db.open_database( db_fname ) - globals.cards = db.load_cards() - # do main processing app = QApplication( sys.argv ) - import main_window - main_window = main_window.MainWindow() + from main_window import MainWindow + main_window = MainWindow( db_fname ) main_window.show() + if os.path.isfile( db_fname ) : + main_window.start_main_app( db_fname ) return app.exec_() # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - def print_help() : - print( "{} {{options}}".format( os.path.split(sys.argv[0])[1] ) ) # FIXME! frozen? + print( "{} {{options}}".format( globals.app_name ) ) print( " {}".format( APP_DESCRIPTION ) ) print() print( " -c --config Config file." ) + print( " -d --db Database file." ) sys.exit() # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/main_window.py b/main_window.py old mode 100755 new mode 100644 index b0a7dd5..8a4b670 --- a/main_window.py +++ b/main_window.py @@ -2,14 +2,15 @@ import sys import os from PyQt5.QtCore import Qt , QPoint , QSize -from PyQt5.QtWidgets import QMainWindow , QVBoxLayout , QHBoxLayout , QWidget , QTabWidget , QLabel +from PyQt5.QtWidgets import QApplication , QMainWindow , QVBoxLayout , QHBoxLayout , QWidget , QTabWidget , QLabel from PyQt5.QtWidgets import QDialog , QMessageBox , QAction from PyQt5.QtGui import QPainter , QPixmap , QIcon , QBrush import asl_cards.db as db from constants import * import globals -import add_card_dialog +from add_card_dialog import AddCardDialog +from startup_widget import StartupWidget # --------------------------------------------------------------------- @@ -42,19 +43,25 @@ class AslCardWidget( QWidget ) : class MainWindow( QMainWindow ) : - def __init__( self ) : + _instance = None + + def __init__( self , db_fname ) : + # initialize super().__init__() + assert MainWindow._instance is None + MainWindow._instance = self # initialize the window self.setWindowTitle( APP_NAME ) self.setWindowIcon( QIcon("resources/app.ico") ) # initialize the menu menu_bar = self.menuBar() file_menu = menu_bar.addMenu( "&File" ) - action = QAction( "&Add" , self ) - action.setShortcut( "Ctrl+A" ) - action.setStatusTip( "Add an ASL Card." ) - action.triggered.connect( self.on_add_card ) - file_menu.addAction( action ) + self.add_card_action = QAction( "&Add" , self ) + self.add_card_action.setEnabled( False ) + self.add_card_action.setShortcut( "Ctrl+A" ) + self.add_card_action.setStatusTip( "Add an ASL Card." ) + self.add_card_action.triggered.connect( self.on_add_card ) + file_menu.addAction( self.add_card_action ) action = QAction( "E&xit" , self ) action.setStatusTip( "Close the program." ) action.triggered.connect( self.close ) @@ -62,22 +69,57 @@ class MainWindow( QMainWindow ) : # load the window settings self.resize( globals.app_settings.value( MAINWINDOW_SIZE , QSize(500,300) ) ) self.move( globals.app_settings.value( MAINWINDOW_POSITION , QPoint(200,200) ) ) - # initialize the window controls + # show the startup form + self.setCentralWidget( + StartupWidget( db_fname , parent=self ) + ) + + def start_main_app( self , db_fname ) : + """Start the main app.""" + # we can now close the startup widget and replace it with the main tab widget self.tab_widget = QTabWidget( self ) self.tab_widget.setTabsClosable( True ) self.setCentralWidget( self.tab_widget ) + # open the database + db.open_database( db_fname , False ) + globals.cards = db.load_cards() + # ask the user to add the first card + self.add_card_action.setEnabled( True ) + self.on_add_card() + + @staticmethod + def show_info_msg( msg ) : + """Show an informational message.""" + QMessageBox.information( MainWindow._instance , APP_NAME , msg ) + @staticmethod + def show_error_msg( msg ) : + """Show an error message.""" + QMessageBox.warning( MainWindow._instance , APP_NAME , msg ) + @staticmethod + def ask( msg , buttons , default ) : + """Show an error message.""" + return QMessageBox.question( MainWindow._instance , APP_NAME , msg , buttons , default ) def closeEvent( self , evt ) : """Handle window close.""" # confirm the close - if globals.app_settings.value( CONFIRM_EXIT , True , type=bool ) : - rc = QMessageBox.question( self , "Confirm close" , - "Do you want to the close the program?" , - QMessageBox.Ok | QMessageBox.Cancel , - QMessageBox.Cancel - ) - if rc != QMessageBox.Ok : + widget = self.centralWidget() + if type(widget) is StartupWidget : + # don't allow this if we are analyzing files + if widget.analyze_thread : + QApplication.beep() evt.ignore() + return + else : + # check if we should confirm the exit + if globals.app_settings.value( CONFIRM_EXIT , True , type=bool ) : + rc = QMessageBox.question( self , "Confirm close" , + "Do you want to the close the program?" , + QMessageBox.Ok | QMessageBox.Cancel , + QMessageBox.Cancel + ) + if rc != QMessageBox.Ok : + evt.ignore() # save the window settings # FIXME! handle fullscreen globals.app_settings.setValue( MAINWINDOW_POSITION , self.pos() ) @@ -89,7 +131,7 @@ class MainWindow( QMainWindow ) : self.close() def on_add_card( self ) : - dlg = add_card_dialog.AddCardDialog( self ) + dlg = AddCardDialog( self ) rc = dlg.exec() if rc == QDialog.Accepted : # add a new tab for the selected card diff --git a/resources/analyze.png b/resources/analyze.png new file mode 100755 index 0000000000000000000000000000000000000000..5a22e4be33cf87b199d756f3547297c4236f9941 GIT binary patch literal 2263 zcmW+&X;@Ro8lEhi1UQgrxOie&f|Vi_B?;DxAQD6o5ZOe*!Y zYg81lYV8#j#9br_TTnnmWf2zwNI*adB6}b`^vBHieD5>!KHvAuyzfkwulFVcy~TO} z00R&A4gOeU&JQw#&4Ku-V_47?xbKJtK%X{0aNvACf`Pj4*fq zEdaz@0w7ESU`mU{&j9>}u?=tmaF777h@Tr=;|2hEi^m3+fVhrOjaKVSuqZh)6;gS; z4ru3QXEftqZuDkMh5W|(n!uRbcsyDB4u{V?6-3$V*zVsllA@gpnN)Yx1Q!PSC@Ui{r_|A6r(>#p{xBHaAbz@ zetVTBOCQb|b_#iTwAWbDRMS{fc9`x*OCCEHE}UM){n`J8W$v^ZuKN$Q_ma9jfxk7$1G0^7Cn^D`~3g#-+J$Bx` zMeI83XQ7h%dWAwg{>2f})s2Fjwyr>(Q|6X3?O57#>_MpqXSqL26$LIR0zefmG3L>e!ya+Dod|hpWOx-gWh@N&FXRj@v7F#&{d_LsxD2$dr zhG8EvS&x-C{r$Ylo}KwFL1KN$(05LyH6s=A0Yoaz+Jl_lS&>*@67_jIdi|3x`j(@3 zVx(f(chE(q6i+=ZeGYeUrk+7Q;^kg!?z+Lxq-xHUL?8Nf2vLpOrd}W@vJ2rx;c34d*Gr<4?ABKUOU&jJMpj1Daq)qCY4Ov6-`BP-I*fSLG(Duan7Zq16Q! z=>elowo&%_Kv3AOAJL|qb*#0IClE{L z(y~&fd#1p`g!r9E^7raLhYRa=c76WgN!#$j&ijWge4pw2S{iPpU?R8@2hv+wT9A)m zRK%2ms$JavJ@KPwMHkXCM6_c)au)6goRbYdynEE(jlP|Jx{l~5iCB@Wm8-kQ63|H7 zPG))BRBl(#%md|8o#&f9cY4BZZrhvX79JwbRZZPjh|I_x6xkL?aqpN~kwsITni7QC z;ePLiI;!2%>A@mPTKvVs6V1_R*`iLiW^&|WSKq`i>@1v|p-^WHvU_`bk${#}h^DgY z-rLqPD}g|e9Q-(ZAil6Qr%(bgjrxZvyV8!qcsw39cTVE@8X57pI`Qe}PL+%nbf#3w z8XFrKKd*#+){A4>Q@D+d2E}iJ=FOQVrjsR!FAXj5>7X!%A~lrEG& ziKP{);KFOmIf({Fm$1V~v?f+N#P65&C%q^vE^bgOyDg#Uh)qy3j|VMz-z4aJzYS8E zr_k??wJ1llB+1Acp&<`<|3rJxTO?V_i=3=_rdxa}h45A?TXnse^v!BnYjAn8-rZfV zjjsPEAlr#_6flJDx9Yq^me2q4DL~9RAIjAo&FL@Wf?KabV1e>It-WDi+o= z)8^MpGT+u64UAevjaqNh>p>mn=b=kdV@dNGdR|K}as+-uUE%Nc6T~x&L8aO`Tv)1U z{uLRLTcJ!eqP!<12x+RAf4!~f7X5lr{H#OT%`=u&dDlL}qLB)C8kN1$?a$)7Z)+TB zoI$+#PBK|jmA-d;@68s;Ia7pf)6fw9ARUwDGRp4q!#VOGvmi;?9d`axLh-oB;9j5X zQ^~=1G0=en2hgYW7Lw(FyWNNkG;1eCUwb%JWCEG4snn6m1HTiEWcQcz1r0#jI(SG)*MhO9+;#lnpnjSMYYOLhdctlY(4;}9Gfc+$>ZyO zf%YYtqm{?wP&t^d$3(FxuvT;dq~Yed|2}Dpb4*qPAy}V z>pBV`z7$Vjga%VfiSq!BfB`xGu9(*ZT22D^y>(U=LJ%5?bYn>7Nm0b|ldvbYlj?WE z38B-^i1J9(8}P!|PF%Ms=Rv0qnGxzFGq|!;D2WuufmLZU`2O+R00)7D10YE}K%-!v P5a6-Vd&8~u+|+*n*`fBb literal 0 HcmV?d00001 diff --git a/resources/dir_dialog.png b/resources/dir_dialog.png new file mode 100755 index 0000000000000000000000000000000000000000..6d0cd5057ae19de66b9af0d2bd454d8b38afd022 GIT binary patch literal 3926 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 literal 0 HcmV?d00001 diff --git a/resources/file_dialog.png b/resources/file_dialog.png new file mode 100755 index 0000000000000000000000000000000000000000..009cb0d259029d1ec8906cfcb80b15f48fd1a850 GIT binary patch literal 1401 zcmZ`(Yfw{H5Z;@cmvBRZGD8p;f>cTbBqS=dpxi*j(1wWkijS5Ud8zoQuWCec6Ep@D zu!Ux+H&m z!4c{#SS?Ra1<+0fAPNCqkd*EP*d_vaq6T=^4B)%*a7J@7721~~lP=oPsd18I0$Q!^ z9c;3PS59;|23{I>jaIe(@vN$Y`=9{*T~}t__h$wHn&6wTp0(nKovtevqcE~AAKO8? zid9<`B`r6mE}TU)?zw^EOhD{*kqHTpV>Q*pMEPBE*4og?=Q-QC`qjwZ+PhPPX~7Nvd=m86ob^bc!Jjf?|YZW;^Na`{#9${)8KmoEA6U_(Q5 z^NcuaRbW^I*Z#C!6eJ*+yFpZQPIPX|fH+<^a9El&?~{h>x0)*}){OaB%BIYuAp%0~ z1uP;?dZZyzp!)W^r?*FWi(p#CDjRksgm^}xfE zrhtP%_q(fe(v*k|_pL#yXB|zaQ3(`9K%+vTz#{Np4Jfkz0@lvA2{AeWlP3yckMtp! zvgdlyD@knm*jg;~(UOtHHz#Q;E};HG7s9p#AECv@c65(4ok!jhxFkLNb{!u zuadrLEih=Tov$!|fjP+0s6yLmc6P{wui^=YVNQ|zBI3bm(4t=I2cG?#qn)lH-<5%` zakBK@eQOjJM65?QO!s0cl}e_UaszX^dKXd_IDBoGsJILsbmRBOoQW21uHNB0s|KN& z_MY^?H}*ez#xmCA^hM`WB)B*FZ3b3X;L3D7_cZBTYRAxHu9>`l|$~-781)|~MXRz)}H8R4%%}0xm>We?; z1PE+-dAyOnuKLiS_l?%iVkC08T-?*^7u`XQDAd^+mn3U*l4l!X?Qy4BIEUdJALVwi zHJR$1$FRNhMDQsV^KyAtUUvsM^f>0wOaG@kdE;b4i|T4=Py+vYMr@O%s_GZZSYjN6 zeaA4>!N7{lA%tpzm8jE^F#8%aq8~q%!yH`Jpwa0W_q!6(#`=ObsfiwvbV&qU?d;tn zW~oXRu*k5MHn4aYfNhbM(^N$D^w3Zb7Y-)PQ{?+OtHg< zDx3lf37}YIilS4w+jHEo{vYy&mvZeF{}Z$9^>5x7;|4Wlw>ICr_a6u$KVkoZ9%Z_5 zuwGqKB^4b0naBz+!Opt($?4lmC1UY3RLB)9@iP?EqV_n#ReKfFDZ*Bk+2_Yi@000VfMObu0Z*X~XX=iA30IUzpIsgCw4s=CWbVG7w zVRUJ4ZXk4NZDjy8_YVmG000SeMObuGZ)S9NVRB^vU2y+80000BbVXQnL}_zlY+-3_ zWpV(wz_gD5000PdMObuKVRCM1Zf5|%8|H@q000McMObuGZ*_8GWdQa6gX;hQ00?wN zSad^gZEa<4bO83umcIZ100wkLSaeirbZlh+sP57y000XbNklzrTOYAXNeO+|cF!NO$s&YkkH) zc|BrNu%=NB)^`$W*hZ+~D*WL_yukpvS0G##LNySgfRtHmtAK5mundDjE=w*kOJ*ic zYI>5)%oO=lnp9TClX>@3ubzoKeQw4-Vi=|D;NakzAeIH#vSo{M*NxeGJL)svsI6^? zw07;KdGj8^tvz@{4XCOEWPmi1G?1?BX8{9{fKrJj0FaJ_k;^g{8)NG9AQL0QOvQ5M zWK#d}zW%na*^U@Zr_;IN;o+6@x+1O0G1ccXAM|-WweR`RL$qK0CwKz&fDPm;hO
1zuvQBosvUEyX012a zqIC4!jVI7hF;c7m6p<7w`}5m{=9iF^;N-zb;8yaqwfm`wcnNxJkD{;x2(RF2O#rG8 zD5_>Fs+OL2P%&9pVk-h>74v{&ft3X(17?a+Dvo6o<`bsQqJKbl3#+{ z0J0tw4=5HAhjKcZK&iU<1&9JpF+pMGEa|Zkk`qyKDI0K6F|Km{!ntICN-POvkn$Lm zGGIC&br2p9uJRaaPWe5=r&!Kybif6U2AUfPEHWm9icmF#5S3B7JX#~b5>ix9 zBq&4j@#4km6S9y=@vEmF#a~-TYI>4g zpL~FDbNh-WSEK3xQlfjkbnm&3*@-AK=UyijKSg|E3dPQ#Dn(pc3AZLu-8TFIOXjm# zEO6?`AzH55N;Z`sdg?ggmI}Zn)*pdK_f~j`ZO; z8q(4DpD7ky6P9T*cyK?aQN-=>qq%i-uNQwPa#_r;0$?5jzy_HEa~g`X2s4Q=Ghi3X zt2C_zxM5Mtw_ZQY;b*>2vxlgjiCrZ zY;*)~u!h8Bl<$4)+|{Sb`Pvuo z*VN%Si|3gu3Q)+ViJcv0YIKP7OpMLFH;_xt($vvKdM3uE>;7=rwk}=ADjAFpyu!xr zo!oNI=Xv%Ue~YRu9_cGu%;~-Ws^+F=_pK?Ks!%+%-*AGq~i-naKoLiH`=a{2OkR|+7XP7oa) zz%&gePn}@f^?Qh)9Y%BMtlM%e3JR&|Sk)Mc5JXzmW7{UrfA6adz5ELzE$bJ9UMYZ7 ze1h1SlY}CT{VGRNu_l$$k#~_;Kpibuc>cDy^MYb7tUhRRG6vc>9eb9C++sdHc;{ z2vwtN=gr)7*Jo(ocHP2x)l$eQ*xFDblX2cS_&k}}X>2K%9m(SUpG85z;|pRMMPTW&C^H-9x&QnZBCQ+f*>f8$o8N=SALRKbzKUa?p9`u2 zoS6zdK9_e7ogFyF(Vsudx#K5D&L%Mo6Dj553gaRs&E;n6&fOe;@#h@>^>esAKAJi@ z3Dq^>3DyvZG?JQG{vuA##K>n-{OOm!!yn!McUUEZkACVO(Of#I>523GQ~|Kx7_U8Q zJIcTB=tw+}%AVVi7?0|;^|gfSyo5pmpVvTFZFHAJ)1C6;(F-J0!Md&i&KK7}vEL)WNYQhL1*!Qh3Q!CxjmO-(r8)fp_e5%AB$)8l1Q8xx!R>a?-3o4f7OrPeK8%kXCm5+` z>&}~b=IdW#OYe=GJp2n#6dKlVLUnuI(eHeP8g6Ri=0E=&Kl}RMAcV???|%qsJM6se zKD@!2g?XUrU1(rnz}&H8M!}^+rs(IhLEwztvbyv&Gt70O^;hE zD#7a)w~r1FGCKG&V?+Jibob{7)-|!NtDEFhlt67g(ph@_4CRTeXZLON?7pp<#g%8` zW5NAi`B#hjTFqj9%>V#>eSH>S_4f7_fjQGw-!^T$fDZ^} zAVB#FxMRLeV&WWTvA~Ay@8hPsKa0O60zmuL>#&R>O;>G02=!f=UNgW2AU5R}V+Q4y z!i)6Gh_PVk;8WjX;`Ez*{L}x8zb3NqeN96P{+bB3WnPw72v@9>Xx+S>V5EW5#|}}* zq!zwcRDsLmWpv;b_CNYCXOF*jN&Q^0YIN^k{{vk+Z{ek%KEYeBA7=XpKMH_1P|Kdb zc!*$K6VkEq*VbQBKP&#(V=emr(WIO%oPW_R0m~?s{|Hj0bbdEVC1-i%2ajCd5r`lE pHBUbIz@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRa0hDk(0RCwCtn|p9v)qTf5ckk}XlC`pp1oBHDW3aJ} z)9?y0NeRe4PNdp-_Vg%myU_ zgyDku%cv-gc;}i>-%}4d$TW}!OrpejoR|3-kOG`f1_5_)JCd7FxnbP@!axa7>JhPE z!2+&ca5c9rS<8Z%3#cEgXIt;7fy1ACRVl7QV$!wgQ=xrHE5Uz^z=zAl0N; zz#bBw-6`j6&Y^^IDwxe2&SW0vP)?ar4V(pQm#yW_3-2TnsvvET@J#b2o;makBb_5K z2z2l$Ti5^$oeVWhO2CevR9|e_veK}mDkMrpS>Q^?m8vTUr~xWU!pen0ctJF%vM>}> z764=ox##T<&F_8p=gECQ1gHW+fB{%Q5SR&+0p&m?7qE&8xRQC)DmAcd@iI33*(M^P z3IKYuz1;csH)(8bM0Nlz{3qM_ccAY?Nf<}K&L`&H9#nbyST3T5D=4d4foqPS4Id;v zB6_d8cknAfGmr&bAH3^lo{;YdVb2jb1E}FrZst>bmHC91uPi13;J*5A^HRf09tnqe zk~dg?q9iC|tv(~H>{Y`RR4uJT4bRH|FEW2sG2qG-6JD_C)itF1cCq;l?fJ(xk30-C zP2mfwzAEOioGxc^Riwn7qpa9O8e0=<&vcJOSG_ zRL_|?LupqOW-YpEBb7_)@+V2rKl%x2B1OF5bs8zt`Ioj{#(~X$MSy`PS)VER!V0(UsN;j(2e3MwL4L`tz#d@acnDB)2jBQ| zWJ@5xlIoRrF!$4UBCK7w=I;^qKBUt;I^8y;=r5SVAT#+HP!kv_8%6I4xAr+h9RtoG zpMa^9oC474rJ2QCLWo6~nQ5*+`-VJtMS;&(f1ckYe}z75&6FWZ>E=BkaXcze0N(m( z_1)p1@@QG*3YOoz1u6P*jaQF5u*BQ;)Ai2FwDmjv>8#i>ze*hls)}1SL%Bjq6e){c zuP}(_M!)6qR|khsR>GcJ_D~U;g`$9>C@3gwYJZw1wmw1nKt{8EA-{HvN*lfRwhh&D zLaOp0KNS6z`-=-;o|Q=`uPF^|0abN0vganquk^57LkArdTS&AGSGzHkFW z(UHr6#XvU@KPCcHRrzN%TtW59J3v|p`@@q`2hBXQoItR)=soE)PfV^9NHGM`0O@*U zNS8ldT!q{&8M2~9;1%C_uc*3`bCy5LQ2WSS=5iSWeCWr5DKBw@0fmaPsuif=3WV7J zu6|MokZv2U)plYR5eCwc98A4G4!q&&l&NK`ID09FBHd_T&iGvUQO_+8mZOO*y2$3iTT;hTJhwg4HR3xn z?7nC&Jjr3hlF^U4>~;V6Sp0clBv^9sAEkr2?fKDSqxEZc^e%!U=mam0;-Bo9i$4O0`TsW zZb3|q2lqXHvlF3t4sa3BQRD*hV+w+4$N(ZO}N{h3=$KV1ZL!I`HWH4^N7|i8H;%or zfzZEP$~U=-A6@ehrD`dbFMG!)U=+Mfun2sIzn+jq*0(Z!B)fJHO9^A!KBEa}2a*BZ zluhZrU89B2DW1c70p!Vb^u=8rMCmU{+dAxN=fQ;xPWD5iS~X#YN(5{$`#aJ zwUM&Qr3mW~IME3|V?Xvfp}Vi;C~&QE*ig85j@V+_)DhH6KX9Vo-Xod-gvk zfWX!U{V>pm*JO8tisGLw-IL}5wwcAUG!zuF+AvybkW9)0S+%5&nk&|!2Eo$s|8Kl?8vy@TZ?pqlekcPS;2b3tSf#+%^u9J{K1^bDbU*77hfSU6zNpI7rno*9%;29wAZIW!$f$i ze%Bd8fQKV9)B$$3F!;6ko#P2QLLxh<0usX=-1f~n4~8tCk%qZwHX=erfNwYWtA&%lTRasm=^e@lprKhIAi1wu5A$S{BY9XH$g{; z30*)pk^s!VoZc6FoHwj+0c;YnPFsZfaJo zLpp;Zm^wyn5hm+^Wk23AsS>ar6py-xc?!EZtW#25nftXJ^Nsa=iKKA1Mv zACwQKqAL7b9qnS1eSgw}|( z<4n7eive;p2E@8N>u=p_bow2iNrT@cF~GZ!YmVgQFH>8mpY`!UOP>JWJrmJRshiS$ zZMU9V_{2HNUV%ITYABRHb7ZicswH)(;R@{Zq3P%U;H5qxyr=m>kchT|w3UV?D;i6Q zZol$7>QQzjrc z93zPuu3&cUwK)Boiz4iFbpcyXlTAhGiaMFS&DLQ*_a9aM6B3Y(_cN#VI`1Co1Jh3a zj%^_v8^_W?NK6yA=|F=Hl>Yi1y~XeObpcb7ZqRaMPicvAxt5B7w6XNGC;w!_$G&64 zb!>!Ve=67^~O>n;a7Jtf02{>oAl=h$Fff_zla#@^7{IfD~?5m89IcM|&!TRra`wt8bJ zk(2GPKZktqnF7mdc)v6`Wwm(Fab*g~%in3=n9sGjAeBo$Qvo}P-^P9GCdPnGjR9+L zihdtBDFLtS)StcMhVpfenNswg-=X(36#1-U>uBkeJQ~>6UT5&bHoL#M#~$>pIM#2+ zTK1$$`FMu{l>B!6S+7>j3S1ow%^(m6O3TcQdvH4iutUCe6TW3nnctpl0+j16FJ1AK zKQH^)(wfi}xoA9;64_YFO*b@IeeWH%`eG@O3r2}az!u8bnvP&6BLKYL@SO)-hWEn* z)d!aHeK+QV%}(I6pBS)5B0vFV;O(88g}2$*z}rM%_!6GBGCq;<{{sM7+-G2IVp9bG O0000 literal 0 HcmV?d00001 diff --git a/resources/stop.png b/resources/stop.png new file mode 100755 index 0000000000000000000000000000000000000000..76e97f2ed94c488f550a7e807ba53048b6b04e50 GIT binary patch literal 3359 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 0 : + self.pb_files.setValue( int( 100*pval + 0.5 ) ) + self.pb_files.setFormat( msg ) + self.pb_pages.setValue( 0 ) + def on_analyze_progress2( self , pval ) : + """Update the analysis progress in the UI.""" + self.pb_pages.setValue( int( 100*pval + 0.5 ) ) + + def on_cancel_analyze( self ) : + """Cancel the analyze worker thread.""" + if not self.analyze_thread or self.analyze_thread.parser.cancelling : + return + rc = MainWindow.ask( "Cancel the analysis?" , QMessageBox.Ok|QMessageBox.Cancel , QMessageBox.Cancel ) + if rc != QMessageBox.Ok : + return + self.analyze_thread.parser.cancelling = True + self.pb_files.setFormat( "Cancelling, please wait..." ) + self.btn_cancel_analyze.setEnabled( False ) + + def on_analyze_completed( self , ex ) : + # clean up + self.analyze_thread = None + # check if the analysis failed + if ex : + MainWindow.show_error_msg( "Analyze failed:\n\n{}".format( ex ) ) + self._update_analyze_ui( True ) + self.frm_analyze_progress.hide() + self.le_cards_dir.setFocus() + return + # the analysis completed successully - start the main app + self.pb_files.setValue( 100 ) + self.pb_pages.setValue( 100 ) + MainWindow.show_info_msg( "The \"ASL Cards\" files were analyzed successully." ) + self.parent().start_main_app( self.le_save_db_fname.text().strip() ) + + def _update_analyze_ui( self , enable ) : + # update the UI + widgets = [ self.lbl_cards_dir , self.le_cards_dir, self.btn_cards_dir ] + widgets.extend( [ self.lbl_save_db_fname , self.le_save_db_fname , self.btn_save_db_fname ] ) + widgets.append( self.btn_analyze ) + for w in widgets : + w.setEnabled( enable ) + + def on_btn_load_db_fname( self ) : + """Let the user browse to a database to load.""" + fname = self.le_load_db_fname.text().strip() + fname = QFileDialog.getOpenFileName( + self , "Load database" , + fname , "Database files (*.db)" + )[ 0 ] + if not fname : + return + self.le_load_db_fname.setText( fname ) + self.btn_load_db.setFocus() + + def on_btn_load_db( self ) : + """Load the database and start the main application.""" + fname = self.le_load_db_fname.text().strip() + if not fname : + MainWindow.show_error_msg( "Please choose a database to load." ) + self.le_load_db_fname.setFocus() + return + if not os.path.isfile( fname ) : + MainWindow.show_error_msg( "Can't find this database file." ) + self.le_load_db_fname.setFocus() + return + # notify the main window it can start the main app + self.parent().start_main_app( fname ) diff --git a/ui/add_card_dialog.ui b/ui/add_card_dialog.ui index 7003d56..a54ffcd 100644 --- a/ui/add_card_dialog.ui +++ b/ui/add_card_dialog.ui @@ -114,19 +114,19 @@ - + - &OK - - - true + Cancel - + - Cancel + &OK + + + true diff --git a/ui/startup_widget.ui b/ui/startup_widget.ui new file mode 100644 index 0000000..86f3e72 --- /dev/null +++ b/ui/startup_widget.ui @@ -0,0 +1,513 @@ + + + StartupWidget + + + + 0 + 0 + 566 + 399 + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + + 50 + 50 + + + + + 50 + 50 + + + + false + + + QFrame::Box + + + + + + + + + + + 2 + + + 8 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + If this is the first time you have run this program, you need to analyze the PDF files first, and save the results in a database. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 2 + + + 0 + + + 8 + + + 0 + + + 0 + + + + + Where the "ASL Cards" &files are: + + + le_cards_dir + + + + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + 30 + 30 + + + + + + + + + + + + + + + + + + 2 + + + 0 + + + 2 + + + 0 + + + 0 + + + + + &Save the results here: + + + le_save_db_fname + + + + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + 30 + 30 + + + + + + + + + + + + + + + + + &Analyze + + + + + + + true + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 24 + + + + + + + + 0 + 15 + + + + + 16777215 + 15 + + + + 24 + + + false + + + + + + + &Cancel + + + + + + + + + + + + + + + + true + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 50 + 50 + + + + + 50 + 50 + + + + QFrame::Box + + + + + + + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + If you have already analyzed the PDF files, open the &database: + + + le_load_db_fname + + + + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + 30 + 30 + + + + + + + + + + + + + + + + + 8 + true + + + + Put the database in the same directory as this program, and it will be loaded automatically, or add a "--db ..." parameter to the command-line arguments. + + + true + + + + + + + + 0 + 0 + + + + &Open + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + le_cards_dir + btn_cards_dir + le_save_db_fname + btn_save_db_fname + btn_analyze + btn_cancel_analyze + le_load_db_fname + btn_load_db_fname + + + +