lotrointerface.com
Search Downloads

LoTROInterface SVN Calendar

[/] [trunk/] [FrostyPlugins/] [Calendar/] [CalendarWindow.lua] - Rev 37

Compare with Previous | Blame | View Log

import "Turbine";
import "Turbine.Gameplay";
import "Turbine.UI";
import "Turbine.UI.Extensions";
import "Turbine.UI.Lotro";
--import "Turbine.Utils";

import "FrostyPlugins.Calendar.Locale";
import "FrostyPlugins.Calendar.Options";
import "FrostyPlugins.Calendar.CalendarEntry";
import "FrostyPlugins.Calendar.CalTools";
import "FrostyPlugins.Calendar.DateTime";
import "FrostyPlugins.Calendar.DateDropDownBox";

--calTools = require "CalTools"; -- "require" not available in LOTRO
calTools = FrostyPlugins.Calendar.CalTools;

-- Get reference to user configurable options (singleton)
--options = FrostyPlugins.Calendar.OptionsInstance();
--table.dump("CalenderWindow Options",Options);

--------------------------------------------------------------------------
-- CalendarWindow is a display of the current month.  
-- You can see entries that are on the calendar and edit them.
--
-- ToDo:
-- add icons for events?

CalendarWindow = class( Turbine.UI.Lotro.Window );

local CalendarView = {
    "Month",
    "Week",
    "Day",
    "List",
}

-- Some constants
local HOURS_PER_DAY = 24;
local DAYS_PER_WEEK = 7;
local GRID_MARGIN = 2;
local HOUR_LABEL_WIDTH = 30;
local WEEK_DAY_HOUR_BUTTON_HEIGHT = 30;

-- Font size conversion
-- Font        Width    Chars   Width/Char
-- Verdana20    156     19      8.21
-- Verdana22    156     18      8.67
-- Arial12      156     
-- ?            156     22      7.10
local FontWidthPerChar = {
    [Turbine.UI.Lotro.Font.Verdana20] = (156/19),
    [Turbine.UI.Lotro.Font.Verdana22] = (156/18),
    --[Turbine.UI.Lotro.Font.Arial12] = (156/?),                
};


--------------------------------------------------------------------------
-- Constructor
--
-- Description: this is where the window is laid out, the fields
-- and buttons are created, and events are trapped.
function CalendarWindow:Constructor()

    --Turbine.Shell.WriteLine( "CalendarWindow:Constructor()" );
    Turbine.UI.Lotro.Window.Constructor( self );

    -- --------------------------------------------
    -- initialize the actual calendar
    -- --------------------------------------------
    self:InitializeCalendar();
    
    -- load the last settings saved
    self:LoadSettings();
    
    -- statics
    self.calendarEntryEditor = FrostyPlugins.Calendar.CalendarEntryEditor();
    self.calendarEntryEditor:SetVisible( false );
    
    -- --------------------------------------------
    -- window layout
    -- --------------------------------------------
    
    self:SetSize( self.settings.width, self.settings.height ); -- width,height
    self:SetPosition( self.settings.positionX, self.settings.positionY );
    self:SetBackColor( Turbine.UI.Color() );
    self:SetText( "Calendar" );
    self:SetOpacity( 1 );
    self:SetAllowDrop( false );
    self:SetResizable(true);
    
    -- Declare the interior area of the window where all controls are placed
    local windowMargin = 20;
    local topMargin = 35;
    local interior = Turbine.UI.Control();
    interior:SetParent( self );
    interior:SetPosition(  -- left,top
        windowMargin, topMargin);
    interior:SetSize( -- width,height
        self:GetWidth() - windowMargin*2,
        self:GetHeight() - interior:GetTop() - 50 );
    interior:SetBlendMode( Turbine.UI.BlendMode.Overlay );
    --interior:SetBackColor(Turbine.UI.Color.Red);
    interior:SetVisible(true); -- for debug window layout
    
    -- on resizing window, resize interior space
    self.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( "CalendarWindow.SizeChanged()" );
        interior:SetSize( -- width,height
            self:GetWidth() - windowMargin*2,
            self:GetHeight() - interior:GetTop() - 50 );    
    end
    
    -- the layout will look something like:

    -- = view < today >  September 2022
    
    -- = view < today >  02/20/2022 - 02/26/2022
    
    -- = view < today >  02/20/2022

    --  -         June          +
    -- 
    --      1   2   3   4   5   6
    --  7   8   9  10  11  12  13
    -- 14  15  16  17  18  19  20
    -- 21  22  23  24  25  26  27
    -- 28  29  30
    --

    -- create the settings button
    self.buttonOptions = Turbine.UI.Button();
    self.buttonOptions:SetBackground( "FrostyPlugins/Calendar/Resources/options.tga" );
    self.buttonOptions:SetBlendMode( Turbine.UI.BlendMode.Overlay );
    self.buttonOptions:SetParent( interior );
    self.buttonOptions:SetSize( 30,30 ); -- width,height
    self.buttonOptions:SetPosition( 0, 0 ); -- left,top
    self.buttonOptions:SetVisible( true );

    self.buttonOptions.MouseEnter = function( sender, args )
      sender:SetBackground( "FrostyPlugins/Calendar/Resources/options_hover.tga" );
    end
    self.buttonOptions.MouseLeave = function( sender, args )
      sender:SetBackground( "FrostyPlugins/Calendar/Resources/options.tga" );
    end

    self.buttonOptions.Click = function( sender, args )
        -- not implemented yet
        --Turbine.Shell.WriteLine( "Options window not yet implemented" );
        Turbine.PluginManager.ShowOptions(Plugins["Calendar"]);
    end
    
    self.buttonView  = FrostyPlugins.Calendar.DropDownBox{
        itemList=CalendarView,
        button=Turbine.UI.Lotro.Button()
        };
    self.buttonView:SetParent( interior );
    --self.buttonView:SetSize( -- width,height
    --    45,
    --    self.buttonOptions:GetHeight() ); 
    self.buttonView:SetPosition( -- left,top
        self.buttonOptions:GetLeft() + self.buttonOptions:GetWidth() + 5,
        self.buttonOptions:GetTop() ); 
    self.buttonView:SetText( "View" );
    self.buttonView:SetVisible( true );
    self.buttonView:SetSelectedValue(self.settings.view);
    
    self.buttonView.SelectionChanged = function( sender, args )
        local idx = self.buttonView:GetSelectedIndex();
        local viewName = self.buttonView:GetSelectedValue();
        --Turbine.Shell.WriteLine( string.format(
        --    "buttonView.SelectionChanged: [%d] = [%s]",
        --    idx, viewName) );
        
        self.settings.view = viewName;
        --Turbine.Shell.WriteLine( "You selected view [" .. self.settings.view .. "]" );
        self:SaveSettings();
        self:RebuildCalendar();
    end
    
    -- create the today button
    --self.buttonToday = Turbine.UI.Lotro.GoldButton();
    self.buttonToday = Turbine.UI.Lotro.Button();
    self.buttonToday:SetBlendMode( Turbine.UI.BlendMode.Overlay );
    self.buttonToday:SetParent( interior );
    --self.buttonToday:SetSize( -- width,height
    --    60,
    --    self.buttonOptions:GetHeight() );
    self.buttonToday:SetPosition( -- left,top
        interior:GetWidth() - self.buttonToday:GetWidth(),
        self.buttonOptions:GetTop() ); 
    self.buttonToday:SetText( "Today" );
    self.buttonToday:SetVisible( true );
    self.buttonToday.Click = function( sender, args )
        -- Set display to now
        self.current = DateTime:now();
        self:RebuildCalendar();
    end
    
    -- create the month/year - button
    self.buttonLeft = Turbine.UI.Button();
    self.buttonLeft:SetBackground( "FrostyPlugins/Calendar/Resources/LeftArrow.tga" );
    self.buttonLeft:SetBlendMode( Turbine.UI.BlendMode.Overlay );
    self.buttonLeft:SetParent( interior );
    self.buttonLeft:SetSize( 20, 20 ); -- width,height
    --  20,
    --  self.buttonOptions:GetHeight());    
    self.buttonLeft:SetPosition( 
        self.buttonView:GetLeft() + self.buttonView:GetWidth() + 5,
        0 ); -- left,top
    self.buttonLeft:SetVisible( true );
    
    self.buttonLeft.MouseEnter = function( sender, args )
        --sender:SetBackColor(Turbine.UI.Color.Gray); -- for debug window layout
        sender:SetBackground( "FrostyPlugins/Calendar/Resources/LeftArrowHighlight.tga" );
    end
    self.buttonLeft.MouseLeave = function( sender, args )
        sender:SetBackground( "FrostyPlugins/Calendar/Resources/LeftArrow.tga" );
    end
    self.buttonLeft.EnabledChanged = function( sender, args )
        sender:SetVisible( sender:IsEnabled() );
    end
    
    self.buttonLeft.Click = function( sender, args )
        self:AdjustView( -1 );
    end
    
    -- create the month/year + button
    self.buttonRight = Turbine.UI.Button();
    self.buttonRight:SetBackground( "FrostyPlugins/Calendar/Resources/RightArrow.tga" );
    self.buttonRight:SetBlendMode( Turbine.UI.BlendMode.Overlay );
    self.buttonRight:SetParent( interior );
    self.buttonRight:SetSize( self.buttonLeft:GetSize() ); -- width,height
    self.buttonRight:SetPosition( -- left,top
        self.buttonToday:GetLeft() - self.buttonRight:GetWidth() - 5,
        self.buttonLeft:GetTop() ); 
    self.buttonRight:SetVisible( true );

    self.buttonRight.MouseEnter = function( sender, args )
      sender:SetBackground( "FrostyPlugins/Calendar/Resources/RightArrowHighlight.tga" );
    end
    self.buttonRight.MouseLeave = function( sender, args )
      sender:SetBackground( "FrostyPlugins/Calendar/Resources/RightArrow.tga" );
    end
    self.buttonRight.EnabledChanged = function( sender, args )
        sender:SetVisible( sender:IsEnabled() );
    end

    self.buttonRight.Click = function( sender, args )
        self:AdjustView( 1 );
    end

    -- create the month label/button
    self.buttonViewTitle = Turbine.UI.Button();
    self.buttonViewTitle:SetParent( interior );
    self.buttonViewTitle:SetSize( -- width,height
        self.buttonRight:GetLeft() - self.buttonLeft:GetLeft() - self.buttonLeft:GetWidth() - 5,
        self.buttonOptions:GetHeight() );
    self.buttonViewTitle:SetPosition( -- left,top
        self.buttonLeft:GetLeft() + self.buttonLeft:GetWidth() + 5,
        self.buttonOptions:GetTop() );
    self.buttonViewTitle:SetMultiline( false ); 
    self.buttonViewTitle:SetSelectable( false );    
    self.buttonViewTitle:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
    self.buttonViewTitle:SetText( "September" ); -- initialize to month with longest name to test control layout
    self.buttonViewTitle:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
    self.buttonViewTitle:SetVisible( true );
    self.buttonViewTitle:SetBackColor(Turbine.UI.Color.Red); -- for debug window layout
    
    -- Create a container to hold the container views
    self.calenderView = Turbine.UI.Control();
    self.calenderView:SetParent( interior );
    self.calenderView:SetSize( -- width,height
        interior:GetWidth(),
        interior:GetHeight() - self.buttonOptions:GetTop() - self.buttonOptions:GetHeight() - 5 );
    self.calenderView:SetPosition(  -- left,top
        0,
        self.buttonOptions:GetTop() + self.buttonOptions:GetHeight() + 5);
    self.calenderView:SetVisible(true); -- for debug window layout
    
    -- Overlay multiple view controls on top of same view control
    -- - one for each kind of view
    self.calenderViews = {}; -- list of controls for each view
    self.calenderViews["Month"] = self:CreateViewMonth();
    self.calenderViews["Week"] = self:CreateViewWeek();
    self.calenderViews["Day"] = self:CreateViewDay();
    self.calenderViews["List"] = self:CreateViewList();

    self.calenderView.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( "self.calenderView.SizeChanged()" );
        for name,view in pairs(self.calenderViews) do
            --Turbine.Shell.WriteLine( string.format("view[%s].SetSize()",name) );
            view:SetSize( self.calenderView:GetSize() ); -- width,height
        end
    end

    
    -- The set of buttons for each event
    -- - will be re-used and arranged by each view
    self.entryListButtons = {};
    self.nextEntryButton = 1; -- The next button to retreive from the above list
    
    -- the radio button for storing data on the character or the account
    local dataLocationLabel = Turbine.UI.Label();
    dataLocationLabel:SetParent( self );
    dataLocationLabel:SetSize( -- width,height
        interior:GetWidth(),
        20 );
    dataLocationLabel:SetPosition( -- left,top
        interior:GetLeft(), 
        interior:GetTop() + interior:GetHeight() + GRID_MARGIN ); 
    --dataLocationLabel:SetBackColor( Turbine.UI.Color.Purple );
    dataLocationLabel:SetFont( Turbine.UI.Lotro.Font.Verdana14 );
    dataLocationLabel:SetMultiline( false );
    dataLocationLabel:SetMouseVisible( true );
    dataLocationLabel:SetOutlineColor( Turbine.UI.Color.Yellow );
    dataLocationLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
    dataLocationLabel:SetVisible( true );

    --if( true == self.dataLocation.StoredOnCharacter ) then
    --  self.dataLocationLabel:SetText( "Save Data on Character" );
    --else
        dataLocationLabel:SetText( "Save Data on Account" );
    --end

    -- fixme: frosty
    --        there is a bug with SetFontStyle - it does not "dirty" the control.
    --        to get around this, toggle the visibility of the field
    dataLocationLabel.MouseEnter = function( sender, args )
        sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    dataLocationLabel.MouseLeave = function( sender, args )
        sender:SetFontStyle( Turbine.UI.FontStyle.None );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    dataLocationLabel.MouseClick = function( sender, args )
        -- toggle data save location from per account vs. per character
        --self.dataLocation.StoredOnCharacter = not self.dataLocation.StoredOnCharacter;
        --self:SaveSettings();
        --if( true == self.dataLocation.StoredOnCharacter ) then
        --  self.dataLocationLabel:SetText( "Save Data on Character" );
        --else
        --  self.dataLocationLabel:SetText( "Save Data on Account" );
        --end
    end
  
    -- this is the label for the dataLocation store
    
    -- make sure we listen for key presses
    self:SetWantsKeyEvents( true );

    -- --------------------------------------------
    -- event handling
    -- --------------------------------------------

    --
    -- 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 escape key is pressed, hide the window
    --
    --self.MouseDown = function( sender, args )
    --  Turbine.Shell.WriteLine("MouseDown");
    --  self.isMouseUp = false;
    --end
    self.MouseUp = function( sender, args )
        --Turbine.Shell.WriteLine("MouseUp");
        --self.isMouseUp = true;
        
        -- save window location after mouse released
        local x,y = self:GetPosition();
        self.settings.positionX = x;
        self.settings.positionY = y;
        
        self.settings.width = self:GetWidth();
        self.settings.height = self:GetHeight();
        
        self:SaveSettings();
    end
    
    --
    -- if the position changes, save the new window location
    --
    self.PositionChanged = function( sender, args )
        -- Only save settings (window location) after the mouse is released
        -- Unfortunately, this event fires BEFORE the MouseUp event
        --if( self.isMouseUp ) then
        --  local x,y = self:GetPosition();
        --  self.settings.positionX = x;
        --  self.settings.positionY = y;
        --  self:SaveSettings();
        --end
    end

    -- handle reize evnets
    interior.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( string.format("view[%s].SizeChanged()",view.Name) );
        
        self.buttonToday:SetPosition( -- left,top
            interior:GetWidth() - self.buttonToday:GetWidth(),
            self.buttonOptions:GetTop() ); 
        
        self.buttonRight:SetPosition( -- left,top
            self.buttonToday:GetLeft() - self.buttonRight:GetWidth() - 5,
            self.buttonLeft:GetTop() ); 
        
        self.buttonViewTitle:SetSize( -- width,height
            self.buttonRight:GetLeft() - self.buttonLeft:GetLeft() - self.buttonLeft:GetWidth() - 5,
            self.buttonOptions:GetHeight() );
                
        self.calenderView:SetSize( -- width,height
            interior:GetWidth(),
            interior:GetHeight() - self.buttonOptions:GetTop() - self.buttonOptions:GetHeight() - 5 );

        dataLocationLabel:SetSize( -- width,height
            interior:GetWidth(),
            20 );
        dataLocationLabel:SetPosition( -- left,top
            interior:GetLeft(), 
            interior:GetTop() + interior:GetHeight() + GRID_MARGIN ); 
    end
    
    SubscribeToChangeNotification(self);
    
    self:RebuildCalendar();

    --Turbine.Shell.WriteLine( "CalendarWindow:Constructor() END" );
end

