Coverage for fpdb.pyw: 0%

899 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 19:33 +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 queue 

22 

23# import qdarkstyle 

24if os.name == "nt": 

25 pass 

26 

27import codecs 

28import Options 

29from functools import partial 

30from L10n import set_locale_translation 

31import logging 

32 

33from PyQt5.QtCore import QCoreApplication, QDate, Qt, QPoint 

34from PyQt5.QtGui import QIcon, QPalette 

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

36from PyQt5.QtWidgets import ( 

37 QAction, 

38 QApplication, 

39 QCalendarWidget, 

40 QCheckBox, 

41 QDateEdit, 

42 QDialog, 

43 QDialogButtonBox, 

44 QFileDialog, 

45 QGridLayout, 

46 QLineEdit, 

47 QMainWindow, 

48 QMessageBox, 

49 QScrollArea, 

50 QTabWidget, 

51 QVBoxLayout, 

52 QComboBox, 

53) 

54 

55import DetectInstalledSites 

56import GuiPrefs 

57import GuiLogView 

58 

59# import GuiDatabase 

60import GuiBulkImport 

61# import GuiTourneyImport 

62 

63import GuiRingPlayerStats 

64import GuiTourneyPlayerStats 

65 

66# import GuiPositionalStats 

67import GuiAutoImport 

68import GuiGraphViewer 

69import GuiTourneyGraphViewer 

70import GuiSessionViewer 

71import GuiHandViewer 

72import GuiTourHandViewer 

73# import GuiOddsCalc 

74# import GuiStove 

75 

76import SQL 

77import Database 

78import Configuration 

79import Card 

80import Exceptions 

81 

82# import api, app 

83import cProfile 

84import pstats 

85import io 

86import interlocks 

87from Exceptions import FpdbError 

88 

89import sqlite3 

90 

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

92import numpy 

93 

94cl_options = ".".join(sys.argv[1:]) 

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

96 

97 

98numpy_version = numpy.__version__ 

99 

100 

101sqlite3_version = sqlite3.version 

102sqlite_version = sqlite3.sqlite_version 

103 

104 

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

106os.makedirs(PROFILE_OUTPUT_DIR, exist_ok=True) 

107 

108profiler = cProfile.Profile() 

109profiler.enable() 

110 

111 

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

113 

114log = logging.getLogger("fpdb") 

115 

116try: 

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

118 import subprocess 

119 

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

121 VERSION = VERSION[:-1] 

122except Exception: 

123 VERSION = "3.0.0alpha" 

124 

125 

126class fpdb(QMainWindow): 

127 # def launch_ppt(self): 

128 # path = os.getcwd() 

129 # if os.name == "nt": 

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

131 # else: 

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

133 # subprocess.call(["java", "-jar", pathcomp]) 

134 

135 def add_and_display_tab(self, new_page, new_tab_name): 

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

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

138 if name == new_tab_name: 

139 self.display_tab(new_tab_name) 

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

141 

142 used_before = False 

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

144 if name == new_tab_name: 

145 used_before = True 

146 page = self.pages[i] 

147 break 

148 

149 if not used_before: 

150 page = new_page 

151 self.pages.append(new_page) 

152 self.nb_tab_names.append(new_tab_name) 

153 

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

155 self.nb.setCurrentIndex(index) 

156 

157 def display_tab(self, new_tab_name): 

158 """displays the indicated tab""" 

159 tab_no = -1 

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

161 if new_tab_name == name: 

162 tab_no = i 

163 break 

164 

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

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

167 else: 

168 self.nb.setCurrentIndex(tab_no) 

169 

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

171 QMessageBox.about( 

172 self, 

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

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

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

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

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

178 + "\n" 

179 + "Your config file is: " 

180 + self.config.file, 

181 ) 

182 return 

183 

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

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

186 # have changed file contents 

187 self.load_profile() 

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

189 # save updated config 

190 self.config.save() 

191 self.reload_config() 

192 

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

194 self.warning_box( 

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

196 diatitle="Database Statistics", 

197 ) 

198 

199 # end def dia_database_stats 

200 

201 @staticmethod 

202 def get_text(widget: QWidget): 

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

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

205 

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

207 dia = QDialog(self) 

208 dia.setWindowTitle("Modifying Huds") 

209 dia.resize(1200, 600) 

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

211 dia.setLayout(QVBoxLayout()) 

212 dia.layout().addWidget(label) 

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

214 dia.layout().addWidget(label2) 

215 self.comboGame = QComboBox() 

216 

217 huds_names = self.config.get_stat_sets() 

218 for hud_name in huds_names: 

219 self.comboGame.addItem(hud_name) 

220 

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

222 self.comboGame.setCurrentIndex(1) 

223 selected_hud_name = self.comboGame.currentText() 

224 

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

226 

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

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

229 

230 self.table = QGridLayout() 

231 self.table.setSpacing(0) 

232 

233 scrolling_frame = QScrollArea(dia) 

234 dia.layout().addWidget(scrolling_frame) 

235 scrolling_frame.setLayout(self.table) 

236 

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

238 

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

240 btns.accepted.connect(dia.accept) 

241 btns.rejected.connect(dia.reject) 

242 dia.layout().addWidget(btns) 

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

244 

245 # Launch Dialog 

246 response = dia.exec_() 

247 

248 # Treat dialog closed event 

249 if self.comboGame.currentIndexChanged and response: 

250 selected_hud_name = self.comboGame.currentText() 

251 # User clicked on "Save" 

252 for y in range(nb_items): 

