diff --git a/eos/db/migration.py b/eos/db/migration.py index c99be1ffcf..a0824f8125 100644 --- a/eos/db/migration.py +++ b/eos/db/migration.py @@ -4,6 +4,9 @@ import re import os import migrations +import logging + +logger = logging.getLogger(__name__) def getVersion(db): cursor = db.execute('PRAGMA user_version') @@ -30,10 +33,9 @@ def update(saveddata_engine): shutil.copyfile(config.saveDB, toFile) for version in xrange(dbVersion, appVersion): - func = migrations.updates[version+1] if func: - print "applying update",version+1 + logger.info("Applying database update: %d", version+1) func(saveddata_engine) # when all is said and done, set version to current diff --git a/eos/db/migrations/upgrade13.py b/eos/db/migrations/upgrade13.py new file mode 100644 index 0000000000..047dc71293 --- /dev/null +++ b/eos/db/migrations/upgrade13.py @@ -0,0 +1,15 @@ +""" +Migration 13 + +- Alters fits table to introduce implant location attribute +""" + +import sqlalchemy + +def upgrade(saveddata_engine): + # Update fits schema to include implant location attribute + try: + saveddata_engine.execute("SELECT implantLocation FROM fits LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE fits ADD COLUMN implantLocation INTEGER;") + saveddata_engine.execute("UPDATE fits SET implantLocation = 0") \ No newline at end of file diff --git a/eos/db/saveddata/__init__.py b/eos/db/saveddata/__init__.py index 683fb499d5..e43c6e6010 100644 --- a/eos/db/saveddata/__init__.py +++ b/eos/db/saveddata/__init__.py @@ -13,6 +13,7 @@ "miscData", "targetResists", "override", - "crest" + "crest", + "implantSet" ] diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index 6600d975ef..e6be52535f 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -36,9 +36,23 @@ Column("ownerID", ForeignKey("users.ID"), nullable = True)) mapper(Character, characters_table, - properties = {"_Character__owner" : relation(User, backref = "characters"), - "_Character__skills" : relation(Skill, backref="character", cascade = "all,delete-orphan"), - "_Character__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all,delete-orphan', single_parent=True, - primaryjoin = charImplants_table.c.charID == characters_table.c.ID, - secondaryjoin = charImplants_table.c.implantID == Implant.ID, - secondary = charImplants_table),}) + properties = { + "savedName": characters_table.c.name, + "_Character__owner": relation( + User, + backref = "characters"), + "_Character__skills": relation( + Skill, + backref="character", + cascade = "all,delete-orphan"), + "_Character__implants": relation( + Implant, + collection_class = HandledImplantBoosterList, + cascade='all,delete-orphan', + backref='character', + single_parent=True, + primaryjoin = charImplants_table.c.charID == characters_table.c.ID, + secondaryjoin = charImplants_table.c.implantID == Implant.ID, + secondary = charImplants_table), + } +) diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 77e6eb04be..29f51ef6c4 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -29,7 +29,7 @@ from eos.db.saveddata.fighter import fighters_table from eos.db.saveddata.cargo import cargo_table from eos.db.saveddata.implant import fitImplants_table -from eos.types import Fit, Module, User, Booster, Drone, Fighter, Cargo, Implant, Character, DamagePattern, TargetResists +from eos.types import Fit, Module, User, Booster, Drone, Fighter, Cargo, Implant, Character, DamagePattern, TargetResists, ImplantLocation from eos.effectHandlerHelpers import * fits_table = Table("fits", saveddata_meta, @@ -43,6 +43,7 @@ Column("booster", Boolean, nullable = False, index = True, default = 0), Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True), Column("modeID", Integer, nullable=True), + Column("implantLocation", Integer, nullable=False, default=ImplantLocation.FIT), ) projectedFits_table = Table("projectedFits", saveddata_meta, diff --git a/eos/db/saveddata/implant.py b/eos/db/saveddata/implant.py index 60e40bff7f..c74def1576 100644 --- a/eos/db/saveddata/implant.py +++ b/eos/db/saveddata/implant.py @@ -36,4 +36,8 @@ Column("charID", ForeignKey("characters.ID"), index = True), Column("implantID", ForeignKey("implants.ID"), primary_key = True)) +implantsSetMap_table = Table("implantSetMap", saveddata_meta, + Column("setID", ForeignKey("implantSets.ID"), index = True), + Column("implantID", ForeignKey("implants.ID"), primary_key = True)) + mapper(Implant, implants_table) diff --git a/eos/db/saveddata/implantSet.py b/eos/db/saveddata/implantSet.py new file mode 100644 index 0000000000..73036de91c --- /dev/null +++ b/eos/db/saveddata/implantSet.py @@ -0,0 +1,45 @@ +#=============================================================================== +# Copyright (C) 2016 Ryan Holmes +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +#=============================================================================== + +from sqlalchemy import Table, Column, Integer, ForeignKey, String +from sqlalchemy.orm import relation, mapper + +from eos.db import saveddata_meta +from eos.db.saveddata.implant import implantsSetMap_table +from eos.types import Implant, ImplantSet +from eos.effectHandlerHelpers import HandledImplantBoosterList + +implant_set_table = Table("implantSets", saveddata_meta, + Column("ID", Integer, primary_key = True), + Column("name", String, nullable = False), +) + +mapper(ImplantSet, implant_set_table, + properties = { + "_ImplantSet__implants": relation( + Implant, + collection_class = HandledImplantBoosterList, + cascade='all, delete, delete-orphan', + backref='set', + single_parent=True, + primaryjoin = implantsSetMap_table.c.setID == implant_set_table.c.ID, + secondaryjoin = implantsSetMap_table.c.implantID == Implant.ID, + secondary = implantsSetMap_table), + } +) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index a305994cd9..5a6dd1bfcc 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -20,7 +20,7 @@ from eos.db.util import processEager, processWhere from eos.db import saveddata_session, sd_lock -from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad, TargetResists, Override, CrestChar +from eos.types import * from eos.db.saveddata.fleet import squadmembers_table from eos.db.saveddata.fit import projectedFits_table from sqlalchemy.sql import and_ @@ -154,7 +154,7 @@ def getCharacter(lookfor, eager=None): elif isinstance(lookfor, basestring): eager = processEager(eager) with sd_lock: - character = saveddata_session.query(Character).options(*eager).filter(Character.name == lookfor).first() + character = saveddata_session.query(Character).options(*eager).filter(Character.savedName == lookfor).first() else: raise TypeError("Need integer or string as argument") return character @@ -349,6 +349,12 @@ def getTargetResistsList(eager=None): patterns = saveddata_session.query(TargetResists).options(*eager).all() return patterns +def getImplantSetList(eager=None): + eager = processEager(eager) + with sd_lock: + sets = saveddata_session.query(ImplantSet).options(*eager).all() + return sets + @cachedQuery(DamagePattern, 1, "lookfor") def getDamagePattern(lookfor, eager=None): if isinstance(lookfor, int): @@ -385,6 +391,24 @@ def getTargetResists(lookfor, eager=None): raise TypeError("Need integer or string as argument") return pattern +@cachedQuery(ImplantSet, 1, "lookfor") +def getImplantSet(lookfor, eager=None): + if isinstance(lookfor, int): + if eager is None: + with sd_lock: + pattern = saveddata_session.query(ImplantSet).get(lookfor) + else: + eager = processEager(eager) + with sd_lock: + pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetResists.ID == lookfor).first() + elif isinstance(lookfor, basestring): + eager = processEager(eager) + with sd_lock: + pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetResists.name == lookfor).first() + else: + raise TypeError("Improper argument") + return pattern + def searchFits(nameLike, where=None, eager=None): if not isinstance(nameLike, basestring): raise TypeError("Need string as argument") diff --git a/eos/effects/angelsetbonus.py b/eos/effects/angelsetbonus.py index b2e5f449bc..711d99c9d6 100644 --- a/eos/effects/angelsetbonus.py +++ b/eos/effects/angelsetbonus.py @@ -5,7 +5,7 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply( + fit.appliedImplants.filteredItemMultiply( lambda implant: "signatureRadiusBonus" in implant.itemModifiedAttributes and "implantSetAngel" in implant.itemModifiedAttributes, "signatureRadiusBonus", implant.getModifiedItemAttr("implantSetAngel")) \ No newline at end of file diff --git a/eos/effects/caldarisetbonus3.py b/eos/effects/caldarisetbonus3.py index 40aade2acd..a31e831e68 100644 --- a/eos/effects/caldarisetbonus3.py +++ b/eos/effects/caldarisetbonus3.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanGravimetricStrengthPercent", implant.getModifiedItemAttr("implantSetCaldariNavy")) diff --git a/eos/effects/caldarisetlgbonus.py b/eos/effects/caldarisetlgbonus.py index abf7ecd8ef..04d1c6f746 100644 --- a/eos/effects/caldarisetlgbonus.py +++ b/eos/effects/caldarisetlgbonus.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanGravimetricStrengthModifier", implant.getModifiedItemAttr("implantSetLGCaldariNavy")) diff --git a/eos/effects/federationsetbonus3.py b/eos/effects/federationsetbonus3.py index aa25dfcc4d..b97317b82d 100644 --- a/eos/effects/federationsetbonus3.py +++ b/eos/effects/federationsetbonus3.py @@ -5,5 +5,5 @@ type = "passive" runTime = "early" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanMagnetometricStrengthPercent", implant.getModifiedItemAttr("implantSetFederationNavy")) diff --git a/eos/effects/federationsetlgbonus.py b/eos/effects/federationsetlgbonus.py index 27c320be94..4328417cd1 100644 --- a/eos/effects/federationsetlgbonus.py +++ b/eos/effects/federationsetlgbonus.py @@ -5,5 +5,5 @@ type = "passive" runTime = "early" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanMagnetometricStrengthModifier", implant.getModifiedItemAttr("implantSetLGFederationNavy")) diff --git a/eos/effects/imperialsetbonus3.py b/eos/effects/imperialsetbonus3.py index 7c764dcad7..d239dcc35a 100644 --- a/eos/effects/imperialsetbonus3.py +++ b/eos/effects/imperialsetbonus3.py @@ -5,5 +5,5 @@ type = "passive" runTime = "early" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanRadarStrengthPercent", implant.getModifiedItemAttr("implantSetImperialNavy")) diff --git a/eos/effects/imperialsetlgbonus.py b/eos/effects/imperialsetlgbonus.py index 6495b9b81d..56369bf227 100644 --- a/eos/effects/imperialsetlgbonus.py +++ b/eos/effects/imperialsetlgbonus.py @@ -5,5 +5,5 @@ type = "passive" runTime = "early" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanRadarStrengthModifier", implant.getModifiedItemAttr("implantSetLGImperialNavy")) diff --git a/eos/effects/implantsetwarpspeed.py b/eos/effects/implantsetwarpspeed.py index a6410ed77d..29fd50e49a 100644 --- a/eos/effects/implantsetwarpspeed.py +++ b/eos/effects/implantsetwarpspeed.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "WarpSBonus", implant.getModifiedItemAttr("implantSetWarpSpeed")) diff --git a/eos/effects/republicsetbonus3.py b/eos/effects/republicsetbonus3.py index 1f3cad903f..5709959ae0 100644 --- a/eos/effects/republicsetbonus3.py +++ b/eos/effects/republicsetbonus3.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanLadarStrengthPercent", implant.getModifiedItemAttr("implantSetRepublicFleet")) diff --git a/eos/effects/republicsetlgbonus.py b/eos/effects/republicsetlgbonus.py index a80ae38e77..bf79499ea6 100644 --- a/eos/effects/republicsetlgbonus.py +++ b/eos/effects/republicsetlgbonus.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "scanLadarStrengthModifier", implant.getModifiedItemAttr("implantSetLGRepublicFleet")) diff --git a/eos/effects/setbonusbloodraider.py b/eos/effects/setbonusbloodraider.py index 718be78e68..73fb305ec0 100644 --- a/eos/effects/setbonusbloodraider.py +++ b/eos/effects/setbonusbloodraider.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "durationBonus", implant.getModifiedItemAttr("implantSetBloodraider")) diff --git a/eos/effects/setbonuschristmasagilitybonus.py b/eos/effects/setbonuschristmasagilitybonus.py index 857cad3555..f0ca5c03fe 100644 --- a/eos/effects/setbonuschristmasagilitybonus.py +++ b/eos/effects/setbonuschristmasagilitybonus.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "agilityBonus", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmasarmorhpbonus2.py b/eos/effects/setbonuschristmasarmorhpbonus2.py index 971644d029..9f5c52fd4c 100644 --- a/eos/effects/setbonuschristmasarmorhpbonus2.py +++ b/eos/effects/setbonuschristmasarmorhpbonus2.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "armorHpBonus2", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmasbonusvelocity.py b/eos/effects/setbonuschristmasbonusvelocity.py index 1a0d7ffcd1..09b1ef21f0 100644 --- a/eos/effects/setbonuschristmasbonusvelocity.py +++ b/eos/effects/setbonuschristmasbonusvelocity.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "implantBonusVelocity", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmascapacitorcapacity.py b/eos/effects/setbonuschristmascapacitorcapacity.py index a0ba4bdd05..cc949b255e 100644 --- a/eos/effects/setbonuschristmascapacitorcapacity.py +++ b/eos/effects/setbonuschristmascapacitorcapacity.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "capacitorCapacityBonus", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmascapacitorrecharge2.py b/eos/effects/setbonuschristmascapacitorrecharge2.py index 7f31e18184..a526176e4f 100644 --- a/eos/effects/setbonuschristmascapacitorrecharge2.py +++ b/eos/effects/setbonuschristmascapacitorrecharge2.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "capRechargeBonus", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmascpuoutput.py b/eos/effects/setbonuschristmascpuoutput.py index 5ba1d538a0..e264c8dd52 100644 --- a/eos/effects/setbonuschristmascpuoutput.py +++ b/eos/effects/setbonuschristmascpuoutput.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "cpuOutputBonus2", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmaspowergrid.py b/eos/effects/setbonuschristmaspowergrid.py index b0fc46366f..9c39919be4 100644 --- a/eos/effects/setbonuschristmaspowergrid.py +++ b/eos/effects/setbonuschristmaspowergrid.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "powerEngineeringOutputBonus", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonuschristmasshieldcapacitybonus.py b/eos/effects/setbonuschristmasshieldcapacitybonus.py index 5841a565e5..c604d02812 100644 --- a/eos/effects/setbonuschristmasshieldcapacitybonus.py +++ b/eos/effects/setbonuschristmasshieldcapacitybonus.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "shieldCapacityBonus", implant.getModifiedItemAttr("implantSetChristmas")) diff --git a/eos/effects/setbonusguristas.py b/eos/effects/setbonusguristas.py index e0e6a14bff..34bbfacc26 100644 --- a/eos/effects/setbonusguristas.py +++ b/eos/effects/setbonusguristas.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "shieldBoostMultiplier", implant.getModifiedItemAttr("implantSetGuristas")) diff --git a/eos/effects/setbonusmordus.py b/eos/effects/setbonusmordus.py index 3bcdb2c07b..eef3675f98 100644 --- a/eos/effects/setbonusmordus.py +++ b/eos/effects/setbonusmordus.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "rangeSkillBonus", implant.getModifiedItemAttr("implantSetMordus")) diff --git a/eos/effects/setbonusore.py b/eos/effects/setbonusore.py index 8e540b81cb..a7342a8fa2 100644 --- a/eos/effects/setbonusore.py +++ b/eos/effects/setbonusore.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "maxRangeBonus", implant.getModifiedItemAttr("implantSetORE")) diff --git a/eos/effects/setbonussansha.py b/eos/effects/setbonussansha.py index e7e0d261c2..df7ae7be3e 100644 --- a/eos/effects/setbonussansha.py +++ b/eos/effects/setbonussansha.py @@ -6,5 +6,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), + fit.appliedImplants.filteredItemMultiply(lambda target: target.item.requiresSkill("Cybernetics"), "armorHpBonus", implant.getModifiedItemAttr("implantSetSansha") or 1) diff --git a/eos/effects/setbonusserpentis.py b/eos/effects/setbonusserpentis.py index 4775993c03..f4de04dcc2 100644 --- a/eos/effects/setbonusserpentis.py +++ b/eos/effects/setbonusserpentis.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "velocityBonus", implant.getModifiedItemAttr("implantSetSerpentis")) diff --git a/eos/effects/setbonussisters.py b/eos/effects/setbonussisters.py index cf67fda489..a43478d52d 100644 --- a/eos/effects/setbonussisters.py +++ b/eos/effects/setbonussisters.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "scanStrengthBonus", implant.getModifiedItemAttr("implantSetSisters")) diff --git a/eos/effects/setbonussyndicate.py b/eos/effects/setbonussyndicate.py index 594c4e90a5..61fc59591e 100644 --- a/eos/effects/setbonussyndicate.py +++ b/eos/effects/setbonussyndicate.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "boosterAttributeModifier", implant.getModifiedItemAttr("implantSetSyndicate")) diff --git a/eos/effects/setbonusthukker.py b/eos/effects/setbonusthukker.py index 073b46c96a..8594ddcfec 100644 --- a/eos/effects/setbonusthukker.py +++ b/eos/effects/setbonusthukker.py @@ -5,5 +5,5 @@ runTime = "early" type = "passive" def handler(fit, implant, context): - fit.implants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", + fit.appliedImplants.filteredItemMultiply(lambda mod: mod.item.group.name == "Cyberimplant", "agilityBonus", implant.getModifiedItemAttr("implantSetThukker")) diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 522ad590ad..24fa086941 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -19,8 +19,9 @@ from sqlalchemy.orm import validates, reconstructor +from itertools import chain -from eos.effectHandlerHelpers import HandledItem +from eos.effectHandlerHelpers import HandledItem, HandledImplantBoosterList import eos.db import eos import logging @@ -89,7 +90,7 @@ def getAll0(cls): return all0 def __init__(self, name, defaultLevel=None, initSkills=True): - self.name = name + self.savedName = name self.__owner = None self.defaultLevel = defaultLevel self.__skills = [] @@ -100,7 +101,7 @@ def __init__(self, name, defaultLevel=None, initSkills=True): for item in self.getSkillList(): self.addSkill(Skill(item.ID, self.defaultLevel)) - self.__implants = eos.saveddata.fit.HandledImplantBoosterList() + self.__implants = HandledImplantBoosterList() self.apiKey = None @reconstructor @@ -128,6 +129,14 @@ def owner(self): def owner(self, owner): self.__owner = owner + @property + def name(self): + return self.savedName if not self.isDirty else "{} *".format(self.savedName) + + @name.setter + def name(self, name): + self.savedName = name + @property def skills(self): return self.__skills @@ -200,8 +209,13 @@ def calculateModifiedAttributes(self, fit, runTime, forceProjected = False): skill.calculateModifiedAttributes(fit, runTime) def clear(self): - for skill in self.skills: - skill.clear() + c = chain( + self.skills, + self.implants + ) + for stuff in c: + if stuff is not None and stuff != self: + stuff.clear() def __deepcopy__(self, memo): copy = Character("%s copy" % self.name, initSkills=False) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 1bda5ee17b..7e742a9393 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -31,6 +31,8 @@ import time import copy from utils.timer import Timer +from eos.enum import Enum + import logging @@ -41,6 +43,10 @@ except ImportError: from utils.compat import OrderedDict +class ImplantLocation(Enum): + FIT = 0 + CHARACTER = 1 + class Fit(object): """Represents a fitting, with modules, ship, implants, etc.""" @@ -324,16 +330,19 @@ def alignTime(self): return -log(0.25) * agility * mass / 1000000 @property - def appliedImplants(self): - implantsBySlot = {} - if self.character: - for implant in self.character.implants: - implantsBySlot[implant.slot] = implant + def implantSource(self): + return self.implantLocation - for implant in self.implants: - implantsBySlot[implant.slot] = implant + @implantSource.setter + def implantSource(self, source): + self.implantLocation = source - return implantsBySlot.values() + @property + def appliedImplants(self): + if self.implantLocation == ImplantLocation.CHARACTER: + return self.character.implants + else: + return self.implants @validates("ID", "ownerID", "shipID") def validator(self, key, val): diff --git a/eos/saveddata/implantSet.py b/eos/saveddata/implantSet.py new file mode 100644 index 0000000000..8e495c5ea8 --- /dev/null +++ b/eos/saveddata/implantSet.py @@ -0,0 +1,58 @@ +#=============================================================================== +# Copyright (C) 2016 Ryan Holmes +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +#=============================================================================== + +from eos.effectHandlerHelpers import HandledImplantBoosterList +from copy import deepcopy + +class ImplantSet(object): + def __init__(self, name=None): + self.name = name + self.__implants = HandledImplantBoosterList() + + @property + def implants(self): + return self.__implants + + @classmethod + def exportSets(cls, *sets): + out = "# Exported from pyfa\n#\n" \ + "# Values are in following format:\n" \ + "# [Implant Set name]\n" \ + "# [Implant name]\n" \ + "# [Implant name]\n" \ + "# ...\n\n" + + for set in sets: + out += "[{}]\n".format(set.name) + for implant in set.implants: + out += "{}\n".format(implant.item.name) + out += "\n" + + return out.strip() + + def __deepcopy__(self, memo): + copy = ImplantSet(self.name) + copy.name = "%s copy" % self.name + + orig = getattr(self, 'implants') + c = getattr(copy, 'implants') + for i in orig: + c.append(deepcopy(i, memo)) + + return copy diff --git a/eos/types.py b/eos/types.py index 26e36ef904..90e0974c05 100644 --- a/eos/types.py +++ b/eos/types.py @@ -30,10 +30,11 @@ from eos.saveddata.fighter import Fighter from eos.saveddata.cargo import Cargo from eos.saveddata.implant import Implant +from eos.saveddata.implantSet import ImplantSet from eos.saveddata.booster import SideEffect from eos.saveddata.booster import Booster from eos.saveddata.ship import Ship -from eos.saveddata.fit import Fit +from eos.saveddata.fit import Fit, ImplantLocation from eos.saveddata.mode import Mode from eos.saveddata.fleet import Fleet, Wing, Squad from eos.saveddata.miscData import MiscData diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index 785e3eebf9..c2a39b1792 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -20,4 +20,5 @@ "priceClear", "amount", "metaSwap", + "implantSets", ] diff --git a/gui/builtinContextMenus/implantSets.py b/gui/builtinContextMenus/implantSets.py new file mode 100644 index 0000000000..6ae43039be --- /dev/null +++ b/gui/builtinContextMenus/implantSets.py @@ -0,0 +1,78 @@ +from gui.contextMenu import ContextMenu +import gui.mainFrame +import service +import gui.globalEvents as GE +import wx + +class ImplantSets(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, srcContext, selection): + return srcContext in ("implantView", "implantEditor") + + def getText(self, itmContext, selection): + return "Add Implant Set" + + def getSubMenu(self, context, selection, rootMenu, i, pitem): + """ + A note on the selection here: Most context menus act on a fit, so it's easy enough to get the active fit from + the MainFrame instance. There's never been a reason to get info from another window, so there's not common + way of doing this. However, we use this context menu within the Character Editor to apply implant sets to a + character, so we need to access the character editor. + + It is for these reasons that I hijack the selection parameter when calling the menu and pass a pointer to the + Character Editor. This way we can use it to get current editing character ID and apply the implants. + + It would probably be better to have a function on the MainFrame to get the currently open Character Editor (as + we do with the item stats window). Eventually... Until then, this long ass note will remain to remind me why + stupid shit like this is even happening. + """ + + m = wx.Menu() + bindmenu = rootMenu if "wxMSW" in wx.PlatformInfo else m + + sIS = service.ImplantSets.getInstance() + implantSets = sIS.getImplantSetList() + + self.context = context + if len(selection) == 1: + self.selection = selection[0] # dirty hack here + + self.idmap = {} + + for set in implantSets: + id = wx.NewId() + mitem = wx.MenuItem(rootMenu, id, set.name) + bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem) + self.idmap[id] = set + m.AppendItem(mitem) + + return m + + def handleSelection(self, event): + set = self.idmap.get(event.Id, None) + + if set is None: + event.Skip() + return + + if self.context == "implantEditor": + # we are calling from character editor, the implant source is different + sChar = service.Character.getInstance() + charID = self.selection.getActiveCharacter() + + for implant in set.implants: + sChar.addImplant(charID, implant.item.ID) + + wx.PostEvent(self.selection, GE.CharChanged()) + else: + sFit = service.Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + for implant in set.implants: + sFit.addImplant(fitID, implant.item.ID, recalc=implant == set.implants[-1]) + + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + + +ImplantSets.register() diff --git a/gui/builtinContextMenus/itemStats.py b/gui/builtinContextMenus/itemStats.py index a05064d249..7669b1f2f0 100644 --- a/gui/builtinContextMenus/itemStats.py +++ b/gui/builtinContextMenus/itemStats.py @@ -9,6 +9,7 @@ def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() def display(self, srcContext, selection): + return srcContext in ("marketItemGroup", "marketItemMisc", "fittingModule", "fittingCharge", "fittingShip", "baseShip", @@ -16,7 +17,7 @@ def display(self, srcContext, selection): "implantItem", "boosterItem", "skillItem", "projectedModule", "projectedDrone", "projectedCharge", - "itemStats", "fighterItem") + "itemStats", "fighterItem", "implantItemChar") def getText(self, itmContext, selection): return "{0} Stats".format(itmContext if itmContext is not None else "Item") diff --git a/gui/builtinContextMenus/marketJump.py b/gui/builtinContextMenus/marketJump.py index 4980bf61fc..d38fd9f1c9 100644 --- a/gui/builtinContextMenus/marketJump.py +++ b/gui/builtinContextMenus/marketJump.py @@ -12,7 +12,8 @@ def display(self, srcContext, selection): "fittingCharge", "droneItem", "implantItem", "boosterItem", "projectedModule", "projectedDrone", - "projectedCharge", "cargoItem") + "projectedCharge", "cargoItem", + "implantItemChar") if not srcContext in validContexts or selection is None or len(selection) < 1: return False @@ -33,12 +34,10 @@ def getText(self, itmContext, selection): def activate(self, fullContext, selection, i): srcContext = fullContext[0] - if srcContext in ("fittingModule", "droneItem", "implantItem", - "boosterItem", "projectedModule", "projectedDrone", - "cargoItem"): - item = selection[0].item - elif srcContext in ("fittingCharge", "projectedCharge"): + if srcContext in ("fittingCharge", "projectedCharge"): item = selection[0].charge + elif hasattr(selection[0], "item"): + item = selection[0].item else: item = selection[0] diff --git a/gui/builtinViewColumns/baseIcon.py b/gui/builtinViewColumns/baseIcon.py index 30cbf6fb23..868c73a213 100644 --- a/gui/builtinViewColumns/baseIcon.py +++ b/gui/builtinViewColumns/baseIcon.py @@ -2,7 +2,7 @@ from gui.viewColumn import ViewColumn from gui.bitmapLoader import BitmapLoader import wx -from eos.types import Drone, Fit, Module, Slot, Rack +from eos.types import Drone, Fit, Module, Slot, Rack, Implant class BaseIcon(ViewColumn): name = "Base Icon" @@ -21,6 +21,11 @@ def getImageId(self, stuff): return self.shipImage if isinstance(stuff, Rack): return -1 + if isinstance(stuff, Implant): + if stuff.character: # if it has a character as it's parent + return self.fittingView.imageList.GetImageIndex("character_small", "gui") + else: + return self.shipImage if isinstance(stuff, Module): if stuff.isEmpty: return self.fittingView.imageList.GetImageIndex("slot_%s_small" % Slot.getName(stuff.slot).lower(), "gui") diff --git a/gui/builtinViewColumns/baseName.py b/gui/builtinViewColumns/baseName.py index e5f59a9e22..edb45044e8 100644 --- a/gui/builtinViewColumns/baseName.py +++ b/gui/builtinViewColumns/baseName.py @@ -22,7 +22,7 @@ import gui.mainFrame import wx -from eos.types import Drone, Cargo, Fit, Module, Slot, Rack +from eos.types import Drone, Cargo, Fit, Module, Slot, Rack, Implant import service class BaseName(ViewColumn): @@ -61,6 +61,8 @@ def getText(self, stuff): return "%s Slot" % Slot.getName(stuff.slot).capitalize() else: return stuff.item.name + elif isinstance(stuff, Implant): + return stuff.item.name else: item = getattr(stuff, "item", stuff) diff --git a/gui/builtinViewColumns/state.py b/gui/builtinViewColumns/state.py index c12db54dfb..cc3dee59b4 100644 --- a/gui/builtinViewColumns/state.py +++ b/gui/builtinViewColumns/state.py @@ -22,7 +22,7 @@ import gui.mainFrame import wx -from eos.types import Drone, Module, Rack, Fit +from eos.types import Drone, Module, Rack, Fit, Implant from eos.types import State as State_ class State(ViewColumn): @@ -67,6 +67,9 @@ def getImageId(self, stuff): if projectionInfo.active: return generic_active return generic_inactive + elif isinstance(stuff, Implant) and stuff.character: + # if we're showing character implants, show an "online" state, which should not be changed + return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(0).lower(), "gui") else: active = getattr(stuff, "active", None) if active is None: diff --git a/gui/builtinViews/__init__.py b/gui/builtinViews/__init__.py index 4d1ed63c69..46a500f7ba 100644 --- a/gui/builtinViews/__init__.py +++ b/gui/builtinViews/__init__.py @@ -1 +1 @@ -__all__ = ["fittingView", "fleetView"] +__all__ = ["fittingView", "fleetView", "implantEditor"] diff --git a/gui/builtinViews/entityEditor.py b/gui/builtinViews/entityEditor.py new file mode 100644 index 0000000000..112c1c63e9 --- /dev/null +++ b/gui/builtinViews/entityEditor.py @@ -0,0 +1,173 @@ +import wx +from gui.bitmapLoader import BitmapLoader +import service + +class BaseValidator(wx.PyValidator): + def __init__(self): + wx.PyValidator.__init__(self) + + def Validate(self, win): + raise NotImplementedError() + + def TransferToWindow(self): + return True + + def TransferFromWindow(self): + return True + +class TextEntryValidatedDialog(wx.TextEntryDialog): + def __init__(self, parent, validator=None, *args, **kargs): + wx.TextEntryDialog.__init__(self, parent, *args, **kargs) + self.parent = parent + + self.txtctrl = self.FindWindowById(3000) + if validator: + self.txtctrl.SetValidator(validator()) + +class EntityEditor (wx.Panel): + """ + Entity Editor is a panel that takes some sort of list as a source and populates a drop down with options to add/ + rename/clone/delete an entity. Comes with dialogs that take user input. Classes that derive this class must override + functions that get the list from the source, what to do when user does an action, and how to validate the input. + """ + + def __init__(self, parent, entityName): + wx.Panel.__init__(self, parent, id=wx.ID_ANY, style=wx.TAB_TRAVERSAL) + self.entityName = entityName + self.validator = None + self.navSizer = wx.BoxSizer(wx.HORIZONTAL) + + self.choices = [] + self.choices.sort(key=lambda p: p.name) + self.entityChoices = wx.Choice(self, choices=map(lambda p: p.name, self.choices)) + self.navSizer.Add(self.entityChoices, 1, wx.ALL, 5) + + buttons = (("new", wx.ART_NEW, self.OnNew), + ("rename", BitmapLoader.getBitmap("rename", "gui"), self.OnRename), + ("copy", wx.ART_COPY, self.OnCopy), + ("delete", wx.ART_DELETE, self.OnDelete)) + + size = None + for name, art, func in buttons: + bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON) if name != "rename" else art + btn = wx.BitmapButton(self, wx.ID_ANY, bitmap) + if size is None: + size = btn.GetSize() + + btn.SetMinSize(size) + btn.SetMaxSize(size) + + btn.SetToolTipString("{} {}".format(name.capitalize(), self.entityName)) + btn.Bind(wx.EVT_BUTTON, func) + setattr(self, "btn%s" % name.capitalize(), btn) + self.navSizer.Add(btn, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + + self.SetSizer(self.navSizer) + self.Layout() + + self.refreshEntityList() + + def SetEditorValidator(self, validator=None): + """ Sets validator class (not an instance of the class) """ + self.validator = validator + + def getEntitiesFromContext(self): + """ Gets list of entities from current context """ + raise NotImplementedError() + + def DoNew(self, name): + """Override method to do new entity logic. Must return the new entity""" + raise NotImplementedError() + + def DoCopy(self, entity, name): + """Override method to copy entity. Must return the copy""" + raise NotImplementedError() + + def DoRename(self, entity, name): + """Override method to rename an entity""" + raise NotImplementedError() + + def DoDelete(self, entity): + """Override method to delete entity""" + raise NotImplementedError() + + def OnNew(self, event): + dlg = TextEntryValidatedDialog(self, self.validator, + "Enter a name for your new {}:".format(self.entityName), + "New {}".format(self.entityName)) + dlg.CenterOnParent() + + if dlg.ShowModal() == wx.ID_OK: + new = self.DoNew(dlg.GetValue().strip()) + self.refreshEntityList(new) + wx.PostEvent(self.entityChoices, wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED)) + else: + return False + + def OnCopy(self, event): + dlg = TextEntryValidatedDialog(self, self.validator, + "Enter a name for your {} copy:".format(self.entityName), + "Copy {}".format(self.entityName)) + active = self.getActiveEntity() + dlg.SetValue("{} Copy".format(active.name)) + dlg.txtctrl.SetInsertionPointEnd() + dlg.CenterOnParent() + + if dlg.ShowModal() == wx.ID_OK: + copy = self.DoCopy(active, dlg.GetValue().strip()) + self.refreshEntityList(copy) + wx.PostEvent(self.entityChoices, wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED)) + + def OnRename(self, event): + dlg = TextEntryValidatedDialog(self, self.validator, + "Enter a new name for your {}:".format(self.entityName), + "Rename {}".format(self.entityName)) + active = self.getActiveEntity() + dlg.SetValue(active.name) + dlg.txtctrl.SetInsertionPointEnd() + dlg.CenterOnParent() + + if dlg.ShowModal() == wx.ID_OK: + self.DoRename(active, dlg.GetValue().strip()) + self.refreshEntityList(active) + wx.PostEvent(self.entityChoices, wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED)) + + def OnDelete(self, event): + dlg = wx.MessageDialog(self, + "Do you really want to delete the {} {}?".format(self.getActiveEntity().name, self.entityName), + "Confirm Delete", wx.YES | wx.NO | wx.ICON_QUESTION) + dlg.CenterOnParent() + + if dlg.ShowModal() == wx.ID_YES: + self.DoDelete(self.getActiveEntity()) + self.refreshEntityList() + wx.PostEvent(self.entityChoices, wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED)) + + def refreshEntityList(self, selected=None): + self.choices = self.getEntitiesFromContext() + self.entityChoices.Clear() + + self.entityChoices.AppendItems(map(lambda p: p.name, self.choices)) + if selected: + idx = self.choices.index(selected) + self.entityChoices.SetSelection(idx) + else: + self.entityChoices.SetSelection(0) + + def getActiveEntity(self): + if len(self.choices) == 0: + return None + + return self.choices[self.entityChoices.GetSelection()] + + def setActiveEntity(self, entity): + self.entityChoices.SetSelection(self.choices.index(entity)) + + def checkEntitiesExist(self): + if len(self.choices) == 0: + self.Parent.Hide() + if self.OnNew(None) is False: + return False + self.Parent.Show() + + return True \ No newline at end of file diff --git a/gui/builtinViews/implantEditor.py b/gui/builtinViews/implantEditor.py new file mode 100644 index 0000000000..bcdb96c7bb --- /dev/null +++ b/gui/builtinViews/implantEditor.py @@ -0,0 +1,258 @@ +import wx +import service +import gui.display as d +from gui.bitmapLoader import BitmapLoader +import gui.PFSearchBox as SBox +from gui.marketBrowser import SearchBox +from wx.lib.buttons import GenBitmapButton + +class BaseImplantEditorView (wx.Panel): + def addMarketViewImage(self, iconFile): + if iconFile is None: + return -1 + bitmap = BitmapLoader.getBitmap(iconFile, "icons") + if bitmap is None: + return -1 + else: + return self.availableImplantsImageList.Add(bitmap) + + def __init__(self, parent): + wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.TAB_TRAVERSAL) + self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) + + pmainSizer = wx.BoxSizer(wx.HORIZONTAL) + + availableSizer = wx.BoxSizer(wx.VERTICAL) + + self.searchBox = SearchBox(self) + self.itemView = ItemView(self) + + self.itemView.Hide() + + availableSizer.Add(self.searchBox, 0, wx.EXPAND) + availableSizer.Add(self.itemView, 1, wx.EXPAND) + + ''' + self.availableImplantsSearch = wx.SearchCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + self.availableImplantsSearch.ShowCancelButton(True) + + availableSizer.Add(self.availableImplantsSearch, 0, wx.BOTTOM | wx.EXPAND, 2) + ''' + + self.availableImplantsTree = wx.TreeCtrl(self, wx.ID_ANY, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT) + root = self.availableRoot = self.availableImplantsTree.AddRoot("Available") + self.availableImplantsImageList = wx.ImageList(16, 16) + self.availableImplantsTree.SetImageList(self.availableImplantsImageList) + + availableSizer.Add(self.availableImplantsTree, 1, wx.EXPAND) + + + pmainSizer.Add(availableSizer, 1, wx.ALL | wx.EXPAND, 5) + + + buttonSizer = wx.BoxSizer(wx.VERTICAL) + buttonSizer.AddSpacer(( 0, 0), 1) + + self.btnAdd = GenBitmapButton(self, wx.ID_ADD, BitmapLoader.getBitmap("fit_add_small", "gui"), style = wx.BORDER_NONE) + buttonSizer.Add(self.btnAdd, 0) + + self.btnRemove = GenBitmapButton(self, wx.ID_REMOVE, BitmapLoader.getBitmap("fit_delete_small", "gui"), style = wx.BORDER_NONE) + buttonSizer.Add(self.btnRemove, 0) + + buttonSizer.AddSpacer(( 0, 0), 1) + pmainSizer.Add(buttonSizer, 0, wx.EXPAND, 0) + + characterImplantSizer = wx.BoxSizer(wx.VERTICAL) + self.pluggedImplantsTree = AvailableImplantsView(self) + characterImplantSizer.Add(self.pluggedImplantsTree, 1, wx.ALL|wx.EXPAND, 5) + pmainSizer.Add(characterImplantSizer, 1, wx.EXPAND, 5) + + self.SetSizer(pmainSizer) + + # Populate the market tree + + sMkt = service.Market.getInstance() + for mktGrp in sMkt.getImplantTree(): + iconId = self.addMarketViewImage(sMkt.getIconByMarketGroup(mktGrp)) + childId = self.availableImplantsTree.AppendItem(root, mktGrp.name, iconId, data=wx.TreeItemData(mktGrp.ID)) + if sMkt.marketGroupHasTypesCheck(mktGrp) is False: + self.availableImplantsTree.AppendItem(childId, "dummy") + + self.availableImplantsTree.SortChildren(self.availableRoot) + + #Bind the event to replace dummies by real data + self.availableImplantsTree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) + self.availableImplantsTree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.itemSelected) + + self.itemView.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.itemSelected) + + #Bind add & remove buttons + self.btnAdd.Bind(wx.EVT_BUTTON, self.itemSelected) + self.btnRemove.Bind(wx.EVT_BUTTON, self.removeItem) + + # We update with an empty list first to set the initial size for Layout(), then update later with actual + # implants for character. This helps with sizing issues. + self.pluggedImplantsTree.update([]) + self.bindContext() + self.Layout() + + self.update() + + def bindContext(self): + # Binds self.contextChanged to whatever changes the context + raise NotImplementedError() + + def getImplantsFromContext(self): + """ Gets list of implants from current context """ + raise NotImplementedError() + + def addImplantToContext(self, item): + """ Adds implant to the current context""" + raise NotImplementedError() + + def removeImplantFromContext(self, implant): + """ Removes implant from the current context""" + raise NotImplementedError() + + def update(self): + """Updates implant list based off the current context""" + self.implants = self.getImplantsFromContext() + self.implants.sort(key=lambda i: int(i.getModifiedItemAttr("implantness"))) + self.pluggedImplantsTree.update(self.implants) + + def contextChanged(self, event): + self.update() + event.Skip() + + def expandLookup(self, event): + tree = self.availableImplantsTree + sMkt = service.Market.getInstance() + parent = event.Item + child, _ = tree.GetFirstChild(parent) + text = tree.GetItemText(child) + + if text == "dummy" or text == "itemdummy": + tree.Delete(child) + + # if the dummy item is a market group, replace with actual market groups + if text == "dummy": + #Add 'real stoof!' instead + currentMktGrp = sMkt.getMarketGroup(tree.GetPyData(parent), eager="children") + for childMktGrp in sMkt.getMarketGroupChildren(currentMktGrp): + iconId = self.addMarketViewImage(sMkt.getIconByMarketGroup(childMktGrp)) + childId = tree.AppendItem(parent, childMktGrp.name, iconId, data=wx.TreeItemData(childMktGrp.ID)) + if sMkt.marketGroupHasTypesCheck(childMktGrp) is False: + tree.AppendItem(childId, "dummy") + else: + tree.AppendItem(childId, "itemdummy") + + # replace dummy with actual items + if text == "itemdummy": + currentMktGrp = sMkt.getMarketGroup(tree.GetPyData(parent)) + items = sMkt.getItemsByMarketGroup(currentMktGrp) + for item in items: + iconId = self.addMarketViewImage(item.icon.iconFile) + tree.AppendItem(parent, item.name, iconId, data=wx.TreeItemData(item)) + + tree.SortChildren(parent) + + def itemSelected(self, event): + if event.EventObject is self.btnAdd: + # janky fix that sets EventObject so that we don't have similar code elsewhere. + if self.itemView.IsShown(): + event.EventObject = self.itemView + else: + event.EventObject = self.availableImplantsTree + + if event.EventObject is self.itemView: + curr = event.EventObject.GetFirstSelected() + + while curr != -1: + item = self.itemView.items[curr] + self.addImplantToContext(item) + + curr = event.EventObject.GetNextSelected(curr) + else: + root = self.availableImplantsTree.GetSelection() + + if not root.IsOk(): + return + + nchilds = self.availableImplantsTree.GetChildrenCount(root) + if nchilds == 0: + item = self.availableImplantsTree.GetPyData(root) + self.addImplantToContext(item) + else: + event.Skip() + return + + self.update() + + def removeItem(self, event): + pos = self.pluggedImplantsTree.GetFirstSelected() + if pos != -1: + self.removeImplantFromContext(self.implants[pos]) + self.update() + +class AvailableImplantsView(d.Display): + DEFAULT_COLS = ["attr:implantness", + "Base Icon", + "Base Name"] + + def __init__(self, parent): + d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL) + self.Bind(wx.EVT_LEFT_DCLICK, parent.removeItem) + +class ItemView(d.Display): + DEFAULT_COLS = ["Base Icon", + "Base Name", + "attr:power,,,True", + "attr:cpu,,,True"] + + def __init__(self, parent): + d.Display.__init__(self, parent) + self.parent = parent + self.searchBox = parent.searchBox + + self.items = [] + + # Bind search actions + self.searchBox.Bind(SBox.EVT_TEXT_ENTER, self.scheduleSearch) + self.searchBox.Bind(SBox.EVT_SEARCH_BTN, self.scheduleSearch) + self.searchBox.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch) + self.searchBox.Bind(SBox.EVT_TEXT, self.scheduleSearch) + + def clearSearch(self, event=None): + if self.IsShown(): + self.parent.availableImplantsTree.Show() + self.Hide() + self.parent.Layout() + + if event: + self.searchBox.Clear() + + self.items = [] + self.update(self.items) + + def scheduleSearch(self, event=None): + sMkt = service.Market.getInstance() + + search = self.searchBox.GetLineText(0) + # Make sure we do not count wildcard as search symbol + realsearch = search.replace("*", "") + # Show nothing if query is too short + if len(realsearch) < 3: + self.clearSearch() + return + + sMkt.searchItems(search, self.populateSearch, ["Implant"]) + + def populateSearch(self, items): + if not self.IsShown(): + self.parent.availableImplantsTree.Hide() + self.Show() + self.parent.Layout() + + self.items = sorted(list(items), key=lambda i: i.name) + + self.update(self.items) \ No newline at end of file diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 7d45dae3d2..72fc0a333f 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -19,94 +19,110 @@ import wx -import gui.mainFrame import wx.lib.newevent import wx.gizmos from gui.bitmapLoader import BitmapLoader import service -import gui.display as d from gui.contextMenu import ContextMenu -from wx.lib.buttons import GenBitmapButton import gui.globalEvents as GE +from gui.builtinViews.implantEditor import BaseImplantEditorView +from gui.builtinViews.entityEditor import EntityEditor, BaseValidator -class CharacterEditor(wx.Frame): - def __init__(self, parent): - wx.Frame.__init__ (self, parent, id=wx.ID_ANY, title=u"pyfa: Character Editor", pos=wx.DefaultPosition, - size=wx.Size(640, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) - i = wx.IconFromBitmap(BitmapLoader.getBitmap("character_small", "gui")) - self.SetIcon(i) +class CharacterTextValidor(BaseValidator): + def __init__(self): + BaseValidator.__init__(self) - self.mainFrame = parent - - #self.disableWin = wx.WindowDisabler(self) - self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) ) + def Clone(self): + return CharacterTextValidor() - mainSizer = wx.BoxSizer(wx.VERTICAL) - self.navSizer = wx.BoxSizer(wx.HORIZONTAL) + def Validate(self, win): + profileEditor = win.Parent + textCtrl = self.GetWindow() + text = textCtrl.GetValue().strip() + + try: + if len(text) == 0: + raise ValueError("You must supply a name for the Character!") + elif text in [x.name for x in profileEditor.entityEditor.choices]: + raise ValueError("Character name already in use, please choose another.") + + return True + except ValueError, e: + wx.MessageBox(u"{}".format(e), "Error") + textCtrl.SetFocus() + return False + + +class CharacterEntityEditor(EntityEditor): + def __init__(self, parent): + EntityEditor.__init__(self, parent, "Character") + self.SetEditorValidator(CharacterTextValidor) + def getEntitiesFromContext(self): sChar = service.Character.getInstance() + charList = sorted(sChar.getCharacterList(), key=lambda c: c.name) - self.btnSave = wx.Button(self, wx.ID_SAVE) - self.btnSave.Hide() - self.btnSave.Bind(wx.EVT_BUTTON, self.processRename) + # Do some processing to ensure that we have All 0 and All 5 at the top + all5 = sChar.all5() + all0 = sChar.all0() - self.characterRename = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) - self.characterRename.Hide() - self.characterRename.Bind(wx.EVT_TEXT_ENTER, self.processRename) + charList.remove(all5) + charList.remove(all0) - self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) - self.navSizer.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 5) + charList.insert(0, all5) + charList.insert(0, all0) - charList = sChar.getCharacterList() + return charList - for id, name, active in charList: - i = self.charChoice.Append(name, id) - if active: - self.charChoice.SetSelection(i) + def DoNew(self, name): + sChar = service.Character.getInstance() + return sChar.new(name) - self.navSizer.Add(self.btnSave, 0, wx.ALL , 5) + def DoRename(self, entity, name): + sChar = service.Character.getInstance() + sChar.rename(entity, name) + def DoCopy(self, entity, name): + sChar = service.Character.getInstance() + copy = sChar.copy(entity) + sChar.rename(copy, name) + return copy - buttons = (("new", wx.ART_NEW), - ("rename", BitmapLoader.getBitmap("rename", "gui")), - ("copy", wx.ART_COPY), - ("delete", wx.ART_DELETE)) + def DoDelete(self, entity): + sChar = service.Character.getInstance() + sChar.delete(entity) - size = None - for name, art in buttons: - bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON) if name != "rename" else art - btn = wx.BitmapButton(self, wx.ID_ANY, bitmap) - if size is None: - size = btn.GetSize() - btn.SetMinSize(size) - btn.SetMaxSize(size) +class CharacterEditor(wx.Frame): + def __init__(self, parent): + wx.Frame.__init__ (self, parent, id=wx.ID_ANY, title=u"pyfa: Character Editor", pos=wx.DefaultPosition, + size=wx.Size(640, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) - btn.SetToolTipString("%s character" % name.capitalize()) - btn.Bind(wx.EVT_BUTTON, getattr(self, name)) - setattr(self, "btn%s" % name.capitalize(), btn) - self.navSizer.Add(btn, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + i = wx.IconFromBitmap(BitmapLoader.getBitmap("character_small", "gui")) + self.SetIcon(i) + self.mainFrame = parent + #self.disableWin = wx.WindowDisabler(self) + sFit = service.Fit.getInstance() - mainSizer.Add(self.navSizer, 0, wx.ALL | wx.EXPAND, 5) + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.entityEditor = CharacterEntityEditor(self) + mainSizer.Add(self.entityEditor, 0, wx.ALL | wx.EXPAND, 2) + # Default drop down to current fit's character + self.entityEditor.setActiveEntity(sFit.character) self.viewsNBContainer = wx.Notebook(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) self.sview = SkillTreeView(self.viewsNBContainer) - #self.iview = ImplantsTreeView(self.viewsNBContainer) - #======================================================================= - # RC2 - #self.iview.Show(False) - #======================================================================= + self.iview = ImplantEditorView(self.viewsNBContainer) self.aview = APIView(self.viewsNBContainer) self.viewsNBContainer.AddPage(self.sview, "Skills") - - #======================================================================= - # Disabled for RC2 - # self.viewsNBContainer.AddPage(self.iview, "Implants") - #======================================================================= + self.viewsNBContainer.AddPage(self.iview, "Implants") self.viewsNBContainer.AddPage(self.aview, "API") mainSizer.Add(self.viewsNBContainer, 1, wx.EXPAND | wx.ALL, 5) @@ -124,7 +140,6 @@ def __init__(self, parent): bSizerButtons.AddStretchSpacer() bSizerButtons.Add(self.btnOK, 0, wx.ALL, 5) - self.btnSaveChar.Bind(wx.EVT_BUTTON, self.saveChar) self.btnSaveAs.Bind(wx.EVT_BUTTON, self.saveCharAs) self.btnRevert.Bind(wx.EVT_BUTTON, self.revertChar) @@ -139,16 +154,12 @@ def __init__(self, parent): self.Centre(wx.BOTH) - charID = self.getActiveCharacter() - if sChar.getCharName(charID) in ("All 0", "All 5"): - self.restrict() - - self.registerEvents() + self.Bind(wx.EVT_CLOSE, self.closeEvent) + self.Bind(GE.CHAR_LIST_UPDATED, self.refreshCharacterList) + self.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) def btnRestrict(self): - sChar = service.Character.getInstance() - charID = self.getActiveCharacter() - char = sChar.getCharacter(charID) + char = self.entityEditor.getActiveEntity() # enable/disable character saving stuff self.btnSaveChar.Enable(not char.ro and char.isDirty) @@ -156,46 +167,34 @@ def btnRestrict(self): self.btnRevert.Enable(char.isDirty) def refreshCharacterList(self, event=None): - sChar = service.Character.getInstance() - charList = sChar.getCharacterList() - active = self.getActiveCharacter() - self.charChoice.Clear() - - for id, name, _ in charList: - i = self.charChoice.Append(name, id) - if active == id: - self.charChoice.SetSelection(i) - + """This is only called when we save a modified character""" + active = self.entityEditor.getActiveEntity() + self.entityEditor.refreshEntityList(active) self.btnRestrict() + if event: + event.Skip() + def editingFinished(self, event): #del self.disableWin wx.PostEvent(self.mainFrame, GE.CharListUpdated()) self.Destroy() - def registerEvents(self): - self.Bind(wx.EVT_CLOSE, self.closeEvent) - self.Bind(GE.CHAR_LIST_UPDATED, self.refreshCharacterList) - self.charChoice.Bind(wx.EVT_CHOICE, self.charChanged) - def saveChar(self, event): sChr = service.Character.getInstance() - charID = self.getActiveCharacter() - sChr.saveCharacter(charID) - self.sview.populateSkillTree() + char = self.entityEditor.getActiveEntity() + sChr.saveCharacter(char.ID) wx.PostEvent(self, GE.CharListUpdated()) def saveCharAs(self, event): - charID = self.getActiveCharacter() - dlg = SaveCharacterAs(self, charID) + char = self.entityEditor.getActiveEntity() + dlg = SaveCharacterAs(self, char.ID) dlg.ShowModal() - self.sview.populateSkillTree() def revertChar(self, event): sChr = service.Character.getInstance() - charID = self.getActiveCharacter() - sChr.revertCharacter(charID) - self.sview.populateSkillTree() + char = self.entityEditor.getActiveEntity() + sChr.revertCharacter(char.ID) wx.PostEvent(self, GE.CharListUpdated()) def closeEvent(self, event): @@ -204,119 +203,24 @@ def closeEvent(self, event): self.Destroy() def restrict(self): - self.btnRename.Enable(False) - self.btnDelete.Enable(False) - self.aview.stDisabledTip.Show() - self.aview.inputID.Enable(False) - self.aview.inputKey.Enable(False) - self.aview.charChoice.Enable(False) - self.aview.btnFetchCharList.Enable(False) - self.aview.btnFetchSkills.Enable(False) - self.aview.stStatus.SetLabel("") - self.aview.Layout() + self.entityEditor.btnRename.Enable(False) + self.entityEditor.btnDelete.Enable(False) def unrestrict(self): - self.btnRename.Enable(True) - self.btnDelete.Enable(True) - self.aview.stDisabledTip.Hide() - self.aview.inputID.Enable(True) - self.aview.inputKey.Enable(True) - self.aview.btnFetchCharList.Enable(True) - self.aview.btnFetchSkills.Enable(True) - self.aview.stStatus.SetLabel("") - self.aview.Layout() + self.entityEditor.btnRename.Enable() + self.entityEditor.btnDelete.Enable() def charChanged(self, event): - self.sview.populateSkillTree() - sChar = service.Character.getInstance() - charID = self.getActiveCharacter() - if sChar.getCharName(charID) in ("All 0", "All 5"): + char = self.entityEditor.getActiveEntity() + if char.name in ("All 0", "All 5"): self.restrict() else: self.unrestrict() - wx.PostEvent(self, GE.CharChanged()) - if event is not None: - event.Skip() - - def getActiveCharacter(self): - selection = self.charChoice.GetCurrentSelection() - return self.charChoice.GetClientData(selection) if selection is not None else None - - def new(self, event): - sChar = service.Character.getInstance() - charID = sChar.new() - id = self.charChoice.Append(sChar.getCharName(charID), charID) - self.charChoice.SetSelection(id) - self.unrestrict() - self.btnSave.SetLabel("Create") - self.rename(None) - self.charChanged(None) + self.btnRestrict() - def rename(self, event): if event is not None: - self.btnSave.SetLabel("Rename") - self.charChoice.Hide() - self.characterRename.Show() - self.navSizer.Replace(self.charChoice, self.characterRename) - self.characterRename.SetFocus() - for btn in (self.btnNew, self.btnCopy, self.btnRename, self.btnDelete): - btn.Hide() - - self.btnSave.Show() - self.navSizer.Layout() - - sChar = service.Character.getInstance() - currName = sChar.getCharName(self.getActiveCharacter()) - self.characterRename.SetValue(currName) - self.characterRename.SetSelection(0, len(currName)) - - def processRename(self, event): - sChar = service.Character.getInstance() - newName = self.characterRename.GetLineText(0) - - if newName == "All 0" or newName == "All 5": - newName = newName + " bases are belong to us" - - charID = self.getActiveCharacter() - sChar.rename(charID, newName) - - self.charChoice.Show() - self.characterRename.Hide() - self.navSizer.Replace(self.characterRename, self.charChoice) - for btn in (self.btnNew, self.btnCopy, self.btnRename, self.btnDelete): - btn.Show() - - self.btnSave.Hide() - self.navSizer.Layout() - self.refreshCharacterList() - - def copy(self, event): - sChar = service.Character.getInstance() - charID = sChar.copy(self.getActiveCharacter()) - id = self.charChoice.Append(sChar.getCharName(charID), charID) - self.charChoice.SetSelection(id) - self.unrestrict() - self.btnSave.SetLabel("Copy") - self.rename(None) - wx.PostEvent(self, GE.CharChanged()) - - def delete(self, event): - dlg = wx.MessageDialog(self, - "Do you really want to delete this character?", - "Confirm Delete", wx.YES | wx.NO | wx.ICON_QUESTION) - - if dlg.ShowModal() == wx.ID_YES: - sChar = service.Character.getInstance() - sChar.delete(self.getActiveCharacter()) - sel = self.charChoice.GetSelection() - self.charChoice.Delete(sel) - self.charChoice.SetSelection(sel - 1) - newSelection = self.getActiveCharacter() - if sChar.getCharName(newSelection) in ("All 0", "All 5"): - self.restrict() - - wx.PostEvent(self, GE.CharChanged()) + event.Skip() def Destroy(self): sFit = service.Fit.getInstance() @@ -329,7 +233,8 @@ def Destroy(self): class SkillTreeView (wx.Panel): def __init__(self, parent): - wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL) + wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.TAB_TRAVERSAL) + self.charEditor = self.Parent.Parent # first parent is Notebook, second is Character Editor self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) pmainSizer = wx.BoxSizer(wx.VERTICAL) @@ -356,6 +261,10 @@ def __init__(self, parent): tree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.scheduleMenu) + # bind the Character selection event + self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.populateSkillTree) + self.charEditor.Bind(GE.CHAR_LIST_UPDATED, self.populateSkillTree) + srcContext = "skillItem" itemContext = "Skill" context = (srcContext, itemContext) @@ -386,11 +295,10 @@ def __init__(self, parent): self.Layout() - def populateSkillTree(self): + def populateSkillTree(self, event=None): sChar = service.Character.getInstance() - charID = self.Parent.Parent.getActiveCharacter() - dirtySkills = sChar.getDirtySkills(charID) - dirtyGroups = set([skill.item.group.ID for skill in dirtySkills]) + char = self.charEditor.entityEditor.getActiveEntity() + dirtyGroups = set([skill.item.group.ID for skill in char.dirtySkills]) groups = sChar.getSkillGroups() imageId = self.skillBookImageId @@ -407,6 +315,9 @@ def populateSkillTree(self): tree.SortChildren(root) + if event: + event.Skip() + def expandLookup(self, event): root = event.Item tree = self.skillTreeListCtrl @@ -416,11 +327,11 @@ def expandLookup(self, event): #Get the real intrestin' stuff sChar = service.Character.getInstance() - char = self.Parent.Parent.getActiveCharacter() + char = self.charEditor.entityEditor.getActiveEntity() for id, name in sChar.getSkills(tree.GetPyData(root)): iconId = self.skillBookImageId childId = tree.AppendItem(root, name, iconId, data=wx.TreeItemData(id)) - level, dirty = sChar.getSkillLevel(char, id) + level, dirty = sChar.getSkillLevel(char.ID, id) tree.SetItemText(childId, "Level %d" % level if isinstance(level, int) else level, 1) if dirty: tree.SetItemTextColour(childId, wx.BLUE) @@ -436,10 +347,9 @@ def spawnMenu(self, item): if self.skillTreeListCtrl.GetChildrenCount(item) > 0: return - sChar = service.Character.getInstance() - charID = self.Parent.Parent.getActiveCharacter() + char = self.charEditor.entityEditor.getActiveEntity() sMkt = service.Market.getInstance() - if sChar.getCharName(charID) not in ("All 0", "All 5"): + if char.name not in ("All 0", "All 5"): self.levelChangeMenu.selection = sMkt.getItem(self.skillTreeListCtrl.GetPyData(item)) self.PopupMenu(self.levelChangeMenu) else: @@ -450,21 +360,21 @@ def changeLevel(self, event): level = self.levelIds.get(event.Id) sChar = service.Character.getInstance() - charID = self.Parent.Parent.getActiveCharacter() + char = self.charEditor.entityEditor.getActiveEntity() selection = self.skillTreeListCtrl.GetSelection() skillID = self.skillTreeListCtrl.GetPyData(selection) if level is not None: self.skillTreeListCtrl.SetItemText(selection, "Level %d" % level if isinstance(level, int) else level, 1) - sChar.changeLevel(charID, skillID, level, persist=True) + sChar.changeLevel(char.ID, skillID, level, persist=True) elif event.Id == self.revertID: - sChar.revertLevel(charID, skillID) + sChar.revertLevel(char.ID, skillID) elif event.Id == self.saveID: - sChar.saveSkill(charID, skillID) + sChar.saveSkill(char.ID, skillID) self.skillTreeListCtrl.SetItemTextColour(selection, None) - dirtySkills = sChar.getDirtySkills(charID) + dirtySkills = sChar.getDirtySkills(char.ID) dirtyGroups = set([skill.item.group.ID for skill in dirtySkills]) parentID = self.skillTreeListCtrl.GetItemParent(selection) @@ -473,156 +383,69 @@ def changeLevel(self, event): if groupID not in dirtyGroups: self.skillTreeListCtrl.SetItemTextColour(parentID, None) - wx.PostEvent(self.Parent.Parent, GE.CharListUpdated()) event.Skip() -class ImplantsTreeView (wx.Panel): - def addMarketViewImage(self, iconFile): - if iconFile is None: - return -1 - bitmap = BitmapLoader.getBitmap(iconFile, "icons") - if bitmap is None: - return -1 - else: - return self.availableImplantsImageList.Add(bitmap) +class ImplantEditorView(BaseImplantEditorView): def __init__(self, parent): - wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL) - - pmainSizer = wx.BoxSizer(wx.HORIZONTAL) - - availableSizer = wx.BoxSizer(wx.VERTICAL) - pmainSizer.Add(availableSizer, 1, wx.ALL | wx.EXPAND, 5) - - self.availableImplantsSearch = wx.SearchCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) - self.availableImplantsSearch.ShowCancelButton(True) - availableSizer.Add(self.availableImplantsSearch, 0, wx.BOTTOM | wx.EXPAND, 2) + BaseImplantEditorView.__init__ (self, parent) - self.availableImplantsTree = wx.TreeCtrl(self, wx.ID_ANY, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT) - root = self.availableRoot = self.availableImplantsTree.AddRoot("Available") - self.availableImplantsImageList = wx.ImageList(16, 16) - self.availableImplantsTree.SetImageList(self.availableImplantsImageList) + self.determineEnabled() - availableSizer.Add(self.availableImplantsTree, 1, wx.EXPAND) + if "__WXGTK__" in wx.PlatformInfo: + self.pluggedImplantsTree.Bind(wx.EVT_RIGHT_UP, self.scheduleMenu) + else: + self.pluggedImplantsTree.Bind(wx.EVT_RIGHT_DOWN, self.scheduleMenu) - buttonSizer = wx.BoxSizer(wx.VERTICAL) - pmainSizer.Add(buttonSizer, 0, wx.TOP, 5) + def bindContext(self): + self.Parent.Parent.entityEditor.Bind(wx.EVT_CHOICE, self.contextChanged) - self.btnAdd = GenBitmapButton(self, wx.ID_ADD, BitmapLoader.getBitmap("fit_add_small", "gui"), style = wx.BORDER_NONE) - buttonSizer.Add(self.btnAdd, 0) - self.btnRemove = GenBitmapButton(self, wx.ID_REMOVE, BitmapLoader.getBitmap("fit_delete_small", "gui"), style = wx.BORDER_NONE) - buttonSizer.Add(self.btnRemove, 0) + def contextChanged(self, event): + BaseImplantEditorView.contextChanged(self, event) + self.determineEnabled() - self.pluggedImplantsTree = AvailableImplantsView(self, style=wx.LC_SINGLE_SEL) + def getImplantsFromContext(self): + sChar = service.Character.getInstance() + char = self.Parent.Parent.entityEditor.getActiveEntity() - pmainSizer.Add(self.pluggedImplantsTree, 1, wx.ALL | wx.EXPAND, 5) + return sChar.getImplants(char.ID) - self.SetSizer(pmainSizer) + def addImplantToContext(self, item): + sChar = service.Character.getInstance() + char = self.Parent.Parent.entityEditor.getActiveEntity() - # Populate the market tree - sMkt = service.Market.getInstance() - for mktGrp in sMkt.getImplantTree(): - iconId = self.addMarketViewImage(sMkt.getIconByMarketGroup(mktGrp)) - childId = self.availableImplantsTree.AppendItem(root, mktGrp.name, iconId, data=wx.TreeItemData(mktGrp.ID)) - if sMkt.marketGroupHasTypesCheck(mktGrp) is False: - self.availableImplantsTree.AppendItem(childId, "dummy") + sChar.addImplant(char.ID, item.ID) - self.availableImplantsTree.SortChildren(self.availableRoot) + def removeImplantFromContext(self, implant): + sChar = service.Character.getInstance() + char = self.Parent.Parent.entityEditor.getActiveEntity() - #Bind the event to replace dummies by real data - self.availableImplantsTree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) + sChar.removeImplant(char.ID, implant) - #Bind add & remove buttons - self.btnAdd.Bind(wx.EVT_BUTTON, self.addImplant) - self.btnRemove.Bind(wx.EVT_BUTTON, self.removeImplant) + def scheduleMenu(self, event): + event.Skip() + wx.CallAfter(self.spawnMenu) - #Bind the change of a character* - self.Parent.Parent.Bind(GE.CHAR_CHANGED, self.charChanged) - self.Enable(False) - self.Layout() + def spawnMenu(self): + context = (("implantEditor",),) + # fuck good coding practices, passing a pointer to the character editor here for [reasons] =D + # (see implantSets context class for info) + menu = ContextMenu.getMenu((self.Parent.Parent,), *context) + self.PopupMenu(menu) - def update(self, implants): - self.implants = implants[:] - self.implants.sort(key=lambda i: int(i.getModifiedItemAttr("implantness"))) - self.pluggedImplantsTree.update(self.implants) + def determineEnabled(self): + char = self.Parent.Parent.entityEditor.getActiveEntity() - def charChanged(self, event): - sChar = service.Character.getInstance() - charID = self.Parent.Parent.getActiveCharacter() - name = sChar.getCharName(charID) - if name == "All 0" or name == "All 5": + if char.name in ("All 0", "All 5"): self.Enable(False) else: - self.Enable(True) - - self.update(sChar.getImplants(charID)) - event.Skip() + self.Enable() - def expandLookup(self, event): - tree = self.availableImplantsTree - root = event.Item - child, cookie = tree.GetFirstChild(root) - text = tree.GetItemText(child) - if text == "dummy" or text == "itemdummy": - sMkt = service.Market.getInstance() - #A DUMMY! Keeeel!!! EBUL DUMMY MUST DIAF! - tree.Delete(child) - - if text == "dummy": - #Add 'real stoof!' instead - for id, name, iconFile, more in sMkt.getChildren(tree.GetPyData(root)): - iconId = self.addMarketViewImage(iconFile) - childId = tree.AppendItem(root, name, iconId, data=wx.TreeItemData(id)) - if more: - tree.AppendItem(childId, "dummy") - else: - tree.AppendItem(childId, "itemdummy") - - if text == "itemdummy": - sMkt = service.Market.getInstance() - data, usedMetas = sMkt.getVariations(tree.GetPyData(root)) - for item in data: - id = item.ID - name = item.name - iconFile = item.icon.iconFile - iconId = self.addMarketViewImage(iconFile) - tree.AppendItem(root, name, iconId, data=wx.TreeItemData(id)) - - tree.SortChildren(root) - - def addImplant(self, event): - root = self.availableImplantsTree.GetSelection() - - if not root.IsOk(): - return - - nchilds = self.availableImplantsTree.GetChildrenCount(root) - sChar = service.Character.getInstance() - charID = self.Parent.Parent.getActiveCharacter() - if nchilds == 0: - itemID = self.availableImplantsTree.GetPyData(root) - sChar.addImplant(charID, itemID) - self.update(sChar.getImplants(charID)) - - def removeImplant(self, event): - pos = self.pluggedImplantsTree.GetFirstSelected() - if pos != -1: - sChar = service.Character.getInstance() - charID = self.Parent.Parent.getActiveCharacter() - sChar.removeImplant(charID, self.implants[pos].slot) - self.update(sChar.getImplants(charID)) - -class AvailableImplantsView(d.Display): - DEFAULT_COLS = ["Base Name", - "attr:implantness"] - - def __init__(self, parent, style): - d.Display.__init__(self, parent, style=style) class APIView (wx.Panel): def __init__(self, parent): wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL) - self.Parent.Parent.Bind(GE.CHAR_CHANGED, self.charChanged) + self.charEditor = self.Parent.Parent # first parent is Notebook, second is Character Editor self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW)) self.apiUrlCreatePredefined = u"https://community.eveonline.com/support/api-key/CreatePredefined?accessMask=8" @@ -706,13 +529,17 @@ def __init__(self, parent): self.hlEveAPI2 = wx.HyperlinkCtrl( self, wx.ID_ANY, self.apiUrlKeyList, self.apiUrlKeyList, wx.DefaultPosition, wx.DefaultSize, wx.HL_DEFAULT_STYLE ) pmainSizer.Add( self.hlEveAPI2, 0, wx.ALL, 2 ) + self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) + self.SetSizer(pmainSizer) self.Layout() self.charChanged(None) def charChanged(self, event): sChar = service.Character.getInstance() - ID, key, char, chars = sChar.getApiDetails(self.Parent.Parent.getActiveCharacter()) + activeChar = self.charEditor.entityEditor.getActiveEntity() + + ID, key, char, chars = sChar.getApiDetails(activeChar.ID) self.inputID.SetValue(str(ID)) self.inputKey.SetValue(key) @@ -730,6 +557,14 @@ def charChanged(self, event): self.charChoice.Enable(False) self.btnFetchSkills.Enable(False) + if activeChar.name in ("All 0", "All 5"): + self.Enable(False) + self.stDisabledTip.Show() + self.Layout() + else: + self.Enable() + self.stDisabledTip.Hide() + self.Layout() if event is not None: event.Skip() diff --git a/gui/characterSelection.py b/gui/characterSelection.py index 990dc8bf56..47df7e0183 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -83,9 +83,9 @@ def refreshCharacterList(self, event=None): charList = sChar.getCharacterList() picked = False - for id, name, active in charList: - currId = choice.Append(name, id) - if id == activeChar: + for char in charList: + currId = choice.Append(char.name, char.ID) + if char.ID == activeChar: choice.SetSelection(currId) self.charChanged(None) picked = True diff --git a/gui/gangView.py b/gui/gangView.py index 2e6d7b1dae..25a457301f 100644 --- a/gui/gangView.py +++ b/gui/gangView.py @@ -346,9 +346,8 @@ def RefreshCharacterList(self, event = None): choice.Clear() currSelFound = False for char in charList: - id,name,_ = char - choice.Append(name, id) - if chCurrData == id: + choice.Append(char.name, char.ID) + if chCurrData == char.ID: currSelFound = True if chCurrSelection == -1: diff --git a/gui/implantView.py b/gui/implantView.py index d878381137..4b47552527 100644 --- a/gui/implantView.py +++ b/gui/implantView.py @@ -21,12 +21,62 @@ import service import gui.display as d import gui.marketBrowser as mb +import gui.mainFrame from gui.builtinViewColumns.state import State from gui.contextMenu import ContextMenu import globalEvents as GE -class ImplantView(d.Display): +from eos.types import ImplantLocation + + +class ImplantView(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, style=wx.TAB_TRAVERSAL ) + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.implantDisplay = ImplantDisplay(self) + mainSizer.Add(self.implantDisplay, 1, wx.EXPAND, 0 ) + + radioSizer = wx.BoxSizer(wx.HORIZONTAL) + radioSizer.AddSpacer(( 0, 0), 1, wx.EXPAND, 5) + self.rbFit = wx.RadioButton(self, id=wx.ID_ANY, label="Use Fit-specific Implants", style=wx.RB_GROUP) + self.rbChar = wx.RadioButton(self, id=wx.ID_ANY, label="Use Character Implants") + radioSizer.Add(self.rbFit, 0, wx.ALL, 5) + radioSizer.Add(self.rbChar, 0, wx.ALL, 5) + radioSizer.AddSpacer((0, 0), 1, wx.EXPAND, 5) + + mainSizer.Add(radioSizer, 0, wx.EXPAND, 5) + + self.SetSizer( mainSizer ) + self.SetAutoLayout(True) + + self.Bind(wx.EVT_RADIOBUTTON, self.OnRadioSelect, self.rbFit) + self.Bind(wx.EVT_RADIOBUTTON, self.OnRadioSelect, self.rbChar) + self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) + + def fitChanged(self, event): + sFit = service.Fit.getInstance() + activeFitID = self.mainFrame.getActiveFit() + fit = sFit.getFit(activeFitID) + if fit: + if fit.implantSource == ImplantLocation.FIT: + self.rbFit.SetValue(True) + else: + self.rbChar.SetValue(True) + + def OnRadioSelect(self, event): + fitID = self.mainFrame.getActiveFit() + sFit = service.Fit.getInstance() + sFit.toggleImplantSource(fitID, ImplantLocation.FIT if self.rbFit.GetValue() else ImplantLocation.CHARACTER) + + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + + +class ImplantDisplay(d.Display): DEFAULT_COLS = ["State", "attr:implantness", + "Base Icon", "Base Name"] def __init__(self, parent): @@ -65,7 +115,7 @@ def fitChanged(self, event): fit = sFit.getFit(event.fitID) self.original = fit.implants if fit is not None else None - self.implants = stuff = fit.implants if fit is not None else None + self.implants = stuff = fit.appliedImplants if fit is not None else None if stuff is not None: stuff.sort(key=lambda implant: implant.slot) if event.fitID != self.lastFitId: @@ -78,8 +128,7 @@ def fitChanged(self, event): self.deselectItems() - self.populate(stuff) - self.refresh(stuff) + self.update(stuff) event.Skip() def addItem(self, event): @@ -123,14 +172,27 @@ def scheduleMenu(self, event): def spawnMenu(self): sel = self.GetFirstSelected() + menu = None + + sFit = service.Fit.getInstance() + fit = sFit.getFit(self.mainFrame.getActiveFit()) + + if not fit: + return + if sel != -1: - sFit = service.Fit.getInstance() - fit = sFit.getFit(self.mainFrame.getActiveFit()) - implant = fit.implants[sel] + implant = fit.appliedImplants[sel] sMkt = service.Market.getInstance() - sourceContext = "implantItem" + sourceContext = "implantItem" if fit.implantSource == ImplantLocation.FIT else "implantItemChar" itemContext = sMkt.getCategoryByItem(implant.item).name menu = ContextMenu.getMenu((implant,), (sourceContext, itemContext)) + elif sel == -1 and fit.implantSource == ImplantLocation.FIT: + fitID = self.mainFrame.getActiveFit() + if fitID is None: + return + context = (("implantView",),) + menu = ContextMenu.getMenu([], *context) + if menu is not None: self.PopupMenu(menu) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 4c0d39ba73..7557813db7 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -50,6 +50,7 @@ from gui.characterSelection import CharacterSelection from gui.patternEditor import DmgPatternEditorDlg from gui.resistsEditor import ResistsEditorDlg +from gui.setEditor import ImplantSetEditorDlg from gui.preferenceDialog import PreferenceDialog from gui.graphFrame import GraphFrame from gui.copySelectDialog import CopySelectDialog @@ -358,15 +359,16 @@ def showAttrEditor(self, event): dlg.Show() def showTargetResistsEditor(self, event): - dlg=ResistsEditorDlg(self) - dlg.ShowModal() - dlg.Destroy() + ResistsEditorDlg(self) def showDamagePatternEditor(self, event): dlg=DmgPatternEditorDlg(self) dlg.ShowModal() dlg.Destroy() + def showImplantSetEditor(self, event): + ImplantSetEditorDlg(self) + def showExportDialog(self, event): """ Export active fit """ sFit = service.Fit.getInstance() @@ -418,6 +420,8 @@ def registerMenu(self): self.Bind(wx.EVT_MENU, self.showDamagePatternEditor, id=menuBar.damagePatternEditorId) # Target Resists editor self.Bind(wx.EVT_MENU, self.showTargetResistsEditor, id=menuBar.targetResistsEditorId) + # Implant Set editor + self.Bind(wx.EVT_MENU, self.showImplantSetEditor, id=menuBar.implantSetEditorId) # Import dialog self.Bind(wx.EVT_MENU, self.fileImportDialog, id=wx.ID_OPEN) # Export dialog diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index 78376fde7d..e8c3cb614f 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -33,6 +33,7 @@ def __init__(self): self.characterEditorId = wx.NewId() self.damagePatternEditorId = wx.NewId() self.targetResistsEditorId = wx.NewId() + self.implantSetEditorId = wx.NewId() self.graphFrameId = wx.NewId() self.backupFitsId = wx.NewId() self.exportSkillsNeededId = wx.NewId() @@ -101,9 +102,13 @@ def __init__(self): windowMenu.AppendItem(damagePatternEditItem) targetResistsEditItem = wx.MenuItem(windowMenu, self.targetResistsEditorId, "Target Resists Editor\tCTRL+R") - targetResistsEditItem.SetBitmap(BitmapLoader.getBitmap("explosive_big", "gui")) + targetResistsEditItem.SetBitmap(BitmapLoader.getBitmap("explosive_small", "gui")) windowMenu.AppendItem(targetResistsEditItem) + implantSetEditItem = wx.MenuItem(windowMenu, self.implantSetEditorId, "Implant Set Editor\tCTRL+I") + implantSetEditItem.SetBitmap(BitmapLoader.getBitmap("hardwire_small", "gui")) + windowMenu.AppendItem(implantSetEditItem) + graphFrameItem = wx.MenuItem(windowMenu, self.graphFrameId, "Graphs\tCTRL+G") graphFrameItem.SetBitmap(BitmapLoader.getBitmap("graphs_small", "gui")) windowMenu.AppendItem(graphFrameItem) diff --git a/gui/patternEditor.py b/gui/patternEditor.py index 40b8e45aae..92454d750b 100644 --- a/gui/patternEditor.py +++ b/gui/patternEditor.py @@ -23,68 +23,77 @@ from wx.lib.intctrl import IntCtrl from gui.utils.clipboard import toClipboard, fromClipboard from service.damagePattern import ImportError - +from gui.builtinViews.entityEditor import EntityEditor, BaseValidator ########################################################################### ## Class DmgPatternEditorDlg ########################################################################### -class DmgPatternEditorDlg(wx.Dialog): - DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive") +class DmgPatternTextValidor(BaseValidator): + def __init__(self): + BaseValidator.__init__(self) + + def Clone(self): + return DmgPatternTextValidor() + + def Validate(self, win): + profileEditor = win.Parent + textCtrl = self.GetWindow() + text = textCtrl.GetValue().strip() + try: + if len(text) == 0: + raise ValueError("You must supply a name for your Damage Profile!") + elif text in [x.name for x in profileEditor.entityEditor.choices]: + raise ValueError("Damage Profile name already in use, please choose another.") + + return True + except ValueError, e: + wx.MessageBox(u"{}".format(e), "Error") + textCtrl.SetFocus() + return False + + +class DmgPatternEntityEditor(EntityEditor): def __init__(self, parent): - wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Damage Pattern Editor", size = wx.Size( 400,240 )) + EntityEditor.__init__(self, parent, "Damage Profile") + self.SetEditorValidator(DmgPatternTextValidor) - self.block = False - self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) + def getEntitiesFromContext(self): + sDP = service.DamagePattern.getInstance() + choices = sorted(sDP.getDamagePatternList(), key=lambda p: p.name) + return [c for c in choices if c.name != "Selected Ammo"] - mainSizer = wx.BoxSizer(wx.VERTICAL) + def DoNew(self, name): + sDP = service.DamagePattern.getInstance() + return sDP.newPattern(name) - self.headerSizer = headerSizer = wx.BoxSizer(wx.HORIZONTAL) + def DoRename(self, entity, name): + sDP = service.DamagePattern.getInstance() + sDP.renamePattern(entity, name) + def DoCopy(self, entity, name): sDP = service.DamagePattern.getInstance() + copy = sDP.copyPattern(entity) + sDP.renamePattern(copy, name) + return copy - self.choices = sDP.getDamagePatternList() - # Remove "Selected Ammo" Damage Pattern - for dp in self.choices: - if dp.name == "Selected Ammo": - self.choices.remove(dp) - # Sort the remaining list and continue on - self.choices.sort(key=lambda p: p.name) - self.ccDmgPattern = wx.Choice(self, choices=map(lambda p: p.name, self.choices)) - self.ccDmgPattern.Bind(wx.EVT_CHOICE, self.patternChanged) - self.ccDmgPattern.SetSelection(0) - - self.namePicker = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) - self.namePicker.Bind(wx.EVT_TEXT_ENTER, self.processRename) - self.namePicker.Hide() - - size = None - headerSizer.Add(self.ccDmgPattern, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT|wx.LEFT, 3) - buttons = (("new", wx.ART_NEW), - ("rename", BitmapLoader.getBitmap("rename", "gui")), - ("copy", wx.ART_COPY), - ("delete", wx.ART_DELETE)) - for name, art in buttons: - bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON) if name != "rename" else art - btn = wx.BitmapButton(self, wx.ID_ANY, bitmap) - if size is None: - size = btn.GetSize() + def DoDelete(self, entity): + sDP = service.DamagePattern.getInstance() + sDP.deletePattern(entity) - btn.SetMinSize(size) - btn.SetMaxSize(size) +class DmgPatternEditorDlg(wx.Dialog): + DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive") - btn.Layout() - setattr(self, name, btn) - btn.Enable(True) - btn.SetToolTipString("%s pattern" % name.capitalize()) - headerSizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL) + def __init__(self, parent): + wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Damage Pattern Editor", size = wx.Size( 400,240 )) - self.btnSave = wx.Button(self, wx.ID_SAVE) - self.btnSave.Hide() - self.btnSave.Bind(wx.EVT_BUTTON, self.processRename) - self.headerSizer.Add(self.btnSave, 0, wx.ALIGN_CENTER) + self.block = False + self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) - mainSizer.Add(headerSizer, 0, wx.EXPAND | wx.ALL, 2) + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.entityEditor = DmgPatternEntityEditor(self) + mainSizer.Add(self.entityEditor, 0, wx.ALL | wx.EXPAND, 2) self.sl = wx.StaticLine(self) mainSizer.Add(self.sl, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) @@ -108,7 +117,7 @@ def __init__(self, parent): bmp = wx.StaticBitmap(self, wx.ID_ANY, BitmapLoader.getBitmap("%s_big"%type, "gui")) if i%2: style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.LEFT - border = 10 + border = 20 else: style = wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT border = 5 @@ -155,6 +164,7 @@ def __init__(self, parent): importExport = (("Import", wx.ART_FILE_OPEN, "from"), ("Export", wx.ART_FILE_SAVE_AS, "to")) + for name, art, direction in importExport: bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON) btn = wx.BitmapButton(self, wx.ID_ANY, bitmap) @@ -170,14 +180,10 @@ def __init__(self, parent): self.Layout() bsize = self.GetBestSize() - self.SetSize((-1,bsize.height)) + self.SetSize((-1, bsize.height)) + self.CenterOnParent() - self.new.Bind(wx.EVT_BUTTON, self.newPattern) - self.rename.Bind(wx.EVT_BUTTON, self.renamePattern) - self.copy.Bind(wx.EVT_BUTTON, self.copyPattern) - self.delete.Bind(wx.EVT_BUTTON, self.deletePattern) - self.Import.Bind(wx.EVT_BUTTON, self.importPatterns) - self.Export.Bind(wx.EVT_BUTTON, self.exportPatterns) + self.Bind(wx.EVT_CHOICE, self.patternChanged) self.patternChanged() @@ -188,7 +194,7 @@ def ValuesUpdated(self, event=None): if self.block: return - p = self.getActivePattern() + p = self.entityEditor.getActiveEntity() total = sum(map(lambda attr: getattr(self, "%sEdit"%attr).GetValue(), self.DAMAGE_TYPES)) for type in self.DAMAGE_TYPES: editObj = getattr(self, "%sEdit"%type) @@ -207,24 +213,18 @@ def restrict(self): for type in self.DAMAGE_TYPES: editObj = getattr(self, "%sEdit"%type) editObj.Enable(False) - self.rename.Enable(False) - self.delete.Enable(False) + self.entityEditor.btnRename.Enable(False) + self.entityEditor.btnDelete.Enable(False) def unrestrict(self): for type in self.DAMAGE_TYPES: editObj = getattr(self, "%sEdit"%type) editObj.Enable() - self.rename.Enable() - self.delete.Enable() - - def getActivePattern(self): - if len(self.choices) == 0: - return None - - return self.choices[self.ccDmgPattern.GetSelection()] + self.entityEditor.btnRename.Enable() + self.entityEditor.btnDelete.Enable() def patternChanged(self, event=None): - p = self.getActivePattern() + p = self.entityEditor.getActiveEntity() if p is None: return @@ -244,126 +244,9 @@ def patternChanged(self, event=None): self.block = False self.ValuesUpdated() - def newPattern(self, event): - self.restrict() - - self.block = True - # reset values - for type in self.DAMAGE_TYPES: - editObj = getattr(self, "%sEdit"%type) - editObj.SetValue(0) - - self.block = False - - self.btnSave.SetLabel("Create") - self.Refresh() - self.renamePattern() - - def renamePattern(self, event=None): - if event is not None: - self.btnSave.SetLabel("Rename") - - self.ccDmgPattern.Hide() - self.namePicker.Show() - self.headerSizer.Replace(self.ccDmgPattern, self.namePicker) - self.namePicker.SetFocus() - - if event is not None: # Rename mode - self.btnSave.SetLabel("Rename") - self.namePicker.SetValue(self.getActivePattern().name) - else: # Create mode - self.namePicker.SetValue("") - - for btn in (self.new, self.rename, self.delete, self.copy): - btn.Hide() - - self.btnSave.Show() - self.headerSizer.Layout() - if event is not None: - event.Skip() - - def processRename(self, event): - newName = self.namePicker.GetLineText(0) - self.stNotice.SetLabel("") - - if newName == "": - self.stNotice.SetLabel("Invalid name.") - return - - sDP = service.DamagePattern.getInstance() - if self.btnSave.Label == "Create": - p = sDP.newPattern() - else: - # we are renaming, so get the current selection - p = self.getActivePattern() - - for pattern in self.choices: - if pattern.name == newName and p != pattern: - self.stNotice.SetLabel("Name already used, please choose another") - return - - sDP.renamePattern(p, newName) - - self.updateChoices(newName) - self.headerSizer.Replace(self.namePicker, self.ccDmgPattern) - self.ccDmgPattern.Show() - self.namePicker.Hide() - self.btnSave.Hide() - for btn in (self.new, self.rename, self.delete, self.copy): - btn.Show() - - sel = self.ccDmgPattern.GetSelection() - self.ccDmgPattern.Delete(sel) - self.ccDmgPattern.Insert(newName, sel) - self.ccDmgPattern.SetSelection(sel) - self.ValuesUpdated() - self.unrestrict() - - def copyPattern(self,event): - sDP = service.DamagePattern.getInstance() - p = sDP.copyPattern(self.getActivePattern()) - self.choices.append(p) - id = self.ccDmgPattern.Append(p.name) - self.ccDmgPattern.SetSelection(id) - self.btnSave.SetLabel("Copy") - self.renamePattern() - self.patternChanged() - - def deletePattern(self,event): - sDP = service.DamagePattern.getInstance() - sel = self.ccDmgPattern.GetSelection() - sDP.deletePattern(self.getActivePattern()) - self.ccDmgPattern.Delete(sel) - self.ccDmgPattern.SetSelection(max(0, sel - 1)) - del self.choices[sel] - self.patternChanged() - - def __del__( self ): + def __del__(self): pass - def updateChoices(self, select=None): - "Gathers list of patterns and updates choice selections" - sDP = service.DamagePattern.getInstance() - self.choices = sDP.getDamagePatternList() - - for dp in self.choices: - if dp.name == "Selected Ammo": # don't include this special butterfly - self.choices.remove(dp) - - # Sort the remaining list and continue on - self.choices.sort(key=lambda p: p.name) - self.ccDmgPattern.Clear() - - for i, choice in enumerate(map(lambda p: p.name, self.choices)): - self.ccDmgPattern.Append(choice) - - if select is not None and choice == select: - self.ccDmgPattern.SetSelection(i) - - if select is None: - self.ccDmgPattern.SetSelection(0) - self.patternChanged() - def importPatterns(self, event): text = fromClipboard() if text: @@ -384,3 +267,6 @@ def exportPatterns(self, event): sDP = service.DamagePattern.getInstance() toClipboard( sDP.exportPatterns() ) self.stNotice.SetLabel("Patterns exported to clipboard") + + def contextChanged(self, event): + print "lol" \ No newline at end of file diff --git a/gui/resistsEditor.py b/gui/resistsEditor.py index 3d8ac6ef22..374f9c1fc0 100644 --- a/gui/resistsEditor.py +++ b/gui/resistsEditor.py @@ -22,64 +22,76 @@ import service from gui.utils.clipboard import toClipboard, fromClipboard from service.targetResists import ImportError +from gui.builtinViews.entityEditor import EntityEditor, BaseValidator -class ResistsEditorDlg(wx.Dialog): - DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive") +class TargetResistsTextValidor(BaseValidator): + def __init__(self): + BaseValidator.__init__(self) - def __init__(self, parent): - wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Target Resists Editor", size = wx.Size( 350,240 )) + def Clone(self): + return TargetResistsTextValidor() - self.block = False - self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) + def Validate(self, win): + profileEditor = win.parent.Parent + textCtrl = self.GetWindow() + text = textCtrl.GetValue().strip() + + try: + if len(text) == 0: + raise ValueError("You must supply a name for your Target Resist Profile!") + elif text in [x.name for x in profileEditor.entityEditor.choices]: + raise ValueError("Target Resist Profile name already in use, please choose another.") + + return True + except ValueError, e: + wx.MessageBox(u"{}".format(e), "Error") + textCtrl.SetFocus() + return False - mainSizer = wx.BoxSizer(wx.VERTICAL) - self.headerSizer = headerSizer = wx.BoxSizer(wx.HORIZONTAL) +class TargetResistsEntityEditor(EntityEditor): + def __init__(self, parent): + EntityEditor.__init__(self, parent, "Target Resist Profile") + self.SetEditorValidator(TargetResistsTextValidor) + def getEntitiesFromContext(self): sTR = service.TargetResists.getInstance() + choices = sorted(sTR.getTargetResistsList(), key=lambda p: p.name) + return choices - self.choices = sTR.getTargetResistsList() + def DoNew(self, name): + sTR = service.TargetResists.getInstance() + return sTR.newPattern(name) - # Sort the remaining list and continue on - self.choices.sort(key=lambda p: p.name) - self.ccResists = wx.Choice(self, choices=map(lambda p: p.name, self.choices)) - self.ccResists.Bind(wx.EVT_CHOICE, self.patternChanged) - self.ccResists.SetSelection(0) + def DoRename(self, entity, name): + sTR = service.TargetResists.getInstance() + sTR.renamePattern(entity, name) - self.namePicker = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) - self.namePicker.Bind(wx.EVT_TEXT_ENTER, self.processRename) - self.namePicker.Hide() + def DoCopy(self, entity, name): + sTR = service.TargetResists.getInstance() + copy = sTR.copyPattern(entity) + sTR.renamePattern(copy, name) + return copy - size = None - headerSizer.Add(self.ccResists, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3) + def DoDelete(self, entity): + sTR = service.TargetResists.getInstance() + sTR.deletePattern(entity) - buttons = (("new", wx.ART_NEW), - ("rename", BitmapLoader.getBitmap("rename", "gui")), - ("copy", wx.ART_COPY), - ("delete", wx.ART_DELETE)) - for name, art in buttons: - bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON) if name != "rename" else art - btn = wx.BitmapButton(self, wx.ID_ANY, bitmap) - if size is None: - size = btn.GetSize() +class ResistsEditorDlg(wx.Dialog): - btn.SetMinSize(size) - btn.SetMaxSize(size) + DAMAGE_TYPES = ("em", "thermal", "kinetic", "explosive") - btn.Layout() - setattr(self, name, btn) - btn.Enable(True) - btn.SetToolTipString("%s resist profile" % name.capitalize()) - headerSizer.Add(btn, 0, wx.ALIGN_CENTER_VERTICAL) + def __init__(self, parent): + wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Target Resists Editor", size = wx.Size( 350,240 )) + self.block = False + self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) - self.btnSave = wx.Button(self, wx.ID_SAVE) - self.btnSave.Hide() - self.btnSave.Bind(wx.EVT_BUTTON, self.processRename) - headerSizer.Add(self.btnSave, 0, wx.ALIGN_CENTER) + mainSizer = wx.BoxSizer(wx.VERTICAL) - mainSizer.Add(headerSizer, 0, wx.EXPAND | wx.ALL, 2) + self.entityEditor = TargetResistsEntityEditor(self) + mainSizer.Add(self.entityEditor, 0, wx.ALL | wx.EXPAND, 2) self.sl = wx.StaticLine(self) mainSizer.Add(self.sl, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) @@ -157,19 +169,21 @@ def __init__(self, parent): btn.SetToolTipString("%s patterns %s clipboard" % (name, direction) ) footerSizer.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_RIGHT) + if not self.entityEditor.checkEntitiesExist(): + self.Destroy() + return + self.Layout() bsize = self.GetBestSize() - self.SetSize((-1,bsize.height)) + self.SetSize((-1, bsize.height)) + self.CenterOnParent() - self.new.Bind(wx.EVT_BUTTON, self.newPattern) - self.rename.Bind(wx.EVT_BUTTON, self.renamePattern) - self.copy.Bind(wx.EVT_BUTTON, self.copyPattern) - self.delete.Bind(wx.EVT_BUTTON, self.deletePattern) - self.Import.Bind(wx.EVT_BUTTON, self.importPatterns) - self.Export.Bind(wx.EVT_BUTTON, self.exportPatterns) + self.Bind(wx.EVT_CHOICE, self.patternChanged) self.patternChanged() + self.ShowModal() + def closeEvent(self, event): self.Destroy() @@ -184,7 +198,7 @@ def ValuesUpdated(self, event=None): return try: - p = self.getActivePattern() + p = self.entityEditor.getActiveEntity() for type in self.DAMAGE_TYPES: editObj = getattr(self, "%sEdit"%type) @@ -220,33 +234,15 @@ def ValuesUpdated(self, event=None): finally: # Refresh for color changes to take effect immediately self.Refresh() - def restrict(self): - for type in self.DAMAGE_TYPES: - editObj = getattr(self, "%sEdit"%type) - editObj.Enable(False) - self.rename.Enable(False) - self.delete.Enable(False) - - def unrestrict(self): - for type in self.DAMAGE_TYPES: - editObj = getattr(self, "%sEdit"%type) - editObj.Enable() - self.rename.Enable() - self.delete.Enable() - - def getActivePattern(self): - if len(self.choices) == 0: - return None - - return self.choices[self.ccResists.GetSelection()] - def patternChanged(self, event=None): "Event fired when user selects pattern. Can also be called from script" - p = self.getActivePattern() + + if not self.entityEditor.checkEntitiesExist(): + self.Destroy() + return + + p = self.entityEditor.getActiveEntity() if p is None: - # This happens when there are no patterns in the DB. As such, force - # user to create one first or exit dlg. - self.newPattern(None) return self.block = True @@ -259,142 +255,9 @@ def patternChanged(self, event=None): self.block = False self.ValuesUpdated() - def newPattern(self, event): - ''' - Simply does new-pattern specifics: replaces label on button, restricts, - and resets values to default. Hands off to the rename function for - further handling. - ''' - self.btnSave.SetLabel("Create") - self.restrict() - # reset values - for type in self.DAMAGE_TYPES: - editObj = getattr(self, "%sEdit"%type) - editObj.ChangeValue("0.0") - editObj.SetForegroundColour(self.colorReset) - - self.Refresh() - self.renamePattern() - - def renamePattern(self, event=None): - "Changes layout to facilitate naming a pattern" - - self.showInput(True) - - if event is not None: # Rename mode - self.btnSave.SetLabel("Rename") - self.namePicker.SetValue(self.getActivePattern().name) - else: # Create mode - self.namePicker.SetValue("") - - if event is not None: - event.Skip() - - def processRename(self, event): - ''' - Processes rename event (which can be new or old patterns). If new - pattern, creates it; if old, selects it. if checks are valid, rename - saves pattern to DB. - - Also resets to default layout and unrestricts. - ''' - newName = self.namePicker.GetLineText(0) - self.stNotice.SetLabel("") - - if newName == "": - self.stNotice.SetLabel("Invalid name") - return - - sTR = service.TargetResists.getInstance() - if self.btnSave.Label == "Create": - p = sTR.newPattern() - else: - # we are renaming, so get the current selection - p = self.getActivePattern() - - # test for patterns of the same name - for pattern in self.choices: - if pattern.name == newName and p != pattern: - self.stNotice.SetLabel("Name already used, please choose another") - return - - # rename regardless of new or rename - sTR.renamePattern(p, newName) - - self.updateChoices(newName) - self.showInput(False) - sel = self.ccResists.GetSelection() - self.ValuesUpdated() - self.unrestrict() - - def copyPattern(self,event): - sTR = service.TargetResists.getInstance() - p = sTR.copyPattern(self.getActivePattern()) - self.choices.append(p) - id = self.ccResists.Append(p.name) - self.ccResists.SetSelection(id) - self.btnSave.SetLabel("Copy") - self.renamePattern() - self.patternChanged() - - def deletePattern(self,event): - sTR = service.TargetResists.getInstance() - sel = self.ccResists.GetSelection() - sTR.deletePattern(self.getActivePattern()) - self.ccResists.Delete(sel) - self.ccResists.SetSelection(max(0, sel - 1)) - del self.choices[sel] - self.patternChanged() - - def showInput(self, bool): - if bool and not self.namePicker.IsShown(): - self.ccResists.Hide() - self.namePicker.Show() - self.headerSizer.Replace(self.ccResists, self.namePicker) - self.namePicker.SetFocus() - for btn in (self.new, self.rename, self.delete, self.copy): - btn.Hide() - self.btnSave.Show() - self.restrict() - self.headerSizer.Layout() - elif not bool and self.namePicker.IsShown(): - self.headerSizer.Replace(self.namePicker, self.ccResists) - self.ccResists.Show() - self.namePicker.Hide() - self.btnSave.Hide() - for btn in (self.new, self.rename, self.delete, self.copy): - btn.Show() - self.unrestrict() - self.headerSizer.Layout() - - def __del__( self ): pass - def updateChoices(self, select=None): - "Gathers list of patterns and updates choice selections" - sTR = service.TargetResists.getInstance() - self.choices = sTR.getTargetResistsList() - - if len(self.choices) == 0: - #self.newPattern(None) - return - - # Sort the remaining list and continue on - self.choices.sort(key=lambda p: p.name) - self.ccResists.Clear() - - for i, choice in enumerate(map(lambda p: p.name, self.choices)): - self.ccResists.Append(choice) - - if select is not None and choice == select: - self.ccResists.SetSelection(i) - - if select is None: - self.ccResists.SetSelection(0) - - self.patternChanged() - def importPatterns(self, event): "Event fired when import from clipboard button is clicked" diff --git a/gui/setEditor.py b/gui/setEditor.py new file mode 100644 index 0000000000..4e8baa9eda --- /dev/null +++ b/gui/setEditor.py @@ -0,0 +1,214 @@ +#=============================================================================== +# Copyright (C) 2016 Ryan Holmes +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +#=============================================================================== + +import wx +from gui.bitmapLoader import BitmapLoader +from gui.builtinViews.implantEditor import BaseImplantEditorView +import service +from gui.utils.clipboard import toClipboard, fromClipboard +from service.implantSet import ImportError +import logging +from gui.builtinViews.entityEditor import EntityEditor, BaseValidator + +logger = logging.getLogger(__name__) + +class ImplantTextValidor(BaseValidator): + def __init__(self): + BaseValidator.__init__(self) + + def Clone(self): + return ImplantTextValidor() + + def Validate(self, win): + profileEditor = win.parent.Parent + textCtrl = self.GetWindow() + text = textCtrl.GetValue().strip() + + try: + if len(text) == 0: + raise ValueError("You must supply a name for the Implant Set!") + elif text in [x.name for x in profileEditor.entityEditor.choices]: + raise ValueError("Imlplant Set name already in use, please choose another.") + + return True + except ValueError, e: + wx.MessageBox(u"{}".format(e), "Error") + textCtrl.SetFocus() + return False + + +class ImplantSetEntityEditor(EntityEditor): + def __init__(self, parent): + EntityEditor.__init__(self, parent, "Implant Set") + self.SetEditorValidator(ImplantTextValidor) + + def getEntitiesFromContext(self): + sIS = service.ImplantSets.getInstance() + return sorted(sIS.getImplantSetList(), key=lambda c: c.name) + + def DoNew(self, name): + sIS = service.ImplantSets.getInstance() + return sIS.newSet(name) + + def DoRename(self, entity, name): + sIS = service.ImplantSets.getInstance() + sIS.renameSet(entity, name) + + def DoCopy(self, entity, name): + sIS = service.ImplantSets.getInstance() + copy = sIS.copySet(entity) + sIS.renameSet(copy, name) + return copy + + def DoDelete(self, entity): + sIS = service.ImplantSets.getInstance() + sIS.deleteSet(entity) + + +class ImplantSetEditor(BaseImplantEditorView): + def __init__(self, parent): + BaseImplantEditorView.__init__(self, parent) + if 'wxMSW' in wx.PlatformInfo: + self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + + def bindContext(self): + self.Parent.entityEditor.Bind(wx.EVT_CHOICE, self.contextChanged) + + def getImplantsFromContext(self): + sIS = service.ImplantSets.getInstance() + set = self.Parent.entityEditor.getActiveEntity() + if set: + return sIS.getImplants(set.ID) + return [] + + def addImplantToContext(self, item): + sIS = service.ImplantSets.getInstance() + set = self.Parent.entityEditor.getActiveEntity() + + sIS.addImplant(set.ID, item.ID) + + def removeImplantFromContext(self, implant): + sIS = service.ImplantSets.getInstance() + set = self.Parent.entityEditor.getActiveEntity() + + sIS.removeImplant(set.ID, implant) + +class ImplantSetEditorDlg(wx.Dialog): + + def __init__(self, parent): + wx.Dialog.__init__(self, parent, id = wx.ID_ANY, title = u"Implant Set Editor", size = wx.Size(640, 600)) + + self.block = False + self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.entityEditor = ImplantSetEntityEditor(self) + mainSizer.Add(self.entityEditor, 0, wx.ALL | wx.EXPAND, 2) + + self.sl = wx.StaticLine(self) + mainSizer.Add(self.sl, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) + + self.iview = ImplantSetEditor(self) + mainSizer.Add(self.iview, 1, wx.ALL | wx.EXPAND, 5) + + self.slfooter = wx.StaticLine(self) + mainSizer.Add(self.slfooter, 0, wx.EXPAND | wx.TOP, 5) + + footerSizer = wx.BoxSizer(wx.HORIZONTAL) + + self.stNotice = wx.StaticText(self, wx.ID_ANY, u"") + self.stNotice.Wrap(-1) + footerSizer.Add(self.stNotice, 1, wx.BOTTOM | wx.TOP | wx.LEFT, 5) + + if "wxGTK" in wx.PlatformInfo: + self.closeBtn = wx.Button( self, wx.ID_ANY, u"Close", wx.DefaultPosition, wx.DefaultSize, 0 ) + mainSizer.Add( self.closeBtn, 0, wx.ALL|wx.ALIGN_RIGHT, 5 ) + self.closeBtn.Bind(wx.EVT_BUTTON, self.closeEvent) + + importExport = (("Import", wx.ART_FILE_OPEN, "from"), + ("Export", wx.ART_FILE_SAVE_AS, "to")) + + for name, art, direction in importExport: + bitmap = wx.ArtProvider.GetBitmap(art, wx.ART_BUTTON) + btn = wx.BitmapButton(self, wx.ID_ANY, bitmap) + + btn.SetMinSize( btn.GetSize() ) + btn.SetMaxSize( btn.GetSize() ) + + btn.Layout() + setattr(self, name, btn) + btn.Enable(True) + btn.SetToolTipString("%s implant sets %s clipboard" % (name, direction) ) + footerSizer.Add(btn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_RIGHT) + + mainSizer.Add(footerSizer, 0, wx.ALL | wx.EXPAND, 5) + + self.SetSizer(mainSizer) + self.Layout() + + if not self.entityEditor.checkEntitiesExist(): + self.Destroy() + return + + self.Bind(wx.EVT_CHOICE, self.entityChanged) + + self.Import.Bind(wx.EVT_BUTTON, self.importPatterns) + self.Export.Bind(wx.EVT_BUTTON, self.exportPatterns) + + self.CenterOnParent() + self.ShowModal() + + def entityChanged(self, event): + if not self.entityEditor.checkEntitiesExist(): + self.Destroy() + return + + def closeEvent(self, event): + self.Destroy() + + def __del__( self ): + pass + + def importPatterns(self, event): + "Event fired when import from clipboard button is clicked" + + text = fromClipboard() + if text: + sIS = service.ImplantSets.getInstance() + try: + sIS.importSets(text) + self.stNotice.SetLabel("Patterns successfully imported from clipboard") + self.showInput(False) + except ImportError, e: + self.stNotice.SetLabel(str(e)) + except Exception, e: + logging.exception("Unhandled Exception") + self.stNotice.SetLabel("Could not import from clipboard: unknown errors") + finally: + self.updateChoices() + else: + self.stNotice.SetLabel("Could not import from clipboard") + + def exportPatterns(self, event): + "Event fired when export to clipboard button is clicked" + + sIS = service.ImplantSets.getInstance() + toClipboard(sIS.exportSets()) + self.stNotice.SetLabel("Sets exported to clipboard") diff --git a/imgs/gui/hardwire_small.png b/imgs/gui/hardwire_small.png new file mode 100644 index 0000000000..58b63b045d Binary files /dev/null and b/imgs/gui/hardwire_small.png differ diff --git a/service/__init__.py b/service/__init__.py index e7e0286d47..b19c6db17a 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -10,6 +10,7 @@ from service.price import Price from service.network import Network from service.eveapi import EVEAPIConnection, ParseXML +from service.implantSet import ImplantSets import wx if not 'wxMac' in wx.PlatformInfo or ('wxMac' in wx.PlatformInfo and wx.VERSION >= (3,0)): diff --git a/service/character.py b/service/character.py index 83e50bbaa4..a4d68c98ac 100644 --- a/service/character.py +++ b/service/character.py @@ -32,7 +32,9 @@ import eos.types import service import config +import logging +logger = logging.getLogger(__name__) class CharacterImportThread(threading.Thread): def __init__(self, paths, callback): @@ -48,9 +50,8 @@ def run(self): # we try to parse api XML data first with open(path, mode='r') as charFile: sheet = service.ParseXML(charFile) - charID = sCharacter.new() - sCharacter.rename(charID, sheet.name+" (imported)") - sCharacter.apiUpdateCharSheet(charID, sheet.skills) + char = sCharacter.new(sheet.name+" (imported)") + sCharacter.apiUpdateCharSheet(char.ID, sheet.skills) except: # if it's not api XML data, try this # this is a horrible logic flow, but whatever @@ -67,9 +68,8 @@ def run(self): "typeID": int(skill.getAttribute("typeID")), "level": int(skill.getAttribute("level")), }) - charID = sCharacter.new() - sCharacter.rename(charID, name+" (EVEMon)") - sCharacter.apiUpdateCharSheet(charID, skills) + char = sCharacter.new(name+" (EVEMon)") + sCharacter.apiUpdateCharSheet(char.ID, skills) except: continue @@ -114,6 +114,11 @@ def getInstance(cls): return cls.instance + def __init__(self): + # Simply initializes default characters in case they aren't in the database yet + self.all0() + self.all5() + def exportText(self): data = "Pyfa exported plan for \""+self.skillReqsDict['charname']+"\"\n" data += "=" * 79 + "\n" @@ -183,10 +188,7 @@ def all5ID(self): return self.all5().ID def getCharacterList(self): - baseChars = [eos.types.Character.getAll0(), eos.types.Character.getAll5()] - sFit = service.Fit.getInstance() - - return map(lambda c: (c.ID, c.name if not c.isDirty else "{} *".format(c.name), c == sFit.character), eos.db.getCharacterList()) + return eos.db.getCharacterList() def getCharacter(self, charID): char = eos.db.getCharacter(charID) @@ -246,25 +248,21 @@ def getDirtySkills(self, charID): def getCharName(self, charID): return eos.db.getCharacter(charID).name - def new(self): - char = eos.types.Character("New Character") + def new(self, name="New Character"): + char = eos.types.Character(name) eos.db.save(char) - return char.ID + return char - def rename(self, charID, newName): - char = eos.db.getCharacter(charID) + def rename(self, char, newName): char.name = newName eos.db.commit() - def copy(self, charID): - char = eos.db.getCharacter(charID) + def copy(self, char): newChar = copy.deepcopy(char) eos.db.save(newChar) - return newChar.ID + return newChar - def delete(self, charID): - char = eos.db.getCharacter(charID) - eos.db.commit() + def delete(self, char): eos.db.remove(char) def getApiDetails(self, charID): @@ -343,13 +341,18 @@ def saveSkill(self, charID, skillID): def addImplant(self, charID, itemID): char = eos.db.getCharacter(charID) + if char.ro: + logger.error("Trying to add implant to read-only character") + return + implant = eos.types.Implant(eos.db.getItem(itemID)) - char.implants.freeSlot(implant.slot) char.implants.append(implant) + eos.db.commit() - def removeImplant(self, charID, slot): + def removeImplant(self, charID, implant): char = eos.db.getCharacter(charID) - char.implants.freeSlot(slot) + char.implants.remove(implant) + eos.db.commit() def getImplants(self, charID): char = eos.db.getCharacter(charID) diff --git a/service/damagePattern.py b/service/damagePattern.py index 374940e0ca..b67b109564 100644 --- a/service/damagePattern.py +++ b/service/damagePattern.py @@ -46,9 +46,10 @@ def getDamagePatternList(self): def getDamagePattern(self, name): return eos.db.getDamagePattern(name) - def newPattern(self): + def newPattern(self, name): p = eos.types.DamagePattern(0, 0, 0, 0) - p.name = "" + p.name = name + eos.db.save(p) return p def renamePattern(self, p, newName): diff --git a/service/fit.py b/service/fit.py index e15faaf8a4..a931061f1d 100644 --- a/service/fit.py +++ b/service/fit.py @@ -277,7 +277,7 @@ def searchFits(self, name): fit.timestamp)) return fits - def addImplant(self, fitID, itemID): + def addImplant(self, fitID, itemID, recalc=True): if fitID is None: return False @@ -289,7 +289,8 @@ def addImplant(self, fitID, itemID): return False fit.implants.append(implant) - self.recalc(fit) + if recalc: + self.recalc(fit) return True def removeImplant(self, fitID, position): @@ -760,6 +761,14 @@ def toggleImplant(self, fitID, i): self.recalc(fit) return True + def toggleImplantSource(self, fitID, source): + fit = eos.db.getFit(fitID) + fit.implantSource = source + + eos.db.commit() + self.recalc(fit) + return True + def toggleBooster(self, fitID, i): fit = eos.db.getFit(fitID) booster = fit.boosters[i] diff --git a/service/implantSet.py b/service/implantSet.py new file mode 100644 index 0000000000..0d0df58084 --- /dev/null +++ b/service/implantSet.py @@ -0,0 +1,125 @@ +#=============================================================================== +# Copyright (C) 2016 Ryan Holmes +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +#=============================================================================== + +import eos.db +import eos.types +import copy +import service.market + +class ImportError(Exception): + pass + +class ImplantSets(): + instance = None + @classmethod + def getInstance(cls): + if cls.instance is None: + cls.instance = ImplantSets() + + return cls.instance + + def getImplantSetList(self): + return eos.db.getImplantSetList(None) + + def getImplantSet(self, name): + return eos.db.getImplantSet(name) + + def getImplants(self, setID): + set = eos.db.getImplantSet(setID) + return set.implants + + def addImplant(self, setID, itemID): + set = eos.db.getImplantSet(setID) + implant = eos.types.Implant(eos.db.getItem(itemID)) + set.implants.append(implant) + eos.db.commit() + + def removeImplant(self, setID, implant): + set = eos.db.getImplantSet(setID) + set.implants.remove(implant) + eos.db.commit() + + def newSet(self, name): + s = eos.types.ImplantSet() + s.name = name + eos.db.save(s) + return s + + def renameSet(self, s, newName): + s.name = newName + eos.db.save(s) + + def deleteSet(self, s): + eos.db.remove(s) + + def copySet(self, s): + newS = copy.deepcopy(s) + eos.db.save(newS) + return newS + + def saveChanges(self, s): + eos.db.save(s) + + def importSets(self, text): + sMkt = service.Market.getInstance() + lines = text.splitlines() + newSets = [] + errors = 0 + current = None + lookup = {} + + for i, line in enumerate(lines): + line = line.strip() + try: + if line == '' or line[0] == "#": # comments / empty string + continue + if line[:1] == "[" and line[-1:] == "]": + current = eos.types.ImplantSet(line[1:-1]) + newSets.append(current) + else: + item = sMkt.getItem(line) + current.implants.append(eos.types.Implant(item)) + except: + errors += 1 + continue + + for set in self.getImplantSetList(): + lookup[set.name] = set + + for set in newSets: + if set.name in lookup: + match = lookup[set.name] + for implant in set.implants: + match.implants.append(eos.types.Implant(implant.item)) + else: + eos.db.save(set) + + eos.db.commit() + + lenImports = len(newSets) + if lenImports == 0: + raise ImportError("No patterns found for import") + if errors > 0: + raise ImportError("%d sets imported from clipboard; %d errors"%(lenImports, errors)) + + def exportSets(self): + patterns = self.getImplantSetList() + patterns.sort(key=lambda p: p.name) + return eos.types.ImplantSet.exportSets(*patterns) + diff --git a/service/market.py b/service/market.py index 00323b78b8..0d8538732a 100644 --- a/service/market.py +++ b/service/market.py @@ -29,12 +29,15 @@ from service.settings import SettingsProvider, NetworkSettings import service import service.conversions as conversions +import logging try: from collections import OrderedDict except ImportError: from utils.compat import OrderedDict +logger = logging.getLogger(__name__) + # Event which tells threads dependent on Market that it's initialized mktRdy = threading.Event() @@ -120,11 +123,14 @@ def processSearches(self): self.searchRequest = None cv.release() sMkt = Market.getInstance() - if filterOn: + if filterOn is True: # Rely on category data provided by eos as we don't hardcode them much in service filter = eos.types.Category.name.in_(sMkt.SEARCH_CATEGORIES) + elif filterOn: # filter by selected categories + filter = eos.types.Category.name.in_(filterOn) else: filter=None + results = eos.db.searchItems(request, where=filter, join=(eos.types.Item.group, eos.types.Group.category), eager=("icon", "group.category", "metaGroup", "metaGroup.parent")) @@ -346,20 +352,25 @@ def __makeRevDict(self, orig): def getItem(self, identity, *args, **kwargs): """Get item by its ID or name""" - if isinstance(identity, eos.types.Item): - item = identity - elif isinstance(identity, int): - item = eos.db.getItem(identity, *args, **kwargs) - elif isinstance(identity, basestring): - # We normally lookup with string when we are using import/export - # features. Check against overrides - identity = conversions.all.get(identity, identity) - item = eos.db.getItem(identity, *args, **kwargs) - elif isinstance(identity, float): - id = int(identity) - item = eos.db.getItem(id, *args, **kwargs) - else: - raise TypeError("Need Item object, integer, float or string as argument") + try: + if isinstance(identity, eos.types.Item): + item = identity + elif isinstance(identity, int): + item = eos.db.getItem(identity, *args, **kwargs) + elif isinstance(identity, basestring): + # We normally lookup with string when we are using import/export + # features. Check against overrides + identity = conversions.all.get(identity, identity) + item = eos.db.getItem(identity, *args, **kwargs) + elif isinstance(identity, float): + id = int(identity) + item = eos.db.getItem(id, *args, **kwargs) + else: + raise TypeError("Need Item object, integer, float or string as argument") + except: + logger.error("Could not get item: %s", identity) + raise + return item def getGroup(self, identity, *args, **kwargs): diff --git a/service/targetResists.py b/service/targetResists.py index a8f61b9f92..2640551f45 100644 --- a/service/targetResists.py +++ b/service/targetResists.py @@ -39,9 +39,10 @@ def getTargetResistsList(self): def getTargetResists(self, name): return eos.db.getTargetResists(name) - def newPattern(self): + def newPattern(self, name): p = eos.types.TargetResists(0.0, 0.0, 0.0, 0.0) - p.name = "" + p.name = name + eos.db.save(p) return p def renamePattern(self, p, newName):