--------------------------------------------------------------------------
--- CreateViewMonth
--
-- Description: Creates the month view control
-- returns: the control for the month view
function CalendarWindow:CreateViewMonth()
    --Turbine.Shell.WriteLine( "CalendarWindow:CreateViewMonth()" );

    local view = Turbine.UI.Control();
    view:SetParent( self.calenderView );
    view:SetSize(self.calenderView:GetSize());
    view:SetPosition(0,0);
    view:SetVisible(true);
    view:SetBackColor(Turbine.UI.Color.DimGray);
    view.Name = 'Month';
    
    -- Add dropdown button to select month (Only visible from month view)
    local MonthLabels = { "January","February","March","April","May","June","July","August","September","October","November","December"};
    
    local buttonSelectMonth = FrostyPlugins.Calendar.DropDownBox{itemList=MonthLabels};
    view.buttonSelectMonth = buttonSelectMonth;
    buttonSelectMonth:SetParent( self.buttonViewTitle );
    buttonSelectMonth:SetSize( -- width,height
        150,
        self.buttonViewTitle:GetHeight() ); 
    buttonSelectMonth:SetPosition( -- left,top
    --    self.buttonOptions:GetLeft() + self.buttonOptions:GetWidth() + 5,
    --    self.buttonOptions:GetTop() ); 
        50, 5);
    --buttonSelectMonth:SetBackColor(Turbine.UI.Color.Purple); -- for debug window layout
    buttonSelectMonth:SetBackColor( self.buttonViewTitle:GetBackColor());
    buttonSelectMonth:SetForeColor(Turbine.UI.Color.White);
    buttonSelectMonth:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleRight );
    buttonSelectMonth:SetOutlineColor( Turbine.UI.Color.Yellow );
    buttonSelectMonth:SetFontStyle( Turbine.UI.FontStyle.None );
    buttonSelectMonth:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
    buttonSelectMonth:SetVisible( false );
    buttonSelectMonth:SetSelectedIndex(self.current.Month);
    buttonSelectMonth:SetText( buttonSelectMonth:GetSelectedValue() );
    buttonSelectMonth.SelectionChanged = function( sender, args )
        local idx = sender:GetSelectedIndex();
        local value = sender:GetSelectedValue();
        --Turbine.Shell.WriteLine( string.format(
        --    "buttonSelectMonth.SelectionChanged: [%d] = [%s]",
        --    idx, value) );
        buttonSelectMonth:SetText( buttonSelectMonth:GetSelectedValue() );
        
        -- Something else (e.g. AdjustView()) has already changed the month
        -- So only adjust month if needed, to avoid an endless notification loop
        if( self.current.Month ~= idx ) then
            self.current:set({Month=idx});
            self:RebuildCalendar();
        end
    end
    
    buttonSelectMonth.MouseEnter = function( sender, args )
        sender:SetForeColor( Turbine.UI.Color.Yellow );
        sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end

    buttonSelectMonth.MouseLeave = function( sender, args )
        sender:SetForeColor( Turbine.UI.Color.White );
        sender:SetFontStyle( Turbine.UI.FontStyle.None );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    local now = DateTime:now();
    local minYear = now.Year - (Options.yearLimit-1);
    local maxYear = now.Year + (Options.yearLimit-1);
    local yearValues = table.fill(minYear,maxYear);
    local buttonSelectYear = FrostyPlugins.Calendar.DropDownBox{itemList=yearValues};
    view.buttonSelectYear = buttonSelectYear;
    buttonSelectYear:SetParent( self.buttonViewTitle );
    buttonSelectYear:SetSize( -- width,height
        100,
        self.buttonViewTitle:GetHeight() ); 
    buttonSelectYear:SetPosition( -- left,top
        buttonSelectMonth:GetLeft() + buttonSelectMonth:GetWidth() + 5,
        buttonSelectMonth:GetTop() ); 
    --buttonSelectYear:SetBackColor(Turbine.UI.Color.Purple); -- for debug window layout
    buttonSelectYear:SetBackColor( self.buttonViewTitle:GetBackColor());
    buttonSelectYear:SetForeColor(Turbine.UI.Color.White);
    buttonSelectYear:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleLeft );
    buttonSelectYear:SetOutlineColor( Turbine.UI.Color.Yellow );
    buttonSelectYear:SetFontStyle( Turbine.UI.FontStyle.None );
    buttonSelectYear:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
    buttonSelectYear:SetVisible( false );
    buttonSelectYear:SetSelectedValue(self.current.Year);
    buttonSelectYear:SetText( buttonSelectYear:GetSelectedValue() );

    buttonSelectYear.SelectionChanged = function( sender, args )
        local idx = sender:GetSelectedIndex();
        local value = sender:GetSelectedValue();
        --Turbine.Shell.WriteLine( string.format(
        --    "buttonSelectYear.SelectionChanged: [%d] = [%s]",
        --    idx, value) );
        buttonSelectYear:SetText( buttonSelectYear:GetSelectedValue() );
        
        -- Something else (e.g. AdjustView()) has already changed the month
        -- So only adjust month if needed, to avoid an endless notification loop
        if( self.current.Year ~= value ) then
            self.current:set({Year=value});
            self:RebuildCalendar();
        end
    end
    
    buttonSelectYear.MouseEnter = function( sender, args )
        sender:SetForeColor( Turbine.UI.Color.Yellow );
        sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end

    buttonSelectYear.MouseLeave = function( sender, args )
        sender:SetForeColor( Turbine.UI.Color.White );
        sender:SetFontStyle( Turbine.UI.FontStyle.None );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    -- Compute size of each grid item based on size of container they are in
    local index = 1;
    local nbrRows = 6;    -- weeks
    local nbrColumns = DAYS_PER_WEEK; -- days
    local gridItemWidth = view:GetWidth() / nbrColumns;
    local gridItemHeight = (view:GetHeight() - GRID_MARGIN*2) / nbrRows;
    --Turbine.Shell.WriteLine( string.format(
    --        "viewHeight: [%d]  gridItemHeight: [%d]",
    --        view:GetHeight(),
    --        gridItemHeight));
    
    -- fill the month grid with placeholders for each day
    view.gridElementsMonth = {};
    for row = 0, nbrRows - 1 do
        local y = row * gridItemHeight;
        for col = 0, nbrColumns - 1 do
            local gridElement = Turbine.UI.Button();
            local x = col * gridItemWidth;
            gridElement:SetParent( view );
            -- Note: Leave some small interior margins
            gridElement:SetPosition( x+GRID_MARGIN, y+GRID_MARGIN ); -- left,top
            gridElement:SetSize( gridItemWidth-GRID_MARGIN*2, gridItemHeight-GRID_MARGIN*2 ); -- width,height
            gridElement:SetBackColor(Turbine.UI.Color.Black);
            --gridElement:SetBackColorBlendMode( Turbine.UI.BlendMode.Overlay );
            gridElement:SetOutlineColor( Turbine.UI.Color.Yellow );
            gridElement:SetMultiline( false );
            gridElement:SetSelectable( false );
            gridElement:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
            gridElement:SetText( tostring(row) .. "." .. tostring(col) );
            gridElement:SetTextAlignment( Turbine.UI.ContentAlignment.TopCenter );
            gridElement:SetVisible( true );
            
            gridElement.MouseEnter = function( sender, args )
                sender:SetForeColor( Turbine.UI.Color.Yellow );
                sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
                sender:SetVisible( false );
                sender:SetVisible( true );
            end
  
            gridElement.MouseLeave = function( sender, args )
                sender:SetForeColor( Turbine.UI.Color.White );
                sender:SetFontStyle( Turbine.UI.FontStyle.None );
                sender:SetVisible( false );
                sender:SetVisible( true );
            end
            
            gridElement.Click = function( sender, args )
                local day = sender:GetText();
                --Turbine.Shell.WriteLine("click day " .. day ..
                --    " with entryKey = [" .. sender.entryKey:tostring() .. "]" );

                local startTime = sender.entryKey:clone();
                local endTime = startTime:clone():set({Hour=23,Minute=59});
                
                self:AddCalendarEntry( 
                    startTime,
                    endTime,
                    true );  -- isAllDay
            end
            
            -- Assign grid elements an index
            view.gridElementsMonth[ index ] = gridElement;
            index = index + 1;
        end
    end

    -- handle resize events
    view.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( string.format("view[%s].SizeChanged()",view.Name) );

        local gridItemWidth = view:GetWidth() / nbrColumns;
        local gridItemHeight = (view:GetHeight() - GRID_MARGIN*2) / nbrRows;

        for row = 0, nbrRows - 1 do
            local y = row * gridItemHeight;
            for col = 0, nbrColumns - 1 do
                local x = col * gridItemWidth;
                local gridElement = view.gridElementsMonth[1 + row * nbrColumns + col];
                gridElement:SetPosition( x+GRID_MARGIN, y+GRID_MARGIN ); -- left,top
                gridElement:SetSize( gridItemWidth-GRID_MARGIN*2, gridItemHeight-GRID_MARGIN*2 ); -- width,height
            end
        end

        self:RebuildCalendar();
    end

    return view;
end

--------------------------------------------------------------------------
--- CreateViewWeek
--
-- Description: Creates the week view control
-- returns: the control for the week view
function CalendarWindow:CreateViewWeek()
    --Turbine.Shell.WriteLine( "CalendarWindow:CreateViewWeek()" );

    local view = Turbine.UI.Control();
    view:SetParent( self.calenderView );
    view:SetSize(self.calenderView:GetSize());
    view:SetPosition(0,0);
    view:SetVisible(false);
    view:SetBackColor(Turbine.UI.Color.DarkGreen);
    view.Name = 'Week';
    
    -- blank placeholder at top left of week display
    local topLeftLabel = Turbine.UI.Label();
    topLeftLabel:SetParent( view );
    topLeftLabel:SetSize( -- width,height
        HOUR_LABEL_WIDTH,
        20 );
    topLeftLabel:SetPosition( -- left,top
        GRID_MARGIN,
        GRID_MARGIN );
    topLeftLabel:SetBackColor(Turbine.UI.Color.Black);
    
    local allDayEventLabel = Turbine.UI.Label();
    allDayEventLabel:SetParent( view );
    allDayEventLabel:SetSize( -- width,height
        topLeftLabel:GetWidth(),
        100 );
    allDayEventLabel:SetPosition( -- left,top
        topLeftLabel:GetLeft(),
        topLeftLabel:GetTop() + topLeftLabel:GetHeight() + GRID_MARGIN );
    allDayEventLabel:SetBackColor(Turbine.UI.Color.Black);
    allDayEventLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
    allDayEventLabel:SetText( "All\nDay" );
    
    -- Add horizontal scrollbar at bottom to scroll left/right through days of week
    local hScroll = Turbine.UI.Lotro.ScrollBar();
    hScroll:SetOrientation(Turbine.UI.Orientation.Horizontal);
    hScroll:SetParent(view);
    hScroll:SetSize( -- width,height
        view:GetWidth() - topLeftLabel:GetWidth() - GRID_MARGIN*2 - 12,
        12); 
    hScroll:SetPosition( -- left,top
        topLeftLabel:GetLeft() + topLeftLabel:GetWidth() + GRID_MARGIN,
        view:GetHeight() - hScroll:GetHeight() - GRID_MARGIN*2 );
    hScroll:SetVisible(true);
    view.hScroll = hScroll;
    
    -- Add vertical scrollbar at right to scroll through hours of day
    local vScroll = Turbine.UI.Lotro.ScrollBar();
    vScroll:SetOrientation(Turbine.UI.Orientation.Vertical);
    vScroll:SetParent(view);
    vScroll:SetSize( -- width,height
        12,
        hScroll:GetTop() - allDayEventLabel:GetTop() - allDayEventLabel:GetHeight() - GRID_MARGIN*2 );
    vScroll:SetPosition( -- left,top
        view:GetWidth() - vScroll:GetWidth(),
        allDayEventLabel:GetTop() + allDayEventLabel:GetHeight() + GRID_MARGIN);
    vScroll:SetVisible(true);
    view.vScroll = vScroll;
    
    -- The height of each grid item
    local gridItemHeight = 30;
    local dayColumnWidth = 150;
    
    ---- Add day labels at top of each day column
    local dayLabelViewPort = Turbine.UI.Control();
    dayLabelViewPort:SetParent( view );
    dayLabelViewPort:SetPosition( -- left,top
        hScroll:GetLeft(), 
        topLeftLabel:GetTop() );
    dayLabelViewPort:SetSize( -- width,height
        hScroll:GetWidth(),
        topLeftLabel:GetHeight() );
    --dayLabelViewPort:SetBackColor(Turbine.UI.Color.Red);
    dayLabelViewPort.entries = Turbine.UI.Control();
    dayLabelViewPort.entries:SetParent( dayLabelViewPort );
    dayLabelViewPort.entries:SetPosition( 0, 0 ); -- left,top
    dayLabelViewPort.entries:SetSize( -- width,height
        DAYS_PER_WEEK * (GRID_MARGIN + dayColumnWidth),
        topLeftLabel:GetHeight() );
    --dayLabelViewPort.entries:SetBackColor(Turbine.UI.Color.Red);
    
    -- Add all-day-event buttons
    local allDayEventsViewPort = Turbine.UI.Control();
    allDayEventsViewPort:SetParent( view );
    allDayEventsViewPort:SetPosition( -- left,top
        hScroll:GetLeft(), 
        allDayEventLabel:GetTop() );
    allDayEventsViewPort:SetSize( -- width,height
        hScroll:GetWidth(),
        allDayEventLabel:GetHeight() );
    --allDayEventsViewPort:SetBackColor(Turbine.UI.Color.Red);
    allDayEventsViewPort.entries = Turbine.UI.Control();
    allDayEventsViewPort.entries:SetParent( allDayEventsViewPort );
    allDayEventsViewPort.entries:SetPosition( 0, 0 ); -- left,top
    allDayEventsViewPort.entries:SetSize( -- width,height
        dayLabelViewPort.entries:GetWidth(),
        allDayEventsViewPort:GetHeight() );
    --allDayEventsViewPort.entries:SetBackColor(Turbine.UI.Color.Red);
    view.allDayEventsViewPort = allDayEventsViewPort;
    
    view.dayLabels = {};
    view.allDayEvents = {};
    for weekday = 1, DAYS_PER_WEEK do
        local colLeft = (weekday-1) * (dayColumnWidth + GRID_MARGIN);
        
        -- add day of week column header        
        local dayLabel = Turbine.UI.Label();
        dayLabel:SetParent( dayLabelViewPort.entries );
        dayLabel:SetPosition( colLeft, 0 );  -- left,top
        dayLabel:SetSize( -- width,height
            dayColumnWidth, 
            dayLabelViewPort:GetHeight() );
        dayLabel:SetBackColor(Turbine.UI.Color.Black);
        dayLabel:SetMultiline( false );
        dayLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
        dayLabel:SetText( CalTools:dayLabel(weekday) );
        dayLabel:SetVisible( true );
        view.dayLabels[weekday] = dayLabel;
        
        -- add all-day button for day of week
        local allDayEvent = Turbine.UI.Button();
        allDayEvent:SetParent( allDayEventsViewPort.entries );
        allDayEvent:SetPosition( colLeft, 0 );  -- left,top
        allDayEvent:SetSize( -- width,height
            dayColumnWidth, 
            allDayEventsViewPort:GetHeight() );
        allDayEvent:SetBackColor(Turbine.UI.Color.Black);
        --allDayEvent:SetBackColor(Turbine.UI.Color.Purple);
        allDayEvent:SetOutlineColor( Turbine.UI.Color.Yellow );
        allDayEvent:SetMultiline( false );
        allDayEvent:SetTextAlignment( Turbine.UI.ContentAlignment.TopCenter );
        allDayEvent:SetText("<click to add>" );
        allDayEvent:SetVisible( true );
        allDayEvent.weekday = weekday; -- add custom property to identify on click
        
        view.allDayEvents[weekday] = allDayEvent;
        
        allDayEvent.MouseEnter = function( sender, args )
            sender:SetBackColor( Turbine.UI.Color(.1, .1, .1) );
            sender:SetForeColor( Turbine.UI.Color.Yellow );
            sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
            sender:SetVisible( false );
            sender:SetVisible( true );            
        end
        
        allDayEvent.MouseLeave = function( sender, args )
            sender:SetBackColor( Turbine.UI.Color.Black );
            sender:SetForeColor( Turbine.UI.Color.White );
            sender:SetFontStyle( Turbine.UI.FontStyle.None );
            sender:SetVisible( false );
            sender:SetVisible( true );
        end
        
        allDayEvent.Click = function( sender, args )
            local startTime = self.current:clone():set({Hour=0,Minute=0}):add({Day=sender.weekday-self.current:dayOfWeek()});
            local endTime = startTime:clone():set({Hour=23,Minute=59});
            --Turbine.Shell.WriteLine( string.format(
            --    "clicked day[%s] startTime=[%s] endTime=[%s]",
            --    tostring(sender.weekday),
            --    startTime:tostring(),
            --    endTime:tostring()));
            self:AddCalendarEntry( 
                startTime,
                endTime,
                true );  -- isAllDay
        end
    end
    
    -- Add column of hour labels
    local weekViewHourLabelsViewPort = Turbine.UI.Control();
    weekViewHourLabelsViewPort:SetParent( view );
    weekViewHourLabelsViewPort:SetSize( -- width,height
        allDayEventLabel:GetWidth(),
        vScroll:GetHeight() );
    weekViewHourLabelsViewPort:SetPosition( -- left,top
        allDayEventLabel:GetLeft(), 
        vScroll:GetTop() );
    --weekViewHourLabelsViewPort:SetBackColor(Turbine.UI.Color.Gray);
    weekViewHourLabelsViewPort.entries = Turbine.UI.Control();
    weekViewHourLabelsViewPort.entries:SetParent( weekViewHourLabelsViewPort );
    weekViewHourLabelsViewPort.entries:SetPosition( 0, 0 ); -- left,top
    weekViewHourLabelsViewPort.entries:SetSize( -- width,height
        dayLabelViewPort.entries:GetWidth(),
        HOURS_PER_DAY * (GRID_MARGIN + WEEK_DAY_HOUR_BUTTON_HEIGHT) );
    
    for hourOfDay = 0, HOURS_PER_DAY - 1 do
        local rowTop = hourOfDay * (gridItemHeight + GRID_MARGIN); 
        
        local hourLabel = Turbine.UI.Label();
        hourLabel:SetParent( weekViewHourLabelsViewPort.entries );
        hourLabel:SetPosition( 0, rowTop);  -- left,top
        hourLabel:SetSize( -- width,height
            weekViewHourLabelsViewPort:GetWidth(),
            WEEK_DAY_HOUR_BUTTON_HEIGHT );
        hourLabel:SetBackColor(Turbine.UI.Color.Black);
        hourLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleRight );
        hourLabel:SetText( CalTools:hourLabel(hourOfDay) );
    end
    
    -- Create the hourly events using a view port instead of a ListBox
    -- this allows placing the entry buttons on top anywhere we need to
    -- ** MY SINCERE AND ENDURING THANKS TO GARAN FOR THE VIEWPORT IDEA **
    local partialDayEventsViewPort = Turbine.UI.Control();
    partialDayEventsViewPort:SetParent( view );
    partialDayEventsViewPort:SetPosition( -- left,top
        hScroll:GetLeft(), 
        vScroll:GetTop() );
    partialDayEventsViewPort:SetSize( -- width,height
        hScroll:GetWidth(),
        vScroll:GetHeight() );
    --partialDayEventsViewPort:SetBackColor(Turbine.UI.Color.Gray);
    --partialDayEventsViewPort:SetBackColor(Turbine.UI.Color.Purple); -- for debug
    -- Create control within the viewport to hold the actual items
    partialDayEventsViewPort.entries = Turbine.UI.Control();
    partialDayEventsViewPort.entries:SetParent( partialDayEventsViewPort );
    partialDayEventsViewPort.entries:SetPosition( 0, 0 ); -- left,top
    partialDayEventsViewPort.entries:SetSize( -- width,height
        dayLabelViewPort.entries:GetWidth(),
        weekViewHourLabelsViewPort.entries:GetWidth());
    view.partialDayEventsViewPort = partialDayEventsViewPort;

    view.partialDayEntries = {};
    
    -- fill grid of buttons for every hour of every day of week 
    for weekday = 1, DAYS_PER_WEEK do
        local colLeft = (weekday-1) * (dayColumnWidth + GRID_MARGIN); 

        view.partialDayEntries[weekday] = {};
        
        for hourOfDay = 0, HOURS_PER_DAY - 1 do
            local rowTop = hourOfDay * (gridItemHeight + GRID_MARGIN); 
            
            local hourButton = Turbine.UI.Button();
            hourButton:SetParent( partialDayEventsViewPort.entries );
            hourButton:SetSize( -- width,height
                dayColumnWidth,
                WEEK_DAY_HOUR_BUTTON_HEIGHT );
            hourButton:SetPosition( colLeft, rowTop);  -- left,top
            hourButton:SetBackColor(Turbine.UI.Color.Black);
            hourButton:SetOutlineColor( Turbine.UI.Color.Yellow );
            --hourButton:SetText( string.format("-- %d.%d --",weekday, hourOfDay) );
            hourButton:SetText( "--" );
            
            hourButton.weekday = weekday; -- add custom property to identify on click
            hourButton.hourOfDay = hourOfDay; -- add custom property to identify on click
            
            view.partialDayEntries[weekday][hourOfDay] = hourButton;
            
            hourButton.MouseEnter = function( sender, args )
                sender:SetBackColor( Turbine.UI.Color(.1, .1, .1) );
                sender:SetForeColor( Turbine.UI.Color.Yellow );
                sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
                sender:SetVisible( false );
                sender:SetVisible( true );
            end
            
            hourButton.MouseLeave = function( sender, args )
                sender:SetBackColor( Turbine.UI.Color.Black );
                sender:SetForeColor( Turbine.UI.Color.White );
                sender:SetFontStyle( Turbine.UI.FontStyle.None );
                sender:SetVisible( false );
                sender:SetVisible( true );
            end
            
            hourButton.Click = function( sender, args )
                --Turbine.Shell.WriteLine( string.format(
                --    "clicked weekday[%s] hour[%s] firstDayOfWeek[%s]",
                --    tostring(sender.weekday),
                --    tostring(sender.hourOfDay),
                --    tostring(view.startOfWeek:tostring())));
                local startTime = view.startOfWeek:clone():add({Day=sender.weekday-1}):set({Hour=hourOfDay});
                local endTime = startTime:clone():set({Minute=59});
                --Turbine.Shell.WriteLine( string.format(
                --    "  start[%s] end[%s]",
                --    startTime:tostring(),
                --    endTime:tostring()));
                self:AddCalendarEntry( 
                    startTime,
                    endTime,
                    false );  -- isAllDay
            end
            
        end
        
    end
    
    -- Set scrollbar for the viewport
    vScroll:SetMinimum(0);
    vScroll:SetMaximum(weekViewHourLabelsViewPort.entries:GetHeight()-weekViewHourLabelsViewPort:GetHeight());
    vScroll:SetValue(0);
    -- set scrollbar ValueChanged event handler to take an action when our value changes, 
    -- in this case, to change the map position relative to the viewport
    vScroll.ValueChanged=function()
        weekViewHourLabelsViewPort.entries:SetTop(0-vScroll:GetValue());
        partialDayEventsViewPort.entries:SetTop(0-vScroll:GetValue());
    end 

    -- Set scrollbar for the viewport
    hScroll:SetMinimum(0);
    hScroll:SetMaximum(dayLabelViewPort.entries:GetWidth()-dayLabelViewPort:GetWidth());
    hScroll:SetValue(0);
    -- set scrollbar ValueChanged event handler to take an action when our value changes, 
    -- in this case, to change the map position relative to the viewport
    hScroll.ValueChanged=function()
        dayLabelViewPort.entries:SetLeft(0-hScroll:GetValue());
        allDayEventsViewPort.entries:SetLeft(0-hScroll:GetValue());
        partialDayEventsViewPort.entries:SetLeft(0-hScroll:GetValue());
    end 
    
    -- forward mouse-wheel from children of viewport to scrollbar
    function forwardMouseWheel(control)
        control.MouseWheel=function(sender,args)
            --Turbine.Shell.WriteLine( string.format(
            --    "partialDayEventsViewPort.entries.MouseWheel value=[%d]",
            --    vScroll:GetValue()));
            --table.dump("sender",sender);
            --table.dump("args",args);
            local oldValue = vScroll:GetValue();
            local newValue = oldValue - args.Direction * vScroll:GetLargeChange();
            if( newValue <= vScroll:GetMinimum() ) then
                newValue = vScroll:GetMinimum()
            elseif( newValue >= vScroll:GetMaximum() ) then
                newValue = vScroll:GetMaximum()
            end
            if( oldValue ~= newValue ) then
                vScroll:SetValue(newValue);
            end
        end
    end
    
    ForEachControlList(weekViewHourLabelsViewPort.entries:GetControls(), forwardMouseWheel);
    ForEachControlList(partialDayEventsViewPort.entries:GetControls(), forwardMouseWheel);

    -- handle resize events
    view.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( string.format("View[%s].SizeChanged()", view.Name) );

        hScroll:SetWidth( -- width,height
            view:GetWidth() - topLeftLabel:GetWidth() - GRID_MARGIN*2 - 12);
        hScroll:SetTop( -- left,top
            view:GetHeight() - hScroll:GetHeight() - GRID_MARGIN*2 );
        
        vScroll:SetHeight( -- width,height
            hScroll:GetTop() - allDayEventLabel:GetTop() - allDayEventLabel:GetHeight() - GRID_MARGIN*2 );
        vScroll:SetLeft( -- left,top
            view:GetWidth() - vScroll:GetWidth());
        
        dayLabelViewPort:SetWidth(hScroll:GetWidth());
        
        allDayEventsViewPort:SetWidth(hScroll:GetWidth());
        
        weekViewHourLabelsViewPort:SetHeight( vScroll:GetHeight() );

        partialDayEventsViewPort:SetSize( -- width,height
            hScroll:GetWidth(),
            vScroll:GetHeight() );
    end
    
    return view;
