From 32f417ce5a9eec8f2423789718e722dcb228612c Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 23 Apr 2017 20:20:04 -0400 Subject: [PATCH] Created first iteration of "Recent Fits" view, in which a nav button is interacted with to show a list of the 100 most recently modified fits. Ship Broswer is littered with ToggleRecentShips() to reset the icon when not in "recent" mode. This should probably be fixed at some point. Removed the FIT_CHANGED binding from FitItem - this was causing very odd issues when the object was destroyed (navigating to another "stage") such as the Fit Changed event for that fit no longer firing (or at least seemingly so) To fix this, simply look at the active fit during FitItem.Refresh() Also creates a new query to get a list of items from the DB, although it's not used (was gonna use it, decided against it, but didn't want to delete the code - could prove handy later) --- eos/db/gamedata/queries.py | 31 +++++++++++++- eos/db/saveddata/fit.py | 17 ++++---- eos/db/saveddata/module.py | 13 +++--- eos/db/saveddata/queries.py | 17 ++++++++ gui/mainFrame.py | 12 +++++- gui/shipBrowser.py | 81 ++++++++++++++++++++++++++---------- imgs/gui/frecent_small.png | Bin 0 -> 793 bytes service/fit.py | 14 +++++++ 8 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 imgs/gui/frecent_small.png diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index 30f51fe6b..7c5ca3397 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -27,12 +27,11 @@ from eos.db.gamedata.group import groups_table from eos.db.util import processEager, processWhere from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, MetaGroup, AttributeInfo, MetaData +cache = {} configVal = getattr(eos.config, "gamedataCache", None) if configVal is True: def cachedQuery(amount, *keywords): def deco(function): - cache = {} - def checkAndReturn(*args, **kwargs): useCache = kwargs.pop("useCache", True) cacheKey = [] @@ -98,6 +97,34 @@ def getItem(lookfor, eager=None): return item +@cachedQuery(1, "lookfor") +def getItems(lookfor, eager=None): + """ + Gets a list of items. Does a bit of cache hackery to get working properly -- cache + is usually based on function calls with the parameters, needed to extract data directly. + Works well enough. Not currently used, but it's here for possible future inclusion + """ + + toGet = [] + results = [] + + for id in lookfor: + if (id, None) in cache: + results.append(cache.get((id, None))) + else: + toGet.append(id) + + if len(toGet) > 0: + # Get items that aren't currently cached, and store them in the cache + items = gamedata_session.query(Item).filter(Item.ID.in_(toGet)).all() + for item in items: + cache[(item.ID, None)] = item + results += items + + # sort the results based on the original indexing + results.sort(key=lambda x: lookfor.index(x.ID)) + return results + @cachedQuery(1, "lookfor") def getAlphaClone(lookfor, eager=None): if isinstance(lookfor, int): diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 9d8de9b7f..5f3c12879 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -18,7 +18,6 @@ # =============================================================================== import datetime -from sqlalchemy import inspect from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql import and_ @@ -244,7 +243,7 @@ mapper(ProjectedFit, projectedFits_table, properties={ "_ProjectedFit__amount": projectedFits_table.c.amount, } - ) +) mapper(CommandFit, commandFits_table) @@ -253,18 +252,22 @@ def rel_listener(target, value, initiator): if not target or (isinstance(value, Module) and value.isEmpty): return - print "{} has has has a relationship changes :(".format(target) + print "{} has had a relationship change :D".format(target) target.modified = datetime.datetime.now() def load_listener(target, context): - # We only want to se these events when the fit is first loaded (otherwise events will fire during the initial + # We only want to see these events when the fit is first loaded (otherwise events will fire during the initial # population of data). This sets listeners for all the relationships on fits. This allows us to update the fit's # modified date whenever something is added/removed from fit # See http://docs.sqlalchemy.org/en/rel_1_0/orm/events.html#sqlalchemy.orm.events.InstanceEvents.load - for rel in inspect(es_Fit).relationships: - listen(rel, 'append', rel_listener) - listen(rel, 'remove', rel_listener) + + # todo: when we can, move over to `inspect(es_Fit).relationships` (when mac binaries are updated) + manager = getattr(es_Fit, "_sa_class_manager", None) + if manager: + for rel in manager.mapper.relationships: + listen(rel, 'append', rel_listener) + listen(rel, 'remove', rel_listener) listen(Module, 'load', load_listener) diff --git a/eos/db/saveddata/module.py b/eos/db/saveddata/module.py index 6e4da73b4..6993e53fa 100644 --- a/eos/db/saveddata/module.py +++ b/eos/db/saveddata/module.py @@ -17,7 +17,6 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import inspect from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime, select from sqlalchemy.orm import relation, mapper from sqlalchemy.event import listen @@ -55,16 +54,20 @@ def update_fit_modified(target, value, oldvalue, initiator): target.owner.modified = datetime.datetime.now() -def my_load_listener(target, context): +def load_listener(target, context): # We only want to se these events when the module is first loaded (otherwise events will fire during the initial # population of data). This runs through all columns and sets up "set" events on each column. We do it with each # column because the alternative would be to do a before/after_update for the Mapper itself, however we're only # allowed to change the local attributes during those events as that's inter-flush. # See http://docs.sqlalchemy.org/en/rel_1_0/orm/session_events.html#mapper-level-events - for col in inspect(Module).column_attrs: - listen(col, 'set', update_fit_modified) + + # @todo replace with `inspect(Module).column_attrs` when mac binaries are updated + manager = getattr(Module, "_sa_class_manager", None) + if manager: + for col in manager.mapper.column_attrs: + listen(col, 'set', update_fit_modified) -listen(Module, 'load', my_load_listener) +listen(Module, 'load', load_listener) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 1088c1b0f..e82d2f744 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -18,6 +18,7 @@ # =============================================================================== from sqlalchemy.sql import and_ +from sqlalchemy import desc, select from eos.db import saveddata_session, sd_lock from eos.db.saveddata.fit import projectedFits_table @@ -242,6 +243,22 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None): return fits +def getRecentFits(ownerID=None, where=None, eager=None): + eager = processEager(eager) + with sd_lock: + q = select(( + Fit.ID, + Fit.shipID, + Fit.name, + Fit.modified, + Fit.created, + Fit.timestamp + )).order_by(desc(Fit.modified), desc(Fit.timestamp)).limit(50) + fits = eos.db.saveddata_session.execute(q).fetchall() + + return fits + + def getFitsWithModules(typeIDs, eager=None): """ Get all the fits that have typeIDs fitted to them diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 5e1ad08c6..8ef05641c 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -20,6 +20,7 @@ import sys import os.path from logbook import Logger +import datetime import sqlalchemy # noinspection PyPackageRequirements @@ -919,7 +920,16 @@ class MainFrame(wx.Frame): wx.PostEvent(self, FitSelected(fitID=fit.ID)) wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True)) else: - wx.PostEvent(self.shipBrowser, ImportSelected(fits=fits, back=True)) + fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) + results = [] + for fit in fits: + results.append(( + fit.ID, + fit.name, + fit.modified or fit.created or datetime.datetime.fromtimestamp(fit.timestamp), + fit.ship.item + )) + wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True)) def closeProgressDialog(self): # Windows apparently handles ProgressDialogs differently. We can diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index 59cbd96b9..78f12b26b 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -338,15 +338,21 @@ class NavigationPanel(SFItem.SFBrowserItem): self.newBmpH = BitmapLoader.getBitmap("fit_add_small", "gui") self.resetBmpH = BitmapLoader.getBitmap("freset_small", "gui") self.switchBmpH = BitmapLoader.getBitmap("fit_switch_view_mode_small", "gui") + self.recentBmpH = BitmapLoader.getBitmap("frecent_small", "gui") switchImg = BitmapLoader.getImage("fit_switch_view_mode_small", "gui") switchImg = switchImg.AdjustChannels(1, 1, 1, 0.4) self.switchBmpD = wx.BitmapFromImage(switchImg) + recentImg = BitmapLoader.getImage("frecent_small", "gui") + recentImg = recentImg.AdjustChannels(1, 1, 1, 0.4) + self.recentBmpD = wx.BitmapFromImage(recentImg) + self.resetBmp = self.AdjustChannels(self.resetBmpH) self.rewBmp = self.AdjustChannels(self.rewBmpH) self.searchBmp = self.AdjustChannels(self.searchBmpH) self.switchBmp = self.AdjustChannels(self.switchBmpH) + self.recentBmp = self.AdjustChannels(self.recentBmpH) self.newBmp = self.AdjustChannels(self.newBmpH) self.toolbar.AddButton(self.resetBmp, "Ship groups", clickCallback=self.OnHistoryReset, @@ -357,6 +363,9 @@ class NavigationPanel(SFItem.SFBrowserItem): self.btnSwitch = self.toolbar.AddButton(self.switchBmpD, "Hide empty ship groups", clickCallback=self.ToggleEmptyGroupsView, hoverBitmap=self.switchBmpH, show=False) + self.btnRecent = self.toolbar.AddButton(self.recentBmpD, "Recent Fits", + clickCallback=self.ToggleRecentShips, hoverBitmap=self.recentBmpH, + show=True) modifier = "CTRL" if 'wxMac' not in wx.PlatformInfo else "CMD" self.toolbar.AddButton(self.searchBmp, "Search fittings ({}+F)".format(modifier), clickCallback=self.ToggleSearchBox, @@ -416,6 +425,27 @@ class NavigationPanel(SFItem.SFBrowserItem): def OnResize(self, event): self.Refresh() + def ToggleRecentShips(self, bool = None, emitEvent = True): + # this is so janky. Need to revaluate pretty much entire ship browser. >.< + toggle = bool if bool is not None else not self.shipBrowser.recentFits + + if not toggle: + self.shipBrowser.recentFits = False + self.btnRecent.label = "Recent Fits" + self.btnRecent.normalBmp = self.recentBmpD + + if emitEvent: + wx.PostEvent(self.shipBrowser, Stage1Selected()) + else: + self.shipBrowser.recentFits = True + self.btnRecent.label = "Hide Recent Fits" + self.btnRecent.normalBmp = self.recentBmp + + if emitEvent: + sFit = Fit.getInstance() + fits = sFit.getRecentFits() + wx.PostEvent(self.shipBrowser, ImportSelected(fits=fits, back=True, recent=True)) + def ToggleEmptyGroupsView(self): if self.shipBrowser.filterShipsWithNoFits: self.shipBrowser.filterShipsWithNoFits = False @@ -454,11 +484,13 @@ class NavigationPanel(SFItem.SFBrowserItem): wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID)) def OnHistoryReset(self): + self.ToggleRecentShips(False, False) if self.shipBrowser.browseHist: self.shipBrowser.browseHist = [] self.gotoStage(1, 0) def OnHistoryBack(self): + self.ToggleRecentShips(False, False) if len(self.shipBrowser.browseHist) > 0: stage, data = self.shipBrowser.browseHist.pop() self.gotoStage(stage, data) @@ -538,6 +570,7 @@ class NavigationPanel(SFItem.SFBrowserItem): self.bkBitmap.mFactor = mFactor def gotoStage(self, stage, data=None): + self.shipBrowser.recentFits = False if stage == 1: wx.PostEvent(self.Parent, Stage1Selected()) elif stage == 2: @@ -573,6 +606,7 @@ class ShipBrowser(wx.Panel): self._stage3ShipName = "" self.fitIDMustEditName = -1 self.filterShipsWithNoFits = False + self.recentFits = False self.racesFilter = {} @@ -629,7 +663,8 @@ class ShipBrowser(wx.Panel): def RefreshList(self, event): stage = self.GetActiveStage() - if stage == 3 or stage == 4: + + if stage in (3, 4, 5): self.lpane.RefreshList(True) event.Skip() @@ -672,6 +707,7 @@ class ShipBrowser(wx.Panel): return self.racesFilter[race] def stage1(self, event): + self.navpanel.ToggleRecentShips(False, False) self._lastStage = self._activeStage self._activeStage = 1 self.lastdata = 0 @@ -727,6 +763,7 @@ class ShipBrowser(wx.Panel): def stage2Callback(self, data): if self.GetActiveStage() != 2: return + self.navpanel.ToggleRecentShips(False, False) categoryID = self._stage2Data ships = list(data[1]) @@ -812,7 +849,7 @@ class ShipBrowser(wx.Panel): return info[1] def stage3(self, event): - + self.navpanel.ToggleRecentShips(False, False) self.lpane.ShowLoading(False) # If back is False, do not append to history. This could be us calling @@ -921,6 +958,10 @@ class ShipBrowser(wx.Panel): self.Layout() def importStage(self, event): + """ + The import stage handles both displaying fits after importing as well as displaying recent fits. todo: need to + reconcile these two better into a more uniform function, right now hacked together to get working + """ self.lpane.ShowLoading(False) self.navpanel.ShowNewFitButton(False) @@ -934,29 +975,26 @@ class ShipBrowser(wx.Panel): fits = event.fits - # sort by ship name, then fit name - fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) - self.lastdata = fits self.lpane.Freeze() self.lpane.RemoveAllChildren() if fits: for fit in fits: - shipTrait = fit.ship.item.traits.traitText if (fit.ship.item.traits is not None) else "" - # empty string if no traits + shipItem = fit[3] + shipTrait = shipItem.traits.traitText if (shipItem.traits is not None) else "" self.lpane.AddWidget(FitItem( self.lpane, - fit.ID, + fit[0], ( - fit.ship.item.name, + shipItem.name, shipTrait, - fit.name, - fit.booster, - fit.modified, + fit[1], + False, + fit[2] ), - fit.ship.item.ID, + shipItem.ID, )) self.lpane.RefreshList(doFocus=False) self.lpane.Thaw() @@ -1534,7 +1572,6 @@ class FitItem(SFItem.SFBrowserItem): self.tcFitName.Bind(wx.EVT_TEXT_ENTER, self.renameFit) self.tcFitName.Bind(wx.EVT_KILL_FOCUS, self.editLostFocus) self.tcFitName.Bind(wx.EVT_KEY_DOWN, self.editCheckEsc) - self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnMouseCaptureLost) self.animTimerId = wx.NewId() @@ -1567,13 +1604,6 @@ class FitItem(SFItem.SFBrowserItem): self.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) self.Bind(wx.EVT_MIDDLE_UP, self.OpenNewTab) - def OnFitChanged(self, evt): - if evt.fitID == self.fitID: - sFit = Fit.getInstance() - fit = sFit.getFit(evt.fitID) - if fit.modified: - self.timestamp = fit.modified - def OpenNewTab(self, evt): self.selectFit(newTab=True) @@ -1960,6 +1990,15 @@ class FitItem(SFItem.SFBrowserItem): state = SFItem.SB_ITEM_NORMAL return state + def Refresh(self): + activeFit = self.mainFrame.getActiveFit() + if activeFit == self.fitID: + sFit = Fit.getInstance() + fit = sFit.getFit(activeFit) + self.timestamp = fit.modified + + SFItem.SFBrowserItem.Refresh(self) + def RenderBackground(self): rect = self.GetRect() diff --git a/imgs/gui/frecent_small.png b/imgs/gui/frecent_small.png new file mode 100644 index 0000000000000000000000000000000000000000..911da3f1d31fca4494a4beb22014e5c5c724c236 GIT binary patch literal 793 zcmV+!1LpjRP)`EB*FHYdKr%;k=xO&(k^EfNlSiKZ>5l+xr|%SFOV@6-ysFmD2F5 ze93OiS+LaQym;|2f6tbH%~V`D+ND?vc>4J^KSLxEMifJQ`8>*~y^+pGr&o-n=LJ zGWB(yB#;DR8&Lhqi{0(#wc#SwSB~jZKzIFx`8od>2Fo-Pfe7*M8^q#qw2yTxiXzd~ zRaz|F*rr78G`JZXG*YX~5K@5k>G@0HdlBo-6v1jHye?%qRwO@+-hO7J^4LlPG>@A1#{ zQFl4x7tnG)+cz_2Mq_f*H!U)kgg{iHqxT)Yr3ec@K!`)z_%h1c0Y2Eu(dMPkrhq5v zY+bKWfx|sTiOEB71HuwS*CDzA%ReBv4*7Zy7RM0n6`5#chfOJK`ze)Y6>d6Z?UmyNHH!3DdsP-ARyDo}1HO+>7_um7 zx_gj{+_aU_OUH_~Jd?KI#ICZujD`of2mDpCv_zFGE%6}tfL|j!WGu_i-u>4%{%d{$ X7`zMSfT21V00000NkvXXu0mjfkBx0` literal 0 HcmV?d00001 diff --git a/service/fit.py b/service/fit.py index 112a790c9..4d2ac94ca 100644 --- a/service/fit.py +++ b/service/fit.py @@ -98,6 +98,20 @@ class Fit(object): return names + @staticmethod + def getRecentFits(): + """ Fetches recently modified fits, used with shipBrowser """ + pyfalog.debug("Fetching recent fits") + fits = eos.db.getRecentFits() + returnInfo = [] + + for fit in fits: + item = eos.db.getItem(fit[1]) + returnInfo.append((fit[0], fit[2], fit[3] or fit[4] or datetime.datetime.fromtimestamp(fit[5]), item)) + # ID name timestamps + + return returnInfo + @staticmethod def getFitsWithModules(typeIDs): """ Lists fits flagged as booster """