253 self.config.edit_hud( 

254 selected_hud_name, 

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

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

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

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

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

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

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

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

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

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

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

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

267 ) 

268 

269 self.config.save() 

270 self.reload_config() 

271 

272 def index_changed(self, index): 

273 # Called when user changes currently selected HUD 

274 log.info("start index_changed") 

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

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

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

278 selected_hud_name = self.comboGame.currentText() 

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

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

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

282 

283 self.table.setSpacing(0) 

284 

285 column_headers = [ 

286 "coordonate", 

287 "stats name", 

288 "click", 

289 "hudcolor", 

290 "hudprefix", 

291 "hudsuffix", 

292 "popup", 

293 "stat_hicolor", 

294 "stat_hith", 

295 "stat_locolor", 

296 "stat_loth", 

297 "tip", 

298 ] # todo ("HUD") 

299 

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

301 label = QLabel(column_headers[header_number]) 

302 label.setAlignment(Qt.AlignCenter) 

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

304 

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

306 ( 

307 self.stat_position_list, 

308 self.stat_name_list, 

309 self.click_list, 

310 self.hudcolor_list, 

311 self.hudprefix_list, 

312 self.hudsuffix_list, 

313 self.popup_list, 

314 self.stat_hicolor_list, 

315 self.stat_hith_list, 

316 self.stat_locolor_list, 

317 self.stat_loth_list, 

318 self.tip_list, 

319 ) = [], [], [], [], [], [], [], [], [], [], [], [] 

320 

321 self.load_profile() 

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

323 y_pos = 1 

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

325 # Column 1: stat position 

326 stat2 = QLabel() 

327 stat2.setText(str(position)) 

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

329 self.stat_position_list.append(stat2) 

330 

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

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

333 if os.name == "nt": 

334 icoPath = os.path.dirname(__file__) 

335 icoPath = f"{icoPath}\\" 

336 else: 

337 icoPath = "icons/" 

338 stat3 = QComboBox() 

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

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

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

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

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

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

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

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

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

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

349 self.stat_name_list.append(stat3) 

350 

351 # Column 3: "click" 

352 stat4 = QLineEdit() 

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

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

355 self.click_list.append(stat4) 

356 

357 # Column 4: "hudcolor" 

358 stat5 = QLineEdit() 

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

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

361 self.hudcolor_list.append(stat5) 

362 

363 # Column 5: "hudprefix" 

364 stat6 = QLineEdit() 

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

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

367 self.hudprefix_list.append(stat6) 

368 

369 # Column 6: "hudsuffix" 

370 stat7 = QLineEdit() 

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

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

373 self.hudsuffix_list.append(stat7) 

374 

375 # Column 7: "popup" 

376 stat8 = QComboBox() 

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

378 stat8.addItem(popup) 

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

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

381 self.popup_list.append(stat8) 

382 

383 # Column 8: "stat_hicolor" 

384 stat9 = QLineEdit() 

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

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

387 self.stat_hicolor_list.append(stat9) 

388 

389 # Column 9: "stat_hith" 

390 stat10 = QLineEdit() 

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

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

393 self.stat_hith_list.append(stat10) 

394 

395 # Column 10: "stat_locolor" 

396 stat11 = QLineEdit() 

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

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

399 self.stat_locolor_list.append(stat11) 

400 

401 # Column 11: "stat_loth" 

402 stat12 = QLineEdit() 

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

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

405 self.stat_loth_list.append(stat12) 

406 

407 # Column 12: "tip" 

408 stat13 = QLineEdit() 

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

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

411 self.tip_list.append(stat13) 

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

413 # pass 

414 

415 y_pos += 1 

416 

417 def dia_import_filters(self, checkState): 

418 dia = QDialog() 

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

420 dia.setLayout(QVBoxLayout()) 

421 checkboxes = {} 

422 filters = self.config.get_import_parameters()["importFilters"] 

423 for game in Card.games: 

424 checkboxes[game] = QCheckBox(game) 

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

426 if game in filters: 

427 checkboxes[game].setChecked(True) 

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

429 dia.layout().addWidget(btns) 

430 btns.accepted.connect(dia.accept) 

431 btns.rejected.connect(dia.reject) 

432 if dia.exec_(): 

433 filterGames = [] 

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

435 if cb.isChecked(): 

436 filterGames.append(game) 

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

438 self.config.save() 

439 

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

441 filename = "database-dump.sql" 

442 result = self.db.dumpDatabase() 

443 

444 with open(filename, "w") as dumpFile: 

445 dumpFile.write(result) 

446 

447 # end def dia_database_stats 

448 

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

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

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

452 dia_confirm = QMessageBox( 

453 QMessageBox.Warning, 

454 "Wipe DB", 

455 "Confirm deleting and recreating tables", 

456 QMessageBox.Yes | QMessageBox.No, 

457 self, 

458 ) 

459 diastring = ( 

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

461 f" the database {self.db.database} on {self.db.host}" 

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

463 ) 

464 

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

466 response = dia_confirm.exec_() 

467 

468 if response == QMessageBox.Yes: 

469 self.db.recreate_tables() 

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

471 for t in self.threads: 

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

473 t.importer.database.resetCache() 

474 self.release_global_lock() 

475 else: 

476 self.release_global_lock() 

477 log.info("User cancelled recreating tables") 

478 else: 

479 self.warning_box( 

480 "Cannot open Database Maintenance window because other" 

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

482 ) 

483 

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

485 if self.obtain_global_lock("dia_recreate_hudcache"): 