end

--------------------------------------------------------------------------
--- CreateViewDay
--
-- Description: Creates the day view control
-- returns: the control for the day view
--
function CalendarWindow:CreateViewDay()
    --Turbine.Shell.WriteLine( "CalendarWindow:CreateViewDay()" );

    local view = Turbine.UI.Control();
    view:SetParent( self.calenderView );
    view:SetSize(self.calenderView:GetSize());
    view:SetPosition(0,0);
    view:SetVisible(false);
    view:SetBackColor(Turbine.UI.Color.Blue);
    view.Name = 'Day';

    -- Add dropdown button to select Year,month,day (Only visible from Day view)
    local now = DateTime:now();
    local minYear = now.Year - (Options.yearLimit-1);
    local maxYear = now.Year + (Options.yearLimit-1);
    local buttonViewDayDropdown = FrostyPlugins.Calendar.DateDropDownBox{format="%y-%m-%d"};
    view.buttonViewDayDropdown = buttonViewDayDropdown;
    buttonViewDayDropdown:SetParent( self.buttonViewTitle );
    buttonViewDayDropdown:SetSize( -- width,height
        150,
        self.buttonViewTitle:GetHeight() ); 
    buttonViewDayDropdown:SetPosition( -- left,top
    --    self.buttonOptions:GetLeft() + self.buttonOptions:GetWidth() + 5,
        110,    0 );
    --buttonViewDayDropdown:SetBackColor(Turbine.UI.Color.Purple); -- for debug window layout
    ----buttonSelectYear:SetBackColor( self.buttonViewTitle:GetBackColor());
    buttonViewDayDropdown:SetForeColor(Turbine.UI.Color.White);
    --buttonViewDayDropdown:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleRight );
    buttonViewDayDropdown:SetOutlineColor( Turbine.UI.Color.Yellow );
    buttonViewDayDropdown:SetFontStyle( Turbine.UI.FontStyle.None );
    buttonViewDayDropdown:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
    buttonViewDayDropdown:SetVisible( true );
    buttonViewDayDropdown:SetYearRange( minYear, maxYear );
    buttonViewDayDropdown:SetValue( self.current );
    
    buttonViewDayDropdown.ValueChanged = function( sender, args )
        --Turbine.Shell.WriteLine( string.format(
        --  "buttonViewDayDropdown.ValueChanged: [%s] was [%s]",
        --  args:tostring(),
        --  self.current:tostring()
        --  ));
        -- Something else (e.g. AdjustView()) has already changed the month
        -- So only adjust month if needed, to avoid an endless notification loop
        if( self.current ~= args ) then
            self.current = args;
            self:RebuildCalendar();
        end
    end 
    
    buttonViewDayDropdown.MouseEnterField = function( sender, args )
        --Turbine.Shell.WriteLine( "MouseEnterField " .. tostring(args.fieldName) );
        args:SetFontStyle( Turbine.UI.FontStyle.Outline );
        args:SetVisible( false );
        args:SetVisible( true );
    end
    
    buttonViewDayDropdown.MouseLeaveField = function( sender, args )
        --Turbine.Shell.WriteLine( "MouseLeaveField " .. tostring(args.fieldName) );
        args:SetFontStyle( Turbine.UI.FontStyle.None );
        args:SetVisible( false );
        args:SetVisible( true );
    end
    
    local allDayEventLabel = Turbine.UI.Button();
    allDayEventLabel:SetParent( view );
    allDayEventLabel:SetSize( -- width,height
        HOUR_LABEL_WIDTH,
        100 );
    allDayEventLabel:SetPosition( -- left,top
        GRID_MARGIN,
        GRID_MARGIN );
    allDayEventLabel:SetBackColor(Turbine.UI.Color.Black);
    allDayEventLabel:SetOutlineColor( Turbine.UI.Color.Yellow );
    allDayEventLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
    allDayEventLabel:SetText( "All\nDay" );
    
    allDayEventLabel.MouseEnter = function( sender, args )
        sender:SetForeColor( Turbine.UI.Color.Yellow );
        sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
        sender:SetVisible( false );
        sender:SetVisible( true );            
    end
    
    allDayEventLabel.MouseLeave = function( sender, args )
        sender:SetForeColor( Turbine.UI.Color.White );
        sender:SetFontStyle( Turbine.UI.FontStyle.None );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end

    allDayEventLabel.Click = function( sender, args )
        --Turbine.Shell.WriteLine("clicked allDayEvent " .. self.current:tostring());
        local startTime = self.current:clone():set({Hour=0,Minute=0});
        local endTime = startTime:clone():set({Hour=23,Minute=59});
        --Turbine.Shell.WriteLine( string.format(
        --  "  start[%s] end[%s]",
        --  startTime:tostring(),
        --  endTime:tostring()));
        self:AddCalendarEntry( 
            startTime,
            endTime,
            true );  -- isAllDay        
    end
    
    local vScroll = Turbine.UI.Lotro.ScrollBar();
    vScroll:SetOrientation(Turbine.UI.Orientation.Vertical);
    vScroll:SetParent(view);
    vScroll:SetSize( -- width,height
        11,
        view:GetHeight() - allDayEventLabel:GetHeight() - GRID_MARGIN*4); 
    vScroll:SetPosition( -- left,top
        view:GetLeft() + view:GetWidth() - vScroll:GetWidth(),
        allDayEventLabel:GetTop() + allDayEventLabel:GetHeight() + GRID_MARGIN);
    vScroll:SetVisible(true);
    view.vScroll = vScroll;
    
    local allDayEvents = Turbine.UI.Button();
    allDayEvents:SetParent( view );
    allDayEvents:SetSize( -- width,height
        vScroll:GetLeft() - (allDayEventLabel:GetLeft() + allDayEventLabel:GetWidth() + GRID_MARGIN),
        allDayEventLabel:GetHeight() );
    allDayEvents:SetPosition( -- left,top
        allDayEventLabel:GetLeft() + allDayEventLabel:GetWidth() + GRID_MARGIN,
        allDayEventLabel:GetTop() );
    allDayEvents:SetBackColor(Turbine.UI.Color.Black);
    allDayEvents:SetOutlineColor( Turbine.UI.Color.Yellow );
    allDayEvents:SetTextAlignment( Turbine.UI.ContentAlignment.TopCenter );
    allDayEvents:SetText( "<click to add>" );
    view.allDayEvents = allDayEvents;
    
    allDayEvents.MouseEnter = function( sender, args )
        sender:SetBackColor( Turbine.UI.Color(.1, .1, .1) );
        sender:SetForeColor( Turbine.UI.Color.Yellow );
        sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
        sender:SetVisible( false );
        sender:SetVisible( true );            
    end
    
    allDayEvents.MouseLeave = function( sender, args )
        sender:SetBackColor( Turbine.UI.Color.Black );
        sender:SetForeColor( Turbine.UI.Color.White );
        sender:SetFontStyle( Turbine.UI.FontStyle.None );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    allDayEvents.Click = function( sender, args )
        --Turbine.Shell.WriteLine("clicked allDayEvent " .. self.current:tostring());
        local startTime = self.current:clone():set({Hour=0,Minute=0});
        local endTime = startTime:clone():set({Hour=23,Minute=59});
        --Turbine.Shell.WriteLine( string.format(
        --  "  start[%s] end[%s]",
        --  startTime:tostring(),
        --  endTime:tostring()));
        self:AddCalendarEntry( 
            startTime,
            endTime,
            true );  -- isAllDay        
    end
    
    -- The height of each grid item
    local gridItemHeight = 30;
    
    -- Create the hourly events using a view port instead of a ListBox
    -- this allows placing the entry buttons on top anywhere we need to
    -- ** MY SINCERE AND ENDURING THANKS TO GARAN FOR THE VIEWPORT IDEA **
    local hourlyViewPort = Turbine.UI.Control();
    hourlyViewPort:SetParent( view );
    hourlyViewPort:SetPosition( -- left,top
        0, 
       vScroll:GetTop() );
    hourlyViewPort:SetSize( -- width,height
        view:GetWidth() - vScroll:GetWidth(),
        vScroll:GetHeight() );
    --hourlyViewPort:SetBackColor(Turbine.UI.Color.Purple); -- for debug
    -- Create control within the viewport to hold the actual items
    hourlyViewPort.entries = Turbine.UI.Control();
    hourlyViewPort.entries:SetParent( hourlyViewPort );
    hourlyViewPort.entries:SetPosition( 0, 0 ); -- left,top
    hourlyViewPort.entries:SetSize( -- width,height
        hourlyViewPort:GetWidth(),
        HOURS_PER_DAY * (GRID_MARGIN + gridItemHeight) );
    view.hourlyViewPort = hourlyViewPort;
    
    -- Set scrollbar for the viewport
    vScroll:SetMinimum(0);
    vScroll:SetMaximum(hourlyViewPort.entries:GetHeight()-hourlyViewPort:GetHeight());
    vScroll:SetValue(0);
    -- set scrollbar ValueChanged event handler to take an action when our value changes, 
    -- in this case, to change the map position relative to the viewport
    vScroll.ValueChanged=function()
        hourlyViewPort.entries:SetTop(0-vScroll:GetValue());
    end 
    
    -- fill the day grid with placeholders for each hour of the day
    view.gridElementsDay = {};
    for hourOfDay = 0, HOURS_PER_DAY - 1 do
        local rowTop = hourOfDay * (gridItemHeight + GRID_MARGIN); 
        
        local hourRowLabel = Turbine.UI.Button();
        hourRowLabel:SetParent( hourlyViewPort.entries );
        hourRowLabel:SetSize( -- width,height
            allDayEventLabel:GetWidth(),
            gridItemHeight);
        hourRowLabel:SetPosition( -- left,top
            GRID_MARGIN, 
            rowTop);
        hourRowLabel:SetBackColor(Turbine.UI.Color.Black);
        hourRowLabel:SetOutlineColor( Turbine.UI.Color.Yellow );
        hourRowLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleRight );
        hourRowLabel:SetText( CalTools:hourLabel(hourOfDay) );
        
        hourRowLabel.MouseEnter = function( sender, args )
            sender:SetBackColor( Turbine.UI.Color(.1, .1, .1) );
            sender:SetForeColor( Turbine.UI.Color.Yellow );
            sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
            sender:SetVisible( false );
            sender:SetVisible( true );
        end
        
        hourRowLabel.MouseLeave = function( sender, args )
            sender:SetBackColor( Turbine.UI.Color.Black );
            sender:SetForeColor( Turbine.UI.Color.White );
            sender:SetFontStyle( Turbine.UI.FontStyle.None );
            sender:SetVisible( false );
            sender:SetVisible( true );
        end
        
        hourRowLabel.Click = function( sender, args )
            --Turbine.Shell.WriteLine("clicked hour " .. tostring(hourOfDay));
            local startTime = self.current:clone():set({Hour=hourOfDay, Minute=0});
            local endTime = startTime:clone():set({Minute=59});
            self:AddCalendarEntry( 
                startTime,
                endTime,
                false );  -- isAllDay
        end
        
        local hourEntryButton = Turbine.UI.Button();
        hourEntryButton:SetParent( hourlyViewPort.entries );
        hourEntryButton:SetSize( -- width,height
            hourlyViewPort.entries:GetWidth(), 
            gridItemHeight ); 
        hourEntryButton:SetPosition( -- left,top
            hourRowLabel:GetLeft() + hourRowLabel:GetWidth() + GRID_MARGIN, 
            rowTop);
        --hourEntryButton:SetBlendMode( Turbine.UI.BlendMode.Normal );
        hourEntryButton:SetBackColor(Turbine.UI.Color.Black);
        hourEntryButton:SetOutlineColor( Turbine.UI.Color.Yellow );
        hourEntryButton:SetText( "<click to add entry>" );
        hourEntryButton.hourOfDay = hourOfDay; -- add custom property to identify on click
        
        -- Assign grid elements to hour of day
        view.gridElementsDay[ hourOfDay+1 ] = hourEntryButton;
        
        hourEntryButton.MouseEnter = function( sender, args )
            sender:SetForeColor( Turbine.UI.Color.Yellow );
            sender:SetBackColor( Turbine.UI.Color(.1, .1, .1) );
            sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
            sender:SetVisible( false );
            sender:SetVisible( true );
        end
        
        hourEntryButton.MouseLeave = function( sender, args )
            sender:SetForeColor( Turbine.UI.Color.White );
            sender:SetBackColor( Turbine.UI.Color.Black );
            sender:SetFontStyle( Turbine.UI.FontStyle.None );
            sender:SetVisible( false );
            sender:SetVisible( true );
        end
        
        hourEntryButton.Click = function( sender, args )
            --Turbine.Shell.WriteLine("clicked hour " .. tostring(sender.hourOfDay));
            local startTime = self.current:clone():set({Hour=sender.hourOfDay, Minute=0});
            local endTime = startTime:clone():set({Minute=59});
            self:AddCalendarEntry( 
                startTime,
                endTime,
                false );  -- isAllDay
        end
        
    end
    
    -- forward mouse-wheel from children of viewport to scrollbar
    function forwardMouseWheel(control)
        control.MouseWheel=function(sender,args)
            --Turbine.Shell.WriteLine( string.format(
            --    "hourlyViewPort.entries.MouseWheel value=[%d]",
            --    vScroll:GetValue()));
            --table.dump("sender",sender);
            --table.dump("args",args);
            local oldValue = vScroll:GetValue();
            local newValue = oldValue - args.Direction * vScroll:GetLargeChange();
            if( newValue <= vScroll:GetMinimum() ) then
                newValue = vScroll:GetMinimum()
            elseif( newValue >= vScroll:GetMaximum() ) then
                newValue = vScroll:GetMaximum()
            end
            if( oldValue ~= newValue ) then
                vScroll:SetValue(newValue);
            end
        end
    end
    ForEachControlList(hourlyViewPort.entries:GetControls(), forwardMouseWheel);
    
    -- handle resize events
    view.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( string.format("view[%s].SizeChanged()",view.Name) );
        
        vScroll:SetHeight(
            view:GetHeight() - allDayEventLabel:GetHeight() - GRID_MARGIN*4);
        vScroll:SetLeft(
            view:GetLeft() + view:GetWidth() - vScroll:GetWidth());
        
        allDayEvents:SetWidth(
            vScroll:GetLeft() - (allDayEventLabel:GetLeft() + allDayEventLabel:GetWidth() + GRID_MARGIN));
        
        hourlyViewPort:SetSize( -- width,height
            view:GetWidth() - vScroll:GetWidth(),
            vScroll:GetHeight() );
        
        hourlyViewPort.entries:SetWidth(hourlyViewPort:GetWidth());
        
        for idx,hourEntryButton in ipairs(view.gridElementsDay) do
            hourEntryButton:SetWidth(hourlyViewPort.entries:GetWidth());
        end
    end
    
    return view;
