Coverage for fpdb.pyw: 0%

932 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 18:50 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3 

4# Copyright 2008-2013 Steffen Schaumburg 

5# This program is free software: you can redistribute it and/or modify 

6# it under the terms of the GNU Affero General Public License as published by 

7# the Free Software Foundation, version 3 of the License. 

8# 

9# This program is distributed in the hope that it will be useful, 

10# but WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

12# GNU General Public License for more details. 

13# 

14# You should have received a copy of the GNU Affero General Public License 

15# along with this program. If not, see <http://www.gnu.org/licenses/>. 

16# In the "official" distribution you can find the license in agpl-3.0.txt. 

17 

18 

19import os 

20import sys 

21import re 

22import queue 

23# import qdarkstyle 

24import multiprocessing 

25import threading 

26import faulthandler 

27if os.name == 'nt': 

28 import win32api 

29 import win32con 

30 

31import codecs 

32import traceback 

33import Options 

34import string 

35from functools import partial 

36 

37cl_options = '.'.join(sys.argv[1:]) 

38(options, argv) = Options.fpdb_options() 

39from L10n import set_locale_translation 

40import logging 

41 

42from PyQt5.QtCore import (QCoreApplication, QDate, Qt, QPoint) 

43from PyQt5.QtGui import (QScreen, QIcon, QPalette) 

44from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QHBoxLayout, QSizePolicy 

45from PyQt5.QtWidgets import (QAction, QApplication, QCalendarWidget, 

46 QCheckBox, QDateEdit, QDialog, 

47 QDialogButtonBox, QFileDialog, 

48 QGridLayout, QHBoxLayout, QInputDialog, 

49 QLabel, QLineEdit, QMainWindow, 

50 QMessageBox, QPushButton, QScrollArea, 

51 QTabWidget, QVBoxLayout, QWidget, QComboBox) 

52 

53import interlocks 

54from Exceptions import * 

55 

56# these imports not required in this module, imported here to report version in About dialog 

57import numpy 

58 

59numpy_version = numpy.__version__ 

60import sqlite3 

61 

62sqlite3_version = sqlite3.version 

63sqlite_version = sqlite3.sqlite_version 

64 

65import DetectInstalledSites 

66import GuiPrefs 

67import GuiLogView 

68# import GuiDatabase 

69import GuiBulkImport 

70#import GuiTourneyImport 

71 

72import GuiRingPlayerStats 

73import GuiTourneyPlayerStats 

74import GuiTourneyViewer 

75import GuiPositionalStats 

76import GuiAutoImport 

77import GuiGraphViewer 

78import GuiTourneyGraphViewer 

79import GuiSessionViewer 

80import GuiHandViewer 

81import GuiTourHandViewer 

82#import GuiOddsCalc 

83#import GuiStove 

84 

85import SQL 

86import Database 

87import Configuration 

88import Card 

89import Exceptions 

90import Stats 

91#import api, app 

92import cProfile 

93import pstats 

94import io 

95 

96 

97PROFILE_OUTPUT_DIR = os.path.join(os.path.expanduser("~"), "fpdb_profiles") 

98os.makedirs(PROFILE_OUTPUT_DIR, exist_ok=True) 

99 

100profiler = cProfile.Profile() 

101profiler.enable() 

102 

103 

104 

105Configuration.set_logfile("fpdb-log.txt") 

106 

107log = logging.getLogger("fpdb") 

108 

109try: 

110 assert not hasattr(sys, 'frozen') # We're surely not in a git repo if this fails 

111 import subprocess 

112 

113 VERSION = subprocess.Popen(["git", "describe", "--tags", "--dirty"], stdout=subprocess.PIPE).communicate()[0] 

114 VERSION = VERSION[:-1] 

115except Exception: 

116 VERSION = "3.0.0alpha" 

117 

118 

119class fpdb(QMainWindow): 

120 def launch_ppt(self): 

121 path = os.getcwd() 

122 if os.name == 'nt': 

123 pathcomp = f"{path}\pyfpdb\ppt\p2.jar" 

124 else: 

125 pathcomp = f"{path}/ppt/p2.jar" 

126 subprocess.call(['java', '-jar', pathcomp]) 

127 

128 def add_and_display_tab(self, new_page, new_tab_name): 

129 """adds a tab, namely creates the button and displays it and appends all the relevant arrays""" 

130 for name in self.nb_tab_names: # todo: check this is valid 

131 if name == new_tab_name: 

132 self.display_tab(new_tab_name) 

133 return # if tab already exists, just go to it 

134 

135 used_before = False 

136 for i, name in enumerate(self.nb_tab_names): 

137 if name == new_tab_name: 

138 used_before = True 

139 page = self.pages[i] 

140 break 

141 

142 if not used_before: 

143 page = new_page 

144 self.pages.append(new_page) 

145 self.nb_tab_names.append(new_tab_name) 

146 

147 index = self.nb.addTab(page, new_tab_name) 

148 self.nb.setCurrentIndex(index) 

149 

150 def display_tab(self, new_tab_name): 

151 """displays the indicated tab""" 

152 tab_no = -1 

153 for i, name in enumerate(self.nb_tab_names): 

154 if new_tab_name == name: 

155 tab_no = i 

156 break 

157 

158 if tab_no < 0 or tab_no >= self.nb.count(): 

159 raise FpdbError(f"invalid tab_no {str(tab_no)}") 

160 else: 

161 self.nb.setCurrentIndex(tab_no) 

162 

163 def dia_about(self, widget, data=None): 

164 QMessageBox.about( 

165 self, 

166 f"FPDB{str(VERSION)}", 

167 "Copyright 2008-2023. See contributors.txt for details" 

168 + "You are free to change, and distribute original or changed versions " 

169 "of fpdb within the rules set out by the license" 

170 + "https://github.com/jejellyroll-fr/fpdb-3" 

171 + "\n" 

172 + "Your config file is: " 

173 + self.config.file, 

174 ) 

175 return 

176 

177 def dia_advanced_preferences(self, widget, data=None): 

178 # force reload of prefs from xml file - needed because HUD could 

179 # have changed file contents 

180 self.load_profile() 

181 if GuiPrefs.GuiPrefs(self.config, self).exec_(): 

182 # save updated config 

183 self.config.save() 

184 self.reload_config() 

185 

186 def dia_database_stats(self, widget, data=None): 

187 self.warning_box( 

188 string=f"Number of Hands: {self.db.getHandCount()}\nNumber of Tourneys: {self.db.getTourneyCount()}\nNumber of TourneyTypes: {self.db.getTourneyTypeCount()}", 

189 diatitle="Database Statistics") 

190 

191 # end def dia_database_stats 

192 

193 @staticmethod 

194 def get_text(widget: QWidget): 

195 """Return text of widget, depending on widget type""" 

196 return widget.currentText() if isinstance(widget, QComboBox) else widget.text() 

197 

198 

199 def dia_hud_preferences(self, widget, data=None): 

200 dia = QDialog(self) 

201 dia.setWindowTitle("Modifying Huds") 

202 dia.resize(1200, 600) 

203 label = QLabel("Please edit your huds.") 

204 dia.setLayout(QVBoxLayout()) 

205 dia.layout().addWidget(label) 

206 label2 = QLabel("Please select the game category for which you want to configure HUD stats:") 

207 dia.layout().addWidget(label2) 

208 self.comboGame = QComboBox() 

209 

210 huds_names = self.config.get_stat_sets() 

211 for hud_name in huds_names: 

212 self.comboGame.addItem(hud_name) 

213 

214 dia.layout().addWidget(self.comboGame) 

215 self.comboGame.setCurrentIndex(1) 

216 selected_hud_name = self.comboGame.currentText() 

217 

218 self.load_profile() # => self.[config, settings] 

219 

220 # HUD column will contain a button that shows favseat and HUD locations. 

221 # TODO: Make it possible to load screenshot to arrange HUD windowlets. 

222 

223 self.table = QGridLayout() 

224 self.table.setSpacing(0) 

225 

