import "Turbine";
import "Turbine.Gameplay";
import "Turbine.UI";
import "Turbine.UI.Extensions";
import "Turbine.UI.Lotro";
import "Turbine.Utils";
import "FrostyPlugins.CustomPluginsManager.PluginSlot"
import "FrostyPlugins.CustomPluginsManager.TooltipWindow"
import "FrostyPlugins.CustomPluginsManager.IconEntryWindow"
-- -----------------------------------------------------------------------
-- CustomPluginsManager is the main window that handles all of the plugins.
-- there are tabs for the currently loaded and available plugins. you can
-- enable and disable plugins and check the state of the plugins.
--
-- ToDo:
-- . is there a way to programatically determine what the icons are ?
-- -----------------------------------------------------------------------
CustomPluginsManager = class( Turbine.UI.Window);
-- --------------------------------------------------------------
-- Constructor
--
-- Description: this is where the window is laid out, the fields
-- and buttons are created, and events are trapped.
-- --------------------------------------------------------------
function CustomPluginsManager:Constructor()
Turbine.UI.Window.Constructor( self );
-- load the last settings saved
self:LoadSettings();
-- static - the maximum number of plugin slot containers per page
self.MAX_SLOTS = 16;
--self.MAX_SLOTS = 4;
-- has the mouse been moved?
self.dragged = false;
-- this is the list of plugin containers
self.pluginContainers = { };
-- the index of the set of plugins displayed on the UI
self.displayedPluginsIndex = 1;
-- the total number of valid plugins that we know about
self.validPluginCount = 0;
-- the click bias for determining if we are moving the window
-- the units of measure is pixels
self.clickMoveBias = 7;
-- the move bias for determining if we are swiping the window
-- the units of measure is pixels
self.clickSwipeBias = 20;
-- --------------------------------------------
-- window layout
-- --------------------------------------------
-- set window properties
self:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/ManagerBackground.tga" );
self:SetBackColorBlendMode( Turbine.UI.BlendMode.Overlay );
self:SetSize( 172, 255 );
self:SetPosition( self.settings.positionX, self.settings.positionY );
self:SetOpacity( 1 );
self:SetAllowDrop( false );
self:SetMouseVisible( true );
-- create the "close window" button. why don't people just use escape?
self.closeButton = Turbine.UI.Button();
self.closeButton:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/CloseButtonNormal.tga" );
self.closeButton:SetBlendMode( Turbine.UI.BlendMode.Overlay );
self.closeButton:SetParent( self );
self.closeButton:SetPosition( 158, 0 );
self.closeButton:SetSize( 14, 14 );
self.closeButton:SetVisible( true );
self.closeButton.MouseEnter = function( sender, args )
self.closeButton:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/CloseButtonHilight.tga" );
end
self.closeButton.MouseLeave = function( sender, args )
self.closeButton:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/CloseButtonNormal.tga" );
end
self.closeButton.Click = function( sender, args )
self:SetVisible( false );
end
-- create the various plugin slots
-- we are always going to create a 4x4 set of plugins icons
local index = 1;
local rowCount = 4;
local columnCount = 4;
for i = 1, rowCount do
for j = 1, columnCount do
local pluginContainer = FrostyPlugins.CustomPluginsManager.PluginSlotContainer();
local x = -22 + j * 37;
local y = -35 + i * 50;
-- the constructor of PluginSlot sets most things up - we just need
-- to know where on the UI this thing is placed
pluginContainer:SetParent( self );
pluginContainer:SetPosition( x, y );
pluginContainer:SetVisible( false );
pluginContainer:SetOwner( self );
pluginContainer.MouseEnter = function( sender, args )
if( pluginContainer.pluginSlot.PluginName ~= "" ) then
local x, y = self:GetPosition();
self.TooltipWindow:SetPosition( x + 172, y );
self.TooltipWindow:Initialize( pluginContainer.pluginSlot );
end
end
pluginContainer.MouseLeave = function( sender, args )
if( pluginContainer.pluginSlot.PluginName ~= "" ) then
self.TooltipWindow:Shutdown();
end
end
-- save the editor
self.pluginContainers[ index ] = pluginContainer;
index = index + 1;
end
end
-- near the bottom of the screen are the previous and next page buttons
-- they will only be displayed when there are items to be paged
self.previousPage = Turbine.UI.Button();
self.previousPage:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/PageLeft.tga" );
self.previousPage:SetBackColorBlendMode( Turbine.UI.BlendMode.Overlay );
self.previousPage:SetParent( self );
self.previousPage:SetPosition( 5, 220 );
self.previousPage:SetSize( 20, 20 );
self.previousPage:SetVisible( false );
self.previousPage.MouseEnter = function( sender, args )
self.previousPage:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/PageLeftHilight.tga" );
end
self.previousPage.MouseLeave = function( sender, args )
self.previousPage:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/PageLeft.tga" );
end
self.previousPage.Click = function( sender, args )
self:AdjustVisiblePlugins( -self.MAX_SLOTS );
end
self.nextPage = Turbine.UI.Button();
self.nextPage:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/PageRight.tga" );
self.nextPage:SetBackColorBlendMode( Turbine.UI.BlendMode.Overlay );
self.nextPage:SetParent( self );
self.nextPage:SetPosition( 147, 220 );
self.nextPage:SetSize( 20, 20 );
self.nextPage:SetVisible( false );
self.nextPage.MouseEnter = function( sender, args )
self.nextPage:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/PageRightHilight.tga" );
end
self.nextPage.MouseLeave = function( sender, args )
self.nextPage:SetBackground( "FrostyPlugins/CustomPluginsManager/Resources/PageRight.tga" );
end
self.nextPage.Click = function( sender, args )
self:AdjustVisiblePlugins( self.MAX_SLOTS );
end
-- label indicating the pages
self.statusBar = Turbine.UI.Label();
self.statusBar:SetMouseVisible( false );
self.statusBar:SetMultiline( false );
self.statusBar:SetFont( Turbine.UI.Lotro.Font.Verdana14 );
self.statusBar:SetParent( self );
self.statusBar:SetPosition( 31, 220 );
self.statusBar:SetSize( 110, 20 );
self.statusBar:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
self.statusBar:SetVisible( true );
-- the tooltip window is always off to the right
self.TooltipWindow = FrostyPlugins.CustomPluginsManager.TooltipWindow();
self.TooltipWindow:SetPosition( self.settings.positionX + 172, self.settings.positionY );
self.TooltipWindow:SetVisible( false );
-- the icon entry window is always off to the right
self.IconEntryWindow = FrostyPlugins.CustomPluginsManager.IconEntryWindow();
self.IconEntryWindow:SetPosition( self.settings.positionX + 172, self.settings.positionY );
self.IconEntryWindow:SetVisible( false );
-- --------------------------------------------
-- event handling
-- --------------------------------------------
-- make sure we listen for key presses
self:SetWantsKeyEvents( true );
--
-- if the escape key is pressed, hide the window
--
self.KeyDown = function( sender, args )
-- do this if the escape key is pressed
if ( args.Action == Turbine.UI.Lotro.Action.Escape ) then
sender:SetVisible( false )
end
end
--
-- if the position changes, save the new window location
--
self.PositionChanged = function( sender, args )
local x,y = self:GetPosition();
self.settings.positionX = x;
self.settings.positionY = y;
self:SaveSettings();
end
--
-- when the mouse is clicked, we may be moving the window or
-- "swiping" the plugins
--
self.MouseDown = function( sender, args )
if ( args.Button == Turbine.UI.MouseButton.Left ) then
-- the user clicked the mosue button
-- if they clicked on / near the border of the window,
-- the we promote this to a "drag" operation
if( ( args.X <= self.clickMoveBias ) or
( args.Y <= self.clickMoveBias ) or
( args.Y >= ( self:GetHeight() - self.clickMoveBias ) ) or
( args.X >= ( self:GetWidth() - self.clickMoveBias ) ) ) then
self.dragStartX = args.X;
self.dragStartY = args.Y;
self.dragging = true;
self.dragged = false;
self:SetBackColor( Turbine.UI.Color( 0.5, 0.5, 0.5, 0 ) );
self.previousPage:SetBackColor( Turbine.UI.Color( 0.5, 0.5, 0.5, 0 ) );
self.nextPage:SetBackColor( Turbine.UI.Color( 0.5, 0.5, 0.5, 0 ) );
else
-- the user clicked somewhere in the middle of the UI.
-- we promote this to a "swiping" operation
self.swipeStartX = args.X;
self.swiping = true;
self.swiped = false;
end
end
if ( args.Button == Turbine.UI.MouseButton.Right ) then
-- context menu?
end
end
self.MouseMove = function( sender, args )
local left, top = self:GetPosition();
if ( self.dragging ) then
self:SetPosition( left + ( args.X - self.dragStartX ), top + ( args.Y - self.dragStartY ) );
self.dragged = true;
else
if( self.swiping ) then
self.swiped = true;
end
end
end
self.MouseUp = function( sender, args )
if ( args.Button == Turbine.UI.MouseButton.Left ) then
self.dragging = false;
self.swiping = false;
-- verify the position is on the main screen
if( self.dragged ) then
local x, y = self:GetPosition();
local validX = x;
local validY = y;
if( x < 0 ) then
x = 0;
end
if( y < 0 ) then
y = 0;
end
if( x + self:GetWidth() > Turbine.UI.Display.GetWidth() ) then
x = Turbine.UI.Display.GetWidth() - self:GetWidth();
end
if( y + self:GetHeight() > Turbine.UI.Display.GetHeight() ) then
y = Turbine.UI.Display.GetHeight() - self:GetHeight();
end
self:SetPosition( x, y );
end
if( self.swiped ) then
local delta = args.X - self.swipeStartX;
if( delta < 0 - self.clickSwipeBias ) then
self.nextPage:Click();
else
if( delta > self.clickSwipeBias ) then
self.previousPage:Click();
end
end
end
self:SetBackColor( Turbine.UI.Color( 0, 0, 0, 0 ) );
self.previousPage:SetBackColor( Turbine.UI.Color( 0, 0, 0, 0 ) );
self.nextPage:SetBackColor( Turbine.UI.Color( 0, 0, 0, 0 ) );
self.dragged = false;
self.swiped = false;
end
end
end
-- --------------------------------------------------------------
-- Initialize
--
-- Description: update the currently displayed plugins with what
-- has been loaded from disk
-- --------------------------------------------------------------
function CustomPluginsManager:Initialize()
-- validate the status of the various plugins
self:FindAndValidatePlugins();
-- reload plugins
self:ReloadPlugins();
-- rebuild the plugin slots based on the state of the manager -
-- on initialization, this shows the loaded plugins
self:RebuildPluginSlots();
end
-- --------------------------------------------------------------
-- FindAndValidatePlugins
--
-- Description: determine the list of all plugins and iterate over them.
-- correlate what has been saved on disk and what exists now.
-- --------------------------------------------------------------
function CustomPluginsManager:FindAndValidatePlugins()
-- iterate over all the existing plugins and mark them as "invalid".
-- we assume invalid, then iterate over all the available plugins
-- and mark existing ones as valid.
-- also, mark the plugins and "not loaded" - we do not want to persist
-- the "was last loaded" flag across runs of the client
for pluginName, pluginData in pairs( self.settings.pluginSlots ) do
self.settings.pluginSlots[ pluginName ].PluginValid = false;
self.settings.pluginSlots[ pluginName ].PluginLoaded = false;
end
self.validPluginCount = 0;
-- what plugins are available through the plugins manager?
-- iterate over all the plugins and assign them to the available list
local pluginSlots = Turbine.PluginManager.GetAvailablePlugins();
for key, value in pairs( pluginSlots ) do
-- we are not going to track the CustomPluginsManager, since that is US!
-- if the name is CustomPluginsManager, skip it
if( value.Name ~= "CPM" ) then
-- if the plugin does not exist in the settings, we create one
if( not self.settings.pluginSlots[ value.Name ] ) then
self.settings.pluginSlots[ value.Name ] = FrostyPlugins.CustomPluginsManager.PluginSlot();
self.settings.pluginSlots[ value.Name ].PluginName = value.Name;
-- fixme: frosty
-- if we ever get configuration data, we could request the
-- icon here
end
-- always update this stuff from the available list, since it will
-- be the most current
self.settings.pluginSlots[ value.Name ].PluginValid = true;
self.settings.pluginSlots[ value.Name ].PluginVersion = value.Version;
self.settings.pluginSlots[ value.Name ].PluginAuthor = value.Author;
self.settings.pluginSlots[ value.Name ].PluginPackage = value.Package;
self.settings.pluginSlots[ value.Name ].PluginScriptState = value.ScriptState;
-- another valid plugin, hooray!
self.validPluginCount = self.validPluginCount + 1;
end
end
end
-- --------------------------------------------------------------
-- ReloadPlugins
--
-- Description: For any plugin that had been marked as a "favorite",
-- load it now!
-- --------------------------------------------------------------
function CustomPluginsManager:ReloadPlugins()
-- iterate over the list of all plugins
for pluginName, pluginData in pairs( self.settings.pluginSlots ) do
-- if the plugin is marked as a "favorite", then load it!
if( pluginData.PluginFavorite == true ) then
-- have the plugin manager load the plugin
Turbine.PluginManager.LoadPlugin( pluginData.PluginName );
self.settings.pluginSlots[ pluginData.PluginName ].PluginLoaded = true;
end
end
end
-- --------------------------------------------------------------
-- AdjustVisiblePlugins
--
-- Description: something (user button click or swipe action) has
-- caused the UI to want to display a new set of slots. validate
-- the current index and rebuild the UI if necessary.
--
-- Params:
-- delta - the amount of slots to move
-- --------------------------------------------------------------
function CustomPluginsManager:AdjustVisiblePlugins( delta )
local oldIndex = self.displayedPluginsIndex;
-- validate the range of the new index
self.displayedPluginsIndex = self.displayedPluginsIndex + delta;
if( self.displayedPluginsIndex < 1 ) then
self.displayedPluginsIndex = 1;
else
if( self.displayedPluginsIndex > self.validPluginCount ) then
self.displayedPluginsIndex = oldIndex;
end
end
-- only rebuild the slots if the index changed
if( self.displayedPluginsIndex ~= oldIndex ) then
self:RebuildPluginSlots();
end
end
-- --------------------------------------------------------------
-- RebuildPluginSlots
--
-- Description: toggle between showing and hiding the plugin slots.
-- this function clears and rebuilds the plugins array and the
-- visual representation.
-- --------------------------------------------------------------
function CustomPluginsManager:RebuildPluginSlots()
-- we are going to update all the plugin slots, so we need to know
-- how many containers were updated.
local nUpdateCount = 0;
-- because the list of plugins can be paged, we need to allow for
-- detecting the number of matches and only show those items for
-- the current (virtual) page
local nMatchCount = 0;
-- iterate over the list of all plugins
for pluginName, pluginData in pairs( self.settings.pluginSlots ) do
-- if the plugin is valid, we will display it...
-- provided that the plugin fits on the screen
nMatchCount = nMatchCount + 1;
if( ( nMatchCount >= self.displayedPluginsIndex ) and ( nMatchCount < ( self.displayedPluginsIndex + self.MAX_SLOTS ) ) ) then
nUpdateCount = nUpdateCount + 1;
self.pluginContainers[ nUpdateCount ]:Initialize( pluginData );
end
end
-- iterate over all the slots that were not updated. there are a
-- max of sixteen plugin slot containers
for i = nUpdateCount + 1, self.MAX_SLOTS do
self.pluginContainers[ i ]:Reset();
end
-- turn the next page and previous page buttons on and off as necessary.
if( self.displayedPluginsIndex == 1 ) then
self.previousPage:SetVisible( false );
else
self.previousPage:SetVisible( true );
end
if( nMatchCount >= ( ( self.displayedPluginsIndex + self.MAX_SLOTS ) ) ) then
self.nextPage:SetVisible( true );
else
self.nextPage:SetVisible( false );
end
-- update the status bar
local minCount = math.floor( self.displayedPluginsIndex / self.MAX_SLOTS ) + 1;
local maxCount = math.floor( self.validPluginCount / self.MAX_SLOTS );
if( ( self.validPluginCount % self.MAX_SLOTS ) ~= 0 ) then
maxCount = maxCount + 1;
end
self.statusBar:SetText( tostring( minCount ) .. " of " .. tostring( maxCount ) );
end
-- --------------------------------------------------------------
-- LoadSettings
--
-- Description: loads our internal settings from disk. For this class,
-- this includes the position of the window and the list of plugins.
-- if the position does not exist, we set it to 0, 0. if we do not
-- know about any plugins, we create a list
-- --------------------------------------------------------------
function CustomPluginsManager:LoadSettings()
-- load the settings. If a value is not available, set a default value
self.settings = Turbine.PluginData.Load( Turbine.DataScope.Character, "CustomPluginsManagerSettings" );
if ( type( self.settings ) ~= "table" ) then
self.settings = { };
end
if ( not self.settings.positionX ) then
self.settings.positionX = 0;
end
if ( not self.settings.positionY ) then
self.settings.positionY = 0;
end
if ( not self.settings.pluginSlots ) then
self.settings.pluginSlots = { };
end
-- sort the list of plugins
table.sort( self.settings.pluginSlots, function(a,b) return (a.PluginName < b.PluginName) end );
end
-- --------------------------------------------------------------
-- SaveSettings
--
-- Description: save our internal settings to disk. For this class,
-- this includes the position of the wrapper window and the list of
-- events that have been created.
-- --------------------------------------------------------------
function CustomPluginsManager:SaveSettings()
-- sort the list of plugins
table.sort( self.settings.pluginSlots, function(a,b) return (a.PluginName < b.PluginName) end );
-- save the settings
Turbine.PluginData.Save( Turbine.DataScope.Character, "CustomPluginsManagerSettings", self.settings );
end
-- --------------------------------------------------------------
-- --------------------------------------------------------------
-- Callbacks
--
-- callbacks are received from the PluginSlotContainers. whenever
-- a PluginSlot changes state such that the CustomPluginsManager
-- should do something (load, unload, mark as favorite, etc.) a
-- callback is generated.
-- --------------------------------------------------------------
-- --------------------------------------------------------------
-- --------------------------------------------------------------
-- LoadPlugin
--
-- Description: load the input plugin.
--
-- Params:
-- pluginSlotContainer - PluginSlotContainer - the plugin slot to be loaded
-- bLoad - boolean - the flag indicating the plugin should be unloaded or loaded
-- --------------------------------------------------------------
function CustomPluginsManager:LoadPlugin( pluginSlotContainer, bLoad )
local pluginSlot = self.settings.pluginSlots[ pluginSlotContainer.pluginSlot.PluginName ];
if( pluginSlot ) then
-- have the plugin manager load the plugin
Turbine.PluginManager.LoadPlugin( pluginSlotContainer.pluginSlot.PluginName );
-- update the "loaded" value
self.settings.pluginSlots[ pluginSlotContainer.pluginSlot.PluginName ].PluginLoaded = bLoad;
pluginSlotContainer.pluginSlot.PluginLoaded = bLoad;
-- refresh the overlays (loaded / favorites)
pluginSlotContainer:RefreshOverlays();
self:SaveSettings();
end
end
-- --------------------------------------------------------------
-- SetFavorite
--
-- Description: set the plugin as a favorite: a favorite plugin will
-- be loaded on startup.
--
-- Params:
-- pluginSlotContainer - PluginSlotContainer - the plugin slot to be marked
-- bLoad - boolean - the flag indicating how the plugin should be maked
-- --------------------------------------------------------------
function CustomPluginsManager:SetFavorite( pluginSlotContainer, bFavorite )
local pluginSlot = self.settings.pluginSlots[ pluginSlotContainer.pluginSlot.PluginName ];
if( pluginSlot ) then
-- update the "loaded" value
self.settings.pluginSlots[ pluginSlotContainer.pluginSlot.PluginName ].PluginFavorite = bFavorite;
pluginSlotContainer.pluginSlot.PluginFavorite = bFavorite;
-- refresh the overlays (loaded / favorites)
pluginSlotContainer:RefreshOverlays();
self:SaveSettings();
end
end
-- --------------------------------------------------------------
-- RefreshAllPlugins
--
-- Description: refresh all plugins and update the display if necessary.
-- --------------------------------------------------------------
function CustomPluginsManager:RefreshAllPlugins()
-- the first thing we do is look for all the valid plugins. we've
-- already got a function to do that!
self:FindAndValidatePlugins();
-- iterate the known, loaded plugins and mark anything as loaded
local pluginSlots = Turbine.PluginManager.GetLoadedPlugins();
for key, value in pairs( pluginSlots ) do
-- we are not going to track the CustomPluginsManager, since that is US!
if( value.Name ~= "CPM" ) then
-- we are assuming that a loaded plugin is also an available plugin.
-- otherwise, this is going to error
self.settings.pluginSlots[ value.Name ].PluginLoaded = true;
end
end
-- rebuild the plugin slots based on the state of the manager -
-- on initialization, this shows the loaded plugins
self:RebuildPluginSlots();
end
-- --------------------------------------------------------------
-- SetIcon
--
-- Description: display the icon editing window, then update the
-- icon field in the plugin
-- --------------------------------------------------------------
function CustomPluginsManager:SetIcon( pluginSlotContainer )
local pluginSlot = self.settings.pluginSlots[ pluginSlotContainer.pluginSlot.PluginName ];
if( pluginSlot ) then
-- create a temporary window to allow the user to enter the plugin icon name
self.IconEntryWindow:Initialize( pluginSlot, self );
end
end
-- --------------------------------------------------------------
-- UpdateIcon
--
-- Description: a callback from the icon editing window. update
-- the named plugin with a new icon.
-- --------------------------------------------------------------
function CustomPluginsManager:UpdateIcon( pluginName, pluginIcon )
local pluginSlot = self.settings.pluginSlots[ pluginName ];
if( pluginSlot ) then
self.settings.pluginSlots[ pluginName ].PluginIcon = pluginIcon;
-- save the settings and rebuild the plugin slots
self:SaveSettings();
self:RebuildPluginSlots();
end
self.IconEntryWindow:Shutdown();
end