end

--------------------------------------------------------------------------
--- CreateViewList
--
-- Description: Creates the list view control
-- returns: the control for the list view
function CalendarWindow:CreateViewList()
    --Turbine.Shell.WriteLine( "CalendarWindow:CreateViewList()" );

    local view = Turbine.UI.Control();
    view:SetParent( self.calenderView );
    view:SetSize(self.calenderView:GetSize());
    view:SetPosition(0,0);
    view:SetVisible(false);
    view:SetBackColor(Turbine.UI.Color.Purple);
    view.Name = 'List';

    ----------------------------------------------------------------------
    -- create the list view
    
    local nbrLabel = Turbine.UI.Label();
    nbrLabel:SetParent( view );
    nbrLabel:SetSize( -- width,height
        30,
        20 );
    nbrLabel:SetPosition( -- left,top
        GRID_MARGIN,
        GRID_MARGIN );
    --dateLabel:SetText( "Date/Time" );
    nbrLabel:SetText( "#" );
    nbrLabel:SetTextAlignment( Turbine.UI.ContentAlignment.TopCenter );
    nbrLabel:SetBackColor(Turbine.UI.Color.Black);
    
    local dateLabel = Turbine.UI.Label();
    dateLabel:SetParent( view );
    dateLabel:SetSize( -- width,height
        145,
        20 );
    dateLabel:SetPosition( -- left,top
        nbrLabel:GetLeft() + nbrLabel:GetWidth() + GRID_MARGIN,
        GRID_MARGIN );
    dateLabel:SetText( "Start Date/Time" );
    --dateLabel:SetText( "yyyy-mm-dd hh:mm" );
    dateLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
    dateLabel:SetBackColor(Turbine.UI.Color.Black);
    
    local vScroll = Turbine.UI.Lotro.ScrollBar();
    vScroll:SetOrientation( Turbine.UI.Orientation.Vertical );
    vScroll:SetParent( view );
    vScroll:SetSize( -- width,height
        11,
        view:GetHeight() - dateLabel:GetHeight() - GRID_MARGIN*5 ); -- why 5?
    vScroll:SetPosition( -- left,top
        view:GetWidth() - vScroll:GetWidth(),
        dateLabel:GetTop() + dateLabel:GetHeight() + GRID_MARGIN);
    vScroll:SetVisible(true);
    
    local descriptionLabel = Turbine.UI.Label();
    descriptionLabel:SetParent( view );
    descriptionLabel:SetSize( -- width,height
        vScroll:GetLeft() - (dateLabel:GetLeft() + dateLabel:GetWidth() + GRID_MARGIN),
        dateLabel:GetHeight() );
    descriptionLabel:SetPosition( -- left,top
        dateLabel:GetLeft() + dateLabel:GetWidth() + GRID_MARGIN,
        dateLabel:GetTop() );
    descriptionLabel:SetBackColor(Turbine.UI.Color.Black);
    descriptionLabel:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
    descriptionLabel:SetText( "Event Description" );
    
    local entriesListBox = Turbine.UI.ListBox();
    --local entriesListBox = Turbine.UI.Label();
    entriesListBox:SetParent( view );
    entriesListBox:SetPosition( -- left,top
        GRID_MARGIN, 
        vScroll:GetTop() ); 
    entriesListBox:SetSize( -- width,height
        view:GetWidth() - vScroll:GetWidth() - GRID_MARGIN,
        vScroll:GetHeight() );
    --entriesListBox:SetBackColor(Turbine.UI.Color.Black);
    --entriesListBox:SetVisible(true);
    entriesListBox:SetVerticalScrollBar(vScroll);
    view.entriesListBox = entriesListBox;
    
    -- A custom function to add a calendar entry
    local descriptionLabels = {};
    entriesListBox.addCalendarEntry = function( sender, entry )
        --Turbine.Shell.WriteLine( "entriesListBox.addCalendarEntry(" .. tostring(entry) .. ")");
        --table.dump("entry",entry);
        
        local entryRow = Turbine.UI.ListBox();
        entryRow:SetParent( entriesListBox );
        entryRow:SetSize( -- width,height
            entriesListBox:GetWidth(), 
            30 );
        entryRow:SetOrientation(Turbine.UI.Orientation.Horizontal);
        entryRow:SetBlendMode( Turbine.UI.BlendMode.Normal );
        --entryRow:SetWantsKeyEvents(true);
        
        --entryRow:SetBackColor(Turbine.UI.Color.Blue);
        --entryRow:SetBackColor(Turbine.UI.Color[entryColor]);

        --entryRow.entryNumber = i;
        entryRow.entry = entry;  -- add custom property to identify on click
        entriesListBox:AddItem(entryRow);
        
        entryRow.MouseEnter = function( sender, args )
            -- Increment by 2 to skip divider labels
            for idx = 1, sender:GetItemCount(), 2 do
                local item = sender:GetItem(idx);
                item:SetFontStyle( Turbine.UI.FontStyle.Outline );
                --item:SetForeColor( Turbine.UI.Color.Yellow );
                item:SetBackColor( Turbine.UI.Color(.1, .1, .1) );
            end
        end
        
        entryRow.MouseLeave = function( sender, args )
            -- Increment by 2 to skip divider labels
            for idx = 1, sender:GetItemCount(), 2 do
                local item = sender:GetItem(idx);
                item:SetFontStyle( Turbine.UI.FontStyle.None );
                --item:SetForeColor( Turbine.UI.Color.White );
                item:SetBackColor( Turbine.UI.Color.Black );
            end
        end
        
        entryRow.MouseClick = function( sender, args )
            --Turbine.Shell.WriteLine("entryRow.MouseClick entry " .. tostring(sender.entry));
            self:UpdateCalendarEvent( sender.entry );
        end
        
        local rowDivider = Turbine.UI.Label();
        rowDivider:SetParent( entryRow );
        rowDivider:SetSize( -- width,height
            entryRow:GetWidth(), 
            GRID_MARGIN );
        entriesListBox:AddItem(rowDivider);     
        
        local entryColor = Turbine.UI.Color[entry.Color or "White"];
        
        local entryNbr = Turbine.UI.Label();
        entryNbr:SetParent( entryRow );
        entryNbr:SetSize( -- width,height
            nbrLabel:GetWidth(),
            entryRow:GetHeight() );
        entryNbr:SetBackColor(Turbine.UI.Color.Black);
        entryNbr:SetForeColor(entryColor);
        entryNbr:SetOutlineColor( Turbine.UI.Color.Yellow );
        entryNbr:SetTextAlignment( nbrLabel:GetTextAlignment() );
        entryNbr:SetText( tostring(entriesListBox:GetItemCount()/2) ); -- /2 because of dividers
        entryRow:AddItem(entryNbr);
        
        --entryNbr.MouseClick = function( sender, args )
        --    --Turbine.Shell.WriteLine("entryNbr.MouseClick entry " .. tostring(sender));
        --    entryRow:MouseClick(sender);
        --end
        
        local divider = Turbine.UI.Label();
        divider:SetParent( entriesListBox );
        divider:SetSize( -- width,height
            GRID_MARGIN,
            entryRow:GetHeight() );
        entryRow:AddItem(divider);
        
        local entryStart = Turbine.UI.Label();
        entryStart:SetParent( entryRow );
        entryStart:SetSize( -- width,height
            dateLabel:GetWidth(),
            entryRow:GetHeight() );
        entryStart:SetBackColor(Turbine.UI.Color.Black);
        entryStart:SetForeColor(entryColor);
        entryStart:SetOutlineColor( Turbine.UI.Color.Yellow );
        entryStart:SetText( entry.Start );
        entryRow:AddItem(entryStart);
        
        local divider = Turbine.UI.Label();
        divider:SetParent( entriesListBox );
        divider:SetSize( -- width,height
            GRID_MARGIN,
            entryRow:GetHeight() );
        entryRow:AddItem(divider);
        
        local entryDescription = Turbine.UI.Label();
        entryDescription:SetParent( entryRow );
        entryDescription:SetSize( -- width,height
            descriptionLabel:GetWidth(), 
            entryRow:GetHeight() );
        entryDescription:SetBackColor(Turbine.UI.Color.Black);
        entryDescription:SetForeColor(entryColor);
        entryDescription:SetOutlineColor( Turbine.UI.Color.Yellow );
        entryDescription:SetText( entry.Description );
        entryRow:AddItem(entryDescription);
        
        entryRow.SizeChanged = function( sender, args )
            --Turbine.Shell.WriteLine( string.format("entryRow.SizeChanged()") );
            entryDescription:SetWidth(
                entryRow:GetWidth() - entryStart:GetWidth() - GRID_MARGIN ); 
        end
        
        -- Modify settings for all controls in row
        for idx=1, entryRow:GetItemCount() do
            childCtl = entryRow:GetItem(idx);
            --Turbine.Shell.WriteLine("set controls for entryNbr " .. entryNbr.GetText());
            -- forward MouseClick event to row MouseClick
            childCtl.MouseClick = function( sender, args )
                --Turbine.Shell.WriteLine("entryNbr.MouseClick entry " .. tostring(sender));
                entryRow:MouseClick(sender);
            end
            
        end
        
    end
    
    -- handle resize events
    view.SizeChanged = function( sender, args )
        --Turbine.Shell.WriteLine( string.format("view[%s].SizeChanged()",view.Name) );
        
        vScroll:SetHeight(
            view:GetHeight() - dateLabel:GetHeight() - GRID_MARGIN*5 ); -- why 5?
        vScroll:SetLeft(
            view:GetWidth() - vScroll:GetWidth());
        
        descriptionLabel:SetWidth(
            vScroll:GetLeft() - (dateLabel:GetLeft() + dateLabel:GetWidth() + GRID_MARGIN));
        
        entriesListBox:SetSize( -- width,height
            view:GetWidth() - vScroll:GetWidth() - GRID_MARGIN,
            vScroll:GetHeight() );
        
        for idx = 1, entriesListBox:GetItemCount() do
            local entryRow = entriesListBox:GetItem(idx);
            entryRow:SetWidth( entriesListBox:GetWidth() );
        end
        
    end

    return view;
end


--------------------------------------------------------------------------
--- InitializeCalendar
--
-- Description: initialize the calendar: get the current day and
-- initialize the starting day and month
function CalendarWindow:InitializeCalendar()

    --Turbine.Shell.WriteLine( "CalendarWindow:InitializeCalendar()" );

    -- track "today"
    --self.today = Turbine.Engine.GetDate();    
    self.today = DateTime:now();
    
    -- initialize the rest of the date members that we use to
    -- display the calendar
    self.current =  DateTime:now();
    --Turbine.Shell.WriteLine("current: " .. self.current:tostring());
    
    --for k,v in pairs(Turbine.Engine.GetDate()) do
    --  Turbine.Shell.WriteLine("k: " .. k .. "=" .. tostring(v));
    --end
    -- Dump what routines are available in LOTRO's Engine package
    --Turbine.Shell.WriteLine("Engine: ");
    --for k,v in pairs(Turbine.Engine) do
    --  Turbine.Shell.WriteLine("k: " .. k .. "=" .. tostring(v));
    --end
    -- Try to see what packages are available
    --Turbine.Shell.WriteLine("package.path=" .. tostring(package.path));
    --Turbine.Shell.WriteLine("package.loaded: ");
    --for k,v in pairs(package.loaded) do
    --    Turbine.Shell.WriteLine("k: " .. k .. "=" .. tostring(v));
    --end   
end

--------------------------------------------------------------------------
--- AdjustView
--
-- Description: the user has pressed the + or - buttons, so adjust
-- the display according to the view selected (month,week,day)
--
-- @param delta - integer - the amount of time to change the month by.
--                   this will be 1 or -1 months
function CalendarWindow:AdjustView( delta )

    --Turbine.Shell.WriteLine( string.format(
    --    "CalendarWindow:AdjustView [%d]  current=[%s]", 
    --    delta,
    --    self.current:tostring()));

    -- Compute new proposed date
    local newDate = self.current:clone();
    if( "Month" == self.settings.view) then
        newDate:add({Month=delta});
    elseif( "Week" == self.settings.view) then
        newDate:add({Day=delta*(DAYS_PER_WEEK)});
    elseif( "Day" == self.settings.view) then
        newDate:add({Day=delta});
    else
        Turbine.Shell.WriteLine( " ignoring view adjustment in list view" );
        return;
    end

    --Turbine.Shell.WriteLine( string.format(
    --    "CalendarWindow:AdjustView new=[%s]", 
    --    newDate:tostring()));
    
    -- Compute limits of adjustments
    local now = DateTime:now();
    local minYear = now.Year - (Options.yearLimit-1);
    local maxYear = now.Year + (Options.yearLimit-1);
    -- Abort if trying to go to date more than 5 years year ago
    if( newDate.Year < minYear ) then
        Turbine.Shell.WriteLine( string.format(
            "Refused to go earlier than [%s]",
            lowerLimit:tostring()));
        return;
    elseif( newDate.Year > maxYear ) then
        Turbine.Shell.WriteLine( string.format(
            "Refused to go later than [%s]",
            upperLimit:tostring()));
        return;
    else
        self.current = newDate;
    end
    
    self:RebuildCalendar();
end

