예제 #1
0
    def new_options_dialog(self):
        """Launch a new options config dialog."""
        logging.debug("New options dialog")
        self.options_dialog = OptionsDialog(self.window)

        def write_options():
            logging.info("Writing options to disk")
            self.options_dialog.write()
            self.update()

        self.options_dialog.dialog.rejected.connect(write_options)
        self.options_dialog.dialog.exec()
예제 #2
0
    def new_options_dialog(self):
        """Launch a new options config dialog."""
        logging.debug("New options dialog")
        self.options_dialog = OptionsDialog(self.window)

        def write_options():
            logging.info("Writing options to disk")
            self.options_dialog.write()
            self.update()

        self.options_dialog.dialog.rejected.connect(write_options)
        self.options_dialog.dialog.exec()
예제 #3
0
    def new_options_dialog(self):
        """Launch a new options config dialog."""
        logging.debug("New options dialog")
        self.options_dialog = OptionsDialog(self.window)

        def write_options():
            logging.info("Writing options to disk")
            # TODO: reload icons on asset update?
            self.ui.statusbar.showMessage("Options have been updated", 3000)

        self.options_dialog.dialog.accepted.connect(write_options)
        self.options_dialog.dialog.exec()
예제 #4
0
class MainWindow():
    def __init__(self):
        # check for new starcheat version online in seperate thread
        update_result = [None]
        update_thread = Thread(target=update_check_worker,
                               args=[update_result],
                               daemon=True)
        update_thread.start()
        """Display the main starcheat window."""
        self.app = QApplication(sys.argv)
        self.window = StarcheatMainWindow(self)
        self.ui = qt_mainwindow.Ui_MainWindow()
        self.ui.setupUi(self.window)

        logging.info("Main window init")

        self.players = None
        self.filename = None

        self.item_browser = None
        # remember the last selected item browser category
        self.remember_browser = "<all>"
        self.options_dialog = None

        # connect action menu
        self.ui.actionSave.triggered.connect(self.save)
        self.ui.actionReload.triggered.connect(self.reload)
        self.ui.actionOpen.triggered.connect(self.open_file)
        self.ui.actionQuit.triggered.connect(self.app.closeAllWindows)
        self.ui.actionOptions.triggered.connect(self.new_options_dialog)
        self.ui.actionItemBrowser.triggered.connect(self.new_item_browser)
        self.ui.actionAbout.triggered.connect(self.new_about_dialog)
        self.ui.actionExport.triggered.connect(self.export_save)
        self.ui.actionExportJSON.triggered.connect(self.export_json)
        self.ui.actionImportJSON.triggered.connect(self.import_json)
        self.ui.actionMods.triggered.connect(self.new_mods_dialog)
        self.ui.actionImageBrowser.triggered.connect(
            self.new_image_browser_dialog)

        # set up bag tables
        bags = ("wieldable", "head", "chest", "legs", "back", "main_bag",
                "action_bar", "tile_bag", "essentials", "mouse")
        for bag in bags:
            logging.debug("Setting up %s bag", bag)
            self.bag_setup(getattr(self.ui, bag), bag)

        # signals
        self.ui.blueprints_button.clicked.connect(self.new_blueprint_edit)
        self.ui.appearance_button.clicked.connect(self.new_appearance_dialog)
        self.ui.techs_button.clicked.connect(self.new_techs_dialog)

        self.ui.name.textChanged.connect(self.set_name)
        self.ui.male.clicked.connect(self.set_gender)
        self.ui.female.clicked.connect(self.set_gender)
        self.ui.description.textChanged.connect(self.set_description)
        self.ui.pixels.valueChanged.connect(self.set_pixels)

        self.ui.health.valueChanged.connect(
            lambda: self.set_stat_slider("health"))
        self.ui.energy.valueChanged.connect(
            lambda: self.set_stat_slider("energy"))
        self.ui.health_button.clicked.connect(lambda: self.max_stat("health"))
        self.ui.energy_button.clicked.connect(lambda: self.max_stat("energy"))

        self.ui.name_button.clicked.connect(self.random_name)
        self.ui.copy_uuid_button.clicked.connect(self.copy_uuid)

        self.window.setWindowModified(False)

        logging.debug("Showing main window")
        self.window.show()

        # launch first setup if we need to
        if not new_setup_dialog(self.window):
            logging.error("Config/index creation failed")
            return
        logging.info("Starbound folder: %s", Config().read("starbound_folder"))

        logging.info("Checking assets hash")
        if not check_index_valid(self.window):
            logging.error("Index creation failed")
            return

        logging.info("Loading assets database")
        self.assets = assets.Assets(Config().read("assets_db"),
                                    Config().read("starbound_folder"))
        self.items = self.assets.items()

        # populate species combobox
        for species in self.assets.species().get_species_list():
            self.ui.race.addItem(species)
        self.ui.race.currentTextChanged.connect(self.update_species)

        # populate game mode combobox
        for mode in sorted(self.assets.player().mode_types.values()):
            self.ui.game_mode.addItem(mode)
        self.ui.game_mode.currentTextChanged.connect(self.set_game_mode)

        # launch open file dialog
        self.player = None
        logging.debug("Open file dialog")
        open_player = self.open_file()
        # we *need* at least an initial save file
        if not open_player:
            logging.warning("No player file selected")
            return

        self.ui.name.setFocus()

        # block for update check result (should be ready now)
        update_thread.join()
        if update_result[0]:
            update_check_dialog(self.window, update_result[0])

        sys.exit(self.app.exec_())

    def update(self):
        """Update all GUI widgets with values from PlayerSave instance."""
        logging.info("Updating main window")
        # uuid / save version
        self.ui.uuid_label.setText(self.player.get_uuid())
        self.ui.ver_label.setText(self.player.get_header())
        # name
        self.ui.name.setText(self.player.get_name())
        # race
        self.ui.race.setCurrentText(self.player.get_race(pretty=True))
        # pixels
        try:
            self.ui.pixels.setValue(self.player.get_pixels())
        except TypeError:
            logging.exception("Unable to set pixels widget")
        # description
        self.ui.description.setPlainText(self.player.get_description())
        # gender
        getattr(self.ui, self.player.get_gender()).toggle()
        # game mode
        game_mode = self.player.get_game_mode()
        try:
            self.ui.game_mode.setCurrentText(
                self.assets.player().mode_types[game_mode])
        except KeyError:
            logging.exception("No game mode set on player")

        # stats
        self.update_stat("health")
        self.update_stat("energy")

        total = 0
        progress = QProgressDialog("Updating item slots...", None, 0, 10,
                                   self.window)

        progress.setWindowTitle("Updating...")
        progress.setWindowModality(QtCore.Qt.ApplicationModal)
        progress.forceShow()
        progress.setValue(total)

        # equipment
        equip_bags = "head", "chest", "legs", "back"
        for bag in equip_bags:
            logging.debug("Updating %s", bag)
            items = []
            for x in getattr(self.player, "get_" + bag)():
                if x is not None:
                    items.append(ItemWidget(x["__content"], self.assets))
                else:
                    items.append(ItemWidget(None, self.assets))

            getattr(self.ui, bag).setItem(0, 0, items[0])
            getattr(self.ui, bag).setItem(0, 1, items[1])
            total += 1
            progress.setValue(total)

        for bag in "wieldable", "main_bag", "tile_bag", "action_bar", "essentials", "mouse":
            self.update_bag(bag)
            total += 1
            progress.setValue(total)

        self.update_player_preview()

    def bag_setup(self, widget, name):
        widget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        # TODO: still issues with drag drop between tables
        widget.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        widget.cellChanged.connect(self.set_edited)

        item_edit = getattr(self, "new_" + name + "_item_edit")

        widget.cellDoubleClicked.connect(lambda: item_edit(False))

        sortable = ("main_bag", "tile_bag")
        clearable = ("wieldable", "action_bar", "essentials")

        edit_action = QAction("Edit...", widget)
        edit_action.triggered.connect(lambda: item_edit(False))
        widget.addAction(edit_action)
        import_json = QAction("Import...", widget)
        import_json.triggered.connect(lambda: item_edit(True))
        widget.addAction(import_json)
        trash_action = QAction("Trash", widget)
        trash_slot = lambda: self.trash_slot(self.window, widget, True)
        trash_action.triggered.connect(trash_slot)
        widget.addAction(trash_action)

        if name in sortable or name in clearable:
            sep_action = QAction(widget)
            sep_action.setSeparator(True)
            widget.addAction(sep_action)
            if name in clearable:
                clear_action = QAction("Clear Held Items", widget)
                clear_action.triggered.connect(self.clear_held_slots)
                widget.addAction(clear_action)
            if name in sortable:
                sort_name = QAction("Sort By Name", widget)
                sort_name.triggered.connect(
                    lambda: self.sort_bag(name, "name"))
                widget.addAction(sort_name)
                sort_type = QAction("Sort By Type", widget)
                sort_type.triggered.connect(
                    lambda: self.sort_bag(name, "category"))
                widget.addAction(sort_type)
                sort_count = QAction("Sort By Count", widget)
                sort_count.triggered.connect(
                    lambda: self.sort_bag(name, "count"))
                widget.addAction(sort_count)

    def update_title(self):
        """Update window title with player name."""
        self.window.setWindowTitle("starcheat - " + self.player.get_name() +
                                   "[*]")

    def save(self):
        """Update internal player dict with GUI values and export to file."""
        logging.info("Saving player file %s", self.player.filename)
        self.set_bags()
        # save and show status
        logging.info("Writing file to disk")
        self.player.export_save(self.player.filename)
        self.update_title()
        self.ui.statusbar.showMessage("Saved " + self.player.filename, 3000)
        self.window.setWindowModified(False)
        self.players[self.player.get_uuid()] = self.player

    def new_item_edit(self, bag, do_import):
        """Display a new item edit dialog using the select cell in a given bag."""
        logging.debug("New item edit dialog")
        row = bag.currentRow()
        column = bag.currentColumn()
        current = bag.currentItem()
        item = saves.empty_slot()
        valid_slot = (type(current) is not QTableWidgetItem
                      and current is not None and current.item is not None)

        if do_import:
            imported = import_json(self.window)
            if imported is False:
                self.ui.statusbar.showMessage(
                    "Error importing item, see starcheat log for details",
                    3000)
                return
            elif imported is None:
                return
            else:
                item = imported

        # cells don't retain ItemSlot widget when they've been dragged away
        if valid_slot:
            item.update(current.item)

        item_edit = ItemEdit(self.window, item, self.player, self.assets,
                             self.remember_browser)

        def update_slot():
            logging.debug("Writing changes to slot")
            try:
                new_slot = ItemWidget(item_edit.get_item(), self.assets)
                if new_slot.item["name"] != "":
                    bag.setItem(row, column, new_slot)
                    self.remember_browser = item_edit.remember_browser
                    self.set_edited()
            except TypeError:
                dialog = QMessageBox(item_edit.dialog)
                dialog.setWindowTitle("Invalid Item ID")
                dialog.setText(
                    "That item ID does not exist in the Starbound assets.")
                dialog.setStandardButtons(QMessageBox.Close)
                dialog.setIcon(QMessageBox.Critical)
                dialog.exec()

        trash_slot = lambda: self.trash_slot(item_edit.dialog, bag)

        item_edit.dialog.accepted.connect(update_slot)
        item_edit.ui.trash_button.clicked.connect(trash_slot)
        got_item = item_edit.launch()
        if got_item:
            item_edit.dialog.exec()

    def trash_slot(self, dialog, bag, standalone=False):
        row = bag.currentRow()
        column = bag.currentColumn()
        ask_dialog = QMessageBox(dialog)
        ask_dialog.setWindowTitle("Trash Item")
        ask_dialog.setText("Are you sure?")
        ask_dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        ask_dialog.setDefaultButton(QMessageBox.No)
        ask_dialog.setIcon(QMessageBox.Question)
        if ask_dialog.exec() == QMessageBox.Yes:
            bag.setItem(row, column, empty_slot())
            if not standalone:
                dialog.close()
            self.set_edited()

    def set_edited(self):
        self.window.setWindowModified(True)

    def sort_bag(self, bag_name, sort_by):
        self.set_bags()
        bag = getattr(self.player, "get_" + bag_name)()
        sorted_bag = self.assets.player().sort_bag(bag, sort_by)
        getattr(self.player, "set_" + bag_name)(sorted_bag)
        self.ui.statusbar.showMessage("Sorting by " + sort_by + "...", 3000)
        self.update()
        self.ui.statusbar.clearMessage()

    def clear_held_slots(self):
        self.player.clear_held_slots()
        self.set_edited()
        self.ui.statusbar.showMessage("All held items have been cleared", 3000)

    def new_blueprint_edit(self):
        """Launch a new blueprint management dialog."""
        logging.debug("New blueprint dialog")
        blueprint_lib = BlueprintLib(self.window, self.player.get_blueprints(),
                                     self.player.get_new_blueprints())

        def update_blueprints():
            logging.debug("Writing blueprints")
            self.player.set_blueprints(blueprint_lib.get_known_list())
            self.player.set_new_blueprints(blueprint_lib.new_blueprints)
            blueprint_lib.dialog.close()
            self.set_edited()

        blueprint_lib.ui.buttonBox.rejected.connect(update_blueprints)
        blueprint_lib.dialog.exec()

    def copy_uuid(self):
        clipboard = self.app.clipboard()
        clipboard.setText(self.player.get_uuid())
        self.ui.statusbar.showMessage("UUID copied to clipboard", 3000)

    def new_item_browser(self):
        """Launch a standalone item browser dialog that does write any changes."""
        self.item_browser = ItemBrowser(self.window, True)
        self.item_browser.dialog.show()

    def new_options_dialog(self):
        """Launch a new options config dialog."""
        logging.debug("New options dialog")
        self.options_dialog = OptionsDialog(self.window)

        def write_options():
            logging.info("Writing options to disk")
            self.options_dialog.write()
            self.update()

        self.options_dialog.dialog.rejected.connect(write_options)
        self.options_dialog.dialog.exec()

    def new_about_dialog(self):
        """Launch a new about dialog."""
        about_dialog = AboutDialog(self.window)
        about_dialog.dialog.exec()

    def new_appearance_dialog(self):
        appearance_dialog = Appearance(self)
        appearance_dialog.dialog.exec()
        appearance_dialog.write_appearance_values()
        self.update_player_preview()

    def new_techs_dialog(self):
        techs_dialog = Techs(self)
        techs_dialog.dialog.rejected.connect(techs_dialog.write_techs)
        techs_dialog.dialog.exec()

    def new_mods_dialog(self):
        mods_dialog = ModsDialog(self.window)
        mods_dialog.dialog.exec()

    def new_image_browser_dialog(self):
        self.image_browser = ImageBrowser(self.window, self.assets)
        self.image_browser.dialog.show()

    def reload(self):
        """Reload the currently open save file and update GUI values."""
        logging.info("Reloading file %s", self.player.filename)
        self.player = saves.PlayerSave(self.player.filename)
        self.update()
        self.update_title()
        self.ui.statusbar.showMessage("Reloaded " + self.player.filename, 3000)
        self.window.setWindowModified(False)

    def open_file(self):
        """Display open file dialog and load selected save."""
        if self.window.isWindowModified():
            button = save_modified_dialog(self.window)
            if button == QMessageBox.Cancel:
                return False
            elif button == QMessageBox.Save:
                self.save()

        character_select = CharacterSelectDialog(self, self.assets)
        character_select.show()

        self.players = character_select.players

        if character_select.selected is None:
            logging.warning("No player selected")
            return False
        else:
            self.player = character_select.selected

        self.update()
        self.update_title()
        self.ui.statusbar.showMessage("Opened " + self.player.filename, 3000)
        self.window.setWindowModified(False)
        return True

    def export_save(self):
        """Save a copy of the current player file to another location.
        Doesn't change the current filename."""
        filename = QFileDialog.getSaveFileName(
            self.window,
            "Export Save File As",
            filter="Player (*.player);;All Files (*)")
        if filename[0] != "":
            self.set_bags()
            self.player.export_save(filename[0])
            self.ui.statusbar.showMessage(
                "Exported save file to " + filename[0], 3000)

    def export_json(self):
        """Export player entity as json."""
        self.set_bags()
        entity = self.player.entity
        json_data = json.dumps(entity,
                               sort_keys=True,
                               indent=4,
                               separators=(',', ': '))
        filename = QFileDialog.getSaveFileName(
            self.window,
            "Export JSON File As",
            filter="JSON (*.json);;All Files (*)")
        if filename[0] != "":
            json_file = open(filename[0], "w")
            json_file.write(json_data)
            json_file.close()
            self.ui.statusbar.showMessage(
                "Exported JSON file to " + filename[0], 3000)

    def import_json(self):
        """Import an exported JSON player entity and merge/update with open player."""
        filename = QFileDialog.getOpenFileName(
            self.window,
            "Import JSON Player File",
            filter="JSON (*.json);;All Files (*)")

        if filename[0] == "":
            logging.debug("No player file selected to import")
            return

        try:
            player_data = json.load(open(filename[0], "r"))
            self.player.entity.update(player_data)
            self.update()
            self.ui.statusbar.showMessage(
                "Imported player file " + filename[0], 3000)
        except:
            logging.exception("Error parsing player: %s", filename[0])
            self.ui.statusbar.showMessage(
                "Error importing player, see starcheat log for details", 3000)

    def get_gender(self):
        if self.ui.male.isChecked():
            return "male"
        else:
            return "female"

    def get_bag(self, name):
        """Return the entire contents of a given non-equipment bag as raw values."""
        logging.debug("Getting %s contents", name)
        row = column = 0
        bag = getattr(self.player, "get_" + name)()

        for i in range(len(bag)):
            item = getattr(self.ui, name).item(row, column)
            empty_item = (item is None or type(item) is QTableWidgetItem
                          or item.item is None)
            if empty_item:
                item = None
            else:
                widget = item.item
                item = saves.new_item(widget["name"], widget["count"],
                                      widget["parameters"])

            bag[i] = item

            # so far all non-equip bags are 10 cols long
            column += 1
            if (column % 10) == 0:
                row += 1
                column = 0

        return bag

    def get_equip(self, name):
        """Return the raw values of both slots in a given equipment bag."""
        logging.debug("Getting %s contents", name)
        equip = getattr(self.ui, name)
        main_cell = equip.item(0, 0)
        glamor_cell = equip.item(0, 1)

        # when you drag itemwidgets around the cell will become empty so just
        # pretend it had an empty slot value
        empty_main = (main_cell is None or type(main_cell) is QTableWidgetItem
                      or main_cell.item is None)
        if empty_main:
            main = None
        else:
            widget = main_cell.item
            main = saves.new_item(widget["name"], widget["count"],
                                  widget["parameters"])

        empty_glamor = (glamor_cell is None
                        or type(glamor_cell) is QTableWidgetItem
                        or glamor_cell.item is None)
        if empty_glamor:
            glamor = None
        else:
            widget = glamor_cell.item
            glamor = saves.new_item(widget["name"], widget["count"],
                                    widget["parameters"])

        return main, glamor

    def update_bag(self, bag_name):
        """Set the entire contents of any given bag with ItemWidgets based off player data."""
        logging.debug("Updating %s contents", bag_name)
        row = column = 0
        bag = getattr(self.player, "get_" + bag_name)()

        for slot in range(len(bag)):
            item = bag[slot]
            if item is not None and "__content" in item:
                widget = ItemWidget(item["__content"], self.assets)
            else:
                widget = ItemWidget(None, self.assets)

            getattr(self.ui, bag_name).setItem(row, column, widget)

            column += 1
            if (column % 10) == 0:
                row += 1
                column = 0

    def update_player_preview(self):
        try:
            image = self.assets.species().render_player(self.player)
            pixmap = QPixmap.fromImage(ImageQt(image))
        except (OSError, TypeError, AttributeError):
            # TODO: more specific error handling. may as well except all errors
            # at this point jeez
            logging.exception("Couldn't load species images")
            pixmap = QPixmap()

        self.ui.player_preview.setPixmap(pixmap)
        self.window.setWindowModified(True)

    def update_species(self):
        species = self.ui.race.currentText()
        if self.player.get_race(pretty=True) == species:
            # don't overwrite appearance values if it didn't really change
            return
        self.player.set_race(species)
        defaults = self.assets.species().get_default_colors(species)
        for key in defaults:
            getattr(self.player, "set_%s_directives" % key)(defaults[key][0])
        self.update_player_preview()
        self.window.setWindowModified(True)

    def set_pixels(self):
        self.player.set_pixels(self.ui.pixels.value())
        self.set_edited()

    def set_name(self):
        self.player.set_name(self.ui.name.text())
        self.set_edited()

    def set_description(self):
        self.player.set_description(self.ui.description.toPlainText())
        self.set_edited()

    def set_gender(self):
        self.player.set_gender(self.get_gender())
        self.update_player_preview()
        self.set_edited()

    def random_name(self):
        species_name = self.player.get_race()
        name = self.assets.species().generate_name(species_name)
        self.ui.name.setText(name)

    def set_game_mode(self):
        self.player.set_game_mode(self.assets.player().get_mode_type(
            self.ui.game_mode.currentText()))
        self.set_edited()

    def set_bags(self):
        # this function mostly just exist to work around the bug of
        # dragndrop not updating player entity. this requires the table view
        # equipment
        equip_bags = "head", "chest", "legs", "back"
        for b in equip_bags:
            bag = self.get_equip(b)
            getattr(self.player, "set_" + b)(bag[0], bag[1])
        # bags
        bags = "wieldable", "main_bag", "tile_bag", "action_bar", "essentials", "mouse"
        for b in bags:
            getattr(self.player, "set_" + b)(self.get_bag(b))

    def max_stat(self, name):
        """Set a stat's current value to its max value."""
        getattr(self.player, "set_" + name)(100)
        self.update_stat(name)

    def set_stat(self, name):
        max = getattr(self.ui, "max_" + name).value()
        getattr(self.player, "set_max_" + name)(float(max))
        self.update_stat(name)

    def set_stat_slider(self, name):
        current = getattr(self.ui, name).value()
        getattr(self.player, "set_" + name)(current)
        self.update_stat(name)

    def update_stat(self, name):
        try:
            current = int(getattr(self.player, "get_" + name)())
            button = getattr(self.ui, name + "_button")
            getattr(self.ui, name).setValue(current)
            button.setEnabled(current != 100)
            self.set_edited()
        except TypeError:
            logging.exception("Unable to set stat %s", name)

    # these are used for connecting the item edit dialog to bag tables
    def new_main_bag_item_edit(self, do_import):
        self.new_item_edit(self.ui.main_bag, do_import)

    def new_tile_bag_item_edit(self, do_import):
        self.new_item_edit(self.ui.tile_bag, do_import)

    def new_action_bar_item_edit(self, do_import):
        self.new_item_edit(self.ui.action_bar, do_import)

    def new_head_item_edit(self, do_import):
        self.new_item_edit(self.ui.head, do_import)

    def new_chest_item_edit(self, do_import):
        self.new_item_edit(self.ui.chest, do_import)

    def new_legs_item_edit(self, do_import):
        self.new_item_edit(self.ui.legs, do_import)

    def new_back_item_edit(self, do_import):
        self.new_item_edit(self.ui.back, do_import)

    def new_wieldable_item_edit(self, do_import):
        self.new_item_edit(self.ui.wieldable, do_import)

    def new_essentials_item_edit(self, do_import):
        self.new_item_edit(self.ui.essentials, do_import)

    def new_mouse_item_edit(self, do_import):
        self.new_item_edit(self.ui.mouse, do_import)
