view Summary.lua @ 62:fee06221176f

Seperated the config from Core.lua. Many other code cleaning up for readability. Added local references for much used globals. Moved widgets to a different file for readability. Re-added global function for slash command handling since we do need it for our chat-hyperlinks. Fixed queueing to properly use the track at property of virtual groups. Fixed queueing to display the item id instead of the item link if the item link could not be loaded. Speed slider at the summary now has an interval of 1% down from 5% and rounds rather than ceils it?s value so 101% will become 100% rather than 105%. Now using the right stock properties at the summary. Added a help group to the config.
author Zerotorescue
date Wed, 22 Dec 2010 19:56:55 +0100
parents d903b0a151d3
children 7ca83ad9d67a
line wrap: on
line source
local addon = select(2, ...); -- Get a reference to the main addon object
local mod = addon:NewModule("Summary", "AceEvent-3.0", "AceTimer-3.0"); -- register a new module, Summary: resposible for building the summary window

local unknownItemName = "Unknown (#%d)";

local CACHE_ITEMS_TOTAL, CACHE_ITEMS_CURRENT, itemsCache = 0, 0, {};
local AceGUI, cacheStart;

local _G = _G; -- prevent looking up of the global table
local printf, sgsub, supper, tinsert, pairs, ceil, GetItemInfo = _G.string.format, _G.string.gsub, _G.string.upper, _G.table.insert, _G.pairs, _G.ceil, _G.GetItemInfo; -- prevent looking up of the most used globals all the time

function mod:OnEnable()
	-- Register the summary specific widget
	addon:GetModule("Widgets"):InlineGroupWithButton();
	
	AceGUI = LibStub("AceGUI-3.0");
	
	-- Register our own slash commands
	-- /im summary
	addon:RegisterSlash(function()
		mod:BuildMain();
		mod:Build();
	end, { "s", "sum", "summary" }, "|Hfunction:InventoriumCommandHandler:summary|h|cff00fff7/im summary|r|h (or /im s) - Show the summary window containing an overview of all items within the groups tracked at this current character.");
	
	-- /im reset
	addon:RegisterSlash(function()
		if mod.frame then
			mod.frame:SetWidth(650);
			mod.frame:SetHeight(600);
			
			print("Resetting width and height of the summary frame.");
		end
	end, { "r", "reset" }, "|Hfunction:InventoriumCommandHandler:reset|h|cff00fff7/im reset|r|h (or /im r) - Reset the size of the summary frame.");
end

local function ShowTooltip(self)
	-- If this function is called from a widget, self is the widget and self.frame the actual frame
	local this = self.frame or self;
	
	GameTooltip:SetOwner(this, "ANCHOR_NONE");
	GameTooltip:SetPoint("BOTTOM", this, "TOP");
	GameTooltip:SetText(this.tooltipTitle, 1, .82, 0, 1);
	
	if type(this.tooltip) == "string" then
		GameTooltip:AddLine(this.tooltip, 1, 1, 1, 1);
	end
	
	GameTooltip:Show();
end

local function HideTooltip()
	GameTooltip:Hide();
end

function mod:BuildMain()
	LibStub("AceConfigDialog-3.0"):Close("InventoriumOptions");
	
	self:CloseFrame();
	
	-- Main Window
	mod.frame = AceGUI:Create("Frame");
	_G["InventoriumSummary"] = mod.frame; -- name the global frame so it can be put in the UISpecialFrames
	mod.frame:SetTitle("Inventory Summary");
	mod.frame:SetLayout("Fill");
	mod.frame:SetCallback("OnClose", function(widget)
		mod:CancelTimer(self.tmrUpdater, true);
		mod:CloseFrame();
	end);
	mod.frame:SetWidth(addon.db.profile.defaults.summary.width);
	mod.frame:SetHeight(addon.db.profile.defaults.summary.height);
	mod.frame.OnWidthSet = function(_, width)
		addon.db.profile.defaults.summary.width = width;
	end;
	mod.frame.OnHeightSet = function(_, height)
		addon.db.profile.defaults.summary.height = height;
	end;
	
	-- Close on escape
	tinsert(UISpecialFrames, "InventoriumSummary");
	
	-- ScrollFrame child
	mod.scrollFrame = AceGUI:Create("ScrollFrame");
	mod.scrollFrame:SetLayout("Flow");
	
	mod.frame:AddChild(mod.scrollFrame);
	
	-- Reset items cache
	table.wipe(itemsCache);