--------------------------------------------------------------------------
--- RebuildCalendar
--
-- Description: update the calendar window with any changes.  
-- Changes include: 
-- * increasing or decreasing months (entire calendar needs to be redrawn), 
-- * saving or clearing events
--
-- fixme: frosty
--        can I pass in a hint so that I don't have to rebuild the
--        entire calendar from scratch every time?  The current method
--        is overkill when saving or clearing the calendar events.
function CalendarWindow:RebuildCalendar()
    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildCalendar()" );
    
    -- Clear/Hide existing event buttons
    -- - instead of creating/destroying buttons each time
    --   just create a list of buttons for each event
    --   and let each view re-arrange the buttons for each view
    -- - How to handle events that wrap multiple weeks?
    for idx,btn in ipairs(self.entryListButtons) do
        local entryButton = self.entryListButtons[idx];
        entryButton:SetParent( nil );
        entryButton:SetVisible(false);
        entryButton:SetEnabled(false);
        --Turbine.Shell.WriteLine( string.format("  entryBtn[%d][%s] enabled[%s] visible[%s]",
        --    idx,
        --    tostring(btn),
        --    tostring(btn:IsEnabled()),
        --    tostring(btn:IsVisible())            
        --    ));
    end
    self.nextEntryButton = 1; -- reset event buttons
    
    -- Enable/Disable left/right buttons depending on view and current date
    -- Compute dates the left/right buttons would generate based on view
    local now = DateTime:now();
    local minYear = now.Year - (Options.yearLimit-1);
    local maxYear = now.Year + (Options.yearLimit-1);
    local newEarlierDate = self.current:clone();
    local newLaterDate   = self.current:clone();
    if( "Month" == self.settings.view) then
        newEarlierDate:add({Month=-1});
        newLaterDate:add({Month=1});
    elseif( "Week" == self.settings.view) then
        newEarlierDate:add({Day=-(DAYS_PER_WEEK-1)});
        newLaterDate:add({Day=(DAYS_PER_WEEK-1)});
    elseif( "Day" == self.settings.view) then
        newEarlierDate:add({Day=-1});
        newLaterDate:add({Day=1});
    else
        -- set past limits to disable buttons
        newEarlierDate = self.current:clone():set{Year=minYear-1};
        newLaterDate   = self.current:clone():set{Year=maxYear+1};
    end
    local isNotAtMinDate = not (newEarlierDate.Year < minYear);
    local isNotAtMaxDate = not (newLaterDate.Year > maxYear);
    --Turbine.Shell.WriteLine( string.format(
    --  "CalendarWindow:RebuildCalendar() enableLeft=[%s] enableRight=[%s]",
    --  tostring(isNotAtMinDate),
    --  tostring(isNotAtMaxDate)));
    self.buttonLeft:SetEnabled(isNotAtMinDate);
    self.buttonRight:SetEnabled(isNotAtMaxDate);

    
    -- enable the selected calendar display
    --Turbine.Shell.WriteLine( "RebuildCalendar view [" .. self.settings.view .. "]" );
    for view,control in pairs(self.calenderViews) do
        if( view == self.settings.view) then
            control:SetVisible(true);
        else
            control:SetVisible(false);
        end
    end
    
    --Turbine.Shell.WriteLine( string.format(
    --    "CalendarWindow:RebuildCalendar() currentDay = [%s]",
    --    self.current:tostring() ));

    local isVisible = self.calenderViews["Month"]:IsVisible();
    self.calenderViews["Month"].buttonSelectMonth:SetVisible( isVisible );
    self.calenderViews["Month"].buttonSelectYear:SetVisible( isVisible );
    local isVisible = self.calenderViews["Day"]:IsVisible();
    --view.buttonSelectYear:SetVisible( isVisible );
    self.calenderViews["Day"].buttonViewDayDropdown:SetVisible( isVisible );
    
    
    -- update the calendar display
    if( self.calenderViews["Month"]:IsVisible() ) then
        self:RebuildViewMonth();
    elseif( self.calenderViews["Week"]:IsVisible() ) then
        self:RebuildViewWeek();
    elseif( self.calenderViews["Day"]:IsVisible() ) then
        self:RebuildViewDay();
    elseif( self.calenderViews["List"]:IsVisible() ) then
        self:RebuildViewList();
    end
end