486 self.dia_confirm = QDialog() 

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

488 self.dia_confirm.setLayout(QVBoxLayout()) 

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

490 

491 hb1 = QHBoxLayout() 

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

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

494 btn = QPushButton("Cal") 

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

496 

497 hb1.addWidget(lbl) 

498 hb1.addStretch() 

499 hb1.addWidget(self.h_start_date) 

500 hb1.addWidget(btn) 

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

502 

503 hb2 = QHBoxLayout() 

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

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

506 btn = QPushButton("Cal") 

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

508 

509 hb2.addWidget(lbl) 

510 hb2.addStretch() 

511 hb2.addWidget(self.start_date) 

512 hb2.addWidget(btn) 

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

514 

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

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

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

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

519 

520 response = self.dia_confirm.exec_() 

521 if response: 

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

523 

524 self.db.rebuild_cache( 

525 self.h_start_date.date().toString("yyyy-MM-dd"), self.start_date.date().toString("yyyy-MM-dd") 

526 ) 

527 else: 

528 log.info("User cancelled rebuilding hud cache") 

529 

530 self.release_global_lock() 

531 else: 

532 self.warning_box( 

533 "Cannot open Database Maintenance window because other windows have been opened. " 

534 "Re-start fpdb to use this option." 

535 ) 

536 

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

538 if self.obtain_global_lock("dia_rebuild_indexes"): 

539 self.dia_confirm = QMessageBox( 

540 QMessageBox.Warning, 

541 "Rebuild DB", 

542 "Confirm rebuilding database indexes", 

543 QMessageBox.Yes | QMessageBox.No, 

544 self, 

545 ) 

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

547 self.dia_confirm.setInformativeText(diastring) 

548 

549 response = self.dia_confirm.exec_() 

550 if response == QMessageBox.Yes: 

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

552 self.db.rebuild_indexes() 

553 

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

555 self.db.vacuumDB() 

556 

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

558 self.db.analyzeDB() 

559 else: 

560 log.info("User cancelled rebuilding db indexes") 

561 

562 self.release_global_lock() 

563 else: 

564 self.warning_box( 

565 "Cannot open Database Maintenance window because" 

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

567 ) 

568 

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

570 """opens the log viewer window""" 

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

572 self.process_close_messages() 

573 

574 viewer = None 

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

576 if str(t.__class__) == "GuiLogView.GuiLogView": 

577 viewer = t 

578 break 

579 

580 if viewer is None: 

581 # print "creating new log viewer" 

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

583 self.threads.append(new_thread) 

584 else: 

585 # print "showing existing log viewer" 

586 viewer.get_dialog().present() 

587 

588 # if lock_set: 

589 # self.release_global_lock() 

590 

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

592 dia = QDialog(self) 

593 dia.setWindowTitle("Seat Preferences") 

594 dia.resize(1200, 600) 

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

596 dia.setLayout(QVBoxLayout()) 

597 dia.layout().addWidget(label) 

598 

599 self.load_profile() 

600 site_names = self.config.site_ids 

601 available_site_names = [] 

602 for site_name in site_names: 

603 try: 

604 self.config.supported_sites[site_name].enabled 

605 available_site_names.append(site_name) 

606 except KeyError: 

607 pass 

608 

609 column_headers = [ 

610 "Site", 

611 "2 players:\nbetween 0 & 2", 

612 "3 players:\nbetween 0 & 3 ", 

613 "4 players:\nbetween 0 & 4", 

614 "5 players:\nbetween 0 & 5", 

615 "6 players:\nbetween 0 & 6", 

616 "7 players:\nbetween 0 & 7", 

617 "8 players:\nbetween 0 & 8", 

618 "9 players:\nbetween 0 & 9", 

619 "10 players:\nbetween 0 & 10", 

620 ] # todo ("HUD") 

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

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

623 

624 table = QGridLayout() 

625 table.setSpacing(0) 

626 

627 scrolling_frame = QScrollArea(dia) 

628 dia.layout().addWidget(scrolling_frame) 

629 scrolling_frame.setLayout(table) 

630 

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

632 label = QLabel(column_headers[header_number]) 

633 label.setAlignment(Qt.AlignCenter) 

634 table.addWidget(label, 0, header_number) 

635 

636 # history_paths = [] 

637 check_buttons = [] 

638 # screen_names = [] 

639 seat2_dict, seat3_dict, seat4_dict, seat5_dict, seat6_dict, seat7_dict, seat8_dict, seat9_dict, seat10_dict = ( 

640 [], 

641 [], 

642 [], 

643 [], 

644 [], 

645 [], 

646 [], 

647 [], 

648 [], 

649 ) 

650 # summary_paths = [] 

651 detector = DetectInstalledSites.DetectInstalledSites() 

652 

653 y_pos = 1 

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

655 check_button = QCheckBox(available_site_names[site_number]) 

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

657 table.addWidget(check_button, y_pos, 0) 

658 check_buttons.append(check_button) 

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

660 

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

662 seat2 = QLineEdit() 

663 

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

665 table.addWidget(seat2, y_pos, 1) 

666 seat2_dict.append(seat2) 

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

668 

669 seat3 = QLineEdit() 

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

671 table.addWidget(seat3, y_pos, 2) 

672 seat3_dict.append(seat3) 

673 

674 seat4 = QLineEdit() 

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

676 table.addWidget(seat4, y_pos, 3) 

677 seat4_dict.append(seat4) 

678 

679 seat5 = QLineEdit() 

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