226 scrolling_frame = QScrollArea(dia) 

227 dia.layout().addWidget(scrolling_frame) 

228 scrolling_frame.setLayout(self.table) 

229 

230 nb_items = len(self.config.stat_sets[selected_hud_name].stats) 

231 

232 btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, dia) 

233 btns.accepted.connect(dia.accept) 

234 btns.rejected.connect(dia.reject) 

235 dia.layout().addWidget(btns) 

236 self.comboGame.currentIndexChanged.connect(self.index_changed) 

237 

238 # Launch Dialog 

239 response = dia.exec_() 

240 

241 # Treat dialog closed event 

242 if self.comboGame.currentIndexChanged and response: 

243 selected_hud_name = self.comboGame.currentText() 

244 # User clicked on "Save" 

245 for y in range(nb_items): 

246 self.config.edit_hud( 

247 selected_hud_name, 

248 self.get_text(self.stat_position_list[y]), 

249 self.get_text(self.stat_name_list[y]), 

250 self.get_text(self.click_list[y]), 

251 self.get_text(self.hudcolor_list[y]), 

252 self.get_text(self.hudprefix_list[y]), 

253 self.get_text(self.hudsuffix_list[y]), 

254 self.get_text(self.popup_list[y]), 

255 self.get_text(self.stat_hicolor_list[y]), 

256 self.get_text(self.stat_hith_list[y]), 

257 self.get_text(self.stat_locolor_list[y]), 

258 self.get_text(self.stat_loth_list[y]), 

259 self.get_text(self.tip_list[y]), 

260 ) 

261 

262 self.config.save() 

263 self.reload_config() 

264 

265 

266 def index_changed(self, index): 

267 # Called when user changes currently selected HUD 

268 log.info("start index_changed") 

269 log.debug(f"index = {index}") 

270 log.debug(f"self.config = {self.config}") 

271 log.debug(f"self.config.stat_sets = {self.config.stat_sets}") 

272 selected_hud_name = self.comboGame.currentText() 

273 log.debug(f"selected_hud_name = {selected_hud_name}") 

274 for i in reversed(range(self.table.count())): 

275 self.table.itemAt(i).widget().deleteLater() 

276 

277 self.table.setSpacing(0) 

278 

279 column_headers = ["coordonate", "stats name", "click", "hudcolor", "hudprefix", "hudsuffix", 

280 "popup", "stat_hicolor", "stat_hith", "stat_locolor", "stat_loth", 

281 "tip"] # todo ("HUD") 

282 

283 for header_number in range(0, len(column_headers)): 

284 label = QLabel(column_headers[header_number]) 

285 label.setAlignment(Qt.AlignCenter) 

286 self.table.addWidget(label, 0, header_number) 

287 

288 # Init lists that will contains QWidgets for each column in table ("stat_position_list" will contain the positions (ex: ["(0,1)", ...])) 

289 self.stat_position_list, self.stat_name_list, self.click_list, self.hudcolor_list, self.hudprefix_list, \ 

290 self.hudsuffix_list, self.popup_list, self.stat_hicolor_list, self.stat_hith_list, self.stat_locolor_list, \ 

291 self.stat_loth_list, self.tip_list = [], [], [], [], [], [], [], [], [], [], [], [] 

292 

293 self.load_profile() 

294 hud_stats = self.config.stat_sets[selected_hud_name] # Configuration.Stat_sets object 

295 y_pos = 1 

296 for position in hud_stats.stats.keys(): 

297 # Column 1: stat position 

298 stat2 = QLabel() 

299 stat2.setText(str(position)) 

300 self.table.addWidget(stat2, y_pos, 0) 

301 self.stat_position_list.append(stat2) 

302 

303 # Column 2: select stat name (between available stats) 

304 # TODO: don't load all stats on each loop ! 

305 if os.name == 'nt': 

306 icoPath = os.path.dirname(__file__) 

307 icoPath = f"{icoPath}\\" 

308 else: 

309 icoPath = "icons/" 

310 stat3 = QComboBox() 

311 stats_cash = self.config.get_gui_cash_stat_params() # Available stats for cash game 

312 for x in range(0, len(stats_cash)): 

313 # print(stats_cash[x][0]) 

314 stat3.addItem(QIcon(f"{icoPath}Letter-C-icon.png"), stats_cash[x][0]) 

315 stats_tour = self.config.get_gui_tour_stat_params() # Available stats for tournament 

316 for x in range(0, len(stats_tour)): 

317 # print(stats_tour[x][0]) 

318 stat3.addItem(QIcon(f"{icoPath}Letter-T-icon.png"), stats_tour[x][0]) 

319 stat3.setCurrentText(str(hud_stats.stats[position].stat_name)) 

320 self.table.addWidget(stat3, y_pos, 1) 

321 self.stat_name_list.append(stat3) 

322 

323 # Column 3: "click" 

324 stat4 = QLineEdit() 

325 stat4.setText(str(hud_stats.stats[position].click)) 

326 self.table.addWidget(stat4, y_pos, 2) 

327 self.click_list.append(stat4) 

328 

329 # Column 4: "hudcolor" 

330 stat5 = QLineEdit() 

331 stat5.setText(str(hud_stats.stats[position].hudcolor)) 

332 self.table.addWidget(stat5, y_pos, 3) 

333 self.hudcolor_list.append(stat5) 

334 

335 # Column 5: "hudprefix" 

336 stat6 = QLineEdit() 

337 stat6.setText(str(hud_stats.stats[position].hudprefix)) 

338 self.table.addWidget(stat6, y_pos, 4) 

339 self.hudprefix_list.append(stat6) 

340 

341 # Column 6: "hudsuffix" 

342 stat7 = QLineEdit() 

343 stat7.setText(str(hud_stats.stats[position].hudsuffix)) 

344 self.table.addWidget(stat7, y_pos, 5) 

345 self.hudsuffix_list.append(stat7) 

346 

347 # Column 7: "popup" 

348 stat8 = QComboBox() 

349 for popup in self.config.popup_windows.keys(): 

350 stat8.addItem(popup) 

351 stat8.setCurrentText(str(hud_stats.stats[position].popup)) 

352 self.table.addWidget(stat8, y_pos, 6) 

353 self.popup_list.append(stat8) 

354 

355 # Column 8: "stat_hicolor" 

356 stat9 = QLineEdit() 

357 stat9.setText(str(hud_stats.stats[position].stat_hicolor)) 

358 self.table.addWidget(stat9, y_pos, 7) 

359 self.stat_hicolor_list.append(stat9) 

360 

361 # Column 9: "stat_hith" 

362 stat10 = QLineEdit() 

363 stat10.setText(str(hud_stats.stats[position].stat_hith)) 

364 self.table.addWidget(stat10, y_pos, 8) 

365 self.stat_hith_list.append(stat10) 

366 

367 # Column 10: "stat_locolor" 

368 stat11 = QLineEdit() 

369 stat11.setText(str(hud_stats.stats[position].stat_locolor)) 

370 self.table.addWidget(stat11, y_pos, 9) 

371 self.stat_locolor_list.append(stat11) 

372 

373 # Column 11: "stat_loth" 

374 stat12 = QLineEdit() 

375 stat12.setText(str(hud_stats.stats[position].stat_loth)) 

376 self.table.addWidget(stat12, y_pos, 10) 

377 self.stat_loth_list.append(stat12) 

378 

379 # Column 12: "tip" 

380 stat13 = QLineEdit() 

381 stat13.setText(str(hud_stats.stats[position].tip)) 

382 self.table.addWidget(stat13, y_pos, 11) 

383 self.tip_list.append(stat13) 

384 # if available_site_names[site_number] in detector.supportedSites: 

385 # pass 

386 

387 y_pos += 1 

388 

389 def dia_import_filters(self, checkState): 

390 dia = QDialog() 

391 dia.setWindowTitle("Skip these games when importing") 

392 dia.setLayout(QVBoxLayout()) 

393 checkboxes = {} 

394 filters = self.config.get_import_parameters()['importFilters'] 