예제 #5
0
class MainWindow():
    def __init__(self):
        # check for new starcheat version online in seperate thread
        update_result = [None]
        update_thread = Thread(target=update_check_worker, args=[update_result], daemon=True)
        update_thread.start()

        """Display the main starcheat window."""
        self.app = QApplication(sys.argv)
        self.window = StarcheatMainWindow(self)
        self.ui = qt_mainwindow.Ui_MainWindow()
        self.ui.setupUi(self.window)

        logging.info("Main window init")

        self.players = None
        self.filename = None

        self.item_browser = None
        # remember the last selected item browser category
        self.remember_browser = "<all>"
        self.options_dialog = None
        self.preview_armor = True
        self.preview_bg = "#ffffff"

        # connect action menu
        self.ui.actionSave.triggered.connect(self.save)
        self.ui.actionReload.triggered.connect(self.reload)
        self.ui.actionOpen.triggered.connect(self.open_file)
        self.ui.actionQuit.triggered.connect(self.app.closeAllWindows)
        self.ui.actionOptions.triggered.connect(self.new_options_dialog)
        self.ui.actionItemBrowser.triggered.connect(self.new_item_browser)
        self.ui.actionAbout.triggered.connect(self.new_about_dialog)
        self.ui.actionMods.triggered.connect(self.new_mods_dialog)
        self.ui.actionImageBrowser.triggered.connect(self.new_image_browser_dialog)

        self.ui.actionExportPlayerBinary.triggered.connect(self.export_save)
        self.ui.actionExportPlayerJSON.triggered.connect(self.export_json)
        self.ui.actionImportPlayerBinary.triggered.connect(self.import_save)
        self.ui.actionImportPlayerJSON.triggered.connect(self.import_json)

        # set up bag tables
        bags = ("wieldable", "head", "chest", "legs", "back", "main_bag",
                "action_bar", "object_bag", "tile_bag", "essentials", "mouse")
        for bag in bags:
            logging.debug("Setting up %s bag", bag)
            self.bag_setup(getattr(self.ui, bag), bag)

        self.preview_setup()

        # signals
        self.ui.blueprints_button.clicked.connect(self.new_blueprint_edit)
        self.ui.appearance_button.clicked.connect(self.new_appearance_dialog)
        self.ui.techs_button.clicked.connect(self.new_techs_dialog)
        self.ui.quests_button.clicked.connect(self.new_quests_dialog)
        self.ui.ship_button.clicked.connect(self.new_ship_dialog)

        self.ui.name.textChanged.connect(self.set_name)
        self.ui.male.clicked.connect(self.set_gender)
        self.ui.female.clicked.connect(self.set_gender)
        self.ui.description.textChanged.connect(self.set_description)
        self.ui.pixels.valueChanged.connect(self.set_pixels)

        self.ui.health.valueChanged.connect(lambda: self.set_stat_slider("health"))
        self.ui.energy.valueChanged.connect(lambda: self.set_stat_slider("energy"))
        self.ui.health_button.clicked.connect(lambda: self.max_stat("health"))
        self.ui.energy_button.clicked.connect(lambda: self.max_stat("energy"))

        self.ui.copy_uuid_button.clicked.connect(self.copy_uuid)

        self.window.setWindowModified(False)

        logging.debug("Showing main window")
        self.window.show()

        # launch first setup if we need to
        if not new_setup_dialog(self.window):
            logging.error("Config/index creation failed")
            return
        logging.info("Starbound folder: %s", Config().read("starbound_folder"))

        logging.info("Checking assets hash")
        if not check_index_valid(self.window):
            logging.error("Index creation failed")
            return

        logging.info("Loading assets database")
        self.assets = Assets(Config().read("assets_db"),
                             Config().read("starbound_folder"))
        self.items = self.assets.items()

        # populate species combobox
        for species in self.assets.species().get_species_list():
            self.ui.race.addItem(species)
        self.ui.race.currentTextChanged.connect(self.update_species)

        # populate game mode combobox
        for mode in sorted(self.assets.player().mode_types.values()):
            self.ui.game_mode.addItem(mode)
        self.ui.game_mode.currentTextChanged.connect(self.set_game_mode)

        # launch open file dialog
        self.player = None
        logging.debug("Open file dialog")
        open_player = self.open_file()
        # we *need* at least an initial save file
        if not open_player:
            logging.warning("No player file selected")
            return

        self.ui.name.setFocus()

        # block for update check result (should be ready now)
        update_thread.join()
        if update_result[0]:
            update_check_dialog(self.window, update_result[0])

        sys.exit(self.app.exec_())

    def update(self):
        """Update all GUI widgets with values from PlayerSave instance."""
        logging.info("Updating main window")
        # uuid / save version
        self.ui.uuid_label.setText(self.player.get_uuid())
        self.ui.ver_label.setText(self.player.get_header())
        # name
        self.ui.name.setText(self.player.get_name())
        # race
        self.ui.race.setCurrentText(self.player.get_race(pretty=True))
        # pixels
        try:
            self.ui.pixels.setValue(self.player.get_pixels())
        except TypeError:
            logging.exception("Unable to set pixels widget")
        # description
        self.ui.description.setPlainText(self.player.get_description())
        # gender
        getattr(self.ui, self.player.get_gender()).toggle()
        # game mode
        game_mode = self.player.get_game_mode()
        try:
            self.ui.game_mode.setCurrentText(self.assets.player().mode_types[game_mode])
        except KeyError:
            logging.exception("No game mode set on player")

        # stats
        self.update_stat("health")
        self.update_stat("energy")

        # quests
        # TODO: re-enable when quests are supported again
        # can_edit_quests = "quests" in self.player.entity
        can_edit_quests = False
        self.ui.quests_button.setEnabled(can_edit_quests)

        # ship
        can_edit_ship = ("shipUpgrades" in self.player.entity and
                         "aiState" in self.player.entity)
        self.ui.ship_button.setEnabled(can_edit_ship)

        # items
        total = 0
        progress = QProgressDialog("Updating item slots...",
                                   None, 0, 11, self.window)

        progress.setWindowTitle("Updating...")
        progress.setWindowModality(QtCore.Qt.ApplicationModal)
        progress.forceShow()
        progress.setValue(total)

        # equipment
        equip_bags = "head", "chest", "legs", "back"
        for bag in equip_bags:
            logging.debug("Updating %s", bag)
            items = []
            for x in getattr(self.player, "get_" + bag)():
                if x is not None:
                    items.append(ItemWidget(x["content"], self.assets))
                else:
                    items.append(ItemWidget(None, self.assets))

            getattr(self.ui, bag).setItem(0, 0, items[0])
            getattr(self.ui, bag).setItem(0, 1, items[1])
            total += 1
            progress.setValue(total)

        for bag in "wieldable", "main_bag", "tile_bag", "object_bag", "action_bar", "essentials", "mouse":
            self.update_bag(bag)
            total += 1
            progress.setValue(total)

        self.update_player_preview()

    def bag_setup(self, widget, name):
        widget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        # TODO: still issues with drag drop between tables
        widget.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        widget.cellChanged.connect(self.set_edited)

        item_edit = getattr(self, "new_" + name + "_item_edit")

        widget.cellDoubleClicked.connect(lambda: item_edit(False))

        sortable = ("main_bag", "tile_bag", "object_bag")
        clearable = ("wieldable", "action_bar", "essentials")

        edit_action = QAction("Edit...", widget)
        edit_action.triggered.connect(lambda: item_edit(False))
        widget.addAction(edit_action)
        edit_json_action = QAction("Edit JSON...", widget)
        edit_json_action.triggered.connect(lambda: item_edit(False, True))
        widget.addAction(edit_json_action)
        import_json = QAction("Import...", widget)
        import_json.triggered.connect(lambda: item_edit(True))
        widget.addAction(import_json)
        trash_action = QAction("Trash", widget)
        trash_slot = lambda: self.trash_slot(self.window, widget, True)
        trash_action.triggered.connect(trash_slot)
        widget.addAction(trash_action)

        if name in sortable or name in clearable:
            sep_action = QAction(widget)
            sep_action.setSeparator(True)
            widget.addAction(sep_action)
            if name in clearable:
                clear_action = QAction("Clear Held Items", widget)
                clear_action.triggered.connect(self.clear_held_slots)
                widget.addAction(clear_action)
            if name in sortable:
                sort_name = QAction("Sort By Name", widget)
                sort_name.triggered.connect(lambda: self.sort_bag(name, "name"))
                widget.addAction(sort_name)
                sort_type = QAction("Sort By Type", widget)
                sort_type.triggered.connect(lambda: self.sort_bag(name, "category"))
                widget.addAction(sort_type)
                sort_count = QAction("Sort By Count", widget)
                sort_count.triggered.connect(lambda: self.sort_bag(name, "count"))
                widget.addAction(sort_count)

    def toggle_preview_armor(self):
        self.preview_armor = not self.preview_armor
        self.update_player_preview()

    def change_preview_background(self):
        qcolor = QColorDialog().getColor(QColor(self.preview_bg),
                                         self.window)

        if qcolor.isValid():
            self.preview_bg = qcolor.name()
            self.update_player_preview()

    def preview_setup(self):
        button = self.ui.preview_config_button
        toggle_armor = QAction("Toggle Armor", button)
        toggle_armor.triggered.connect(self.toggle_preview_armor)
        button.addAction(toggle_armor)
        change_bg = QAction("Change Background...", button)
        change_bg.triggered.connect(self.change_preview_background)
        button.addAction(change_bg)

    def update_title(self):
        """Update window title with player name."""
        self.window.setWindowTitle("starcheat - " + self.player.get_name() + "[*]")

    def save(self):
        """Update internal player dict with GUI values and export to file."""
        logging.info("Saving player file %s", self.player.filename)
        self.set_bags()
        # save and show status
        logging.info("Writing file to disk")
        self.player.export_save(self.player.filename)
        self.update_title()
        self.ui.statusbar.showMessage("Saved " + self.player.filename, 3000)
        self.window.setWindowModified(False)
        self.players[self.player.get_uuid()] = self.player

    def new_item_edit(self, bag, do_import, json_edit=False):
        """Display a new item edit dialog using the select cell in a given bag."""
        logging.debug("New item edit dialog")
        row = bag.currentRow()
        column = bag.currentColumn()
        current = bag.currentItem()
        item = saves.empty_slot()
        valid_slot = (type(current) is not QTableWidgetItem and
                      current is not None and
                      current.item is not None)

        if do_import:
            imported = import_json(self.window)
            if imported is False:
                self.ui.statusbar.showMessage("Error importing item, see starcheat log for details", 3000)
                return
            elif imported is None:
                return
            else:
                item = imported

        # cells don't retain ItemSlot widget when they've been dragged away
        if valid_slot:
            item.update(current.item)

        if not json_edit:
            item_edit = ItemEdit(self.window, item,
                                 self.player, self.assets,
                                 self.remember_browser)
        else:
            item_edit = ItemEditOptions(self.window,
                                        item["name"],
                                        item,
                                        "Edit Item Data")

        def update_slot():
            logging.debug("Writing changes to slot")
            try:
                if not json_edit:
                    data = item_edit.get_item()
                else:
                    name, data = item_edit.get_option()

                new_slot = ItemWidget(data, self.assets)

                if new_slot.item["name"] != "":
                    bag.setItem(row, column, new_slot)
                    if not json_edit:
                        self.remember_browser = item_edit.remember_browser
                    self.set_bags()
                    self.update_player_preview()
                    self.set_edited()
            except (TypeError, KeyError):
                logging.exception("Error updating item slot")
                self.ui.statusbar.showMessage("Error updating item slot, see starcheat log for details", 3000)

        item_edit.dialog.accepted.connect(update_slot)

        if not json_edit:
            trash_slot = lambda: self.trash_slot(item_edit.dialog, bag)

            item_edit.ui.trash_button.clicked.connect(trash_slot)
            got_item = item_edit.launch()
            if got_item:
                item_edit.dialog.exec()
        else:
            item_edit.dialog.exec()

    def trash_slot(self, dialog, bag, standalone=False):
        row = bag.currentRow()
        column = bag.currentColumn()
        ask_dialog = QMessageBox(dialog)
        ask_dialog.setWindowTitle("Trash Item")
        ask_dialog.setText("Are you sure?")
        ask_dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        ask_dialog.setDefaultButton(QMessageBox.No)
        ask_dialog.setIcon(QMessageBox.Question)
        if ask_dialog.exec() == QMessageBox.Yes:
            bag.setItem(row, column, empty_slot())
            if not standalone:
                dialog.close()
            self.set_bags()
            self.update_player_preview()
            self.set_edited()

    def set_edited(self):
        self.window.setWindowModified(True)

    def sort_bag(self, bag_name, sort_by):
        self.set_bags()
        bag = getattr(self.player, "get_" + bag_name)()
        sorted_bag = self.assets.player().sort_bag(bag, sort_by)
        getattr(self.player, "set_" + bag_name)(sorted_bag)
        self.ui.statusbar.showMessage("Sorting by " + sort_by + "...", 3000)
        self.update()
        self.ui.statusbar.clearMessage()

    def clear_held_slots(self):
        self.player.clear_held_slots()
        self.set_edited()
        self.ui.statusbar.showMessage("All held items have been cleared", 3000)

    def new_blueprint_edit(self):
        """Launch a new blueprint management dialog."""
        logging.debug("New blueprint dialog")
        blueprint_lib = BlueprintLib(self.window,
                                     self.player.get_blueprints(),
                                     self.player.get_new_blueprints())

        def update_blueprints():
            logging.debug("Writing blueprints")
            self.player.set_blueprints(blueprint_lib.get_known_list())
            self.player.set_new_blueprints(blueprint_lib.new_blueprints)
            blueprint_lib.dialog.close()
            self.set_edited()

        blueprint_lib.ui.buttonBox.rejected.connect(update_blueprints)
        blueprint_lib.dialog.exec()

    def copy_uuid(self):
        clipboard = self.app.clipboard()
        clipboard.setText(self.player.get_uuid())
        self.ui.statusbar.showMessage("UUID copied to clipboard", 3000)

    def new_item_browser(self):
        """Launch a standalone item browser dialog that does write any changes."""
        self.item_browser = ItemBrowser(self.window, True)
        self.item_browser.dialog.show()

    def new_options_dialog(self):
        """Launch a new options config dialog."""
        logging.debug("New options dialog")
        self.options_dialog = OptionsDialog(self.window)

        def write_options():
            logging.info("Writing options to disk")
            self.options_dialog.write()
            self.update()

        self.options_dialog.dialog.rejected.connect(write_options)
        self.options_dialog.dialog.exec()

    def new_about_dialog(self):
        """Launch a new about dialog."""
        about_dialog = AboutDialog(self.window)
        about_dialog.dialog.exec()

    def new_appearance_dialog(self):
        appearance_dialog = Appearance(self)
        appearance_dialog.dialog.exec()
        appearance_dialog.write_appearance_values()
        self.update_player_preview()

    def new_techs_dialog(self):
        techs_dialog = Techs(self)
        techs_dialog.dialog.rejected.connect(techs_dialog.write_techs)
        techs_dialog.dialog.exec()

    def new_quests_dialog(self):
        quests_dialog = Quests(self)
        quests_dialog.dialog.rejected.connect(quests_dialog.write_quests)
        quests_dialog.dialog.exec()

    def new_ship_dialog(self):
        ship_dialog = Ship(self)
        ship_dialog.dialog.rejected.connect(ship_dialog.write_ship)
        ship_dialog.dialog.exec()

    def new_mods_dialog(self):
        mods_dialog = ModsDialog(self.window)
        mods_dialog.dialog.exec()

    def new_image_browser_dialog(self):
        self.image_browser = ImageBrowser(self.window, self.assets)
        self.image_browser.dialog.show()

    def reload(self):
        """Reload the currently open save file and update GUI values."""
        logging.info("Reloading file %s", self.player.filename)
        self.player = saves.PlayerSave(self.player.filename)
        self.update()
        self.update_title()
        self.ui.statusbar.showMessage("Reloaded " + self.player.filename, 3000)
        self.window.setWindowModified(False)

    def open_file(self):
        """Display open file dialog and load selected save."""
        if self.window.isWindowModified():
            button = save_modified_dialog(self.window)
            if button == QMessageBox.Cancel:
                return False
            elif button == QMessageBox.Save:
                self.save()

        character_select = CharacterSelectDialog(self, self.assets)
        character_select.show()

        self.players = character_select.players

        if character_select.selected is None:
            logging.warning("No player selected")
            return False
        else:
            self.player = character_select.selected

        self.update()
        self.update_title()
        self.ui.statusbar.showMessage("Opened " + self.player.filename, 3000)
        self.window.setWindowModified(False)
        return True

    # export save stuff
    def export_save(self, kind="player"):
        """Save a copy of the current player file to another location.
        Doesn't change the current filename."""
        export_func = lambda: self.player.export_save(filename[0])
        title = "Export Player File As"
        filetype = "Player (*.player);;All Files (*)"
        status = "Exported player file to "

        filename = QFileDialog.getSaveFileName(self.window, title, filter=filetype)

        if filename[0] != "":
            self.set_bags()
            export_func()
            self.ui.statusbar.showMessage(status + filename[0], 3000)

    def export_json(self, kind="player"):
        """Export player entity as json."""
        data = self.player.entity
        title = "Export Player JSON File As"
        filetype = "JSON (*.json);;All Files (*)"
        status = "Exported player JSON file to "

        filename = QFileDialog.getSaveFileName(self.window, title, filter=filetype)

        if filename[0] != "":
            self.set_bags()
            json_data = json.dumps(data, sort_keys=True,
                                   indent=4, separators=(',', ': '))
            json_file = open(filename[0], "w")
            json_file.write(json_data)
            json_file.close()
            self.ui.statusbar.showMessage(status + filename[0], 3000)

    # import save stuff
    def import_save(self, kind="player"):
        """Import a .player file over the top of current player."""
        import_func = self.player.import_save
        title = "Import Player File"
        filetype = "Player (*.player);;All Files (*)"
        status = "Imported player file from "

        filename = QFileDialog.getOpenFileName(self.window, title, filter=filetype)

        if filename[0] == "":
            return

        try:
            import_func(filename[0])
            self.update()
            self.ui.statusbar.showMessage(status + filename[0], 3000)
        except:
            logging.exception("Error reading file: %s", filename[0])
            self.ui.statusbar.showMessage("Error reading file, see starcheat log for details", 3000)

    def import_json(self, kind="player"):
        """Import an exported JSON file and merge/update with open player."""
        update_func = lambda: self.player.entity.update(data)
        title = "Import JSON Player File"
        status = "Imported player file "

        filename = QFileDialog.getOpenFileName(self.window, title,
                                               filter="JSON (*.json);;All Files (*)")

        if filename[0] == "":
            logging.debug("No file selected to import")
            return

        try:
            data = json.load(open(filename[0], "r"))
            update_func()
            self.update()
            self.ui.statusbar.showMessage(status + filename[0], 3000)
        except:
            logging.exception("Error reading file: %s", filename[0])
            self.ui.statusbar.showMessage("Error importing file, see starcheat log for details", 3000)

    def get_gender(self):
        if self.ui.male.isChecked():
            return "male"
        else:
            return "female"

    def get_bag(self, name):
        """Return the entire contents of a given non-equipment bag as raw values."""
        logging.debug("Getting %s contents", name)
        row = column = 0
        bag = getattr(self.player, "get_" + name)()

        for i in range(len(bag)):
            item = getattr(self.ui, name).item(row, column)
            empty_item = (item is None or
                          type(item) is QTableWidgetItem or
                          item.item is None)
            if empty_item:
                item = None
            else:
                widget = item.item
                item = saves.new_item(widget["name"],
                                      widget["count"],
                                      widget["parameters"])

            bag[i] = item

            # so far all non-equip bags are 10 cols long
            column += 1
            if (column % 10) == 0:
                row += 1
                column = 0

        return bag

    def get_equip(self, name):
        """Return the raw values of both slots in a given equipment bag."""
        logging.debug("Getting %s contents", name)
        equip = getattr(self.ui, name)
        main_cell = equip.item(0, 0)
        glamor_cell = equip.item(0, 1)

        # when you drag itemwidgets around the cell will become empty so just
        # pretend it had an empty slot value
        empty_main = (main_cell is None or
                      type(main_cell) is QTableWidgetItem or
                      main_cell.item is None)
        if empty_main:
            main = None
        else:
            widget = main_cell.item
            main = saves.new_item(widget["name"],
                                  widget["count"],
                                  widget["parameters"])

        empty_glamor = (glamor_cell is None or
                        type(glamor_cell) is QTableWidgetItem or
                        glamor_cell.item is None)
        if empty_glamor:
            glamor = None
        else:
            widget = glamor_cell.item
            glamor = saves.new_item(widget["name"],
                                    widget["count"],
                                    widget["parameters"])

        return main, glamor

    def update_bag(self, bag_name):
        """Set the entire contents of any given bag with ItemWidgets based off player data."""
        logging.debug("Updating %s contents", bag_name)
        row = column = 0
        bag = getattr(self.player, "get_" + bag_name)()

        for slot in range(len(bag)):
            item = bag[slot]
            if item is not None and "content" in item:
                widget = ItemWidget(item["content"], self.assets)
            else:
                widget = ItemWidget(None, self.assets)

            getattr(self.ui, bag_name).setItem(row, column, widget)

            column += 1
            if (column % 10) == 0:
                row += 1
                column = 0

    def update_player_preview(self):
        try:
            image = self.assets.species().render_player(self.player,
                                                        self.preview_armor)
            pixmap = QPixmap.fromImage(ImageQt(image))
        except (OSError, TypeError, AttributeError):
            # TODO: more specific error handling. may as well except all errors
            # at this point jeez
            logging.exception("Couldn't load species images")
            pixmap = QPixmap()

        self.ui.player_preview.setStyleSheet("background-color: %s;" % self.preview_bg)
        self.ui.player_preview.setPixmap(pixmap)
        self.window.setWindowModified(True)

    def update_species(self):
        species = self.ui.race.currentText()
        if self.player.get_race(pretty=True) == species:
            # don't overwrite appearance values if it didn't really change
            return
        self.player.set_race(species)
        defaults = self.assets.species().get_default_colors(species)
        for key in defaults:
            getattr(self.player, "set_%s_directives" % key)(defaults[key][0])
        self.update_player_preview()
        self.window.setWindowModified(True)

    def set_pixels(self):
        self.player.set_pixels(self.ui.pixels.value())
        self.set_edited()

    def set_name(self):
        self.player.set_name(self.ui.name.text())
        self.set_edited()

    def set_description(self):
        self.player.set_description(self.ui.description.toPlainText())
        self.set_edited()

    def set_gender(self):
        self.player.set_gender(self.get_gender())
        self.update_player_preview()
        self.set_edited()

    def set_game_mode(self):
        self.player.set_game_mode(self.assets.player().get_mode_type(self.ui.game_mode.currentText()))
        self.set_edited()

    def set_bags(self):
        # this function mostly just exist to work around the bug of
        # dragndrop not updating player entity. this requires the table view
        # equipment
        equip_bags = "head", "chest", "legs", "back"
        for b in equip_bags:
            bag = self.get_equip(b)
            getattr(self.player, "set_" + b)(bag[0], bag[1])
        # bags
        bags = "wieldable", "main_bag", "tile_bag", "action_bar", "essentials", "mouse", "object_bag"
        for b in bags:
            getattr(self.player, "set_" + b)(self.get_bag(b))

    def max_stat(self, name):
        """Set a stat's current value to its max value."""
        getattr(self.player, "set_"+name)(100)
        self.update_stat(name)

    def set_stat(self, name):
        max = getattr(self.ui, "max_"+name).value()
        getattr(self.player, "set_max_"+name)(float(max))
        self.update_stat(name)

    def set_stat_slider(self, name):
        current = getattr(self.ui, name).value()
        getattr(self.player, "set_"+name)(current)
        self.update_stat(name)

    def update_stat(self, name):
        try:
            current = int(getattr(self.player, "get_"+name)())
            button = getattr(self.ui, name+"_button")
            getattr(self.ui, name).setValue(current)
            button.setEnabled(current != 100)
            self.set_edited()
        except TypeError:
            logging.exception("Unable to set stat %s", name)

    # these are used for connecting the item edit dialog to bag tables
    def new_main_bag_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.main_bag, do_import, json_edit)

    def new_tile_bag_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.tile_bag, do_import, json_edit)

    def new_object_bag_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.object_bag, do_import, json_edit)

    def new_action_bar_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.action_bar, do_import, json_edit)

    def new_head_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.head, do_import, json_edit)

    def new_chest_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.chest, do_import, json_edit)

    def new_legs_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.legs, do_import, json_edit)

    def new_back_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.back, do_import, json_edit)

    def new_wieldable_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.wieldable, do_import, json_edit)

    def new_essentials_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.essentials, do_import, json_edit)

    def new_mouse_item_edit(self, do_import, json_edit=False):
        self.new_item_edit(self.ui.mouse, do_import, json_edit)