681 table.addWidget(seat5, y_pos, 4) 

682 seat5_dict.append(seat5) 

683 

684 seat6 = QLineEdit() 

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

686 table.addWidget(seat6, y_pos, 5) 

687 seat6_dict.append(seat6) 

688 

689 seat7 = QLineEdit() 

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

691 table.addWidget(seat7, y_pos, 6) 

692 seat7_dict.append(seat7) 

693 

694 seat8 = QLineEdit() 

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

696 table.addWidget(seat8, y_pos, 7) 

697 seat8_dict.append(seat8) 

698 

699 seat9 = QLineEdit() 

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

701 table.addWidget(seat9, y_pos, 8) 

702 seat9_dict.append(seat9) 

703 

704 seat10 = QLineEdit() 

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

706 table.addWidget(seat10, y_pos, 9) 

707 seat10_dict.append(seat10) 

708 

709 if available_site_names[site_number] in detector.supportedSites: 

710 pass 

711 

712 y_pos += 1 

713 

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

715 btns.accepted.connect(dia.accept) 

716 btns.rejected.connect(dia.reject) 

717 dia.layout().addWidget(btns) 

718 

719 response = dia.exec_() 

720 if response: 

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

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

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

724 # history_paths[site_number].get_text()) 

725 self.config.edit_fav_seat( 

726 available_site_names[site_number], 

727 str(check_buttons[site_number].isChecked()), 

728 seat2_dict[site_number].text(), 

729 seat3_dict[site_number].text(), 

730 seat4_dict[site_number].text(), 

731 seat5_dict[site_number].text(), 

732 seat6_dict[site_number].text(), 

733 seat7_dict[site_number].text(), 

734 seat8_dict[site_number].text(), 

735 seat9_dict[site_number].text(), 

736 seat10_dict[site_number].text(), 

737 ) 

738 

739 self.config.save() 

740 self.reload_config() 

741 

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

743 dia = QDialog(self) 

744 dia.setWindowTitle("Site Preferences") 

745 dia.resize(1200, 600) 

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

747 dia.setLayout(QVBoxLayout()) 

748 dia.layout().addWidget(label) 

749 

750 self.load_profile() 

751 site_names = self.config.site_ids 

752 available_site_names = [] 

753 for site_name in site_names: 

754 try: 

755 self.config.supported_sites[site_name].enabled 

756 available_site_names.append(site_name) 

757 except KeyError: 

758 pass 

759 

760 column_headers = [ 

761 "Site", 

762 "Detect", 

763 "Screen Name", 

764 "Hand History Path", 

765 "", 

766 "Tournament Summary Path", 

767 "", 

768 "Favorite seat", 

769 ] # todo ("HUD") 

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

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

772 

773 table = QGridLayout() 

774 table.setSpacing(0) 

775 

776 scrolling_frame = QScrollArea(dia) 

777 dia.layout().addWidget(scrolling_frame) 

778 scrolling_frame.setLayout(table) 

779 

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

781 label = QLabel(column_headers[header_number]) 

782 label.setAlignment(Qt.AlignCenter) 

783 table.addWidget(label, 0, header_number) 

784 

785 check_buttons = [] 

786 screen_names = [] 

787 history_paths = [] 

788 summary_paths = [] 

789 detector = DetectInstalledSites.DetectInstalledSites() 

790 

791 y_pos = 1 

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

793 check_button = QCheckBox(available_site_names[site_number]) 

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

795 table.addWidget(check_button, y_pos, 0) 

796 check_buttons.append(check_button) 

797 

798 hero = QLineEdit() 

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

800 table.addWidget(hero, y_pos, 2) 

801 screen_names.append(hero) 

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

803 

804 entry = QLineEdit() 

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

806 table.addWidget(entry, y_pos, 3) 

807 history_paths.append(entry) 

808 

809 choose1 = QPushButton("Browse") 

810 table.addWidget(choose1, y_pos, 4) 

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

812 

813 entry = QLineEdit() 

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

815 table.addWidget(entry, y_pos, 5) 

816 summary_paths.append(entry) 

817 

818 choose2 = QPushButton("Browse") 

819 table.addWidget(choose2, y_pos, 6) 

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

821 

822 if available_site_names[site_number] in detector.supportedSites: 

823 button = QPushButton("Detect") 

824 table.addWidget(button, y_pos, 1) 

825 button.clicked.connect( 

826 partial( 

827 self.detect_clicked, 

828 data=( 

829 detector, 

830 available_site_names[site_number], 

831 screen_names[site_number], 

832 history_paths[site_number], 

833 summary_paths[site_number], 

834 ), 

835 ) 

836 ) 

837 y_pos += 1 

838 

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

840 btns.accepted.connect(dia.accept) 

841 btns.rejected.connect(dia.reject) 

842 dia.layout().addWidget(btns) 

843 

844 response = dia.exec_() 

845 if response: 

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

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

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

849 # history_paths[site_number].get_text()) 

850 self.config.edit_site( 

851 available_site_names[site_number], 

852 str(check_buttons[site_number].isChecked()), 

853 screen_names[site_number].text(), 

854 history_paths[site_number].text(), 

855 summary_paths[site_number].text(), 

856 ) 

857 

858 self.config.save() 

859 self.reload_config() 

860 

861 def autoenableSite(self, text, checkbox): 

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

863 checkbox.setChecked(True) 

864 

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

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

867 

868 newpath = QFileDialog.getExistingDirectory( 

869 parent, "Please choose the path that you want to Auto Import", path.text() 

870 ) 