--------------------------------------------------------------------------
--- getNextEntryButton
--
-- Description: gets the next calendar entry button
-- Creates a new button as needed
--
function CalendarWindow:getNextEntryButton()
    --Turbine.Shell.WriteLine( string.format(
    --  "CalendarWindow:getNextEntryButton() nbr[%d] next[%d]",
    --  #self.entryListButtons,
    --  self.nextEntryButton));
    
    -- Get next button as needed (add button as needed)
    if( self.nextEntryButton > #self.entryListButtons ) then
        --Turbine.Shell.WriteLine( "CalendarWindow:getNextEntryButton() creating new button");
        local btn = Turbine.UI.Button();
        table.insert(self.entryListButtons, btn);
        --table.dump("entryListButtons",self.entryListButtons);
        --Turbine.Shell.WriteLine( string.format(
        --  "CalendarWindow:getNextEntryButton() add nbr[%d] btn[%s]",
        --  #self.entryListButtons,
        --  tostring(btn)));        
    end
    --table.dump("entryListButtons",self.entryListButtons);
    local entryButton = self.entryListButtons[self.nextEntryButton];
    assert( nil ~= entryButton, "failed to obtain entry button");
    self.nextEntryButton = self.nextEntryButton+1;

    --Turbine.Shell.WriteLine( string.format(
    --  "CalendarWindow:getNextEntryButton() returning[%s]",
    --  tostring(entryButton)));

    entryButton:SetEnabled( true );
    entryButton:SetVisible( true );

    -- Override the SetBackColor to also contract the outline color
    if( nil == entryButton.BaseSetBackColor) then
        -- save the original function
        entryButton.BaseSetBackColor = entryButton.SetBackColor;
    end
    entryButton.SetBackColor = function( self, color )
        self:BaseSetBackColor(color);
        -- Colors have R,G,B components with values 0 to 1
        -- to invert color, take inverse of each color component
        local contrastColor = Turbine.UI.Color(
            1-color.R,
            1-color.G,
            1-color.B);
        self:SetOutlineColor( contrastColor );        
    end
    
    --entryButton:SetOutlineColor( Turbine.UI.Color.Yellow );

    entryButton.MouseEnter = function( sender, args )
        sender:SetFontStyle( Turbine.UI.FontStyle.Outline );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    entryButton.MouseLeave = function( sender, args )
        sender:SetFontStyle( Turbine.UI.FontStyle.None );
        sender:SetVisible( false );
        sender:SetVisible( true );
    end
    
    return entryButton;
end

--------------------------------------------------------------------------
--- AssignLanesToEntries
--
-- Description: Assign specified entries a lane for layout on calendar
-- where a lane is either a row in the monthly view or weekly all-day row
-- or a column in the daily or weekly view hourly events
-- @param entriesToDisplay = the entries to assign
-- @param granularity   = the granularity for events
-- @return laneForEntry = a lookup table of the lanes for each entry
function CalendarWindow:AssignLanesToEntries(entriesToDisplay, granularity)
    --Turbine.Shell.WriteLine( string.format(
    --    "CalendarWindow:AssignLanesToEntries() entriesToDisplay[%s] nbr[%d] granularity[%s]",
    --    tostring(entriesToDisplay),
    --    #entriesToDisplay,
    --    granularity));
    
    if( ("day" ~= granularity) and ("hour" ~= granularity) ) then
        error(string.format("Unsupported granularity [%s]",granularity),3);
    end
    
    --table.dump("BEFORE entriesToDisplay",entriesToDisplay);
    --Turbine.Shell.WriteLine( "  BEFORE entriesToDisplay:");
    --for idx,entry in ipairs(entriesToDisplay) do
    --    Turbine.Shell.WriteLine(string.format("  [%d][%s] start[%s] end[%s] desc[%s] ",
    --        idx,
    --        tostring(entry),
    --        entry.Start,
    --        entry.End,
    --        entry.Description
    --        ));
    --end
    
    -- order events by start date and longest duration
    function orderByStartAndDuration(a,b)
        -- if start times are identical 
        -- then pick the longer duration first
        if( a.Start == b.Start) then
            local dtStartA = DateTime:parse(a.Start);
            local dtEndA = DateTime:parse(a.End);
            local dtStartB = DateTime:parse(b.Start);
            local dtEndB = DateTime:parse(b.End);
            local aDuration = dtEndA:MinutesSince(dtStartA);
            local bDuration = dtEndB:MinutesSince(dtStartB);
            return aDuration > bDuration;
        else
            -- otherwise pick the one that starts first
            return a.Start < b.Start;
        end
    end
    table.sort(entriesToDisplay, orderByStartAndDuration);
    
    --table.dump("SORTED entriesToDisplay",entriesToDisplay);
    --Turbine.Shell.WriteLine( "  SORTED entriesToDisplay:");
    --for idx,entry in ipairs(entriesToDisplay) do
    --  Turbine.Shell.WriteLine(string.format("  [%d] start[%s] end[%s] desc[%s]",
    --      idx,
    --      entry.Start,
    --      entry.End,
    --      entry.Description
    --      ));
    --end
    
    -- layout calendar entry algorithm
    -- https://stackoverflow.com/questions/50512059/algorithm-to-organise-calendar-events-using-minimum-positions   
    -- - initialize a list of free lanes
    -- - for each event e,
    --     1. check which occupied lanes are free for e.startTime
    --     2. assign e.lane to a free lane, or add a new free lane if none empty
    --     3. mark the e.lane as occupied until e.endTime is reached
    local laneExpiration = {}; -- keep track of when lane assignment expires
    local laneForEntry = {}; -- Lookup of entry to lane on day
    -- Assumes events are ordered by start date, longest item first
    for idx,entry in ipairs(entriesToDisplay) do        
        --Turbine.Shell.WriteLine(string.format(
        --  "  entriesToDisplay[%d] start [%s] end [%s] description [%s]", 
        --  idx, 
        --  entry.Start,
        --  entry.End,
        --  entry.Description) );       
        -- Find next free lane for entry (add one if needed)
        for lane = 1, #laneExpiration+1 do
            --Turbine.Shell.WriteLine(string.format(
            --  "    ? lane [%d] expire [%s]",
            --  lane,
            --  tostring(laneExpiration[lane])));
            
            -- treat each entry as all-day for month display
            local entryStart;
            local entryEnd;
            if( "day" == granularity) then
                entryStart = DateTime:parse(entry.Start):set({Hour=0,Minute=0});
                entryEnd   = DateTime:parse(entry.End):set({Hour=23,Minute=59});
            elseif( "hour" == granularity) then
                entryStart = DateTime:parse(entry.Start):set({Minute=0});
                entryEnd   = DateTime:parse(entry.End):set({Minute=59});
            else
                error("Unsupported granularity",2);
            end
            
            -- if lane not assigned, or has expired
            -- then assign entry to lane, tracking expiration for lane
            if( (not laneExpiration[lane]) or (laneExpiration[lane] < entryStart) ) then
                laneForEntry[entry] = lane; -- assign entry to lane
                laneExpiration[lane] = entryEnd; -- track expiration of lane
                break; -- is now assigned, break out of lane-search loop
            end
        end
        
        --Turbine.Shell.WriteLine(string.format(
        --  "    => lane [%d] expire [%s]", 
        --  laneForEntry[entry],
        --  laneExpiration[laneForEntry[entry]] ) );
    end

    return laneForEntry;
end

--------------------------------------------------------------------------
--- RebuildViewMonth
--
-- Description: update the calendar month view 
--
function CalendarWindow:RebuildViewMonth()
    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildViewMonth()" );

    local view = self.calenderViews["Month"];

    -- update the month label: month year
    --local updateLabel = string.format("%s %s",
    --  calTools:monthLabel(self.current.Month),
    --  tostring(self.current.Year));
    --self.buttonViewTitle:SetText( updateLabel );
    self.buttonViewTitle:SetText( "" ); -- clear

    local buttonSelectMonth = view.buttonSelectMonth;
    buttonSelectMonth:SetSelectedIndex(self.current.Month);
    
    local buttonSelectYear = view.buttonSelectYear;
    local now = DateTime:now();
    --Turbine.Shell.WriteLine( string.format("yearLimit[%d]",
    --    Options.yearLimit));
    local minYear = now.Year - (Options.yearLimit-1);
    local maxYear = now.Year + (Options.yearLimit-1);
    local yearValues = table.fill(minYear,maxYear);
    --table.dump("yearValues",yearValues);
    buttonSelectYear:SetDropDownList(yearValues);
    buttonSelectYear:SetSelectedValue(self.current.Year);
    
    -- determine the number of days in the month
    local numberOfDays = self.current:daysInMonth();
    --Turbine.Shell.WriteLine( string.format(
    --  "days in month [%s] = [%d]",
    --  self.current:ymd(),
    --  numberOfDays) );
    
    -- determine day that month starts
    local startDay = calTools:dayOfWeek( 
        self.current.Year, 
        self.current.Month, 
        1);
    --Turbine.Shell.WriteLine( string.format(
    --  "Month [%s] starts on day [%d]",
    --  self.current:ymd(),
    --  startDay) );
    
    -- create a key string for today (yyyy-mm-dd)
    local todayString = DateTime:now():ymd();
    --Turbine.Shell.WriteLine( "todayString = [" .. todayString .. "]" );
    
    -- Enable the today button if not viewing current month
    self.buttonToday:SetEnabled( 
        (self.current.Month ~= self.today.Month) or
        (self.current.Year ~= self.today.Year) 
    );
    
    -- intersection of
    -- - all events that start before or on day
    -- - all events that end after or on day

    -- maintain indices of:
    --   startIndices = all events that start on day
    --   endIndices   = all events that end on day

    local dayIndex = 1;
    for i = 1, #view.gridElementsMonth do
        local gridElement = view.gridElementsMonth[ i ];
        gridElement:SetVisible(true);
        -- reset any indicators that the date has an event on it
        gridElement:SetBackColor( Turbine.UI.Color.Black );
        -- disable mouse click on inactive grid cells
        gridElement:SetMouseVisible(false);

        gridElement.entryKey = nil; -- clear old entry key
        local newLabel = "";
        if( i > startDay ) then
            if( i < ( startDay + numberOfDays + 1 ) ) then
                -- give slightly brighter color to active grid cells
                gridElement:SetBackColor( 
                    --Turbine.UI.Color.MidnightBlue
                    --Turbine.UI.Color.DarkGray -- too light
                    --Turbine.UI.Color.DarkBlue -- too light
                    Turbine.UI.Color(0.1,0.1,0.1) -- r,b,g
                    );
                -- enable mouse clicks on active grid clls
                gridElement:SetMouseVisible(true);
                
                -- compute the day of the month label for this grid entry
                local dayIndex = i - startDay;
                newLabel = tostring( dayIndex );
                
                -- if this date has an entry on it, change the back color
                --local entryKey = string.format("%04d-%02d-%02d",
                --    self.current.Year,
                --    self.current.Month,
                --    dayIndex );
                local entryKey = self.current:clone():set({Day=dayIndex,Hour=0,Minute=0});
                gridElement.entryKey = entryKey; -- Assign date for mouseClick
                --Turbine.Shell.WriteLine( string.format(
                --  "gridElement[%d] entryKey=[%s] %s",
                --  i, 
                --  entryKey:tostring(),
                --  tostring(gridElement)) );
                
                -- highlight day for events
                --local entriesForDay = self.settings.CalendarEntries[ entryKey:ymd() ];
                --if( entriesForDay and (#entriesForDay > 0) ) then
                --    gridElement:SetBackColor( Turbine.UI.Color( .75, .5, .5, 0 ) );
                --end
                
                -- highlight if this day is today
                if( entryKey:ymd() == todayString ) then
                    gridElement:SetBackColor( Turbine.UI.Color.DarkSlateBlue );
                end
            end
        end
        gridElement:SetText( newLabel );
        --Turbine.Shell.WriteLine(string.format(
        --  "  Grid [%d] text [%s]", i, newLabel) );
    end
    
    self:RebuildViewMonthEvents();
end

--------------------------------------------------------------------------
--- RebuildViewMonthEvents
--
-- Description: update the calendar month view with buttons for each event
--
function CalendarWindow:RebuildViewMonthEvents()
    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildViewMonthEvents()" );

    -- Retreive all entries to display for this month
    -- - this will also retreive entries that started before this month
    -- - also need to include entries that end after this month
    local somDateTime = self.current:clone():set({Day=1,Hour=0,Minute=0});
    local numberOfDays = self.current:daysInMonth();
    local eomDateTime = somDateTime:clone():set({Day=numberOfDays,Hour=23,Minute=59});

    local entriesToDisplay = self:GetCalendarEntriesBetween(somDateTime, eomDateTime);

    -- Assign entries to lanes (e.g. rows)
    local laneForEntry = self:AssignLanesToEntries(entriesToDisplay, "day");
    
    -- determine day that month starts
    local monthStartDay = somDateTime:dayOfWeek();
    
    -- Iterate over each entry, and place buttons on calendar grid
    -- - Note: Entries that span weeks will have multiple buttons
    for idx,entry in ipairs(entriesToDisplay) do
        -- since month view, adjust start/end dates to appear to be all day
        local entryStart = DateTime:parse(entry.Start):set({Hour=0,Minute=0});
        local entryEnd = DateTime:parse(entry.End):set({Hour=23,Minute=59});
        local duration = entryEnd:DaysSince(entryStart)+1;
        --Turbine.Shell.WriteLine(string.format(
        --  "  entry [%d] Start [%s] End [%s] duration[%d] Lane[%d] [%s]", 
        --  idx,
        --  tostring(entry.Start),
        --  tostring(entry.End),
        --  duration,
        --  laneForEntry[entry],
        --  entry.Description) );
        
        --local nbrDaysLeft = duration;
        if( entryStart < somDateTime ) then
            -- if started in prior month, adjust to start of month
            -- and adjust days left 
            entryStart = somDateTime;
        end
        if( entryEnd > eomDateTime ) then
            -- if end in next month, clip to start of month
            entryEnd = eomDateTime;
        end
        local nbrDaysLeft = entryEnd:DaysSince(entryStart)+1;
        
        -- Iterate over days for entry to determine how many individual buttons to create
        local setsOfButtons = {}; -- startIdx, endIdx
        local buttonStartDay = entryStart;
        local countdown = 20; -- prevent hang if bug in loop (can't have more than 6 weeks of buttons per event)
        while( buttonStartDay < entryEnd ) do
            local dayOfWeek = buttonStartDay:dayOfWeek();
            local nbrDaysLeftInWeek = DAYS_PER_WEEK - dayOfWeek + 1;
            local nbrDaysForButton = nbrDaysLeft; -- assume to end
            if( nbrDaysLeft > nbrDaysLeftInWeek ) then
                nbrDaysForButton = nbrDaysLeftInWeek;
            end
            nbrDaysLeft = nbrDaysLeft - nbrDaysForButton;
            local buttonEndDay = buttonStartDay:clone():add({Day=nbrDaysForButton-1}):set({Hour=23,Minute=59});
            local idxGridStart = monthStartDay + buttonStartDay.Day - 1;
            local idxGridEnd   = monthStartDay + buttonEndDay.Day - 1;
            setsOfButtons[idxGridStart] = idxGridEnd;
            buttonStartDay = buttonEndDay:clone():add({Day=1}):set({Hour=0,Minute=0}); -- move to next day
            countdown = countdown - 1;
            if( countdown <= 0 ) then
                error("Detected error in loop!");
                break; -- prevent infinite loop
            end
        end
        
        -- TODO - display elipses '...' if not all events can be displayed in a cell
        -- - click on '...' could bring to day/week display for
        
        -- Iterate over buttons for event, add each button to month display
        local view = self.calenderViews["Month"]
        assert(view,"Missing view");
        for idxGridStart,idxGridEnd in pairs(setsOfButtons) do
            local gridElementStart = view.gridElementsMonth[ idxGridStart ];
            local gridElementEnd = view.gridElementsMonth[ idxGridEnd ];
            local lane = laneForEntry[entry];
            --Turbine.Shell.WriteLine(string.format(
            --  "    event [%d] lane [%d] startGrid=[%d] endGrid=[%d]",
            --  idx,
            --  lane,
            --  idxGridStart,
            --  idxGridEnd) );
            
            -- Get next entry button
            local entryButton = self:getNextEntryButton();
            entryButton:SetParent( view );
            entryButton:SetSize( -- width,height
                gridElementEnd:GetLeft() + gridElementEnd:GetWidth() - gridElementStart:GetLeft(),
                20 );
            local top = gridElementStart:GetTop() +
                (entryButton:GetHeight() * lane );
            entryButton:SetPosition( -- left,top
                gridElementStart:GetLeft(),
                top);
            --entryButton:SetBackColor( gridElementStart:GetBackColor() );
            --entryButton:SetBackColor( Turbine.UI.Color.DarkBlue );
            entryButton:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleLeft );
            --Turbine.Shell.WriteLine(string.format(
            --  "    event [%d] color [%s]",
            --  idx, tostring(entry.Color)));
            local entryColor = "White"; -- default
            if( "string" == type(entry.Color) ) then
                --Turbine.Shell.WriteLine(string.format(
                --  "    event [%d] setcolor [%s]",
                --  idx, tostring(entry.Color)));
                entryColor = entry.Color;
            end
            entryButton:SetForeColor( Turbine.UI.Color[entryColor] );
            --entryButton:SetForeColor( Turbine.UI.Color.Orange );
            entryButton:SetBackColor( gridElementStart:GetBackColor() );
            entryButton:SetOutlineColor( Turbine.UI.Color.Yellow );
            entryButton:SetMultiline( false ); 
            entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
            --entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana22 );
            entryButton:SetText( entry.Description );
            
            -- For items that span multiple days, append ' ---|' to show entry duration
            if( idxGridStart ~= idxGridEnd ) then
                --local maxTextLength = entryButton:GetWidth() / 7;
                local maxTextLength = entryButton:GetWidth() / 
                    FontWidthPerChar[entryButton:GetFont()];
                
                local charsLeft = maxTextLength - string.len(entryButton:GetText()) -1;
                --Turbine.Shell.WriteLine(string.format(
                --  "    event [%d] btnWidth [%d] maxTextLength [%d] charsLeft=[%d] font [%s]",
                --  idx, entryButton:GetWidth(), maxTextLength, charsLeft, entryButton:GetFont()));
                if( charsLeft > 3 ) then -- Leave roomt for at least " -|"
                    entryButton:AppendText( " " .. string.rep("-",charsLeft-1) .. "|");
                end
            end
            
            -- Hide if more than 3 lanes
            if( lane > 3 ) then
                entryButton:SetVisible(false);
                entryButton:SetEnabled(false);
            else
                entryButton:SetVisible(true);
                entryButton:SetEnabled(true);
            end
            entryButton.entry = entry;
            entryButton.Click = function( sender, args )
                local day = sender:GetText();
                --Turbine.Shell.WriteLine("click entry:\n" .. 
                --    "  [" .. sender.entry.Description .. "]" );
                self:UpdateCalendarEvent( sender.entry );
            end
            
        end
    end
    
end

--------------------------------------------------------------------------
--- RebuildViewWeek
--
-- Description: update the calendar week view 
--
function CalendarWindow:RebuildViewWeek()
    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildViewWeek" );
    
    -- update the month label: y/m/d - y/m/d
    -- - set the startDay to start of week, endDay to end of week.
    local now = DateTime:now();
    local currentDayOfWeek = self.current:dayOfWeek();
    --Turbine.Shell.WriteLine( "  currentDayOfWeek=[" .. currentDayOfWeek .. "]" );
    local startOfWeek = self.current:clone():set({Hour=0, Minute=0}):add({Day = -(currentDayOfWeek-1)});
    local endOfWeek = startOfWeek:clone():add({Day=6, Hour=23, Minute=59});
    --Turbine.Shell.WriteLine( "  startOfWeek=[" .. startOfWeek:tostring() .. "]" );
    --Turbine.Shell.WriteLine( "  endOfWeek=[" .. endOfWeek:tostring() .. "]" );

    local updateLabel = string.format("%s - %s",
        startOfWeek:ymd(),
        endOfWeek:ymd() );
    self.buttonViewTitle:SetText( updateLabel );

    -- Enable the today button if not viewing current month
    local isCurrentWeek = (startOfWeek < now ) and (now < endOfWeek);
    --Turbine.Shell.WriteLine( "  isCurrentWeek=[" .. tostring(isCurrentWeek) .. "]" );
    self.buttonToday:SetEnabled( not isCurrentWeek );

    -- Save first day of week so that click on hour buttons can compute day
    local view = self.calenderViews["Week"];
    view.startOfWeek = startOfWeek;
    
    -- highlight day label if the day is today
    local today = now:dayOfWeek();
    for weekday = 1, DAYS_PER_WEEK do
        local dayLabel = view.dayLabels[weekday];
        local bgcolor = Turbine.UI.Color.Black;
        if( isCurrentWeek and (weekday == today)) then
            --Turbine.Shell.WriteLine( string.format(
            --    "  setting active color on weekday[%d] [%s]",
            --    weekday,
            --    dayLabel:GetText()));
            bgcolor = Turbine.UI.Color.DarkSlateBlue;
        end
        dayLabel:SetBackColor( bgcolor );
    end
    
    self:RebuildViewWeekEvents();
end

--------------------------------------------------------------------------
function CalendarWindow:RebuildViewWeekEvents()
    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildViewWeekEvents" );

    local view = self.calenderViews["Week"];
    assert(view,"Missing view");
    
    -- Retreive all entries to display for this month
    -- - this will also retreive entries that started before this month
    -- - also need to include entries that end after this month
    local startOfWeek = view.startOfWeek;
    local endOfWeek = startOfWeek:clone():add({Day=6, Hour=23, Minute=59});
    --Turbine.Shell.WriteLine( "  startOfWeek=[" .. startOfWeek:tostring() .. "]" );
    --Turbine.Shell.WriteLine( "  endOfWeek=[" .. endOfWeek:tostring() .. "]" );
    
    local entriesToDisplay = self:GetCalendarEntriesBetween(startOfWeek, endOfWeek);
    
    -- split out all-day from partial-day entries
    local allDayEntries = {};
    local partialDayEntries = {};
    for idx,entry in ipairs(entriesToDisplay) do        
        --Turbine.Shell.WriteLine(string.format(
        --    "  entriesToDisplay[%d] start [%s] end [%s] isAllDay[%s] description [%s]", 
        --    idx, 
        --    entry.Start,
        --    entry.End,
        --    tostring(entry.isAllDay),
        --    entry.Description) );
        if( entry.isAllDay ) then
            table.insert(allDayEntries, entry);
        else
            table.insert(partialDayEntries, entry);
        end     
    end
    
    --table.dump("allDayEntries",allDayEntries);
    --table.dump("partialDayEntries",partialDayEntries);
    
    -- Assign allDay entries to lanes (e.g. rows)
    local laneForEntry = self:AssignLanesToEntries(allDayEntries, "day");
    -- Get max lane, ensure always at least 1 lane in case no entries exist
    local maxLane = math.max(1,unpack(table.values(laneForEntry)));
    
    -- Add buttons for all day events
    local allDayEvents = view.allDayEvents;
    for idx,entry in ipairs(allDayEntries) do
        local entryStart = DateTime:parse(entry.Start):set({Hour=0,Minute=0});
        local entryEnd = DateTime:parse(entry.End):set({Hour=23,Minute=59});
        local duration = entryEnd:DaysSince(entryStart)+1;
        --Turbine.Shell.WriteLine(string.format(
        --  "  allday idx [%d] Start [%s] End [%s] duration[%d] Lane[%d] [%s]", 
        --  idx,
        --  tostring(entry.Start),
        --  tostring(entry.End),
        --  duration,
        --  laneForEntry[entry],
        --  entry.Description) );

        if( entryStart < startOfWeek ) then
            -- if started in prior month, adjust to start of month
            -- and adjust days left 
            entryStart = startOfWeek;
        end
        if( entryEnd > endOfWeek ) then
            -- if end in next month, clip to start of month
            entryEnd = endOfWeek;
        end
        local nbrDaysLeft = entryEnd:DaysSince(entryStart)+1;
        
        local idxGridStart = entryStart:dayOfWeek();
        local idxGridEnd = entryEnd:dayOfWeek();
        local weekdayStart =  view.allDayEvents[ idxGridStart ];
        local weekdayEnd =  view.allDayEvents[ idxGridEnd ];
        --Turbine.Shell.WriteLine(string.format(
        --  "    idxStart [%s] idxEnd [%s] start[%s] end[%s]", 
        --  idxGridStart,
        --  idxGridEnd,
        --  tostring(weekdayStart),
        --  tostring(weekdayEnd) ));
        
        -- Get next button as needed (add button as needed)
        local entryButton = self:getNextEntryButton();
        
        entryButton:SetParent( view.allDayEventsViewPort.entries );
        entryButton:SetSize( -- width,height
            weekdayEnd:GetLeft() + weekdayEnd:GetWidth() - weekdayStart:GetLeft(),
            20 );
        entryButton:SetPosition( -- left,top
            weekdayStart:GetLeft(),
            entryButton:GetHeight() * laneForEntry[entry]);
        --Turbine.Shell.WriteLine(string.format(
        --  "    btn left[%d] top[%d] wid[%d] height[%d]",
        --  entryButton:GetLeft(), 
        --  entryButton:GetTop(), 
        --  entryButton:GetWidth(), 
        --  entryButton:GetHeight()));
        entryButton:SetBackColor( Turbine.UI.Color.Black );
        --entryButton:SetBackColor( Turbine.UI.Color.Red );
        --Turbine.Shell.WriteLine(string.format(
        --    "    color [%s]",
        --    tostring(entry.Color)));
        local entryColor = entry.Color or "White";
        entryButton:SetForeColor( Turbine.UI.Color[entryColor] );
        --entryButton:SetForeColor( Turbine.UI.Color.Orange );
        entryButton:SetOutlineColor( Turbine.UI.Color.Yellow );
        entryButton:SetMultiline( false ); 
        entryButton:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
        entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
        --entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana22 );
        entryButton:SetText( entry.Description );
        
        -- For items that span multiple days, append ' ---|' to show entry duration
        if( idxGridStart ~= idxGridEnd ) then
            --local maxTextLength = entryButton:GetWidth() / 7;
            local maxTextLength = entryButton:GetWidth() / 
                FontWidthPerChar[entryButton:GetFont()];
            
            local charsLeft = maxTextLength - string.len(entryButton:GetText()) -1;
            --Turbine.Shell.WriteLine(string.format(
            --  "    btnWidth [%d] maxTextLength [%d] charsLeft=[%d] font [%s]",
            --  entryButton:GetWidth(), maxTextLength, charsLeft, entryButton:GetFont()));
            if( charsLeft > 3 ) then -- Leave roomt for at least " -|"
                entryButton:AppendText( " " .. string.rep("-",charsLeft-1) .. "|");
            end
        end
        
        -- Hide if more than N rows
        entryButton:SetVisible(idx < 4);
        entryButton:SetEnabled(idx < 4);
        
        entryButton.entry = entry;
        entryButton.Click = function( sender, args )
            --Turbine.Shell.WriteLine("click entry:\n" .. 
            --    "  [" .. sender.entry.Description .. "]" );
            self:UpdateCalendarEvent( sender.entry );
        end
        
    end

    -- Assign allDay entries to lanes (e.g. cols)
    local laneForEntry = self:AssignLanesToEntries(partialDayEntries, "hour");
    -- Get max lane, ensure always at least 1 lane in case no entries exist
    local maxLane = math.max(1,unpack(table.values(laneForEntry)));
    -- Compute the entry width as a percentage of the hour column width
    -- - subtract a little more to leave as a divider between entries
    local width = (view.partialDayEntries[1][1]:GetWidth()/maxLane) -
        (GRID_MARGIN * maxLane);
    
    -- Add buttons for all day events
    local partialDayEvents = view.partialDayEventsViewPort.entries;
    for idx,entry in ipairs(partialDayEntries) do
        local entryStart = DateTime:parse(entry.Start);
        local entryEnd = DateTime:parse(entry.End);
        local duration = entryEnd:MinutesSince(entryStart)+1;
        --Turbine.Shell.WriteLine(string.format(
        --  "  pday idx [%d] Start [%s] End [%s] duration[%d] Lane[%d] [%s]", 
        --  idx,
        --  tostring(entry.Start),
        --  tostring(entry.End),
        --  duration,
        --  laneForEntry[entry],
        --  entry.Description) );
        
        local weekdayStart = entryStart:dayOfWeek();
        local weekdayEnd = entryEnd:dayOfWeek();
        --Turbine.Shell.WriteLine(string.format(
        --  "    start day [%d] end day [%d]", 
        --  weekdayStart, weekdayEnd));
        
        if( entryStart < startOfWeek ) then
            -- if started in prior month, adjust to start of month
            -- and adjust days left 
            weekdayStart = 1;
        end
        if( entryEnd > endOfWeek ) then
            -- if end in next month, clip to start of month
            weekdayEnd = DAYS_PER_WEEK;
        end
        
        -- Iterate over all days to add buttons
        --local weekdayStart =  view.allDayEvents[ idxGridStart ];
        --local weekdayEnd =  view.allDayEvents[ idxGridEnd ];
        local btn = 0;
        for weekday = weekdayStart, weekdayEnd do
            btn = btn + 1;
            local startOfDay = view.startOfWeek:clone():add({Day=weekday-1}):set({Hour=0});
            local endOfDay = startOfDay:clone():set({Hour=23});
            local lane = laneForEntry[entry];
            local hourStart = entryStart.Hour;
            if( entryStart < startOfDay ) then
                hourStart = 0;
            end
            local hourEnd = entryEnd.Hour;
            if( entryEnd > endOfDay ) then
                hourEnd = 23;
            end
            
            --Turbine.Shell.WriteLine(string.format(
            --    "    btn [%d] day [%s] hrStart [%d] hrEnd [%d]",
            --    btn, startOfDay:tostring(), hourStart, hourEnd));

            local firstHourEntry = view.partialDayEntries[weekday][hourStart];
            local lastHourEntry = view.partialDayEntries[weekday][hourEnd];
            local top = firstHourEntry:GetTop();
            local height = lastHourEntry:GetTop() + lastHourEntry:GetHeight() - top;
            local left = firstHourEntry:GetLeft() + (width * (lane-1)) + (GRID_MARGIN * (lane-1));
            --Turbine.Shell.WriteLine(string.format(
            --    "    btn [%d] top [%d] height [%d] left [%d] width [%d]",
            --    btn, top, height, left, width));
            
            -- Get next entry button
            local entryButton = self:getNextEntryButton();
            entryButton:SetParent( view.partialDayEventsViewPort.entries );
            entryButton:SetSize( width, height ); -- width,height
            entryButton:SetPosition( left, top ); -- left,top
            --entryButton:SetBackColor( gridElementStart:GetBackColor() );
            --entryButton:SetBackColor( Turbine.UI.Color.DarkBlue );
            entryButton:SetTextAlignment( Turbine.UI.ContentAlignment.TopLeft );
            --Turbine.Shell.WriteLine(string.format(
            --  "    event [%d] color [%s]",
            --  idx, tostring(entry.Color)));
            local entryColor = entry.Color or "White";
            
            entryButton:SetForeColor( Turbine.UI.Color.Black );
            --entryButton:SetForeColor( Turbine.UI.Color.Orange );
            --entryButton:SetBackColor( Turbine.UI.Black );
            entryButton:SetBackColor( Turbine.UI.Color[entryColor] );
            --entryButton:SetOutlineColor( Turbine.UI.Color.Yellow );
            entryButton:SetMultiline( false ); 
            entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
            --entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana22 );
            entryButton:SetText( entry.Description );
            entryButton:SetVisible( true );
            entryButton.entry = entry;
            entryButton.Click = function( sender, args )
                local day = sender:GetText();
                --Turbine.Shell.WriteLine("click entry:\n" .. 
                --    "  [" .. sender.entry.Description .. "]" );
                self:UpdateCalendarEvent( sender.entry );
            end
            
            -- set callbacks to forward mouse-wheel from children of viewport to scrollbar
            entryButton.MouseWheel=function(sender,args)
                --Turbine.Shell.WriteLine( string.format(
                --    "hourlyViewPort.entries.entryButton.MouseWheel value=[%d]",
                --    view.vScroll:GetValue()));
                --table.dump("sender",sender);
                --table.dump("args",args);
                local oldValue = view.vScroll:GetValue();
                local newValue = oldValue - args.Direction * view.vScroll:GetLargeChange();
                if( newValue <= view.vScroll:GetMinimum() ) then
                    newValue = view.vScroll:GetMinimum()
                elseif( newValue >= view.vScroll:GetMaximum() ) then
                    newValue = view.vScroll:GetMaximum()
                end
                if( oldValue ~= newValue ) then
                    view.vScroll:SetValue(newValue);
                end
            end

        end
        
    end