395 for game in Card.games: 

396 checkboxes[game] = QCheckBox(game) 

397 dia.layout().addWidget(checkboxes[game]) 

398 if game in filters: 

399 checkboxes[game].setChecked(True) 

400 btns = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 

401 dia.layout().addWidget(btns) 

402 btns.accepted.connect(dia.accept) 

403 btns.rejected.connect(dia.reject) 

404 if dia.exec_(): 

405 filterGames = [] 

406 for game, cb in list(checkboxes.items()): 

407 if cb.isChecked(): 

408 filterGames.append(game) 

409 self.config.editImportFilters(",".join(filterGames)) 

410 self.config.save() 

411 

412 def dia_dump_db(self, widget, data=None): 

413 filename = "database-dump.sql" 

414 result = self.db.dumpDatabase() 

415 

416 with open(filename, 'w') as dumpFile: 

417 dumpFile.write(result) 

418 

419 # end def dia_database_stats 

420 

421 def dia_recreate_tables(self, widget, data=None): 

422 """Dialogue that asks user to confirm that he wants to delete and recreate the tables""" 

423 if self.obtain_global_lock("fpdb.dia_recreate_tables"): # returns true if successful 

424 dia_confirm = QMessageBox(QMessageBox.Warning, "Wipe DB", "Confirm deleting and recreating tables", 

425 QMessageBox.Yes | QMessageBox.No, self) 

426 diastring = f"Please confirm that you want to (re-)create the tables. If there already are tables in" \ 

427 f" the database {self.db.database} on {self.db.host}" \ 

428 f" they will be deleted and you will have to re-import your histories.\nThis may take a while." 

429 

430 dia_confirm.setInformativeText(diastring) # todo: make above string with bold for db, host and deleted 

431 response = dia_confirm.exec_() 

432 

433 if response == QMessageBox.Yes: 

434 self.db.recreate_tables() 

435 # find any guibulkimport/guiautoimport windows and clear cache: 

436 for t in self.threads: 

437 if isinstance(t, GuiBulkImport.GuiBulkImport) or isinstance(t, GuiAutoImport.GuiAutoImport): 

438 t.importer.database.resetCache() 

439 self.release_global_lock() 

440 else: 

441 self.release_global_lock() 

442 log.info('User cancelled recreating tables') 

443 else: 

444 self.warning_box("Cannot open Database Maintenance window because other" 

445 " windows have been opened. Re-start fpdb to use this option.") 

446 

447 def dia_recreate_hudcache(self, widget, data=None): 

448 if self.obtain_global_lock("dia_recreate_hudcache"): 

449 self.dia_confirm = QDialog() 

450 self.dia_confirm.setWindowTitle("Confirm recreating HUD cache") 

451 self.dia_confirm.setLayout(QVBoxLayout()) 

452 self.dia_confirm.layout().addWidget(QLabel("Please confirm that you want to re-create the HUD cache.")) 

453 

454 hb1 = QHBoxLayout() 

455 self.h_start_date = QDateEdit(QDate.fromString(self.db.get_hero_hudcache_start(), "yyyy-MM-dd")) 

456 lbl = QLabel(" Hero's cache starts: ") 

457 btn = QPushButton("Cal") 

458 btn.clicked.connect(partial(self.__calendar_dialog, entry=self.h_start_date)) 

459 

460 hb1.addWidget(lbl) 

461 hb1.addStretch() 

462 hb1.addWidget(self.h_start_date) 

463 hb1.addWidget(btn) 

464 self.dia_confirm.layout().addLayout(hb1) 

465 

466 hb2 = QHBoxLayout() 

467 self.start_date = QDateEdit(QDate.fromString(self.db.get_hero_hudcache_start(), "yyyy-MM-dd")) 

468 lbl = QLabel(" Villains' cache starts: ") 

469 btn = QPushButton("Cal") 

470 btn.clicked.connect(partial(self.__calendar_dialog, entry=self.start_date)) 

471 

472 hb2.addWidget(lbl) 

473 hb2.addStretch() 

474 hb2.addWidget(self.start_date) 

475 hb2.addWidget(btn) 

476 self.dia_confirm.layout().addLayout(hb2) 

477 

478 btns = QDialogButtonBox(QDialogButtonBox.Yes | QDialogButtonBox.No) 

479 self.dia_confirm.layout().addWidget(btns) 

480 btns.accepted.connect(self.dia_confirm.accept) 

481 btns.rejected.connect(self.dia_confirm.reject) 

482 

483 response = self.dia_confirm.exec_() 

484 if response: 

485 log.info("Rebuilding HUD Cache ...") 

486 

487 self.db.rebuild_cache(self.h_start_date.date().toString("yyyy-MM-dd"), 

488 self.start_date.date().toString("yyyy-MM-dd")) 

489 else: 

490 log.info('User cancelled rebuilding hud cache') 

491 

492 self.release_global_lock() 

493 else: 

494 self.warning_box("Cannot open Database Maintenance window because other windows have been opened. " 

495 "Re-start fpdb to use this option.") 

496 

497 def dia_rebuild_indexes(self, widget, data=None): 

498 if self.obtain_global_lock("dia_rebuild_indexes"): 

499 self.dia_confirm = QMessageBox(QMessageBox.Warning, 

500 "Rebuild DB", 

501 "Confirm rebuilding database indexes", 

502 QMessageBox.Yes | QMessageBox.No, 

503 self) 

504 diastring = "Please confirm that you want to rebuild the database indexes." 

505 self.dia_confirm.setInformativeText(diastring) 

506 

507 response = self.dia_confirm.exec_() 

508 if response == QMessageBox.Yes: 

509 log.info(" Rebuilding Indexes ... ") 

510 self.db.rebuild_indexes() 

511 

512 log.info(" Cleaning Database ... ") 

513 self.db.vacuumDB() 

514 

515 log.info(" Analyzing Database ... ") 

516 self.db.analyzeDB() 

517 else: 

518 log.info('User cancelled rebuilding db indexes') 

519 

520 self.release_global_lock() 

521 else: 

522 self.warning_box("Cannot open Database Maintenance window because" 

523 " other windows have been opened. Re-start fpdb to use this option.") 

524 

525 def dia_logs(self, widget, data=None): 

526 """opens the log viewer window""" 

527 # remove members from self.threads if close messages received 

528 self.process_close_messages() 

529 

530 viewer = None 

531 for i, t in enumerate(self.threads): 

532 if str(t.__class__) == 'GuiLogView.GuiLogView': 

533 viewer = t 

534 break 

535 

536 if viewer is None: 

537 # print "creating new log viewer" 

538 new_thread = GuiLogView.GuiLogView(self.config, self.window, self.closeq) 

539 self.threads.append(new_thread) 

540 else: 

541 # print "showing existing log viewer" 

542 viewer.get_dialog().present() 

543 

544 # if lock_set: 

545 # self.release_global_lock() 

546 

547 def dia_site_preferences_seat(self, widget, data=None): 

548 dia = QDialog(self) 

549 dia.setWindowTitle("Seat Preferences") 

550 dia.resize(1200, 600) 

551 label = QLabel("Please select your prefered seat.") 

552 dia.setLayout(QVBoxLayout()) 

553 dia.layout().addWidget(label) 

554 

555 self.load_profile() 

556 site_names = self.config.site_ids 

557 available_site_names = [] 

558 for site_name in site_names: 

559 try: 

560 tmp = self.config.supported_sites[site_name].enabled 

561 available_site_names.append(site_name) 

562 except KeyError: 

563 pass 

564 

565 column_headers = ["Site", "2 players:\nbetween 0 & 2", "3 players:\nbetween 0 & 3 ", 

566 "4 players:\nbetween 0 & 4", "5 players:\nbetween 0 & 5", "6 players:\nbetween 0 & 6", 

567 "7 players:\nbetween 0 & 7", "8 players:\nbetween 0 & 8", "9 players:\nbetween 0 & 9", 

568 "10 players:\nbetween 0 & 10"] # todo ("HUD") 