871 if newpath: 

872 path.setText(newpath) 

873 

874 def detect_clicked(self, widget, data): 

875 detector = data[0] 

876 site_name = data[1] 

877 entry_screen_name = data[2] 

878 entry_history_path = data[3] 

879 entry_summary_path = data[4] 

880 if detector.sitestatusdict[site_name]["detected"]: 

881 entry_screen_name.setText(detector.sitestatusdict[site_name]["heroname"]) 

882 entry_history_path.setText(detector.sitestatusdict[site_name]["hhpath"]) 

883 if detector.sitestatusdict[site_name]["tspath"]: 

884 entry_summary_path.setText(detector.sitestatusdict[site_name]["tspath"]) 

885 

886 def reload_config(self): 

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

888 # only main tab open, reload profile 

889 self.load_profile() 

890 self.warning_box( 

891 "Configuration settings have been updated," " Fpdb needs to be restarted now\n\nClick OK to close Fpdb" 

892 ) 

893 sys.exit() 

894 else: 

895 self.warning_box( 

896 "Updated preferences have not been loaded because windows are open." " Re-start fpdb to load them." 

897 ) 

898 

899 def process_close_messages(self): 

900 # check for close messages 

901 try: 

902 while True: 

903 name = self.closeq.get(False) 

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

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

906 # thread has ended so remove from list: 

907 del self.threads[i] 

908 break 

909 except queue.Empty: 

910 # no close messages on queue, do nothing 

911 pass 

912 

913 def __calendar_dialog(self, widget, entry): 

914 d = QDialog(self.dia_confirm) 

915 d.setWindowTitle("Pick a date") 

916 

917 vb = QVBoxLayout() 

918 d.setLayout(vb) 

919 cal = QCalendarWidget() 

920 vb.addWidget(cal) 

921 

922 btn = QPushButton("Done") 

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

924 

925 vb.addWidget(btn) 

926 

927 d.exec_() 

928 return 

929 

930 def createMenuBar(self): 

931 mb = self.menuBar() 

932 configMenu = mb.addMenu("Configure") 

933 importMenu = mb.addMenu("Import") 

934 hudMenu = mb.addMenu("HUD") 

935 cashMenu = mb.addMenu("Cash") 

936 tournamentMenu = mb.addMenu("Tournament") 

937 maintenanceMenu = mb.addMenu("Maintenance") 

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

939 helpMenu = mb.addMenu("Help") 

940 themeMenu = mb.addMenu("Themes") 

941 

942 # Create actions 

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

944 action = QAction(name, self) 

945 if shortcut: 

946 action.setShortcut(shortcut) 

947 if tip: 

948 action.setToolTip(tip) 

949 action.triggered.connect(callback) 

950 return action 

951 

952 configMenu.addAction(makeAction("Site Settings", self.dia_site_preferences)) 

953 configMenu.addAction(makeAction("Seat Settings", self.dia_site_preferences_seat)) 

954 configMenu.addAction(makeAction("Hud Settings", self.dia_hud_preferences)) 

955 configMenu.addAction(makeAction("Adv Preferences", self.dia_advanced_preferences, tip="Edit your preferences")) 

956 configMenu.addAction(makeAction("Import filters", self.dia_import_filters)) 

957 configMenu.addSeparator() 

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

959 

960 importMenu.addAction(makeAction("Bulk Import", self.tab_bulk_import, "Ctrl+B")) 

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

962 cashMenu.addAction(makeAction("Graphs", self.tabGraphViewer, "Ctrl+G")) 

963 cashMenu.addAction(makeAction("Ring Player Stats", self.tab_ring_player_stats, "Ctrl+P")) 

964 cashMenu.addAction(makeAction("Hand Viewer", self.tab_hand_viewer)) 

965 cashMenu.addAction(makeAction("Session Stats", self.tab_session_stats, "Ctrl+S")) 

966 tournamentMenu.addAction(makeAction("Tourney Graphs", self.tabTourneyGraphViewer)) 

967 tournamentMenu.addAction(makeAction("Tourney Stats", self.tab_tourney_player_stats, "Ctrl+T")) 

968 tournamentMenu.addAction(makeAction("Tourney Hand Viewer", self.tab_tourney_viewer_stats)) 

969 maintenanceMenu.addAction(makeAction("Statistics", self.dia_database_stats, "View Database Statistics")) 

970 maintenanceMenu.addAction(makeAction("Create or Recreate Tables", self.dia_recreate_tables)) 

971 maintenanceMenu.addAction(makeAction("Rebuild HUD Cache", self.dia_recreate_hudcache)) 

972 maintenanceMenu.addAction(makeAction("Rebuild DB Indexes", self.dia_rebuild_indexes)) 

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

974 

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

976 helpMenu.addAction(makeAction("Log Messages", self.dia_logs, "Log and Debug Messages")) 

977 helpMenu.addAction(makeAction("Help Tab", self.tab_main_help)) 

978 helpMenu.addSeparator() 

979 helpMenu.addAction(makeAction("Infos", self.dia_about, "About the program")) 

980 

981 themes = [ 

982 "dark_purple.xml", 

983 "dark_teal.xml", 

984 "dark_blue.xml", 

985 "dark_cyan.xml", 

986 "dark_pink.xml", 

987 "dark_red.xml", 

988 "dark_lime.xml", 

989 "light_purple.xml", 

990 "light_teal.xml", 

991 "light_blue.xml", 

992 "light_cyan.xml", 

993 "light_pink.xml", 

994 "light_red.xml", 

995 "light_lime.xml", 

996 ] 