end

function mod:CloseFrame()
	if mod.frame then
		mod.frame:Release();
		mod.frame = nil;
		
		-- Stop caching
		
		-- Stop timer
		self:CancelTimer(self.tmrUpdater, true);
		
		-- Reset trackers
		CACHE_ITEMS_TOTAL = 0;
		CACHE_ITEMS_CURRENT = 0;
	end
end

local sortMethod = "item";
local sortDirectory = "ASC";
local function ReSort(subject)
	if sortMethod == subject then
		sortDirectory = (sortDirectory == "ASC" and "DESC") or "ASC";
	else
		sortDirectory = "ASC";
	end
	sortMethod = subject;
	
	mod:Build();
end

-- From http://www.wowwiki.com/API_sort
local function pairsByKeys (t, f)
	local a = {}
	for n in pairs(t) do tinsert(a, n) end
		table.sort(a, f)
	local i = 0      -- iterator variable
	local iter = function ()   -- iterator function
		i = i + 1
		if a[i] == nil then return nil
		else return a[i], t[a[i]]
		end
	end
	return iter
end

function mod:Build()
	local buildStartTime, times = GetTime(), {};
	
	-- We are going to add hunderds of widgets to this container, but don't want it to also cause hunderds of reflows, thus pause reflowing and just do it once when everything is prepared
	-- This appears to be required for each container we wish to pause, so also do this for the contents
	mod.scrollFrame:PauseLayout();
	
	mod.scrollFrame:ReleaseChildren();
	
	-- Refresh button
	local btnRefresh = AceGUI:Create("Button");
	btnRefresh:SetText("Refresh");
	btnRefresh:SetRelativeWidth(.2);
	btnRefresh:SetCallback("OnClick", function()
		-- Reset items cache
		table.wipe(itemsCache);
		
		-- Rebuild itemlist and start caching
		mod:Build();
	end);
	btnRefresh:SetCallback("OnEnter", ShowTooltip);
	btnRefresh:SetCallback("OnLeave", HideTooltip);
	btnRefresh.frame.tooltipTitle = "Refresh Cache";
	btnRefresh.frame.tooltip = "Refresh the list and recache the item counts and auction values.";
	
	mod.scrollFrame:AddChild(btnRefresh);
	
	local lblSpacer = AceGUI:Create("Label");
	lblSpacer:SetRelativeWidth(.03);
	
	mod.scrollFrame:AddChild(lblSpacer);
	
	-- Speed slider
	local sdrSpeed = AceGUI:Create("Slider");
	sdrSpeed:SetLabel("Processing speed");
	sdrSpeed:SetSliderValues(0.01, 5, 0.01); -- min, max, interval
	sdrSpeed:SetIsPercent(true);
	sdrSpeed:SetRelativeWidth(.3);
	sdrSpeed:SetCallback("OnMouseUp", function(self, event, value)
		addon.db.profile.defaults.summary.speed = ceil( ( ( value * 100 ) / 5) - .5 );
		
		CACHE_ITEMS_PER_UPDATE = addon.db.profile.defaults.summary.speed; -- max = 20, min = 1
	end);
	sdrSpeed:SetValue( addon.db.profile.defaults.summary.speed * 5 / 100 );
	sdrSpeed:SetCallback("OnEnter", ShowTooltip);
	sdrSpeed:SetCallback("OnLeave", HideTooltip);
	sdrSpeed.frame.tooltipTitle = "Caching Processing Speed";
	sdrSpeed.frame.tooltip = "Change the speed at which item counts and auction values are being cached. Higher is faster but may drastically reduce your FPS while caching.\n\nAnything above 100% will probably become uncomfortable.";
	
	mod.scrollFrame:AddChild(sdrSpeed);
	
	local lblSpacer = AceGUI:Create("Label");
	lblSpacer:SetRelativeWidth(.23);
	
	mod.scrollFrame:AddChild(lblSpacer);
	
	-- Config button
	--[[local btnConfig = AceGUI:Create("Button");
	btnConfig:SetText("Config");
	btnConfig:SetRelativeWidth(.2);
	btnConfig:SetCallback("OnClick", function()
		--TODO: Tidy up
		SlashCmdList["INVENTORIUM"]("config");
	end);
	btnConfig:SetCallback("OnEnter", ShowTooltip);
	btnConfig:SetCallback("OnLeave", HideTooltip);
	btnConfig.frame.tooltipTitle = "Config";
	btnConfig.frame.tooltip = "Click to open the config window. This will close the current window.";
	
	mod.scrollFrame:AddChild(btnConfig);]]
	
	local lblSpacer = AceGUI:Create("Label");
	lblSpacer:SetRelativeWidth(.03);
	
	mod.scrollFrame:AddChild(lblSpacer);
	
	-- Queue all button
	local btnQueueAll = AceGUI:Create("Button");
	btnQueueAll:SetText("Queue All");
	btnQueueAll:SetRelativeWidth(.2);
	btnQueueAll:SetCallback("OnClick", function()
		self:SendMessage("IM_QUEUE_ALL");
	end);
	btnQueueAll:SetCallback("OnEnter", ShowTooltip);
	btnQueueAll:SetCallback("OnLeave", HideTooltip);
	btnQueueAll.frame.tooltipTitle = "Queue all";
	btnQueueAll.frame.tooltip = "Queue everything that requires restocking within every single visible group.";
	
	mod.scrollFrame:AddChild(btnQueueAll);
	
	times.init = ceil( ( GetTime() - buildStartTime ) * 1000 );
	addon:Debug("Time spent legend: (init / sorting / preparing / building / all).");
	
	local playerName = UnitName("player");
	
	-- Go through all our stored groups
	for groupName, values in pairsByKeys(addon.db.profile.groups, function(a, b) return a:lower() < b:lower(); end) do
		local groupStartTime, groupTimes = GetTime(), {};
		
		local trackAt = addon:GetOptionByKey(groupName, "trackAtCharacters");
		
		
		-- Does this group have any items and do we want to track it at this char?
		if values.items and trackAt[playerName] then
		
			
			-- Get group settings
			local minLocalStock = addon:GetOptionByKey(groupName, "minLocalStock");
			local minGlobalStock = addon:GetOptionByKey(groupName, "minGlobalStock");
			local showWhenBelow = addon:GetOptionByKey(groupName, "summaryThresholdShow");
			local priceThreshold = addon:GetOptionByKey(groupName, "priceThreshold");
			local hideWhenBelowPriceThreshold = addon:GetOptionByKey(groupName, "summaryHidePriceThreshold");
			local alwaysGetAuctionValue = addon:GetOptionByKey(groupName, "alwaysGetAuctionValue");
			
			-- Make group container
			local iGroup = AceGUI:Create("InlineGroupWithButton");
			iGroup:PauseLayout();
			iGroup:SetTitle(groupName);
			iGroup:SetFullWidth(true);
			iGroup:SetLayout("Flow");
			iGroup:MakeButton({
				name = "Queue",
				desc = "Queue all items in this group.",
				exec = function()
					print("Queueing all items within " .. groupName .. " craftable by the currently open profession.");
					self:SendMessage("IM_QUEUE_GROUP", groupName);
				end,
			});
			
			
			
			-- Headers
			
			-- Itemlink
			local lblItem = AceGUI:Create("InteractiveLabel");
			lblItem:SetText("|cfffed000Item|r");
			lblItem:SetFontObject(GameFontHighlight);
			lblItem:SetRelativeWidth(.7);
			lblItem:SetCallback("OnClick", function() ReSort("item"); end);
			lblItem:SetCallback("OnEnter", ShowTooltip);
			lblItem:SetCallback("OnLeave", HideTooltip);
			lblItem.frame.tooltipTitle = "Item";
			lblItem.frame.tooltip = "Sort on the item quality, then name.";
			
			iGroup:AddChild(lblItem);
			
			-- Local quantity
			local lblLocal = AceGUI:Create("InteractiveLabel");
			lblLocal:SetText("|cfffed000Loc.|r");
			lblLocal:SetFontObject(GameFontHighlight);
			lblLocal:SetRelativeWidth(.099);
			lblLocal:SetCallback("OnClick", function() ReSort("local"); end);
			lblLocal:SetCallback("OnEnter", ShowTooltip);
			lblLocal:SetCallback("OnLeave", HideTooltip);
			lblLocal.frame.tooltipTitle = "Local stock";
			lblLocal.frame.tooltip = "Sort on the amount of items currently in local stock.";
			
			iGroup:AddChild(lblLocal);
			
			-- Current quantity
			local lblQuantity = AceGUI:Create("InteractiveLabel");
			lblQuantity:SetText("|cfffed000Cur.|r");
			lblQuantity:SetFontObject(GameFontHighlight);
			lblQuantity:SetRelativeWidth(.099);
			lblQuantity:SetCallback("OnClick", function() ReSort("current"); end);
			lblQuantity:SetCallback("OnEnter", ShowTooltip);
			lblQuantity:SetCallback("OnLeave", HideTooltip);
			lblQuantity.frame.tooltipTitle = "Current stock";
			lblQuantity.frame.tooltip = "Sort on the amount of items currently in stock.";
			
			iGroup:AddChild(lblQuantity);
			
			-- Lowest value
			local lblValue = AceGUI:Create("InteractiveLabel");
			lblValue:SetText("|cfffed000Value|r");
			lblValue:SetFontObject(GameFontHighlight);
			lblValue:SetRelativeWidth(.099);
			lblValue:SetCallback("OnClick", function() ReSort("value"); end);
			lblValue:SetCallback("OnEnter", ShowTooltip);
			lblValue:SetCallback("OnLeave", HideTooltip);
			lblValue.frame.tooltipTitle = "Value";
			lblValue.frame.tooltip = "Sort on the item auction value.";
			
			iGroup:AddChild(lblValue);
			
			
			-- Retrieve items list
			if not itemsCache[groupName] then
				itemsCache[groupName] = {};
				
				-- Sort item list
				for itemId, _ in pairs(values.items) do
					local itemName, itemLink, itemRarity = GetItemInfo(itemId);
					
					tinsert(itemsCache[groupName], {
						id = itemId,
						name = itemName or printf(unknownItemName, itemId),
						link = itemLink,
						value = ((priceThreshold == 0 and not alwaysGetAuctionValue) and -4) or -3,-- if (no price threshold is set for this item and you don't want to always get auction value), then don't look it up either --addon:GetAuctionValue(itemLink),
						rarity = itemRarity or 1,
						count = -3,--addon:GetItemCount(itemId, groupName),
						localCount = -3,
						set = {},
					});
					CACHE_ITEMS_TOTAL = CACHE_ITEMS_TOTAL + 1;
				end
			end
			
			groupTimes.init = ceil( ( GetTime() - groupStartTime ) * 1000 );
			
			
			
			-- Sort items
			table.sort(itemsCache[groupName], function(a, b)
				if sortMethod == "item" and a.rarity == b.rarity then
					-- Do a name-compare for items of the same rarity
					-- Otherwise epics first, then junk
					if sortDirectory == "ASC" then
						return supper(a.name) < supper(b.name);
					else
						return supper(a.name) > supper(b.name);
					end
				elseif sortMethod == "item"  then
					if sortDirectory == "ASC" then
						return a.rarity > b.rarity; -- the comparers were reversed because we want epics first
					else
						return a.rarity < b.rarity; -- the comparers were reversed because we want epics first
					end
				elseif sortMethod == "current" then
					if sortDirectory == "ASC" then
						return a.count < b.count;
					else
						return a.count > b.count;
					end
				elseif sortMethod == "local" then
					if sortDirectory == "ASC" then
						return a.localCount < b.localCount;
					else
						return a.localCount > b.localCount;
					end
				elseif sortMethod == "value" then
					if sortDirectory == "ASC" then
						return a.value < b.value;
					else
						return a.value > b.value;
					end
				end
			end);
			
			groupTimes.sorting = ceil( ( GetTime() - groupStartTime ) * 1000 );
			
			
			
			
			-- Show itemslist
			for i, item in pairs(itemsCache[groupName]) do
				-- Go through all items for this group
				
				if (( item.count / minGlobalStock ) < showWhenBelow or ( item.localCount / minLocalStock ) < showWhenBelow) and (not hideWhenBelowPriceThreshold or priceThreshold == 0 or item.value < 0 or item.value >= priceThreshold) then
																-- if the option "hide when below threshold" is disabled or no price threshold is set or the value is above the price threshold or the value could not be determined, proceed
					
					local btnItemLink = AceGUI:Create("ItemLinkButton");
					btnItemLink:SetUserData("exec", function(_, itemId, _, buttonName)
						local itemName, itemLink = GetItemInfo(itemId);
						
						if buttonName == "LeftButton" and IsShiftKeyDown() and itemLink then
							ChatEdit_InsertLink(itemLink);
						elseif buttonName == "LeftButton" and IsAltKeyDown() and itemName and AuctionFrame and AuctionFrame:IsShown() and AuctionFrameBrowse then
							-- Start search at page 0
							AuctionFrameBrowse.page = 0;
							
							-- Set the search field (even though we could use "ChatEdit_InsertLink", this ensures the right field is updated)
							BrowseName:SetText(itemName)
							
							QueryAuctionItems(itemName, nil, nil, 0, 0, 0, 0, 0, 0);
						end
					end);
					btnItemLink:SetRelativeWidth(.7);
					btnItemLink:SetItemId(item.id);
					
					iGroup:AddChild(btnItemLink);
					
					-- Local quantity
					local lblLocal = AceGUI:Create("Label");
					lblLocal:SetText(self:DisplayItemCount(item.localCount, minLocalStock));
					lblLocal:SetRelativeWidth(.099);
					
					iGroup:AddChild(lblLocal);
					
					-- Current quantity
					local lblQuantity = AceGUI:Create("Label");
					lblQuantity:SetText(self:DisplayItemCount(item.count, minGlobalStock));
					lblQuantity:SetRelativeWidth(.099);
					
					iGroup:AddChild(lblQuantity);
					
					-- Value
					local lblValue = AceGUI:Create("Label");
					lblValue:SetText(self:DisplayMoney(item.value, priceThreshold));
					lblValue:SetRelativeWidth(.099);
					
					iGroup:AddChild(lblValue);
					
					-- Remember references to the value and current fields so we can fill them later
					if item.set then
						-- -3 means the price is unknown, queue look up
						if item.value == -3 then
							item.set.value = lblValue;
						end
						if item.count == -3 then
							item.set.current = lblQuantity;
						end
						if item.localCount == -3 then
							item.set.localCount = lblLocal;
						end
						
						-- Don't queue if we already know everything we want to know
						if item.value ~= -3 and item.count ~= -3 and item.localCount ~= -3 then
							item.set = nil;
						end
					end
				end
			end
			
			groupTimes.preparing = ceil( ( GetTime() - groupStartTime ) * 1000 );
			
			iGroup:ResumeLayout();
			mod.scrollFrame:AddChild(iGroup); -- this can take up to .5 seconds, might need to look into again at a later time
			
			groupTimes.building = ceil( ( GetTime() - groupStartTime ) * 1000 );
		end
		
		if groupStartTime and groupTimes then
			addon:Debug(printf("Building of %s took %d ms (%d / %d / %d / %d / %d).", groupName, ceil( ( GetTime() - groupStartTime ) * 1000 ), groupTimes.init or 0, groupTimes.sorting or 0, groupTimes.preparing or 0, groupTimes.building or 0, ceil( ( GetTime() - buildStartTime ) * 1000 )));
		end
	end
	
	mod.scrollFrame:ResumeLayout();
	mod.scrollFrame:DoLayout();
	
	addon:Debug(printf("Done building summary after %d ms.", ceil( ( GetTime() - buildStartTime ) * 1000 )));
	
	if CACHE_ITEMS_TOTAL > 0 then
		cacheStart = GetTime();
		self:CancelTimer(self.tmrUpdater, true);
		self.tmrUpdater = self:ScheduleRepeatingTimer("UpdateNextItem", .01); -- Once every 100 frames (or once every x frames if you have less than 100 FPS, basically, once every frame)
	end
end

function mod:UpdateNextItem()
	local i = 0;
	
	for groupName, items in pairs(itemsCache) do
		local minGlobalStock = addon:GetOptionByKey(groupName, "minGlobalStock");
		local minLocalStock = addon:GetOptionByKey(groupName, "minLocalStock");
		local priceThreshold = addon:GetOptionByKey(groupName, "priceThreshold");
		
		for _, item in pairs(items) do
			if item.set then
				if item.count == -3 then
					-- Only if item count was queued, update it
					item.count = addon:GetItemCount(item.id, groupName);
					if item.set.current and item.set.current.SetText then
						item.set.current:SetText(self:DisplayItemCount(item.count, minGlobalStock));
					end
				end
				
				if item.localCount == -3 then
					-- Only if item count was queued, update it
					item.localCount = addon:GetLocalItemCount(item.id, groupName);
					if item.set.localCount and item.set.localCount.SetText then
						item.set.localCount:SetText(self:DisplayItemCount(item.localCount, minLocalStock));
					end
				end
				
				if item.value == -3 then
					-- Only if item value was queued, update it
					
					-- The itemlink might not have been loaded yet in which case we retry
					item.link = item.link or select(2, GetItemInfo(item.id));
					
					if item.link then
						item.value = addon:GetAuctionValue(item.link, groupName);
						if item.set.value and item.set.value.SetText then
							item.set.value:SetText(self:DisplayMoney(item.value, priceThreshold));
						end
					end
				end
				
				item.set = nil;
				
				i = i + 1;
				CACHE_ITEMS_CURRENT = CACHE_ITEMS_CURRENT + 1;
				
				if mod.frame then
					mod.frame:SetStatusText(printf("Caching auction values and item-counts... %d%% has already been processed.", floor(CACHE_ITEMS_CURRENT / CACHE_ITEMS_TOTAL * 100)));
				end
				
				if i >= addon.db.profile.defaults.summary.speed then
					return;
				end
			end
		end
	end
	
	-- Reset trackers
	CACHE_ITEMS_TOTAL = 0;
	CACHE_ITEMS_CURRENT = 0;
	
	-- Stop timer
	self:CancelTimer(self.tmrUpdater, true);
		
	-- Rebuild list so hidden items due to too low prices get added
	self:Build();
	
	-- Announce
	mod.frame:SetStatusText("All required prices and itemcounts have been cached. This process took " .. ceil(GetTime() - cacheStart) .. " seconds.");
	
	-- Forget time
	cacheStart = nil;
end

function mod:ColorCode(num, required)
	local percentage = ( num / required );
	
	if percentage >= addon.db.profile.defaults.colors.green then
		return printf("|cff00ff00%d|r", num);
	elseif percentage >= addon.db.profile.defaults.colors.yellow then
		return printf("|cffffff00%d|r", num);
	elseif percentage >= addon.db.profile.defaults.colors.orange then
		return printf("|cffff9933%d|r", num);
	elseif percentage >= addon.db.profile.defaults.colors.red then
		return printf("|cffff0000%d|r", num);
	end
end

function mod:DisplayMoney(value, priceThreshold)
	if value == -1 then
		return "|cff0000ffNone up|r";
	elseif value == -2 then
		return "|cff0000ffNo AH mod|r";
	elseif value == -3 then
		return "|cffffff00Unknown|r";
	elseif value == -4 then
		return "|cff00ff00-|r";
	elseif value == -5 then
		return "|cffff9933Error|r";
	elseif priceThreshold and value < priceThreshold then
		return printf("|cffaaaaaa%s|r", sgsub(addon:ReadableMoney(value or 0, true), "|([a-fA-F0-9]+)([gsc]+)|r", "%2"));
	else
		return addon:ReadableMoney(value or 0, true);
	end
end

function mod:DisplayItemCount(value, minimumStock)
	if value == -1 then
		return "|cffffff00Unknown|r";
	elseif value == -3 then
		return "|cffffff00Unknown|r";
	else
		return printf("%s / %d", self:ColorCode(value, minimumStock), minimumStock);
	end
end

function mod:NumberFormat(num)
	local formatted = sgsub(num, "(%d)(%d%d%d)$", "%1,%2", 1);
	
	while true do
		formatted, matches = sgsub(formatted, "(%d)(%d%d%d),", "%1,%2,", 1);
		
		if matches == 0 then
			break;
		end
	end
	
	return formatted;
end