569 # HUD column will contain a button that shows favseat and HUD locations. 

570 # Make it possible to load screenshot to arrange HUD windowlets. 

571 

572 table = QGridLayout() 

573 table.setSpacing(0) 

574 

575 scrolling_frame = QScrollArea(dia) 

576 dia.layout().addWidget(scrolling_frame) 

577 scrolling_frame.setLayout(table) 

578 

579 for header_number in range(0, len(column_headers)): 

580 label = QLabel(column_headers[header_number]) 

581 label.setAlignment(Qt.AlignCenter) 

582 table.addWidget(label, 0, header_number) 

583 

584 history_paths = [] 

585 check_buttons = [] 

586 screen_names = [] 

587 seat2_dict, seat3_dict, seat4_dict, seat5_dict, seat6_dict, seat7_dict, seat8_dict, \ 

588 seat9_dict, seat10_dict = [], [], [], [], [], [], [], [], [] 

589 summary_paths = [] 

590 detector = DetectInstalledSites.DetectInstalledSites() 

591 

592 y_pos = 1 

593 for site_number in range(0, len(available_site_names)): 

594 check_button = QCheckBox(available_site_names[site_number]) 

595 check_button.setChecked(self.config.supported_sites[available_site_names[site_number]].enabled) 

596 table.addWidget(check_button, y_pos, 0) 

597 check_buttons.append(check_button) 

598 hud_seat = self.config.supported_sites[available_site_names[site_number]].fav_seat[2] 

599 

600 # print('hud seat ps:', type(hud_seat), hud_seat) 

601 seat2 = QLineEdit() 

602 

603 seat2.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[2])) 

604 table.addWidget(seat2, y_pos, 1) 

605 seat2_dict.append(seat2) 

606 seat2.textChanged.connect(partial(self.autoenableSite, checkbox=check_buttons[site_number])) 

607 

608 seat3 = QLineEdit() 

609 seat3.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[3])) 

610 table.addWidget(seat3, y_pos, 2) 

611 seat3_dict.append(seat3) 

612 

613 seat4 = QLineEdit() 

614 seat4.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[4])) 

615 table.addWidget(seat4, y_pos, 3) 

616 seat4_dict.append(seat4) 

617 

618 seat5 = QLineEdit() 

619 seat5.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[5])) 

620 table.addWidget(seat5, y_pos, 4) 

621 seat5_dict.append(seat5) 

622 

623 seat6 = QLineEdit() 

624 seat6.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[6])) 

625 table.addWidget(seat6, y_pos, 5) 

626 seat6_dict.append(seat6) 

627 

628 seat7 = QLineEdit() 

629 seat7.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[7])) 

630 table.addWidget(seat7, y_pos, 6) 

631 seat7_dict.append(seat7) 

632 

633 seat8 = QLineEdit() 

634 seat8.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[8])) 

635 table.addWidget(seat8, y_pos, 7) 

636 seat8_dict.append(seat8) 

637 

638 seat9 = QLineEdit() 

639 seat9.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[9])) 

640 table.addWidget(seat9, y_pos, 8) 

641 seat9_dict.append(seat9) 

642 

643 seat10 = QLineEdit() 

644 seat10.setText(str(self.config.supported_sites[available_site_names[site_number]].fav_seat[10])) 

645 table.addWidget(seat10, y_pos, 9) 

646 seat10_dict.append(seat10) 

647 

648 if available_site_names[site_number] in detector.supportedSites: 

649 pass 

650 

651 y_pos += 1 

652 

653 btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, dia) 

654 btns.accepted.connect(dia.accept) 

655 btns.rejected.connect(dia.reject) 

656 dia.layout().addWidget(btns) 

657 

658 response = dia.exec_() 

659 if response: 

660 for site_number in range(0, len(available_site_names)): 

661 # print "site %s enabled=%s name=%s" % (available_site_names[site_number], 

662 # check_buttons[site_number].get_active(), screen_names[site_number].get_text(), 

663 # history_paths[site_number].get_text()) 

664 self.config.edit_fav_seat(available_site_names[site_number], 

665 str(check_buttons[site_number].isChecked()), seat2_dict[site_number].text(), 

666 seat3_dict[site_number].text(), seat4_dict[site_number].text(), 

667 seat5_dict[site_number].text(), seat6_dict[site_number].text(), 

668 seat7_dict[site_number].text(), seat8_dict[site_number].text(), 

669 seat9_dict[site_number].text(), seat10_dict[site_number].text()) 

670 

671 self.config.save() 

672 self.reload_config() 

673 

674 def dia_site_preferences(self, widget, data=None): 

675 dia = QDialog(self) 

676 dia.setWindowTitle("Site Preferences") 

677 dia.resize(1200, 600) 

678 label = QLabel("Please select which sites you play on and enter your usernames.") 

679 dia.setLayout(QVBoxLayout()) 

680 dia.layout().addWidget(label) 

681 

682 self.load_profile() 

683 site_names = self.config.site_ids 

684 available_site_names = [] 

685 for site_name in site_names: 

686 try: 

687 tmp = self.config.supported_sites[site_name].enabled 

688 available_site_names.append(site_name) 

689 except KeyError: 

690 pass 

691 

692 column_headers = ["Site", "Detect", "Screen Name", "Hand History Path", "", 

693 "Tournament Summary Path", "", "Favorite seat"] # todo ("HUD") 

694 # HUD column will contain a button that shows favseat and HUD locations. 

695 # Make it possible to load screenshot to arrange HUD windowlets. 

696 

697 table = QGridLayout() 

698 table.setSpacing(0) 

699 

700 scrolling_frame = QScrollArea(dia) 

701 dia.layout().addWidget(scrolling_frame) 

702 scrolling_frame.setLayout(table) 

703 

704 for header_number in range(0, len(column_headers)): 

705 label = QLabel(column_headers[header_number]) 

706 label.setAlignment(Qt.AlignCenter) 

707 table.addWidget(label, 0, header_number) 

708 

709 check_buttons = [] 

710 screen_names = [] 

711 history_paths = [] 

712 summary_paths = [] 

713 detector = DetectInstalledSites.DetectInstalledSites() 

714 

715 y_pos = 1 

716 for site_number in range(0, len(available_site_names)): 

717 check_button = QCheckBox(available_site_names[site_number]) 

718 check_button.setChecked(self.config.supported_sites[available_site_names[site_number]].enabled) 

719 table.addWidget(check_button, y_pos, 0) 

720 check_buttons.append(check_button) 

721 

722 hero = QLineEdit() 

723 hero.setText(self.config.supported_sites[available_site_names[site_number]].screen_name) 

724 table.addWidget(hero, y_pos, 2) 

725 screen_names.append(hero) 

726 hero.textChanged.connect(partial(self.autoenableSite, checkbox=check_buttons[site_number])) 

727 

728 entry = QLineEdit() 

729 entry.setText(self.config.supported_sites[available_site_names[site_number]].HH_path) 

730 table.addWidget(entry, y_pos, 3) 

731 history_paths.append(entry) 

732 

733 choose1 = QPushButton("Browse") 

734 table.addWidget(choose1, y_pos, 4) 

735 choose1.clicked.connect(partial(self.browseClicked, parent=dia, path=history_paths[site_number])) 

736 

737 entry = QLineEdit() 

738 entry.setText(self.config.supported_sites[available_site_names[site_number]].TS_path) 

739 table.addWidget(entry, y_pos, 5) 

740 summary_paths.append(entry) 

741 

742 choose2 = QPushButton("Browse") 

743 table.addWidget(choose2, y_pos, 6) 

744 choose2.clicked.connect(partial(self.browseClicked, parent=dia, path=summary_paths[site_number])) 

745 

746 if available_site_names[site_number] in detector.supportedSites: 

747 button = QPushButton("Detect") 

748 table.addWidget(button, y_pos, 1) 

749 button.clicked.connect(partial(self.detect_clicked, data=(detector, available_site_names[site_number], 

750 screen_names[site_number], 

751 history_paths[site_number], 

752 summary_paths[site_number]))) 