end

--------------------------------------------------------------------------
--- RebuildViewDay
--
-- Description: update the calendar day view 
--
function CalendarWindow:RebuildViewDay()

    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildViewDay" );

    -- update the month label: month year
    --local updateLabel = self.current:ymd();
    --self.buttonViewTitle:SetText( updateLabel );
    self.buttonViewTitle:SetText( "" ); -- clear

    --local buttonSelectMonth = self.calenderViewMonth.buttonSelectMonth;
    --buttonSelectMonth:SetSelectedIndex(self.current.Month);

    local buttonViewDayDropdown = self.calenderViews["Day"].buttonViewDayDropdown;
    buttonViewDayDropdown:SetValue(self.current);
    
    -- Enable the today button if not the current day
    local now = DateTime:now();
    self.buttonToday:SetEnabled( updateLabel ~= now:ymd() );
    
    self:RebuildViewDayEvents();
    
end

--------------------------------------------------------------------------
function CalendarWindow:RebuildViewDayEvents()
    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildViewDayEvents" );

    -- Retreive all entries to display for this month
    -- - this will also retreive entries that started before this month
    -- - also need to include entries that end after this month
    local sodDateTime = self.current:clone():set({Hour=0,Minute=0});
    local eodDateTime = sodDateTime:clone():set({Hour=23,Minute=59});
    
    local entriesToDisplay = self:GetCalendarEntriesBetween(sodDateTime, eodDateTime);
    
    -- split out all-day from partial-day entries
    local allDayEntries = {};
    local partialDayEntries = {};
    for idx,entry in ipairs(entriesToDisplay) do        
        --Turbine.Shell.WriteLine(string.format(
        --    "  entriesToDisplay[%d] start [%s] end [%s] isAllDay[%s] description [%s]", 
        --    idx, 
        --    entry.Start,
        --    entry.End,
        --    tostring(entry.isAllDay),
        --    entry.Description) );
        if( entry.isAllDay ) then
            table.insert(allDayEntries, entry);
        else
            table.insert(partialDayEntries, entry);
        end     
    end
    
    --table.dump("allDayEntries",allDayEntries);
    --table.dump("partialDayEntries",partialDayEntries);
    
    -- Add buttons for all-day entries in top row
    local view = self.calenderViews["Day"];
    local allDayEvents = view.allDayEvents;
    for idx,entry in ipairs(allDayEntries) do
        -- Get next button as needed (add button as needed)
        local entryButton = self:getNextEntryButton();
        
        entryButton:SetParent( allDayEvents );
        entryButton:SetSize( -- width,height
            allDayEvents:GetWidth(),
            20 );
        local top = entryButton:GetHeight() * idx;
        entryButton:SetPosition( -- left,top
            0,
            top);
        --Turbine.Shell.WriteLine(string.format(
        --  "  entry [%d] btn left[%d] top[%d] wid[%d] height[%d]",
        --  idx, 
        --  entryButton:GetLeft(), 
        --  entryButton:GetTop(), 
        --  entryButton:GetWidth(), 
        --  entryButton:GetHeight()));
        entryButton:SetBackColor( allDayEvents:GetBackColor() );
        --entryButton:SetBackColor( Turbine.UI.Color.Red );
        --Turbine.Shell.WriteLine(string.format(
        --  "    entry [%d] color [%s]",
        --  idx, tostring(entry.Color)));
        local entryColor = entry.Color or "White";
        entryButton:SetForeColor( Turbine.UI.Color[entryColor] );
        --entryButton:SetForeColor( Turbine.UI.Color.Orange );
        entryButton:SetOutlineColor( Turbine.UI.Color.Yellow );
        entryButton:SetMultiline( false ); 
        entryButton:SetTextAlignment( Turbine.UI.ContentAlignment.MiddleCenter );
        entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
        --entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana22 );
        entryButton:SetText( entry.Description );
        
        -- Hide if more than N rows
        entryButton:SetVisible(idx < 4);
        entryButton:SetEnabled(idx < 4);
        
        entryButton.entry = entry;
        entryButton.Click = function( sender, args )
            --Turbine.Shell.WriteLine("click entry:\n" .. 
            --    "  [" .. sender.entry.Description .. "]" );
            self:UpdateCalendarEvent( sender.entry );
        end
        
    end

    -- Assign entries to lanes (e.g. columns)
    local laneForEntry = self:AssignLanesToEntries(partialDayEntries, "hour");
    -- Get max lane, ensure always at least 1 lane in case no entries exist
    local maxLane = math.max(1,unpack(table.values(laneForEntry)));
    -- Get the width as a percentage of the 3rd column
    -- - subtract a little more to leave as a divider between entries
    local width = (view.allDayEvents:GetWidth()/maxLane) -
        (GRID_MARGIN * maxLane);
    
    -- Add buttons for partial-day entries to hourly rows
    for idx,entry in ipairs(partialDayEntries) do
        local entryStart = DateTime:parse(entry.Start):set({Minute=0});
        local entryEnd = DateTime:parse(entry.End):set({Minute=59});

        -- Determine which hourly rows the entry start/end
        idxStart = 1;
        if( entryStart:ymd() == self.current:ymd() ) then
            idxStart = entryStart.Hour+1;
        end
        idxEnd = 24;
        if( entryEnd:ymd() == self.current:ymd() ) then
            idxEnd = entryEnd.Hour+1;
        end
        local lane = laneForEntry[entry];
        
        --Turbine.Shell.WriteLine(string.format(
        --    "  entry [%d] btn idxStart[%d] idxEnd[%d] lane[%d] width[%d]",
        --    idx, 
        --    idxStart,
        --    idxEnd,
        --    lane,
        --    width));
        
        -- Select the start/end row elements in the hourly column
        
        local gridStart = view.gridElementsDay[idxStart];
        local gridEnd = view.gridElementsDay[idxEnd];
        
        -- Get next entry button
        local entryButton = self:getNextEntryButton();
        entryButton:SetParent( view.hourlyViewPort.entries );
        entryButton:SetPosition( -- left,top
            gridStart:GetLeft() + (width * (lane-1)) + (GRID_MARGIN * (lane-1)),
            gridStart:GetTop());
        entryButton:SetSize( -- width,height
            width,
            gridEnd:GetTop() + gridEnd:GetHeight() - gridStart:GetTop());
        --entryButton:SetBackColor( gridElementStart:GetBackColor() );
        entryButton:SetTextAlignment( Turbine.UI.ContentAlignment.TopLeft );
        local entryColor = entry.Color or "White";
        --entryButton:SetForeColor( Turbine.UI.Color[entryColor] );
        entryButton:SetForeColor( Turbine.UI.Color.Black );
        --entryButton:SetForeColor( Turbine.UI.Color.Orange );
        --entryButton:SetBackColor( rowElementStart:GetBackColor() );
        entryButton:SetBackColor( Turbine.UI.Color[entryColor] );
        --entryButton:SetOutlineColor( Turbine.UI.Color.Yellow );
        entryButton:SetMultiline( false ); 
        entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana20 );
        --entryButton:SetFont( Turbine.UI.Lotro.Font.Verdana22 );
        entryButton:SetText( entry.Description );
        entryButton:SetVisible( true );
        entryButton.entry = entry;
        entryButton.Click = function( sender, args )
            local day = sender:GetText();
            --Turbine.Shell.WriteLine("click entry:\n" .. 
            --    "  [" .. sender.entry.Description .. "]" );
            self:UpdateCalendarEvent( sender.entry );
        end
        
        -- set callbacks to forward mouse-wheel from children of viewport to scrollbar
        entryButton.MouseWheel=function(sender,args)
            --Turbine.Shell.WriteLine( string.format(
            --    "hourlyViewPort.entries.entryButton.MouseWheel value=[%d]",
            --    view.vScroll:GetValue()));
            --table.dump("sender",sender);
            --table.dump("args",args);
            local oldValue = view.vScroll:GetValue();
            local newValue = oldValue - args.Direction * view.vScroll:GetLargeChange();
            if( newValue <= view.vScroll:GetMinimum() ) then
                newValue = view.vScroll:GetMinimum()
            elseif( newValue >= view.vScroll:GetMaximum() ) then
                newValue = view.vScroll:GetMaximum()
            end
            if( oldValue ~= newValue ) then
                view.vScroll:SetValue(newValue);
            end
        end
        
        --Turbine.Shell.WriteLine(string.format(
        --    "    event [%d] btn[%s] evt[%s] enabled[%s] visible[%s]",
        --    idx, 
        --    tostring(entryButton),
        --    tostring(entryButton.entry),
        --    tostring(entryButton:IsEnabled()),
        --    tostring(entryButton:IsVisible())
        --    ));
        
    end
    
    
    
end

--------------------------------------------------------------------------
--- RebuildListView
--
-- Description: update the calendar list view
--
function CalendarWindow:RebuildViewList()

    --Turbine.Shell.WriteLine( "CalendarWindow:RebuildListView()" );

    self.buttonViewTitle:SetText( "List of all entries" );

    -- Clear out existing events
    local entriesListBox = self.calenderViews["List"].entriesListBox;
    entriesListBox:ClearItems();

    -- Rebuild list of all indexed events
    for entryKey,entriesForDay in table.pairsBySortedKeys(self.index) do
        for idx,entry in pairs(entriesForDay) do
            entriesListBox:addCalendarEntry(entry);
        end
    end

end

--------------------------------------------------------------------------
--- IndexCalendarEntries
--
-- Description: Re-indexes all calendar entries in the index
--
function CalendarWindow:IndexCalendarEntries()
    --Turbine.Shell.WriteLine( "CalendarWindow:IndexCalendarEntries()" );
    -- Initialize calendar entry index
    self.index = {};
    self.reverseIndex = {};
    --table.dump("Options.subscribedCalendars",Options.subscribedCalendars);
    for k,v in pairs(Options.subscribedCalendars) do
        if( v ) then
            --Turbine.Shell.WriteLine(string.format("Indexing [%s] Entries ...",k));
            local entriesList = self.entries[k];
            for idx,entry in pairs(entriesList) do
                self:AddEntryToIndex(entry);
            end
        end
    end
    
    --table.dump("index",self.index);
    --table.dump("reverseIndex",self.reverseIndex);
    --for k,v in pairs(self.reverseIndex) do
    --    Turbine.Shell.WriteLine(string.format("%s  k[%s] v[%s]",
    --        "reverseIndex",
    --        tostring(k),
    --        tostring(v)));
    --end
end

--------------------------------------------------------------------------
--- AddEntryToIndex
--
-- Description: Adds the specified item to the event entry index
function CalendarWindow:AddEntryToIndex(calendarEntry)
    --Turbine.Shell.WriteLine( string.format(
    --    "CalendarWindow:AddEntryToIndex [%s] [%s] [%s]",
    --    tostring(calendarEntry),
    --    tostring(calendarEntry.Start),
    --    tostring(calendarEntry.Description)));
    assert( calendarEntry, "AddEntryToIndex : Missing calendar entry");
    
    -- - Index is multi-tiered map based on the start date:
    --     index[yyyy-mm-dd][idx]
    -- - This allows for:
    --   1) fast lookup per day and by extension month
    --   2) multiple entries that start on same day
    
    -- Also keep a reverse index of each entry to the index it is in.
    -- This provides for fast lookup of which index to remove item from.
    -- - Keep in mind that after an update, the entry.Start may have changed
    --   so can't rely on this to determine the index it is in
    
    local dateStart = DateTime:parse(calendarEntry.Start);
    local dayKey = dateStart:ymd();
    
    local entriesForDay = self.index[ dayKey ];
    -- If entry list for day does not exist yet, then create it
    if( not entriesForDay ) then
        --Turbine.Shell.WriteLine(string.format("Adding index for date [%s]", dayKey));
        entriesForDay = {};
        self.index[ dayKey ] = entriesForDay;
    end
    
    table.insert( entriesForDay, calendarEntry);
    self.reverseIndex[calendarEntry] = dayKey;

end