997 

998 for theme in themes: 

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

1000 

1001 def load_profile(self, create_db=False): 

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

1003 Set: 

1004 - self.settings 

1005 - self.config 

1006 - self.db 

1007 """ 

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

1009 if self.config.file_error: 

1010 self.warning_box( 

1011 f"There is an error in your config file" f" {self.config.file}:\n{str(self.config.file_error)}", 

1012 diatitle="CONFIG FILE ERROR", 

1013 ) 

1014 sys.exit() 

1015 

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

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

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

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

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

1021 self.info_box( 

1022 "Config file", 

1023 [ 

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

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

1026 " (Main menu) before trying to import hands.", 

1027 ], 

1028 ) 

1029 

1030 self.display_config_created_dialogue = False 

1031 elif self.config.wrongConfigVersion: 

1032 diaConfigVersionWarning = QDialog() 

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

1034 diaConfigVersionWarning.setLayout(QVBoxLayout()) 

1035 label = QLabel(["\nYour local configuration file needs to be updated."]) 

1036 diaConfigVersionWarning.layout().addWidget(label) 

1037 label = QLabel( 

1038 [ 

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

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

1041 ] 

1042 ) 

1043 

1044 diaConfigVersionWarning.layout().addWidget(label) 

1045 label = QLabel( 

1046 [ 

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

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

1049 ] 

1050 ) 

1051 

1052 label.setTextInteractionFlags(Qt.TextSelectableByMouse) 

1053 diaConfigVersionWarning.layout().addWidget(label) 

1054 label = QLabel( 

1055 [ 

1056 "A new configuration will destroy all personal settings" 

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

1058 ] 

1059 ) 

1060 

1061 diaConfigVersionWarning.layout().addWidget(label) 

1062 

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

1064 diaConfigVersionWarning.layout().addWidget(label) 

1065 

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

1067 diaConfigVersionWarning.layout().addWidget(label) 

1068 

1069 btns = QDialogButtonBox(QDialogButtonBox.Ok) 

1070 btns.accepted.connect(diaConfigVersionWarning.accept) 

1071 diaConfigVersionWarning.layout().addWidget(btns) 

1072 

1073 diaConfigVersionWarning.exec_() 

1074 self.config.wrongConfigVersion = False 

1075 

1076 self.settings = {} 

1077 self.settings["global_lock"] = self.lock 

1078 if os.sep == "/": 

1079 self.settings["os"] = "linuxmac" 

1080 else: 

1081 self.settings["os"] = "windows" 

1082 

1083 self.settings.update({"cl_options": cl_options}) 

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

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

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

1087 

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

1089 self.db.disconnect() 

1090 

1091 self.sql = SQL.Sql(db_server=self.settings["db-server"]) 

1092 err_msg = None 

1093 try: 

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

1095 if self.db.get_backend_name() == "SQLite": 

1096 # tell sqlite users where the db file is 

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

1098 except Exceptions.FpdbMySQLAccessDenied: 

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

1100 except Exceptions.FpdbMySQLNoDatabase: 

1101 err_msg = ( 

1102 "MySQL client reports: 2002 or 2003 error." 

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

1104 ) 

1105 

1106 except Exceptions.FpdbPostgresqlAccessDenied: 

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

1108 except Exceptions.FpdbPostgresqlNoDatabase: 

1109 err_msg = ( 

1110 "PostgreSQL client reports: Unable to connect -" 

1111 "Please check that the PostgreSQL service has been started." 

1112 ) 

1113 if err_msg is not None: 

1114 self.db = None 

1115 self.warning_box(err_msg) 

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

1117 self.db = None 

1118 

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

1120 diaDbVersionWarning = QMessageBox( 

1121 QMessageBox.Warning, 

1122 "Strong Warning - Invalid database version", 

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

1124 QMessageBox.Ok, 

1125 self, 

1126 ) 

1127 diaDbVersionWarning.setInformativeText( 

1128 "This error is not necessarily fatal but it is strongly" 

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

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

1131 ) 

1132 

1133 diaDbVersionWarning.exec_() 

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

1135 self.statusBar().showMessage( 

1136 f"Status: Connected to {self.db.get_backend_name()}" 

1137 f" database named {self.db.database} on host {self.db.host}" 

1138 ) 

1139 

1140 # rollback to make sure any locks are cleared: 

1141 self.db.rollback() 

1142 

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

1144 # otherwise the end user gets bombarded with false messages 

1145 # about every site not existing 

1146 if hasattr(self.db, "wrongDbVersion"): 

1147 if not self.db.wrongDbVersion: 

1148 self.validate_config() 

1149 

1150 def obtain_global_lock(self, source): 

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

1152 if ret: 

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

1154 self.lockTakenBy = source 

1155 else: 

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

1157 return ret 

1158 # need to release it later: 

1159 # self.lock.release() 

1160 

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

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

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

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

1165 if not self.quitting: 

1166 log.info("Quitting normally") 

1167 self.quitting = True 

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

1169 

1170 if self.db is not None: 

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

1172 try: 

1173 import _mysql_exceptions 

1174 

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

1176 self.db.disconnect() 

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

1178 pass 

1179 else: 

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

1181 self.db.disconnect() 

1182 else: 

1183 pass 

1184 # self.statusIcon.set_visible(False) 

1185 QCoreApplication.quit() 

1186 

1187 def release_global_lock(self): 

1188 self.lock.release() 

1189 self.lockTakenBy = None 

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

1191 

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

1193 """opens the auto import tab""" 

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

1195 self.threads.append(new_aimp_thread) 

1196 self.add_and_display_tab(new_aimp_thread, "HUD") 

1197 if options.autoimport: 

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

1199 options.autoimport = False 

1200 

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

1202 """opens a tab for bulk importing""" 

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

1204 self.threads.append(new_import_thread) 

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

1206 

1207 # def tab_tourney_import(self, widget, data=None): 

1208 # """opens a tab for bulk importing tournament summaries""" 

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

1210 # self.threads.append(new_import_thread) 

1211 # bulk_tab = new_import_thread.get_vbox() 

1212 # self.add_and_display_tab(bulk_tab, "Tournament Results Import") 

1213 

1214 # end def tab_import_imap_summaries 

1215 

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

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

1218 self.threads.append(new_ps_thread) 

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

1220 

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

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

1223 self.threads.append(new_ps_thread) 

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

1225 

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

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

1228 self.threads.append(new_thread) 

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

1230 

1231 # def tab_positional_stats(self, widget, data=None): 

1232 # new_ps_thread = GuiPositionalStats.GuiPositionalStats(self.config, self.sql) 

1233 # self.threads.append(new_ps_thread) 

1234 # ps_tab = new_ps_thread.get_vbox() 

1235 # self.add_and_display_tab(ps_tab, "Positional Stats") 

1236 

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

1238 colors = self.get_theme_colors() 

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

1240 self.threads.append(new_ps_thread) 

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

1242 

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

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

1245 self.threads.append(new_ps_thread) 

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

1247 

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

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

1250 mh_tab = QLabel( 

1251 ( 

1252 """ 