753 y_pos += 1 

754 

755 btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, dia) 

756 btns.accepted.connect(dia.accept) 

757 btns.rejected.connect(dia.reject) 

758 dia.layout().addWidget(btns) 

759 

760 response = dia.exec_() 

761 if response: 

762 for site_number in range(0, len(available_site_names)): 

763 # print "site %s enabled=%s name=%s" % (available_site_names[site_number], 

764 # check_buttons[site_number].get_active(), screen_names[site_number].get_text(), 

765 # history_paths[site_number].get_text()) 

766 self.config.edit_site(available_site_names[site_number], str(check_buttons[site_number].isChecked()), 

767 screen_names[site_number].text(), history_paths[site_number].text(), 

768 summary_paths[site_number].text()) 

769 

770 self.config.save() 

771 self.reload_config() 

772 

773 def autoenableSite(self, text, checkbox): 

774 # autoactivate site if something gets typed in the screename field 

775 checkbox.setChecked(True) 

776 

777 def browseClicked(self, widget, parent, path): 

778 """runs when user clicks one of the browse buttons for the TS folder""" 

779 

780 newpath = QFileDialog.getExistingDirectory(parent, "Please choose the path that you want to Auto Import", 

781 path.text()) 

782 if newpath: 

783 path.setText(newpath) 

784 

785 def detect_clicked(self, widget, data): 

786 detector = data[0] 

787 site_name = data[1] 

788 entry_screen_name = data[2] 

789 entry_history_path = data[3] 

790 entry_summary_path = data[4] 

791 if detector.sitestatusdict[site_name]['detected']: 

792 entry_screen_name.setText(detector.sitestatusdict[site_name]['heroname']) 

793 entry_history_path.setText(detector.sitestatusdict[site_name]['hhpath']) 

794 if detector.sitestatusdict[site_name]['tspath']: 

795 entry_summary_path.setText(detector.sitestatusdict[site_name]['tspath']) 

796 

797 def reload_config(self): 

798 if len(self.nb_tab_names) == 1: 

799 # only main tab open, reload profile 

800 self.load_profile() 

801 self.warning_box(f"Configuration settings have been updated," 

802 f" Fpdb needs to be restarted now\n\nClick OK to close Fpdb") 

803 sys.exit() 

804 else: 

805 self.warning_box(f"Updated preferences have not been loaded because windows are open." 

806 f" Re-start fpdb to load them.") 

807 

808 def process_close_messages(self): 

809 # check for close messages 

810 try: 

811 while True: 

812 name = self.closeq.get(False) 

813 for i, t in enumerate(self.threads): 

814 if str(t.__class__) == str(name): 

815 # thread has ended so remove from list: 

816 del self.threads[i] 

817 break 

818 except queue.Empty: 

819 # no close messages on queue, do nothing 

820 pass 

821 

822 def __calendar_dialog(self, widget, entry): 

823 d = QDialog(self.dia_confirm) 

824 d.setWindowTitle('Pick a date') 

825 

826 vb = QVBoxLayout() 

827 d.setLayout(vb) 

828 cal = QCalendarWidget() 

829 vb.addWidget(cal) 

830 

831 btn = QPushButton('Done') 

832 btn.clicked.connect(partial(self.__get_date, calendar=cal, entry=entry, win=d)) 

833 

834 vb.addWidget(btn) 

835 

836 d.exec_() 

837 return 

838 

839 def createMenuBar(self): 

840 mb = self.menuBar() 

841 configMenu = mb.addMenu('Configure') 

842 importMenu = mb.addMenu('Import') 

843 hudMenu = mb.addMenu('HUD') 

844 cashMenu = mb.addMenu('Cash') 

845 tournamentMenu = mb.addMenu('Tournament') 

846 maintenanceMenu = mb.addMenu('Maintenance') 

847 #toolsMenu = mb.addMenu('Tools') 

848 helpMenu = mb.addMenu('Help') 

849 themeMenu = mb.addMenu('Themes') 

850 

851 # Create actions 

852 def makeAction(name, callback, shortcut=None, tip=None): 

853 action = QAction(name, self) 

854 if shortcut: 

855 action.setShortcut(shortcut) 

856 if tip: 

857 action.setToolTip(tip) 

858 action.triggered.connect(callback) 

859 return action 

860 

861 configMenu.addAction(makeAction('Site Settings', self.dia_site_preferences)) 

862 configMenu.addAction(makeAction('Seat Settings', self.dia_site_preferences_seat)) 

863 configMenu.addAction(makeAction('Hud Settings', self.dia_hud_preferences)) 

864 configMenu.addAction( 

865 makeAction('Adv Preferences', self.dia_advanced_preferences, tip='Edit your preferences')) 

866 configMenu.addAction(makeAction('Import filters', self.dia_import_filters)) 

867 configMenu.addSeparator() 

868 configMenu.addAction(makeAction('Close Fpdb', self.quit, 'Ctrl+Q', 'Quit the Program')) 

869 

870 importMenu.addAction(makeAction('Bulk Import', self.tab_bulk_import, 'Ctrl+B')) 

871 hudMenu.addAction(makeAction('HUD and Auto Import', self.tab_auto_import, 'Ctrl+A')) 

872 cashMenu.addAction(makeAction('Graphs', self.tabGraphViewer, 'Ctrl+G')) 

873 cashMenu.addAction(makeAction('Ring Player Stats', self.tab_ring_player_stats, 'Ctrl+P')) 

874 cashMenu.addAction(makeAction('Hand Viewer', self.tab_hand_viewer)) 

875 cashMenu.addAction(makeAction('Session Stats', self.tab_session_stats, 'Ctrl+S')) 

876 tournamentMenu.addAction(makeAction('Tourney Graphs', self.tabTourneyGraphViewer)) 

877 tournamentMenu.addAction(makeAction('Tourney Stats', self.tab_tourney_player_stats, 'Ctrl+T')) 

878 tournamentMenu.addAction(makeAction('Tourney Hand Viewer', self.tab_tourney_viewer_stats)) 

879 maintenanceMenu.addAction(makeAction('Statistics', self.dia_database_stats, 'View Database Statistics')) 

880 maintenanceMenu.addAction(makeAction('Create or Recreate Tables', self.dia_recreate_tables)) 

881 maintenanceMenu.addAction(makeAction('Rebuild HUD Cache', self.dia_recreate_hudcache)) 

882 maintenanceMenu.addAction(makeAction('Rebuild DB Indexes', self.dia_rebuild_indexes)) 

883 maintenanceMenu.addAction(makeAction('Dump Database to Textfile (takes ALOT of time)', self.dia_dump_db)) 

884 

885 #toolsMenu.addAction(makeAction('PokerProTools', self.launch_ppt)) 

886 helpMenu.addAction(makeAction('Log Messages', self.dia_logs, 'Log and Debug Messages')) 

887 helpMenu.addAction(makeAction('Help Tab', self.tab_main_help)) 

888 helpMenu.addSeparator() 

889 helpMenu.addAction(makeAction('Infos', self.dia_about, 'About the program')) 

890 

891 themes = [ 

892 'dark_purple.xml', 'dark_teal.xml', 'dark_blue.xml', 'dark_cyan.xml', 

893 'dark_pink.xml', 'dark_red.xml', 'dark_lime.xml', 'light_purple.xml', 

894 'light_teal.xml', 'light_blue.xml', 'light_cyan.xml', 'light_pink.xml', 

895 'light_red.xml', 'light_lime.xml' 

896 ] 

897 

898 for theme in themes: 

899 themeMenu.addAction(QAction(theme, self, triggered=partial(self.change_theme, theme))) 

900 

901 

902 def load_profile(self, create_db=False): 

903 """Loads profile from the provided path name. 

904 Set: 

905 - self.settings 

906 - self.config 

907 - self.db 

908 """ 

909 self.config = Configuration.Config(file=options.config, dbname=options.dbname) 

910 if self.config.file_error: 

911 self.warning_box(f"There is an error in your config file" 

912 f" {self.config.file}:\n{str(self.config.file_error)}", diatitle="CONFIG FILE ERROR") 