--------------------------------------------------------------------------
--- RemoveEntryFromIndex
--
-- Removes the entry from the index
--
-- Params:
-- @param calendarEntry - a reference to the event to search for
--
function CalendarWindow:RemoveEntryFromIndex( calendarEntry )
    --Turbine.Shell.WriteLine(string.format(
    --    "CalendarWindow:RemoveEntryFromIndex calendarEntry=[%s]", 
    --    tostring(calendarEntry) ));
    assert( calendarEntry, "RemoveEntryFromIndex : Missing calendar entry");
    
    --table.dump("reverseIndex",self.reverseIndex);
    
    -- Find the index from the reverseIndex[entry]
    local dayKey = self.reverseIndex[calendarEntry];
    self.reverseIndex[calendarEntry] = nil;
    if( nil == dayKey ) then
        --Turbine.Shell.WriteLine(string.format(
        --    "  entry [%s] not found in reverseIndex", 
        --    tostring(calendarEntry)));
        return false;
    end
    local entriesForDay = self.index[ dayKey ];
    --Turbine.Shell.WriteLine(string.format(
    --    "  Date [%s] has [%d] entries", 
    --    dayKey, #entriesForDay));
    
    -- Find this entry in the existing index for that day
    for idx,entryOnDay in ipairs(entriesForDay) do
        --Turbine.Shell.WriteLine(string.format("  Checking [%s] = [%s] ",
        --    idx, tostring(entryOnDay) ) );
        if( entryOnDay == calendarEntry ) then
            table.remove( entriesForDay, idx);
            --Turbine.Shell.WriteLine(string.format(
            --    "  Removed from index [%s][%d]",
            --    dayKey,
            --    idx));
            
            if( 0 == #entriesForDay ) then
                self.index[ dayKey ] = nil;
                --Turbine.Shell.WriteLine(string.format(
                --    "  Removed index list for [%s]",
                --    dayKey));
            end
            
            return true; -- Found it!
        end
    end
    
    return false; -- did not find it
end

--------------------------------------------------------------------------
--- GetCalendarEntriesBetween
--
-- Finds calendar entries between the specified date/times
--
-- Params:
-- @param startDateTime - the earliest time to accept
-- @param endDateTime   - the latest time to accept
-- @return a list of all entries found
function CalendarWindow:GetCalendarEntriesBetween( startDateTime, endDateTime)

    --Turbine.Shell.WriteLine(string.format("%s\n  startDateTime=[%s]\n   endDateTime=[%s]",
    --    "CalendarWindow:GetCalendarEntriesBetween",
    --    startDateTime:tostring(),
    --    endDateTime:tostring() ) );
    assert( startDateTime:validate(), "startDateTime expected to be a valid DateTime");
    assert( endDateTime:validate(),   "endDateTime expected to be a valid DateTime");

    local endDateYMD = endDateTime:ymd();
    -- Include entries that start before the endDateTime, 
    -- but do not finish before the startDateTime

    local entriesToReturn = {};
    for entryKey,entriesForDay in table.pairsBySortedKeys(self.index) do
        --Turbine.Shell.WriteLine(string.format("  entryKey [%s] nbr=[%d]",
        --  entryKey,
        --  #entriesForDay) );
        if( entryKey <= endDateYMD) then
            for idx,entry in pairs(entriesForDay) do
                
--              Turbine.Shell.WriteLine(string.format("Calendar: entryKey [%s]",
--                  entryKey) );
                if ( nil == entry.End ) then
                    print("Calendar Invalid Entry!")
                    table.dump("entry",entry);
                else
                    local endDateTime = DateTime:parse(entry.End);
                    local rslt="skip";
                    if (endDateTime > startDateTime ) then
                        rslt="add";
                        table.insert(entriesToReturn, entry);
                    end
                end
                --Turbine.Shell.WriteLine(string.format("  idx [%d] start=[%s] end=[%s] => [%s]",
                --  idx,
                --  entry.Start,
                --  entry.End,
                --  rslt) );
            end
        end
    end

    --for idx,entry in ipairs(entriesToReturn) do        
    --    Turbine.Shell.WriteLine(string.format("  idx[%d] start[%s] end[%s] desc[%s]",
    --      idx,
    --      entry.Start,
    --      entry.End,
    --      entry.Description) );
    --end
    
    return entriesToReturn;
end

--------------------------------------------------------------------------
--- AddCalendarEntry
--
-- Description: the user has clicked on a date in the calendar.
-- retrieve the existing entry (if it exists) and initialize the
-- CalendarEntryEditor with the entry.
--
-- Params:
-- @param startTime - the start time in UTC
-- @param endTime - the end time in UTC
-- @param isAllDay - true if the event is all day, false otherwise
--
function  CalendarWindow:AddCalendarEntry( startTime, endTime, isAllDay )
    --Turbine.Shell.WriteLine( "CalendarWindow:AddCalendarEntry(" .. 
    --    "\n  start = [" .. startTime:tostring() .. "]" ..
    --    "\n  end = [" .. endTime:tostring() .. "]" ..
    --    "\n  isAllDay = [" .. tostring(isAllDay) .. "]" );

    -- Create new entry
    newCalendarEntry = FrostyPlugins.Calendar.CalendarEntry();
    newCalendarEntry.Start = startTime:tostring();
    newCalendarEntry.End = endTime:tostring();
    newCalendarEntry.isAllDay = isAllDay;
    
    -- display the window that edits the CalendarEntry
    -- the editor will take care of calling us back to save the entry
    self.calendarEntryEditor:InitializeEntry( newCalendarEntry, self );

end

--------------------------------------------------------------------------
--- UpdateCalendarEntry
--
-- Description: the user has clicked on a date in the calendar.
-- retrieve the existing entry (if it exists) and initialize the
-- CalendarEntryEditor with the entry.
--
-- @param calendarEntry - a reference to the Calendar entry item
--         to update
--
-- fixme: frosty
--        currently, the existing system only allows for a single
--        event per day.
--
function  CalendarWindow:UpdateCalendarEvent( calendarEvent )
    --Turbine.Shell.WriteLine( "CalendarWindow:UpdateCalendarEvent(" .. tostring(calendarEvent) .. ")");

    assert( calendarEvent, "Missing entry!" );
    
    -- display the window that edits the CalendarEntry
    -- the editor notify by callback to save entry updates
    self.calendarEntryEditor:InitializeEntry( calendarEvent, self );
end

--------------------------------------------------------------------------
--- NotifySaveEntry
--
-- Description: the user has modified an entry on the calendar and
-- elected to save it. look up the entry to see if it is something
-- we know about.  if there is no current entry, create one.  update the
-- entry with the information on the editor and save the entry to disk.
-- finally, rebuild the calendar.
--
-- @param entryEditor - CalendarEntryEditor - the editor window that contains
--                      the CalendarEntry that is being saved
-- @param entryIndex - the index to the item
function CalendarWindow:NotifySaveEntry( calendarEntry )

    --Turbine.Shell.WriteLine("NotifySaveEntry: " .. tostring(calendarEntry) );
    assert( calendarEntry, "NotifySaveEntry : Missing calendar entry");
    
    --table.dump("calendarEntry",calendarEntry);
    
    -- Find item to PersonalCalendarEntries
    local entryIdx;
    for idx,entry in ipairs(self.settings.PersonalCalendarEntries) do
        --Turbine.Shell.WriteLine(string.format("  Comparing to entry [%d][%s]",
        --    idx,
        --    tostring(entry)));
        if( entry == calendarEntry ) then
            entryIdx = idx;
            break;
        end
    end
    
    --Turbine.Shell.WriteLine(string.format("  Matched entry at [%s][%s]",
    --    tostring(entryIdx),
    --    tostring(calendarEntry)));
    
    if( nil == entryIdx ) then
        -- If entry list for day does not exist yet, then create it
        --Turbine.Shell.WriteLine(string.format("Adding entry [%s]",
        --    tostring(calendarEntry)));
        table.insert( self.settings.PersonalCalendarEntries, calendarEntry);    
    else
        -- Entry *DOES* exist
        --Turbine.Shell.WriteLine(string.format("Found entry [%s] at [%d]",
        --    tostring(calendarEntry),
        --    entryIdx));
        
        -- Remove existing entry from index + reverseIndex
        self:RemoveEntryFromIndex( calendarEntry );
        
        self.settings.PersonalCalendarEntries[entryIdx] = calendarEntry;
    end
    
    -- Re-Add entry to index (presumably at new position)
    self:AddEntryToIndex( calendarEntry )
    
    self:SaveSettings();
    self:RebuildCalendar();
    
end

--------------------------------------------------------------------------
--- NotifyDeleteEntry
--
-- Description: the user has cleared an entry from the calendar.
-- look up the entry to make sure it is something we know about.
-- if so, delete it from the list and rebuild the calendar.
--
-- @param entryEditor - the CalendarEntryEditor window that contains
--               the CalendarEntry that is being deleted
function CalendarWindow:NotifyDeleteEntry( entryEditor )

    local calendarEntry = entryEditor.calendarEntry;
    Turbine.Shell.WriteLine(string.format(
        "NotifyDeleteEntry: entry[%s]",
        tostring(calendarEntry)) );
    assert( calendarEntry, "NotifyDeleteEntry : Missing calendar entry");
    
    --for idx,entry in ipairs(self.settings.PersonalCalendarEntries) do    
    --    Turbine.Shell.WriteLine(string.format("  BEFORE [%d] = [%s] ",
    --        idx, tostring(entry) ) );
    --end
    
    -- Find this entry in the existing entries    
    self:RemoveEntryFromIndex( calendarEntry );
    
    -- Find item to PersonalCalendarEntries
    -- CAUTION: If there are gaps in the numerical indices of the list
    -- then '#table' operator, table.remove(table,idx) and ipairs()
    -- won't work anymore.  (All will stop at the first gap).
    -- Therefore, since we are already doing a linear search, just
    -- iterate using pairs() instead.
    local entryIdx;
    for idx,entry in pairs(self.settings.PersonalCalendarEntries) do
        --Turbine.Shell.WriteLine(string.format("  Checking [%d] = [%s] ",
        --    idx, tostring(entry) ) );
        if( entry == calendarEntry ) then
            entryIdx = idx;
            break;
        end
    end
    if( nil == entryIdx ) then
        Turbine.Shell.WriteLine(string.format("Entry not found [%s]",
            tostring(calendarEntry)));
    else
        -- Entry *DOES* exist
        --Turbine.Shell.WriteLine(string.format("Removing entry [%s] at [%d]",
        --    tostring(calendarEntry),
        --    entryIdx));

        -- CAUTION: table.remove() won't work if there are gaps in the table.
        -- so assign to nil instead.
        --table.remove(self.settings.PersonalCalendarEntries, entryIdx);
        self.settings.PersonalCalendarEntries[entryIdx] = nil;
    end
    
    self:SaveSettings();
    self:RebuildCalendar();

end

--------------------------------------------------------------------------
--- NotifyOptionsChanged
--
-- Description: Notification that Options has changed
function CalendarWindow:NotifyOptionsChanged()
    self:IndexCalendarEntries();
    self:RebuildCalendar();
end

--------------------------------------------------------------------------
--- LoadSettings
--
-- Description: loads our internal settings from disk.  For this class,
-- this includes the position of the calendar window and the list of
-- events that have been created.  if the position does not exist, we
-- attempt to place it in the center of the window.  if the entry list
-- does not exist, we create it.
function CalendarWindow:LoadSettings()
    --Turbine.Shell.WriteLine( "CalendarWindow:LoadSettings()" );

    -- the first thing to check: where are we storing the settings for this
    -- user?  at the account level or the character level?  to do this, we
    -- read a special object that contains this information.
    --self.dataLocation = Turbine.PluginData.Load( 
    --      Turbine.DataScope.Character, 
    --      "CalendarDataSettings" );
    --if( type( self.dataLocation ) ~= "table" ) then
    --  self.dataLocation = { };
    --end
    --if( nil == self.dataLocation.StoredOnCharacter ) then
    --  self.dataLocation.StoredOnCharacter = true;
    --end
    
    -- load the settings from either the character or the account scope.
    --if( true == self.dataLocation.StoredOnCharacter ) then
    --    self.settings = Turbine.PluginData.Load( 
    --      Turbine.DataScope.Character, 
    --      "CalendarWindowSettings" );
    --else
    self.settings = Turbine.PluginData.Load( 
        Turbine.DataScope.Account, 
        "CalendarWindowSettings" );
    --end
    
    -- If any of the values are not available, set a default value
    if ( type( self.settings ) ~= "table" ) then
        self.settings = { };
    end
    
    -- Set calendar to middle of display if prior position unknown or offscreen
    if( (nil == self.settings.positionX) or 
        (self.settings.positionX < 0 ) or
        (self.settings.positionX >= Turbine.UI.Display.GetWidth()-10) ) then
        self.settings.positionX = (Turbine.UI.Display.GetWidth() / 2) - (self:GetWidth() / 2 ); 
    end
    
    if( (nil == self.settings.positionY) or
        (self.settings.positionY < 0) or
        (self.settings.positionY >= Turbine.UI.Display.GetHeight()-10) ) then
        self.settings.positionY = (Turbine.UI.Display.GetHeight() / 2) - self:GetHeight();
    end
    
    if( nil == self.settings.width ) then
        self.settings.width = 600; 
    end
    if( nil == self.settings.height ) then
        self.settings.height = 650; 
    end

    --table.dump("settings",self.settings);
    
    -- If no personal entries exist, then create empty table
    if( nil == self.settings.PersonalCalendarEntries ) then
        self.settings.PersonalCalendarEntries = {};
    else
        Turbine.Shell.WriteLine(string.format(
            "Loaded [%d] Personal Calendar Entries ...",
            table.size(self.settings.PersonalCalendarEntries)));
        --table.dump("self.settings.PersonalCalendarEntries",
        --    self.settings.PersonalCalendarEntries);
        --for idx,entry in ipairs(self.settings.PersonalCalendarEntries) do
        --    Turbine.Shell.WriteLine( string.format(
        --        "  [%d][%s] = [%s] [%s] [%s] [%s] [%s]",
        --        idx, tostring(entry),
        --        tostring(entry.Start),
        --        tostring(entry.End),
        --        tostring(entry.Color),
        --        tostring(entry.isAllDay),
        --        tostring(entry.Description)));
        --end
    end

    -- Convert old entries from old version of calendar
    if( self.settings.CalendarEntryArray ) then
        Turbine.Shell.WriteLine("Converting entries from prior version of Calendar ...");
        table.dump("self.settings.CalendarEntryArray",self.settings.CalendarEntryArray);
        for oldEntryKey,oldEntry in pairs(self.settings.CalendarEntryArray) do
            --if( self.settings.CalendarEntryArray[ entryKey ] = calendarEntry;
            -- entryKey = 'day..monthLabel..year' (e.g. "5April2022")
            -- Old Fields:
            --DateEntry : 'day..monthLabel..year' (e.g. "5April2022")
            --TimeEntry
            --Description
            local day,monthLabel,year;
            local idxStart,idxEnd,day,monthLabel,year = string.find(
                oldEntry.DateEntry,"(%d+)(%a+)(%d%d%d%d)");
            Turbine.Shell.WriteLine( string.format(
                "    day=[%s] monthLabel=[%s] year=[%d]",
                day, monthLabel, year));
            local month = CalTools:monthForLabel(monthLabel);
            Turbine.Shell.WriteLine( string.format(
                "    month=[%d]",
                tostring(month)));
            local szStartDate = string.format("%4d-%02d-%02dT00:00:00",
                year,month,day);
            
            local dtStart = DateTime:parse(szStartDate);
            local dtEnd   = dtStart:clone():set({Hour=23,Minute=59});
            Turbine.Shell.WriteLine( string.format(
                "    start[%s] end[%s]",
                dtStart:tostring(),
                dtEnd:tostring()));
            
            local newCalendarEntry = FrostyPlugins.Calendar.CalendarEntry();
            newCalendarEntry.Start = dtStart:tostring();
            newCalendarEntry.End = dtEnd:tostring();
            newCalendarEntry.isAllDay = true;
            newCalendarEntry.Description = oldEntry.Description;
            newCalendarEntry.Color = "HotPink";
            
            table.insert(self.settings.PersonalCalendarEntries, newCalendarEntry);
            
            -- Remove old entry
            self.settings.CalendarEntryArray[oldEntryKey] = nil;
        end
        if( table.size( self.settings.CalendarEntryArray ) < 1 ) then
            -- Remove old table
            self.settings.CalendarEntryArray = nil;
            Turbine.Shell.WriteLine("Removing calendar entry array from prior version of Calendar ...");
        end
        
    end

    --table.dump("self.settings.PersonalCalendarEntries",self.settings.PersonalCalendarEntries);
    --for idx = 1,#self.settings.PersonalCalendarEntries do
    --    local entry = self.settings.PersonalCalendarEntries[idx];
    --    Turbine.Shell.WriteLine( string.format(
    --        "self.settings.PersonalCalendarEntries[%d] [%s]",
    --        idx, tostring(entry)));
    --    --table.dump(string.format("self.settings.PersonalCalendarEntries[%d]",idx),entry);
    --    if( nil == entry ) then
    --        table.remove(self.settings.PersonalCalendarEntries,idx);
    --    end
    --end

    --for idx = 1,table.size(self.settings.PersonalCalendarEntries) do
    --    local entry = self.settings.PersonalCalendarEntries[idx];
    -- look for gaps in table, reform table to close gaps as needed
    if( #self.settings.PersonalCalendarEntries ~= table.size(self.settings.PersonalCalendarEntries) ) then
        Turbine.Shell.WriteLine("CAUTION: Found Gaps in PersonalCalendarEntries! Reforming ...");
        for idx,entry in table.pairsBySortedKeys(self.settings.PersonalCalendarEntries) do
            Turbine.Shell.WriteLine(string.format("  [%s] = [%s] [%s]",
                idx, 
                tostring(entry),
                entry and entry.Description or '(n/a)'));
        end
        local newTable = {};
        for idx,entry in pairs(self.settings.PersonalCalendarEntries) do
            table.insert(newTable, entry);
        end
        self.settings.PersonalCalendarEntries = newTable;
    end
    
    --error("ABORT!");
    -- Look for entries with junk
    --for idx,entry in pairs(self.settings.PersonalCalendarEntries) do
    --    local gotJunk = 
    --        ( nil ~= entry.entryKey ) or
    --        ( nil ~= entry.Invitees ) or
    --        ( nil ~= entry.Organizer );
    --    if( gotJunk ) then
    --        --Turbine.Shell.WriteLine( string.format(
    --        --    "  [%d][%s] = [%s] [%s] [%s] [%s] [%s] [%s]",
    --        --    idx, tostring(entry),
    --        --    tostring(entry.entryKey),
    --        --    tostring(entry.Start),
    --        --    tostring(entry.End),
    --        --    tostring(entry.Color),
    --        --    tostring(entry.isAllDay),
    --        --    tostring(entry.Description)));
    --        table.dump(string.format("got junk entry[%d]",idx),entry);
    --        entry.entryKey = nil;
    --        entry.Invitees = nil;
    --    end
    --end
    
    self.entries = {
        Global = {};
        Personal = self.settings.PersonalCalendarEntries;
    };
    
    --Turbine.Shell.WriteLine( string.format( 
    --    "\n  PersonalCalendarEntries[%s]\n  entries.Personal       [%s]",
    --    tostring(self.settings.PersonalCalendarEntries), 
    --    tostring(self.entries.Personal)));
    
    
    -- setup global calendar
    for idx,globalEntry in ipairs(globalEvents) do
        local szTitle = globalEntry[1];
        local szStart = globalEntry[2];
        local szEnd   = globalEntry[3];
        
        local newCalendarEntry = FrostyPlugins.Calendar.CalendarEntry();
        newCalendarEntry.ReadOnly = true;
        newCalendarEntry.Start = szStart;
        newCalendarEntry.End = szEnd;
        newCalendarEntry.isAllDay = true;
        newCalendarEntry.Description = szTitle;
        newCalendarEntry.Color = "LightGreen";
        
        -- append to table of global entries
        table.insert(self.entries.Global, newCalendarEntry);
        
        --Turbine.Shell.WriteLine( string.format("  [%d] [%s] [%s] [%s]",
        --    idx, 
        --    szTitle,
        --    tostring(szStart),
        --    tostring(szEnd)));
    end
    Turbine.Shell.WriteLine(string.format(
        "Loaded [%d] Global Calendar Entries ...",
        #self.entries.Global));
    
    -- Initialize calendar entry index
    self:IndexCalendarEntries();
    
    --assert(false,"ABORT!");
    
    -- Set default view style as needed
    if( nil == self.settings.view ) then
        self.settings.view = CalendarView[1]; -- Month
    end
    --Turbine.Shell.WriteLine( "view = " .. self.settings.view );
    
    --local epoch = os.time(); -- Not available to LOTRO clients
    --local epoch = Turbine.Engine.GetLocalTime();
    --Turbine.Shell.WriteLine("epoch: " .. epoch);
    
    -- Keep track of the timezone because we'll need to translate 
    -- date/time to a global standard (UTC) to reasonably share calendar
    -- entries between people in different timezones
    -- - Unfortunately there doesn't appear to be a way to obtain
    --   the timezone from Turbine.Engine.
    --   - GetLocale() does NOT return timezone
    -- - so we'll have to save this as a calendar setting
    --local locale = Turbine.Engine.GetLocale();  -- en = ?
    --Turbine.Shell.WriteLine("locale: " .. locale);
    -- - This could be messy, since would have to seemlessly handle when
    --   daylight savings starts/ends
    --if( nil == self.settings.timeZone ) then
    --  self.settings.timeZone = -5; # EDT
    --end
    
end

--------------------------------------------------------------------------
--- SaveSettings
--
-- Description: save our internal settings to disk.  For this class,
-- this includes the position of the calendar window and the list of
-- events that have been created.
function CalendarWindow:SaveSettings()
    --Turbine.Shell.WriteLine( "CalendarWindow:SaveSettings()" );
    -- always save the dataLocation to the character scope!
    --Turbine.PluginData.Save( Turbine.DataScope.Character, "CalendarDataSettings", self.dataLocation );
    
    -- save the settings depending on the dataLocation
    local scope = Turbine.DataScope.Account;
    --if( true == self.dataLocation.StoredOnCharacter ) then
    --  scope = Turbine.DataScope.Character;
    --end
    Turbine.PluginData.Save( 
        scope, 
        "CalendarWindowSettings", 
        self.settings );
end

--------------------------------------------------------------------------
--- ListEntries
--
-- Description: Lists the calendar entries to command line
function CalendarWindow:ListEntries()
    local entriesToReturn = {};
    Turbine.Shell.WriteLine("Calendar Entries:" );
    for entryKey,entriesForDay in table.pairsBySortedKeys(self.index) do
        for idx,entry in ipairs(entriesForDay) do
            Turbine.Shell.WriteLine( string.format(
                "  start[%s] desc[%s]",
                --"  [%s][%d] = start[%s] desc[%s]",
                --entryKey, idx, 
                entry.Start,
                tostring(entry.Description)
                ) );
        end
    end
end

Compare with Previous | Blame


All times are GMT -5. The time now is 11:18 PM.


Our Network
EQInterface | EQ2Interface | Minion | WoWInterface | ESOUI | LoTROInterface | MMOUI | Swtorui