import math import sys import gi gi.require_version('Gtk', '3.0') import time, datetime import os import threading from gi.repository import Gtk, GObject from cardvault import util, application from legacy.search import SearchHandlers from legacy.library import LibraryHandlers from legacy.wants import WantsHandlers class Handlers(SearchHandlers, LibraryHandlers, WantsHandlers): def __init__(self, app: 'application.Application'): """Initialize handlers for UI signals""" self.app = app # Token to cancel a running download self.cancel_token = False # Call constructor of view handlers classes SearchHandlers.__init__(self, app) LibraryHandlers.__init__(self, app) WantsHandlers.__init__(self, app) # --------------------------------- Main Window Handlers ---------------------------------------------- def do_save_library(self, item): self.app.save_data() def do_export_library(self, item): dialog = Gtk.FileChooserDialog("Export Library", self.app.ui.get_object("mainWindow"), Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_current_name("mtg_export-" + datetime.datetime.now().strftime("%Y-%m-%d")) dialog.set_current_folder(os.path.expanduser("~")) response = dialog.run() if response == Gtk.ResponseType.OK: # prepare export file file = {"library": self.app.library, "tags": self.app.tags, "wants": self.app.wants} util.export_library(dialog.get_filename(), file) self.app.push_status("Library exported") dialog.destroy() def export_cell_toggled(self, widget, pos): model = self.app.ui.get_object("export_treestore") iter = model.get_iter(pos) model.set_value(iter, 0, not widget.get_active()) if len(pos.split(":")) > 1: # A child node has been clicked pass def do_export_json(self, item): """ Export user data to file Called By: Export menu item """ # dialog = self.app.ui.get_object("export_dialog") # dialog.set_transient_for(self.app.ui.get_object("mainWindow")) # # store = self.app.ui.get_object("export_treestore") # type: Gtk.TreeStore # store.clear() # store.append(None, [True, False, "Library"]) # store.append(None, [True, False, "Decks"]) # store.append(None, [True, False, "Wants Lists"]) # # lib_iter = store.get_iter_first() # deck_iter = store.iter_next(lib_iter) # wants_iter = store.iter_next(deck_iter) # # store.append(lib_iter, [True, True, "Untagged Cards"]) # for tag in self.app.tags.keys(): # store.append(lib_iter, [True, True, tag]) # # for name in self.app.wants.keys(): # store.append(wants_iter, [True, True, name]) # # self.app.ui.get_object("export_sel_tree").expand_all() # # response = dialog.run() # dialog.hide() # # if not response == Gtk.ResponseType.OK: # return # TODO Read treemodel to select witch parts to export dialog = Gtk.FileChooserDialog("Export Library", self.app.ui.get_object("mainWindow"), Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_current_name("mtg_export-" + datetime.datetime.now().strftime("%Y-%m-%d") + ".json") dialog.set_current_folder(os.path.expanduser("~")) response = dialog.run() if response == Gtk.ResponseType.OK: # prepare export file file = {"library": self.app.library, "tags": self.app.tags, "wants": self.app.wants} util.export_json(dialog.get_filename(), file) self.app.push_status("Library exported") dialog.destroy() def do_import_library(self, item): """Called by menu item import library""" # Show file picker dialog for import dialog = Gtk.FileChooserDialog("Import Library", self.app.ui.get_object("mainWindow"), Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_current_folder(os.path.expanduser("~")) response = dialog.run() if response == Gtk.ResponseType.OK: override_question = self.app.show_dialog_yn( "Import Library", "Importing a library will override your current library.\nProceed?") if override_question == Gtk.ResponseType.YES: imports = util.import_library(dialog.get_filename()) self.app.library = imports[0] self.app.tags = imports[1] self.app.wants = imports[2] self.app.db_override_user_data() self.app.current_page.emit('show') dialog.destroy() def do_card_data_user(self, menu_item): """ Handler for Clear User Data menu item. """ response = self.app.show_dialog_yn("Deleting All User Data", "You are about to delete all data in the " "library.\nThis can not be undone.\nProceed?") if response == Gtk.ResponseType.YES: util.log("Deleting all local card data", util.LogLevel.Info) self.app.db_delete_user_data() util.log("Done", util.LogLevel.Info) self.app.push_status("Library deleted") def do_card_data_card(self, item): """Handler for Clear Card Data menu item""" response = self.app.show_dialog_yn("Deleting All Card Data", "You are about to delete all local card data.\n" "Further searches will use the internet to search " "for cards.\nProceed?") if response == Gtk.ResponseType.YES: util.log("Deleting all library data", util.LogLevel.Info) self.app.db_delete_card_data() util.log("Done", util.LogLevel.Info) self.app.push_status("Local card data deleted. Switching to online mode.") def prefs_open(self, item): """ Handler for open preferences menu item Called By: prefs_item menu item """ self.app.show_preferences_dialog() def on_view_changed(self, item): if item.get_active(): container = self.app.ui.get_object("contentPage") new_page = self.app.pages[item.get_name()] if self.app.current_page: container.remove(self.app.current_page) self.app.current_page = new_page container.pack_start(self.app.current_page, True, True, 0) container.show_all() self.app.current_page.emit('show') app_title = new_page.get_name() + " - " + util.APPLICATION_TITLE self.app.ui.get_object("mainWindow").set_title(app_title) self.app.config["last_viewed"] = new_page.get_name().lower() self.app.save_config() def do_delete_event(self, arg1, arg2): if self.app.unsaved_changes(): response = self.app.show_dialog_ync("Unsaved Changes", "You have unsaved changes in your library. " "Save before exiting?") if response == Gtk.ResponseType.YES: self.app.save_data() return False elif response == Gtk.ResponseType.CANCEL: return True def do_cancel_download(self, item: Gtk.MenuItem): """The cancel button was pressed, set cancel_token to stop download thread""" self.cancel_token = True # Delete Dialog self.app.ui.get_object("loadDataDialog").hide() self.app.push_status("Download canceled") util.log("Download canceled by user", util.LogLevel.Info) def download_canceled(self): """The download thread was canceled and finished executing""" self.cancel_token = False util.log("Download thread ended", util.LogLevel.Info) def download_failed(self, err): # Delete Dialog self.app.ui.get_object("loadDataDialog").hide() self.app.push_status("Download canceled") self.app.show_message("Download Failed", str(err)) def download_finished(self): """Download thread finished without errors""" self.cancel_token = False self.app.set_online(False) self.app.ui.get_object("loadDataDialog").hide() self.app.push_status("Card data downloaded") util.log("Card data download finished", util.LogLevel.Info) def do_download_card_data(self, item: Gtk.MenuItem): """Download button was pressed in the menu bar. Starts a thread to load data from the internet""" info_string = "Start downloading card information from the internet?\n" \ "You can cancel the download at any point." response = self.app.show_dialog_yn("Download Card Data", info_string) if response == Gtk.ResponseType.NO: return # Launch download info dialog dl_dialog = self.app.ui.get_object("loadDataDialog") dl_dialog.set_transient_for(self.app.ui.get_object("mainWindow")) dl_dialog.show() # Hide Progress UI until download started self.app.ui.get_object("dl_progress_bar").set_visible(False) self.app.ui.get_object("dl_progress_label").set_visible(False) # Create and start the download in a separate thread so it will not block the UI thread = threading.Thread(target=self.load_thread) thread.daemon = True thread.start() util.log("Attempt downloading all cards. This may take a while...", util.LogLevel.Info) def load_thread(self): """Worker thread to download info using the mtgsdk""" # Gatherer uses rate limit on Card.all() # Takes ~10 minutes to download all cards # all = self.load_thread_gatherer() # Download from mtgjson.com GObject.idle_add(self.load_show_insert_ui, "Downloading...") # Waiting in case a canceled thread is still running. while self.cancel_token: continue util.log("Starting download", util.LogLevel.Info) s = time.time() all_json = util.net_all_cards_mtgjson() e = time.time() util.log("Finished in {}s".format(round(e - s, 3)), util.LogLevel.Info) if self.cancel_token: GObject.idle_add(self.download_canceled) return self.app.db_delete_card_data() GObject.idle_add(self.load_show_insert_ui, "Saving data to disk...") util.log("Saving to sqlite", util.LogLevel.Info) s = time.time() GObject.idle_add(self.app.db.db_insert_data_card, all_json) e = time.time() util.log("Finished in {}s".format(round(e - s, 3)), util.LogLevel.Info) self.download_finished() def load_thread_gatherer(self): all = [] all_num = util.get_all_cards_num() all_pages = int(math.ceil(all_num / 100)) # Paging for ui control between downloads for i in range(all_pages): req_start = time.time() try: new_cards = Card.where(page=i).where(pageSize=100).all() except MtgException as err: util.log(str(err), util.LogLevel.Error) return all = all + new_cards req_end = time.time() # Check if the action was canceled during download if self.cancel_token: GObject.idle_add(self.download_canceled) return # Activate download UI self.app.ui.get_object("dl_spinner").set_visible(False) self.app.ui.get_object("dl_progress_bar").set_visible(True) self.app.ui.get_object("dl_progress_label").set_visible(True) passed = str(round(req_end - req_start, 3)) GObject.idle_add(self.load_update_ui, all, all_num, passed) return all def load_update_ui(self, current_list: list, max_cards: int, time_passed: str): """Called from withing the worker thread. Updates the download dialog with infos.""" # Get info widgets info_label = self.app.ui.get_object("dl_info_label") progress_label = self.app.ui.get_object("dl_progress_label") bar = self.app.ui.get_object("dl_progress_bar") # Compute numbers for display size_human = util.sizeof_fmt(sys.getsizeof(current_list)) size_bytes = sys.getsizeof(current_list) percent = len(current_list) / max_cards # Update UI info_label.set_text("Downloading Cards...") progress_label.set_text("{:.1%} ({})".format(percent, size_human)) bar.set_fraction(percent) util.log("Downloading: {:.1%} | {} Bytes | {}s".format(percent, size_bytes, time_passed), util.LogLevel.Info) def load_show_insert_ui(self, info: str): """Called from worker thread after download finished. Sets UI to display the passed string""" self.app.ui.get_object("dl_info_label").set_text(info) self.app.ui.get_object("dl_spinner").set_visible(True) self.app.ui.get_object("dl_progress_bar").set_visible(False) self.app.ui.get_object("dl_progress_label").set_visible(False)