913 sys.exit() 

914 

915 log.info(f"Logfile is {os.path.join(self.config.dir_log, self.config.log_file)}") 

916 log.info(f"load profiles {self.config.example_copy}") 

917 log.info(f"{self.display_config_created_dialogue}") 

918 log.info(f"{self.config.wrongConfigVersion}") 

919 if self.config.example_copy or self.display_config_created_dialogue: 

920 self.info_box("Config file", [ 

921 "Config file has been created at " + self.config.file + ".", 

922 "Enter your screen_name and hand history path in the Site Preferences window" 

923 " (Main menu) before trying to import hands." 

924 ]) 

925 

926 self.display_config_created_dialogue = False 

927 elif self.config.wrongConfigVersion: 

928 diaConfigVersionWarning = QDialog() 

929 diaConfigVersionWarning.setWindowTitle("Strong Warning - Local configuration out of date") 

930 diaConfigVersionWarning.setLayout(QVBoxLayout()) 

931 label = QLabel([ 

932 "\nYour local configuration file needs to be updated." 

933 ]) 

934 diaConfigVersionWarning.layout().addWidget(label) 

935 label = QLabel([ 

936 "\nYour local configuration file needs to be updated.", 

937 "This error is not necessarily fatal but it is strongly recommended that you update the configuration." 

938 ]) 

939 

940 diaConfigVersionWarning.layout().addWidget(label) 

941 label = QLabel([ 

942 "To create a new configuration, see:", 

943 "fpdb.sourceforge.net/apps/mediawiki/fpdb/index.php?title=Reset_Configuration" 

944 ]) 

945 

946 label.setTextInteractionFlags(Qt.TextSelectableByMouse) 

947 diaConfigVersionWarning.layout().addWidget(label) 

948 label = QLabel([ 

949 f"A new configuration will destroy all personal settings" 

950 f" (hud layout, site folders, screennames, favourite seats).\n" 

951 ]) 

952 

953 diaConfigVersionWarning.layout().addWidget(label) 

954 

955 label = QLabel("To keep existing personal settings, you must edit the local file.") 

956 diaConfigVersionWarning.layout().addWidget(label) 

957 

958 label = QLabel("See the release note for information about the edits needed") 

959 diaConfigVersionWarning.layout().addWidget(label) 

960 

961 btns = QDialogButtonBox(QDialogButtonBox.Ok) 

962 btns.accepted.connect(diaConfigVersionWarning.accept) 

963 diaConfigVersionWarning.layout().addWidget(btns) 

964 

965 diaConfigVersionWarning.exec_() 

966 self.config.wrongConfigVersion = False 

967 

968 self.settings = {} 

969 self.settings['global_lock'] = self.lock 

970 if os.sep == "/": 

971 self.settings['os'] = "linuxmac" 

972 else: 

973 self.settings['os'] = "windows" 

974 

975 self.settings.update({'cl_options': cl_options}) 

976 self.settings.update(self.config.get_db_parameters()) 

977 self.settings.update(self.config.get_import_parameters()) 

978 self.settings.update(self.config.get_default_paths()) 

979 

980 if self.db is not None and self.db.is_connected(): 

981 self.db.disconnect() 

982 

983 self.sql = SQL.Sql(db_server=self.settings['db-server']) 

984 err_msg = None 

985 try: 

986 self.db = Database.Database(self.config, sql=self.sql) 

987 if self.db.get_backend_name() == 'SQLite': 

988 # tell sqlite users where the db file is 

989 log.info(f"Connected to SQLite: {self.db.db_path}") 

990 except Exceptions.FpdbMySQLAccessDenied: 

991 err_msg = "MySQL Server reports: Access denied. Are your permissions set correctly?" 

992 except Exceptions.FpdbMySQLNoDatabase: 

993 err_msg = f"MySQL client reports: 2002 or 2003 error." \ 

994 f" Unable to connect - Please check that the MySQL service has been started." 

995 

996 except Exceptions.FpdbPostgresqlAccessDenied: 

997 err_msg = "PostgreSQL Server reports: Access denied. Are your permissions set correctly?" 

998 except Exceptions.FpdbPostgresqlNoDatabase: 

999 err_msg = f"PostgreSQL client reports: Unable to connect -" \ 

1000 f"Please check that the PostgreSQL service has been started." 

1001 if err_msg is not None: 

1002 self.db = None 

1003 self.warning_box(err_msg) 

1004 if self.db is not None and not self.db.is_connected(): 

1005 self.db = None 

1006 

1007 if self.db is not None and self.db.wrongDbVersion: 

1008 diaDbVersionWarning = QMessageBox(QMessageBox.Warning, "Strong Warning - Invalid database version", 

1009 "An invalid DB version or missing tables have been detected.", 

1010 QMessageBox.Ok, self) 

1011 diaDbVersionWarning.setInformativeText( 

1012 f"This error is not necessarily fatal but it is strongly" 

1013 f" recommended that you recreate the tables by using the Database menu." 

1014 f"Not doing this will likely lead to misbehaviour including fpdb crashes, corrupt data etc." 

1015 ) 

1016 

1017 diaDbVersionWarning.exec_() 

1018 if self.db is not None and self.db.is_connected(): 

1019 self.statusBar().showMessage(f"Status: Connected to {self.db.get_backend_name()}" 

1020 f" database named {self.db.database} on host {self.db.host}") 

1021 

1022 # rollback to make sure any locks are cleared: 

1023 self.db.rollback() 

1024 

1025 # If the db-version is out of date, don't validate the config 

1026 # otherwise the end user gets bombarded with false messages 

1027 # about every site not existing 

1028 if hasattr(self.db, 'wrongDbVersion'): 

1029 if not self.db.wrongDbVersion: 

1030 self.validate_config() 

1031 

1032 def obtain_global_lock(self, source): 

1033 ret = self.lock.acquire(source=source) # will return false if lock is already held 

1034 if ret: 

1035 log.info(f"Global lock taken by {source}") 

1036 self.lockTakenBy = source 

1037 else: 

1038 log.info(f"Failed to get global lock, it is currently held by {source}") 

1039 return ret 

1040 # need to release it later: 

1041 # self.lock.release() 

1042 

1043 def quit(self, widget, data=None): 

1044 # TODO: can we get some / all of the stuff done in this function to execute on any kind of abort? 

1045 # FIXME get two "quitting normally" messages, following the addition of the self.window.destroy() call 

1046 # ... because self.window.destroy() leads to self.destroy() which calls this! 

1047 if not self.quitting: 

1048 log.info("Quitting normally") 

1049 self.quitting = True 

1050 # TODO: check if current settings differ from profile, if so offer to save or abort 

1051 

1052 if self.db is not None: 

1053 if self.db.backend == self.db.MYSQL_INNODB: 

1054 try: 

1055 import _mysql_exceptions 

1056 if self.db is not None and self.db.is_connected(): 

1057 self.db.disconnect() 

1058 except _mysql_exceptions.OperationalError: # oh, damn, we're already disconnected 

1059 pass 

1060 else: 

1061 if self.db is not None and self.db.is_connected(): 

1062 self.db.disconnect() 

1063 else: 

1064 pass 

1065 # self.statusIcon.set_visible(False) 

1066 QCoreApplication.quit() 

1067 

1068 def release_global_lock(self): 

1069 self.lock.release() 

1070 self.lockTakenBy = None 

1071 log.info("Global lock released.") 

1072 

1073 def tab_auto_import(self, widget, data=None): 

1074 """opens the auto import tab""" 

1075 new_aimp_thread = GuiAutoImport.GuiAutoImport(self.settings, self.config, self.sql, self) 

1076 self.threads.append(new_aimp_thread) 

1077 self.add_and_display_tab(new_aimp_thread, "HUD") 

1078 if options.autoimport: 

1079 new_aimp_thread.startClicked(new_aimp_thread.startButton, "autostart") 

1080 options.autoimport = False 

1081 

