diff --git a/eos/config.py b/eos/config.py index 35e2fe0dc..042605466 100644 --- a/eos/config.py +++ b/eos/config.py @@ -5,7 +5,7 @@ debug = False gamedataCache = True saveddataCache = True gamedata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "staticdata", "eve.db")), sys.getfilesystemencoding()) -saveddata_connectionstring = 'sqlite:///:memory:' +saveddata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db")), sys.getfilesystemencoding()) #Autodetect path, only change if the autodetection bugs out. path = dirname(unicode(__file__, sys.getfilesystemencoding())) diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 84e5a6763..747eea2e3 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -21,11 +21,10 @@ from sqlalchemy.orm import validates, reconstructor from eos.effectHandlerHelpers import HandledItem +import eos.db import eos class Character(object): - __all5 = None - __all0 = None __itemList = None __itemIDMap = None __itemNameMap = None @@ -33,7 +32,6 @@ class Character(object): @classmethod def getSkillList(cls): if cls.__itemList is None: - import eos.db cls.__itemList = eos.db.getItemsByCategory("Skill") return cls.__itemList @@ -66,36 +64,38 @@ class Character(object): @classmethod def getAll5(cls): - if cls.__all5 is None: - import eos.db - all5 = eos.db.getCharacter("All 5") - if all5 is None: - all5 = Character("All 5") - all5.defaultLevel = 5 - eos.db.add(all5) + all5 = eos.db.getCharacter("All 5") - cls.__all5 = all5 - return cls.__all5 + if all5 is None: + # We do not have to be afraid of committing here and saving + # edited character data. If this ever runs, it will be during the + # get character list phase when pyfa first starts + all5 = Character("All 5", 5) + eos.db.save(all5) + + return all5 @classmethod def getAll0(cls): - if cls.__all0 is None: - import eos.db - all0 = eos.db.getCharacter("All 0") - if all0 is None: - all0 = Character("All 0") - all0.defaultLevel = None - eos.db.add(all0) + all0 = eos.db.getCharacter("All 0") - cls.__all0 = all0 - return cls.__all0 + if all0 is None: + all0 = Character("All 0") + eos.db.save(all0) - def __init__(self, name): + return all0 + + def __init__(self, name, defaultLevel=None): self.name = name self.__owner = None - self.defaultLevel = None + self.defaultLevel = defaultLevel self.__skills = [] self.__skillIdMap = {} + self.dirtySkills = set() + + for item in self.getSkillList(): + self.addSkill(Skill(item.ID, self.defaultLevel)) + self.__implants = eos.saveddata.fit.HandledImplantBoosterList() self.apiKey = None @@ -104,12 +104,12 @@ class Character(object): self.__skillIdMap = {} for skill in self.__skills: self.__skillIdMap[skill.itemID] = skill + self.dirtySkills = set() def apiUpdateCharSheet(self, skills): del self.__skills[:] self.__skillIdMap.clear() for skillRow in skills: - self.addSkill(Skill(skillRow["typeID"], skillRow["level"])) @property @@ -120,6 +120,10 @@ class Character(object): def owner(self, owner): self.__owner = owner + @property + def skills(self): + return self.__skills + def addSkill(self, skill): self.__skills.append(skill) self.__skillIdMap[skill.itemID] = skill @@ -137,11 +141,7 @@ class Character(object): skill = self.__skillIdMap.get(item.ID) if skill is None: - if self.defaultLevel is None: - skill = Skill(item, 0, False, False) - else: - skill = Skill(item, self.defaultLevel, False, True) - + skill = Skill(item, self.defaultLevel, False, True) self.addSkill(skill) return skill @@ -150,40 +150,51 @@ class Character(object): def implants(self): return self.__implants - def iterSkills(self): - for item in self.getSkillList(): - yield self.getSkill(item) + @property + def isDirty(self): + return len(self.dirtySkills) > 0 + + def saveLevels(self): + if self == self.getAll5() or self == self.getAll0(): + raise ReadOnlyException("This character is read-only") + + for skill in self.dirtySkills: + skill.saveLevel() + + self.dirtySkills = set() + eos.db.commit() def filteredSkillIncrease(self, filter, *args, **kwargs): - for element in self.iterSkills(): + for element in self.skills: if filter(element): element.increaseItemAttr(*args, **kwargs) def filteredSkillMultiply(self, filter, *args, **kwargs): - for element in self.iterSkills(): + for element in self.skills: if filter(element): element.multiplyItemAttr(*args, **kwargs) def filteredSkillBoost(self, filter, *args, **kwargs): - for element in self.iterSkills(): + for element in self.skills: if filter(element): element.boostItemAttr(*args, **kwargs) def calculateModifiedAttributes(self, fit, runTime, forceProjected = False): if forceProjected: return - for skill in self.iterSkills(): + for skill in self.skills: fit.register(skill) skill.calculateModifiedAttributes(fit, runTime) def clear(self): - for skill in self.iterSkills(): + for skill in self.skills: skill.clear() def __deepcopy__(self, memo): copy = Character("%s copy" % self.name) copy.apiKey = self.apiKey copy.apiID = self.apiID - for skill in self.iterSkills(): + + for skill in self.skills: copy.addSkill(Skill(skill.itemID, skill.level, False, skill.learned)) return copy @@ -199,7 +210,7 @@ class Character(object): else: return val class Skill(HandledItem): - def __init__(self, item, level = 0, ro = False, learned = True): + def __init__(self, item, level=0, ro=False, learned=True): self.__item = item if not isinstance(item, int) else None self.itemID = item.ID if not isinstance(item, int) else item self.__level = level if learned else None @@ -214,14 +225,18 @@ class Skill(HandledItem): def build(self, ro): self.__ro = ro self.__suppressed = False + self.activeLevel = self.__level + + def saveLevel(self): + self.__level = self.activeLevel @property def learned(self): - return self.__level is not None + return self.activeLevel is not None @property def level(self): - return self.__level or 0 + return self.activeLevel or 0 @level.setter def level(self, level): @@ -231,7 +246,11 @@ class Skill(HandledItem): if hasattr(self, "_Skill__ro") and self.__ro == True: raise ReadOnlyException() - self.__level = level + self.activeLevel = level + if self.activeLevel == self.__level and self in self.character.dirtySkills: + self.character.dirtySkills.remove(self) + else: + self.character.dirtySkills.add(self) @property def item(self): diff --git a/eos/saveddata/fleet.py b/eos/saveddata/fleet.py index f1445ae3a..c1db0e97e 100644 --- a/eos/saveddata/fleet.py +++ b/eos/saveddata/fleet.py @@ -247,7 +247,7 @@ class Store(object): dict.clear() # Go through everything which can be used as gang booster - for thing in chain(fitBooster.modules, fitBooster.implants, fitBooster.character.iterSkills(), (fitBooster.ship,)): + for thing in chain(fitBooster.modules, fitBooster.implants, fitBooster.character.skills, (fitBooster.ship,)): if thing.item is None: continue for effect in thing.item.effects.itervalues(): diff --git a/gui/builtinContextMenus/changeAffectingSkills.py b/gui/builtinContextMenus/changeAffectingSkills.py index 32530bdb6..077584e95 100644 --- a/gui/builtinContextMenus/changeAffectingSkills.py +++ b/gui/builtinContextMenus/changeAffectingSkills.py @@ -21,8 +21,8 @@ class ChangeAffectingSkills(ContextMenu): self.charID = fit.character.ID - if self.sChar.getCharName(self.charID) in ("All 0", "All 5"): - return False + #if self.sChar.getCharName(self.charID) in ("All 0", "All 5"): + # return False if srcContext == "fittingShip": fitID = self.mainFrame.getActiveFit() @@ -94,6 +94,7 @@ class ChangeAffectingSkills(ContextMenu): fitID = self.mainFrame.getActiveFit() self.sFit.changeChar(fitID, self.charID) + wx.PostEvent(self.mainFrame, GE.CharListUpdated()) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) ChangeAffectingSkills.register() diff --git a/gui/characterEditor.py b/gui/characterEditor.py index f036313db..1ffa921bb 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -34,6 +34,8 @@ class CharacterEditor(wx.Frame): wx.Frame.__init__ (self, parent, id=wx.ID_ANY, title=u"pyfa: Character Editor", pos=wx.DefaultPosition, size=wx.Size(641, 600), style=wx.DEFAULT_FRAME_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.TAB_TRAVERSAL) + self.mainFrame = parent + i = wx.IconFromBitmap(bitmapLoader.getBitmap("character_small", "icons")) self.SetIcon(i) @@ -45,8 +47,6 @@ class CharacterEditor(wx.Frame): self.navSizer = wx.BoxSizer(wx.HORIZONTAL) sChar = service.Character.getInstance() - charList = sChar.getCharacterList() - charList.sort(key=lambda t: t[1]) self.btnSave = wx.Button(self, wx.ID_SAVE) self.btnSave.Hide() @@ -56,14 +56,17 @@ class CharacterEditor(wx.Frame): self.characterRename.Hide() self.characterRename.Bind(wx.EVT_TEXT_ENTER, self.processRename) - self.skillTreeChoice = wx.Choice(self, wx.ID_ANY, style=0) + self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) + self.navSizer.Add(self.charChoice, 1, wx.ALL | wx.EXPAND, 5) + self.refreshCharacterList() + + ''' for id, name, active in charList: - i = self.skillTreeChoice.Append(name, id) + i = self.charChoice.Append(name, id) if active: - self.skillTreeChoice.SetSelection(i) - - self.navSizer.Add(self.skillTreeChoice, 1, wx.ALL | wx.EXPAND, 5) + self.charChoice.SetSelection(i) + ''' buttons = (("new", wx.ART_NEW), ("rename", bitmapLoader.getBitmap("rename", "icons")), @@ -128,7 +131,16 @@ class CharacterEditor(wx.Frame): self.registerEvents() - self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def refreshCharacterList(self, event=None): + sChar = service.Character.getInstance() + charList = sChar.getCharacterList() + self.charChoice.Clear() + + for id, name, active in charList: + i = self.charChoice.Append(name, id) + if active: + self.charChoice.SetSelection(i) def editingFinished(self, event): del self.disableWin @@ -137,7 +149,8 @@ class CharacterEditor(wx.Frame): def registerEvents(self): self.Bind(wx.EVT_CLOSE, self.closeEvent) - self.skillTreeChoice.Bind(wx.EVT_CHOICE, self.charChanged) + self.Bind(GE.CHAR_LIST_UPDATED, self.refreshCharacterList) + self.charChoice.Bind(wx.EVT_CHOICE, self.charChanged) def closeEvent(self, event): del self.disableWin @@ -147,7 +160,7 @@ class CharacterEditor(wx.Frame): def restrict(self): self.btnRename.Enable(False) self.btnDelete.Enable(False) - self.aview.stDisabledTip.Show(True) + self.aview.stDisabledTip.Show() self.aview.inputID.Enable(False) self.aview.inputKey.Enable(False) self.aview.charChoice.Enable(False) @@ -159,7 +172,7 @@ class CharacterEditor(wx.Frame): def unrestrict(self): self.btnRename.Enable(True) self.btnDelete.Enable(True) - self.aview.stDisabledTip.Show(False) + self.aview.stDisabledTip.Hide() self.aview.inputID.Enable(True) self.aview.inputKey.Enable(True) self.aview.btnFetchCharList.Enable(True) @@ -182,14 +195,14 @@ class CharacterEditor(wx.Frame): event.Skip() def getActiveCharacter(self): - selection = self.skillTreeChoice.GetCurrentSelection() - return self.skillTreeChoice.GetClientData(selection) if selection is not None else None + 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.skillTreeChoice.Append(sChar.getCharName(charID), charID) - self.skillTreeChoice.SetSelection(id) + id = self.charChoice.Append(sChar.getCharName(charID), charID) + self.charChoice.SetSelection(id) self.unrestrict() self.btnSave.SetLabel("Create") self.rename(None) @@ -198,9 +211,9 @@ class CharacterEditor(wx.Frame): def rename(self, event): if event is not None: self.btnSave.SetLabel("Rename") - self.skillTreeChoice.Hide() + self.charChoice.Hide() self.characterRename.Show() - self.navSizer.Replace(self.skillTreeChoice, self.characterRename) + self.navSizer.Replace(self.charChoice, self.characterRename) self.characterRename.SetFocus() for btn in (self.btnNew, self.btnCopy, self.btnRename, self.btnDelete): btn.Hide() @@ -225,9 +238,9 @@ class CharacterEditor(wx.Frame): charID = self.getActiveCharacter() sChar.rename(charID, newName) - self.skillTreeChoice.Show() + self.charChoice.Show() self.characterRename.Hide() - self.navSizer.Replace(self.characterRename, self.skillTreeChoice) + self.navSizer.Replace(self.characterRename, self.charChoice) for btn in (self.btnNew, self.btnCopy, self.btnRename, self.btnDelete): btn.Show() self.navSizer.Add(btn, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) @@ -235,16 +248,13 @@ class CharacterEditor(wx.Frame): self.navSizer.Remove(self.btnSave) self.btnSave.Hide() self.navSizer.Layout() - selection = self.skillTreeChoice.GetCurrentSelection() - self.skillTreeChoice.Delete(selection) - self.skillTreeChoice.Insert(newName, selection, charID) - self.skillTreeChoice.SetSelection(selection) + self.refreshCharacterList() def copy(self, event): sChar = service.Character.getInstance() charID = sChar.copy(self.getActiveCharacter()) - id = self.skillTreeChoice.Append(sChar.getCharName(charID), charID) - self.skillTreeChoice.SetSelection(id) + id = self.charChoice.Append(sChar.getCharName(charID), charID) + self.charChoice.SetSelection(id) self.unrestrict() self.btnSave.SetLabel("Copy") self.rename(None) @@ -253,9 +263,9 @@ class CharacterEditor(wx.Frame): def delete(self, event): sChar = service.Character.getInstance() sChar.delete(self.getActiveCharacter()) - sel = self.skillTreeChoice.GetSelection() - self.skillTreeChoice.Delete(sel) - self.skillTreeChoice.SetSelection(sel - 1) + 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() @@ -366,12 +376,8 @@ class SkillTreeView (wx.Panel): sChar = service.Character.getInstance() charID = self.Parent.Parent.getActiveCharacter() sMkt = service.Market.getInstance() - if sChar.getCharName(charID) not in ("All 0", "All 5"): - self.levelChangeMenu.selection = sMkt.getItem(self.skillTreeListCtrl.GetPyData(item)) - self.PopupMenu(self.levelChangeMenu) - else: - self.statsMenu.selection = sMkt.getItem(self.skillTreeListCtrl.GetPyData(item)) - self.PopupMenu(self.statsMenu) + self.levelChangeMenu.selection = sMkt.getItem(self.skillTreeListCtrl.GetPyData(item)) + self.PopupMenu(self.levelChangeMenu) def changeLevel(self, event): level = self.levelIds.get(event.Id) @@ -383,6 +389,8 @@ class SkillTreeView (wx.Panel): self.skillTreeListCtrl.SetItemText(selection, "Level %d" % level if isinstance(level, int) else level, 1) sChar.changeLevel(charID, skillID, level) + sChar.saveCharacter(charID) + wx.PostEvent(self.Parent.Parent, GE.CharListUpdated()) event.Skip() @@ -547,6 +555,7 @@ class APIView (wx.Panel): u"Please select another character or make a new one.", style=wx.ALIGN_CENTER ) self.stDisabledTip.Wrap( -1 ) hintSizer.Add( self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10 ) + self.stDisabledTip.Hide() hintSizer.AddStretchSpacer() pmainSizer.Add(hintSizer, 0, wx.EXPAND, 5) @@ -652,7 +661,7 @@ class APIView (wx.Panel): sChar = service.Character.getInstance() try: - list = sChar.charList(self.Parent.Parent.getActiveCharacter(), self.inputID.GetLineText(0), self.inputKey.GetLineText(0)) + list = sChar.apiCharList(self.Parent.Parent.getActiveCharacter(), self.inputID.GetLineText(0), self.inputKey.GetLineText(0)) except service.network.AuthenticationError, e: self.stStatus.SetLabel("Authentication failure. Please check keyID and vCode combination.") except service.network.TimeoutError, e: diff --git a/gui/characterSelection.py b/gui/characterSelection.py index 855882e78..c9328750a 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -81,7 +81,6 @@ class CharacterSelection(wx.Panel): choice.Clear() charList = sChar.getCharacterList() - sChar.getCharacterList() picked = False for id, name, active in charList: diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 83bff353a..90eebfac5 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -418,6 +418,8 @@ class MainFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.goWiki, id = menuBar.wikiId) # EVE Forums self.Bind(wx.EVT_MENU, self.goForums, id = menuBar.forumId) + # Save current character + self.Bind(wx.EVT_MENU, self.saveChar, id = menuBar.saveCharId) #Clipboard exports self.Bind(wx.EVT_MENU, self.exportToClipboard, id=wx.ID_COPY) @@ -482,6 +484,12 @@ class MainFrame(wx.Frame): atable = wx.AcceleratorTable(actb) self.SetAcceleratorTable(atable) + def saveChar(self, event): + sChr = service.Character.getInstance() + charID = self.charSelection.getActiveCharacter() + sChr.saveCharacter(charID) + wx.PostEvent(self, GE.CharListUpdated()) + def AdditionsTabSelect(self, event): selTab = self.additionsSelect.index(event.GetId()) diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index 06e1dfabf..756bec594 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -36,6 +36,7 @@ class MainMenuBar(wx.MenuBar): self.exportHtmlId = wx.NewId() self.wikiId = wx.NewId() self.forumId = wx.NewId() + self.saveCharId = wx.NewId() self.mainFrame = gui.mainFrame.MainFrame.getInstance() @@ -70,6 +71,8 @@ class MainMenuBar(wx.MenuBar): pasteText = "&From Clipboard" + ("\tCTRL+V" if 'wxMSW' in wx.PlatformInfo else "") editMenu.Append(wx.ID_COPY, copyText, "Export a fit to the clipboard") editMenu.Append(wx.ID_PASTE, pasteText, "Import a fit from the clipboard") + editMenu.AppendSeparator() + editMenu.Append(self.saveCharId, "Save Character") # Character menu windowMenu = wx.Menu() diff --git a/service/character.py b/service/character.py index 1f2c78900..6e11b2c31 100644 --- a/service/character.py +++ b/service/character.py @@ -171,32 +171,34 @@ class Character(object): thread.start() def all0(self): - all0 = eos.types.Character.getAll0() - eos.db.commit() - return all0 + return eos.types.Character.getAll0() def all0ID(self): return self.all0().ID def all5(self): - all5 = eos.types.Character.getAll5() - eos.db.commit() - return all5 + return eos.types.Character.getAll5() def all5ID(self): return self.all5().ID def getCharacterList(self): baseChars = [eos.types.Character.getAll0(), eos.types.Character.getAll5()] - # Flush incase all0 & all5 weren't in the db yet - eos.db.commit() sFit = service.Fit.getInstance() - return map(lambda c: (c.ID, c.name, c == sFit.character), eos.db.getCharacterList()) + + return map(lambda c: (c.ID, c.name if not c.isDirty else "{} *".format(c.name), c == sFit.character), eos.db.getCharacterList()) def getCharacter(self, charID): char = eos.db.getCharacter(charID) return char + def saveCharacter(self, charID): + """Save edited skills""" + if charID == self.all5ID() or charID == self.all0ID(): + return + char = eos.db.getCharacter(charID) + char.saveLevels() + def getSkillGroups(self): cat = eos.db.getCategory(16) groups = [] @@ -223,18 +225,18 @@ class Character(object): skill = eos.db.getCharacter(charID).getSkill(skillID) return skill.level if skill.learned else "Not learned" - def rename(self, charID, newName): - char = eos.db.getCharacter(charID) - char.name = newName - eos.db.commit() + def getCharName(self, charID): + return eos.db.getCharacter(charID).name def new(self): char = eos.types.Character("New Character") eos.db.save(char) return char.ID - def getCharName(self, charID): - return eos.db.getCharacter(charID).name + def rename(self, charID, newName): + char = eos.db.getCharacter(charID) + char.name = newName + eos.db.commit() def copy(self, charID): char = eos.db.getCharacter(charID) @@ -259,7 +261,7 @@ class Character(object): id, key, default, _ = self.getApiDetails(charID) return id is not "" and key is not "" and default is not "" - def charList(self, charID, userID, apiKey): + def apiCharList(self, charID, userID, apiKey): char = eos.db.getCharacter(charID) char.apiID = userID @@ -306,8 +308,6 @@ class Character(object): else: skill.level = level - eos.db.commit() - def addImplant(self, charID, itemID): char = eos.db.getCharacter(charID) implant = eos.types.Implant(eos.db.getItem(itemID))