1253 Welcome to Fpdb! 

1254 

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

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

1257 after an update, if necessary. 

1258 

1259 all configuration now happens in HUD_config.xml. 

1260 

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

1262 and partially under GPL2 or later. 

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

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

1265 and mit.txt in the fpdb installation directory.""" 

1266 ) 

1267 ) 

1268 self.add_and_display_tab(mh_tab, "Help") 

1269 

1270 def get_theme_colors(self): 

1271 """ 

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

1273 

1274 The dictionary contains the following keys: 

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

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

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

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

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

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

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

1282 

1283 Returns: 

1284 dict: A dictionary containing the theme colors. 

1285 """ 

1286 return { 

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

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

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

1290 "line_up": "g", 

1291 "line_down": "r", 

1292 "line_showdown": "b", 

1293 "line_nonshowdown": "m", 

1294 "line_ev": "orange", 

1295 "line_hands": "c", 

1296 } 

1297 

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

1299 """opens a graph viewer tab""" 

1300 colors = self.get_theme_colors() 

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

1302 self.threads.append(new_gv_thread) 

1303 self.add_and_display_tab(new_gv_thread, "Graphs") 

1304 

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

1306 """opens a graph viewer tab""" 

1307 colors = self.get_theme_colors() 

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

1309 self.threads.append(new_gv_thread) 

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

1311 

1312 # def tabStove(self, widget, data=None): 

1313 # """opens a tab for poker stove""" 

1314 # thread = GuiStove.GuiStove(self.config, self) 

1315 # self.threads.append(thread) 

1316 # # tab = thread.get_vbox() 

1317 # self.add_and_display_tab(thread, "Stove") 

1318 

1319 def validate_config(self): 

1320 # check if sites in config file are in DB 

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

1322 try: 

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

1324 except KeyError: 

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

1326 dia = QMessageBox() 

1327 dia.setIcon(QMessageBox.Warning) 

1328 dia.setText("Unknown Site") 

1329 dia.setStandardButtons(QMessageBox.Ok) 

1330 dia.exec_() 

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

1332 dia.format_secondary_text(diastring) 

1333 dia.run() 

1334 dia.destroy() 

1335 

1336 def info_box(self, str1, str2): 

1337 diapath = QMessageBox(self) 

1338 diapath.setWindowTitle(str1) 

1339 diapath.setText(str2) 

1340 return diapath.exec_() 

1341 

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

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

1344 

1345 def change_theme(self, theme): 

1346 apply_stylesheet(app, theme=theme) 

1347 

1348 def update_title_bar_theme(self): 

1349 # Apply the stylesheet to the custom title bar 

1350 self.custom_title_bar.update_theme() 

1351 

1352 def close_tab(self, index): 

1353 item = self.nb.widget(index) 

1354 self.nb.removeTab(index) 

1355 self.nb_tab_names.pop(index) 

1356 

1357 try: 

1358 self.threads.remove(item) 

1359 except ValueError: 

1360 pass 

1361 

1362 item.deleteLater() 

1363 

1364 def __init__(self): 

1365 super().__init__() 

1366 if sys.platform == "darwin": 

1367 pass 

1368 else: 

1369 self.setWindowFlags(Qt.FramelessWindowHint) 

1370 cards = os.path.join(Configuration.GRAPHICS_PATH, "tribal.jpg") 

1371 if os.path.exists(cards): 

1372 self.setWindowIcon(QIcon(cards)) 

1373 set_locale_translation() 

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

1375 self.db = None 

1376 self.status_bar = None 

1377 self.quitting = False 

1378 self.visible = False 

1379 self.threads = [] 

1380 self.closeq = queue.Queue(20) 

1381 

1382 self.oldPos = self.pos() 

1383 

1384 if options.initialRun: 

1385 self.display_config_created_dialogue = True 

1386 self.display_site_preferences = True 

1387 else: 

1388 self.display_config_created_dialogue = False 

1389 self.display_site_preferences = False 

1390 

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

1392 if options.xloc is None: 

1393 options.xloc = 0 

1394 if options.yloc is None: 

1395 options.yloc = 0 

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

1397 

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

1399 defx, defy = 1920, 1080 

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

1401 if sg.width() < defx: 

1402 defx = sg.width() 

1403 if sg.height() < defy: 

1404 defy = sg.height() 

1405 self.resize(defx, defy) 

1406 

1407 if sys.platform == "darwin": 

1408 pass 

1409 else: 

1410 # Create custom title bar 

1411 self.custom_title_bar = CustomTitleBar(self) 

1412 # Create central widget and layout 

1413 self.central_widget = QWidget(self) 

1414 self.central_layout = QVBoxLayout(self.central_widget) 

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

1416 self.central_layout.setSpacing(0) 

1417 

1418 if sys.platform == "darwin": 

1419 # Add title bar and menu bar to layout 

1420 self.custom_title_bar = CustomTitleBar(self) 

1421 self.central_layout.addWidget(self.custom_title_bar) 

1422 self.setMenuBar(self.menuBar()) 

1423 else: 

1424 # Add title bar and menu bar to layout 

1425 self.central_layout.addWidget(self.custom_title_bar) 

1426 self.menu_bar = self.menuBar() 

1427 self.central_layout.setMenuBar(self.menu_bar) 

1428 

1429 self.nb = QTabWidget() 

1430 self.nb.setTabsClosable(True) 

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

1432 self.central_layout.addWidget(self.nb) 

1433 self.setCentralWidget(self.central_widget) 

1434 

1435 self.createMenuBar() 

1436 

1437 self.pages = [] 

1438 self.nb_tab_names = [] 

1439 

1440 self.tab_main_help(None, None) 

1441 

1442 if options.minimized: 

1443 self.showMinimized() 

1444 if options.hidden: 

1445 self.hide() 

1446 

1447 if not options.hidden: 

1448 self.show() 

1449 self.visible = True 

1450 

1451 self.load_profile(create_db=True) 

1452 

1453 if self.config.install_method == "app": 

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

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

1456 break 

1457 else: 

1458 options.initialRun = True 

1459 self.display_config_created_dialogue = True 

1460 self.display_site_preferences = True 

1461 

1462 if options.initialRun and self.display_site_preferences: 

1463 self.dia_site_preferences(None, None) 

1464 self.display_site_preferences = False 

1465 

1466 if not options.errorsToConsole: 

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

1468 log.info( 

1469 f"Note: error output is being diverted to {self.config.dir_log}. Any major error will be reported there _only_." 

1470 ) 

1471 errorFile = codecs.open(fileName, "w", "utf-8") 

1472 sys.stderr = errorFile 

1473 

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

1475 

1476 if options.autoimport: 

1477 self.tab_auto_import(None) 

1478 

1479 

1480class CustomTitleBar(QWidget): 

1481 def __init__(self, parent=None): 

1482 super().__init__(parent) 

1483 self.setAutoFillBackground(True) 

1484 self.main_window = parent 

1485 

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

1487 self.title.setAlignment(Qt.AlignCenter) 

1488 

1489 self.btn_minimize = QPushButton("-") 

1490 self.btn_maximize = QPushButton("+") 

1491 self.btn_close = QPushButton("x") 

1492 

1493 button_size = 20 

1494 self.btn_minimize.setFixedSize(button_size, button_size) 

1495 self.btn_maximize.setFixedSize(button_size, button_size) 

1496 self.btn_close.setFixedSize(button_size, button_size) 

1497 

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

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

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

1501 

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

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

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

1505 

1506 layout = QHBoxLayout() 

1507 layout.addWidget(self.title) 

1508 layout.addStretch() 

1509 layout.addWidget(self.btn_minimize) 

1510 layout.addWidget(self.btn_maximize) 

1511 layout.addWidget(self.btn_close) 

1512 self.setLayout(layout) 

1513 

1514 self.is_maximized = False 

1515 if sys.platform == "darwin": 

1516 pass 

1517 else: 

1518 self.moving = False 

1519 self.offset = None 

1520 

1521 def toggle_maximize_restore(self): 

1522 if self.is_maximized: 

1523 self.main_window.showNormal() 

1524 else: 

1525 self.main_window.showMaximized() 

1526 self.is_maximized = not self.is_maximized 

1527 

1528 def update_theme(self): 

1529 self.setStyleSheet(self.main_window.styleSheet()) 

1530 

1531 def mousePressEvent(self, event): 

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

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

1534 

1535 def mouseMoveEvent(self, event): 

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

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

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

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

1540 

1541 

1542if __name__ == "__main__": 

1543 from qt_material import apply_stylesheet 

1544 import time 

1545 

1546 try: 

1547 Configuration.get_config("HUD_config.xml", True) 

1548 app = QApplication([]) 

1549 apply_stylesheet(app, theme="dark_purple.xml") 

1550 me = fpdb() 

1551 app.exec_() 

1552 finally: 

1553 profiler.disable() 

1554 s = io.StringIO() 

1555 ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative") 

1556 ps.print_stats() 

1557 

1558 # Use timestamp or process ID for unique filenames 

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

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

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

1562 

1563 with open(results_file, "w") as f: 

1564 f.write(s.getvalue()) 

1565 

1566 profiler.dump_stats(profile_file)