1082 def tab_bulk_import(self, widget, data=None): 

1083 """opens a tab for bulk importing""" 

1084 new_import_thread = GuiBulkImport.GuiBulkImport(self.settings, self.config, self.sql, self) 

1085 self.threads.append(new_import_thread) 

1086 self.add_and_display_tab(new_import_thread, "Bulk Import") 

1087 

1088 

1089 

1090 def tab_tourney_import(self, widget, data=None): 

1091 """opens a tab for bulk importing tournament summaries""" 

1092 new_import_thread = GuiTourneyImport.GuiTourneyImport(self.settings, self.config, self.sql, self.window) 

1093 self.threads.append(new_import_thread) 

1094 bulk_tab = new_import_thread.get_vbox() 

1095 self.add_and_display_tab(bulk_tab, "Tournament Results Import") 

1096 

1097 

1098 

1099 # end def tab_import_imap_summaries 

1100 

1101 def tab_ring_player_stats(self, widget, data=None): 

1102 new_ps_thread = GuiRingPlayerStats.GuiRingPlayerStats(self.config, self.sql, self) 

1103 self.threads.append(new_ps_thread) 

1104 self.add_and_display_tab(new_ps_thread, "Ring Player Stats") 

1105 

1106 def tab_tourney_player_stats(self, widget, data=None): 

1107 new_ps_thread = GuiTourneyPlayerStats.GuiTourneyPlayerStats(self.config, self.db, self.sql, self) 

1108 self.threads.append(new_ps_thread) 

1109 self.add_and_display_tab(new_ps_thread, "Tourney Stats") 

1110 

1111 def tab_tourney_viewer_stats(self, widget, data=None): 

1112 new_thread = GuiTourHandViewer.TourHandViewer(self.config, self.sql, self) 

1113 self.threads.append(new_thread) 

1114 self.add_and_display_tab(new_thread, "Tourney Viewer") 

1115 

1116 def tab_positional_stats(self, widget, data=None): 

1117 new_ps_thread = GuiPositionalStats.GuiPositionalStats(self.config, self.sql) 

1118 self.threads.append(new_ps_thread) 

1119 ps_tab = new_ps_thread.get_vbox() 

1120 self.add_and_display_tab(ps_tab, "Positional Stats") 

1121 

1122 def tab_session_stats(self, widget, data=None): 

1123 colors = self.get_theme_colors() 

1124 new_ps_thread = GuiSessionViewer.GuiSessionViewer(self.config, self.sql, self, self, colors=colors) 

1125 self.threads.append(new_ps_thread) 

1126 self.add_and_display_tab(new_ps_thread, "Session Stats") 

1127 

1128 def tab_hand_viewer(self, widget, data=None): 

1129 new_ps_thread = GuiHandViewer.GuiHandViewer(self.config, self.sql, self) 

1130 self.threads.append(new_ps_thread) 

1131 self.add_and_display_tab(new_ps_thread, "Hand Viewer") 

1132 

1133 def tab_main_help(self, widget, data=None): 

1134 """Displays a tab with the main fpdb help screen""" 

1135 mh_tab = QLabel((""" 

1136 Welcome to Fpdb! 

1137 

1138 This program is currently in an alpha-state, so our database format is still sometimes changed. 

1139 You should therefore always keep your hand history files so that you can re-import 

1140 after an update, if necessary. 

1141 

1142 all configuration now happens in HUD_config.xml. 

1143 

1144 This program is free/libre open source software licensed partially under the AGPL3, 

1145 and partially under GPL2 or later. 

1146 The Windows installer package includes code licensed under the MIT license. 

1147 You can find the full license texts in agpl-3.0.txt, gpl-2.0.txt, gpl-3.0.txt 

1148 and mit.txt in the fpdb installation directory.""")) 

1149 self.add_and_display_tab(mh_tab, "Help") 

1150 

1151 def get_theme_colors(self): 

1152 """ 

1153 Returns a dictionary containing the theme colors used in the application. 

1154 

1155 The dictionary contains the following keys: 

1156 - "background": the name of the color used for the background. 

1157 - "foreground": the name of the color used for the foreground. 

1158 - "grid": the name of the color used for the grid. 

1159 - "line_showdown": the name of the color used for the showdown line. 

1160 - "line_nonshowdown": the name of the color used for the non-showdown line. 

1161 - "line_ev": the name of the color used for the event line. 

1162 - "line_hands": the name of the color used for the hands line. 

1163 

1164 Returns: 

1165 dict: A dictionary containing the theme colors. 

1166 """ 

1167 return { 

1168 "background": self.palette().color(QPalette.Window).name(), 

1169 "foreground": self.palette().color(QPalette.WindowText).name(), 

1170 "grid": "#444444", # to customize 

1171 "line_showdown": "#0000FF", 

1172 "line_nonshowdown": "#FF0000", 

1173 "line_ev": "#FFA500", 

1174 "line_hands": "#00FF00", 

1175 'line_up': 'g', 

1176 'line_down': 'r', 

1177 'line_showdown': 'b', 

1178 'line_nonshowdown': 'm', 

1179 'line_ev': 'orange', 

1180 'line_hands': 'c' 

1181 } 

1182 

1183 def tabGraphViewer(self, widget, data=None): 

1184 """opens a graph viewer tab""" 

1185 colors = self.get_theme_colors() 

1186 new_gv_thread = GuiGraphViewer.GuiGraphViewer(self.sql, self.config, self, colors=colors) 

1187 self.threads.append(new_gv_thread) 

1188 self.add_and_display_tab(new_gv_thread, "Graphs") 

1189 

1190 def tabTourneyGraphViewer(self, widget, data=None): 

1191 """opens a graph viewer tab""" 

1192 colors = self.get_theme_colors() 

1193 new_gv_thread = GuiTourneyGraphViewer.GuiTourneyGraphViewer(self.sql, self.config, self, colors=colors) 

1194 self.threads.append(new_gv_thread) 

1195 self.add_and_display_tab(new_gv_thread, "Tourney Graphs") 

1196 

1197 def tabStove(self, widget, data=None): 

1198 """opens a tab for poker stove""" 

1199 thread = GuiStove.GuiStove(self.config, self) 

1200 self.threads.append(thread) 

1201 # tab = thread.get_vbox() 

1202 self.add_and_display_tab(thread, "Stove") 

1203 

1204 def validate_config(self): 

1205 # check if sites in config file are in DB 

1206 for site in self.config.supported_sites: # get site names from config file 

1207 try: 

1208 self.config.get_site_id(site) # and check against list from db 

1209 except KeyError as exc: 

1210 log.warning(f"site {site} missing from db") 

1211 dia = QMessageBox() 

1212 dia.setIcon(QMessageBox.Warning) 

1213 dia.setText("Unknown Site") 

1214 dia.setStandardButtons(QMessageBox.Ok) 

1215 dia.exec_() 

1216 diastring = f"Warning: Unable to find site '{site}'" 

1217 dia.format_secondary_text(diastring) 

1218 dia.run() 

1219 dia.destroy() 

1220 

1221 def info_box(self, str1, str2): 

1222 diapath = QMessageBox(self) 

1223 diapath.setWindowTitle(str1) 

1224 diapath.setText(str2) 

1225 return diapath.exec_() 

1226 

1227 def warning_box(self, string, diatitle="FPDB WARNING"): 

1228 return QMessageBox(QMessageBox.Warning, diatitle, string).exec_() 

1229 

1230 def change_theme(self, theme): 

1231 apply_stylesheet(app, theme=theme) 

1232 

1233 

1234 

1235 def update_title_bar_theme(self): 

1236 # Apply the stylesheet to the custom title bar 

1237 self.custom_title_bar.update_theme() 

1238 

1239 def close_tab(self, index): 

1240 item = self.nb.widget(index) 

1241 self.nb.removeTab(index) 

1242 self.nb_tab_names.pop(index) 

1243 

1244 try: 

1245 self.threads.remove(item) 

1246 except ValueError: 

1247 pass 

1248 

1249 item.deleteLater() 

1250 

1251 def __init__(self): 

1252 super().__init__() 

1253 if sys.platform == 'darwin': 

1254 pass 

1255 else: 

1256 self.setWindowFlags(Qt.FramelessWindowHint) 

1257 cards = os.path.join(Configuration.GRAPHICS_PATH, 'tribal.jpg') 

1258 if os.path.exists(cards): 

1259 self.setWindowIcon(QIcon(cards)) 

1260 set_locale_translation() 

1261 self.lock = interlocks.InterProcessLock(name="fpdb_global_lock") 

1262 self.db = None 

1263 self.status_bar = None 

1264 self.quitting = False 

1265 self.visible = False 

1266 self.threads = [] 

1267 self.closeq = queue.Queue(20) 

1268 

1269 self.oldPos = self.pos() 

1270 

1271 

1272 

1273 if options.initialRun: 

1274 self.display_config_created_dialogue = True 

1275 self.display_site_preferences = True 

1276 else: 

1277 self.display_config_created_dialogue = False 

1278 self.display_site_preferences = False 

1279 

1280 if options.xloc is not None or options.yloc is not None: 

1281 if options.xloc is None: 

1282 options.xloc = 0 

1283 if options.yloc is None: 

1284 options.yloc = 0 

1285 self.move(options.xloc, options.yloc) 

1286 

1287 self.setWindowTitle("Free Poker DB 3") 

1288 defx, defy = 1920, 1080 

1289 sg = QApplication.primaryScreen().availableGeometry() 

1290 if sg.width() < defx: 

1291 defx = sg.width() 

1292 if sg.height() < defy: 

1293 defy = sg.height() 

1294 self.resize(defx, defy) 

1295 

1296 if sys.platform == 'darwin': 

1297 pass 

1298 else: 

1299 # Create custom title bar 

1300 self.custom_title_bar = CustomTitleBar(self) 

1301 # Create central widget and layout 

1302 self.central_widget = QWidget(self) 

1303 self.central_layout = QVBoxLayout(self.central_widget) 

1304 self.central_layout.setContentsMargins(0, 0, 0, 0) 

1305 self.central_layout.setSpacing(0) 

1306 

1307 if sys.platform == 'darwin': 

1308 # Add title bar and menu bar to layout 

1309 self.custom_title_bar = CustomTitleBar(self) 

1310 self.central_layout.addWidget(self.custom_title_bar) 

1311 self.setMenuBar(self.menuBar()) 

1312 else: 

1313 # Add title bar and menu bar to layout 

1314 self.central_layout.addWidget(self.custom_title_bar) 

1315 self.menu_bar = self.menuBar() 

1316 self.central_layout.setMenuBar(self.menu_bar) 

1317 

1318 self.nb = QTabWidget() 

1319 self.nb.setTabsClosable(True) 

1320 self.nb.tabCloseRequested.connect(self.close_tab) 

1321 self.central_layout.addWidget(self.nb) 

1322 self.setCentralWidget(self.central_widget) 

1323 

1324 self.createMenuBar() 

1325 

1326 self.pages = [] 

1327 self.nb_tab_names = [] 

1328 

1329 self.tab_main_help(None, None) 

1330 

1331 if options.minimized: 

1332 self.showMinimized() 

1333 if options.hidden: 

1334 self.hide() 

1335 

1336 if not options.hidden: 

1337 self.show() 

1338 self.visible = True 

1339 

1340 self.load_profile(create_db=True) 

1341 

1342 if self.config.install_method == 'app': 

1343 for site in list(self.config.supported_sites.values()): 

1344 if site.screen_name != "YOUR SCREEN NAME HERE": 

1345 break 

1346 else: 

1347 options.initialRun = True 

1348 self.display_config_created_dialogue = True 

1349 self.display_site_preferences = True 

1350 

1351 if options.initialRun and self.display_site_preferences: 

1352 self.dia_site_preferences(None, None) 

1353 self.display_site_preferences = False 

1354 

1355 if not options.errorsToConsole: 

1356 fileName = os.path.join(self.config.dir_log, 'fpdb-errors.txt') 

1357 log.info(f"Note: error output is being diverted to {self.config.dir_log}. Any major error will be reported there _only_.") 

1358 errorFile = codecs.open(fileName, 'w', 'utf-8') 

1359 sys.stderr = errorFile 

1360 

1361 sys.stderr.write("fpdb starting ...") 

1362 

1363 if options.autoimport: 

1364 self.tab_auto_import(None) 

1365 

1366 

1367 

1368class CustomTitleBar(QWidget): 

1369 def __init__(self, parent=None): 

1370 super().__init__(parent) 

1371 self.setAutoFillBackground(True) 

1372 self.main_window = parent 

1373 

1374 self.title = QLabel("Free Poker DB 3") 

1375 self.title.setAlignment(Qt.AlignCenter) 

1376 

1377 self.btn_minimize = QPushButton("-") 

1378 self.btn_maximize = QPushButton("+") 

1379 self.btn_close = QPushButton("x") 

1380 

1381 button_size = 20 

1382 self.btn_minimize.setFixedSize(button_size, button_size) 

1383 self.btn_maximize.setFixedSize(button_size, button_size) 

1384 self.btn_close.setFixedSize(button_size, button_size) 

1385 

1386 self.btn_minimize.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 

1387 self.btn_maximize.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 

1388 self.btn_close.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 

1389 

1390 self.btn_minimize.clicked.connect(parent.showMinimized) 

1391 self.btn_maximize.clicked.connect(self.toggle_maximize_restore) 

1392 self.btn_close.clicked.connect(parent.close) 

1393 

1394 layout = QHBoxLayout() 

1395 layout.addWidget(self.title) 

1396 layout.addStretch() 

1397 layout.addWidget(self.btn_minimize) 

1398 layout.addWidget(self.btn_maximize) 

1399 layout.addWidget(self.btn_close) 

1400 self.setLayout(layout) 

1401 

1402 self.is_maximized = False 

1403 if sys.platform == 'darwin': 

1404 pass 

1405 else: 

1406 self.moving = False 

1407 self.offset = None 

1408 

1409 def toggle_maximize_restore(self): 

1410 if self.is_maximized: 

1411 self.main_window.showNormal() 

1412 else: 

1413 self.main_window.showMaximized() 

1414 self.is_maximized = not self.is_maximized 

1415 

1416 def update_theme(self): 

1417 self.setStyleSheet(self.main_window.styleSheet()) 

1418 

1419 def mousePressEvent(self, event): 

1420 if event.button() == Qt.LeftButton: 

1421 self.main_window.oldPos = event.globalPos() 

1422 

1423 def mouseMoveEvent(self, event): 

1424 if event.buttons() == Qt.LeftButton: 

1425 delta = QPoint(event.globalPos() - self.main_window.oldPos) 

1426 self.main_window.move(self.main_window.x() + delta.x(), self.main_window.y() + delta.y()) 

1427 self.main_window.oldPos = event.globalPos() 

1428 

1429 

1430 

1431if __name__ == "__main__": 

1432 from qt_material import apply_stylesheet 

1433 import time 

1434 try: 

1435 app = QApplication([]) 

1436 apply_stylesheet(app, theme='dark_purple.xml') 

1437 me = fpdb() 

1438 app.exec_() 

1439 finally: 

1440 profiler.disable() 

1441 s = io.StringIO() 

1442 ps = pstats.Stats(profiler, stream=s).sort_stats('cumulative') 

1443 ps.print_stats() 

1444 

1445 # Use timestamp or process ID for unique filenames 

1446 timestamp = time.strftime("%Y%m%d-%H%M%S") 

1447 results_file = os.path.join(PROFILE_OUTPUT_DIR, f'fpdb_profile_results_{timestamp}.txt') 

1448 profile_file = os.path.join(PROFILE_OUTPUT_DIR, f'fpdb_profile_{timestamp}.prof') 

1449 

1450 with open(results_file, 'w') as f: 

1451 f.write(s.getvalue()) 

1452 

1453 profiler.dump_stats(profile_file)