Posted to tcl by Napier at Wed Apr 15 19:58:24 GMT 2015view pretty

### Tree Database for URC Interfaces
### Author: Braden R. Napier
## Description:
# This Script will provide a simple means to handling navigation of
# elements in a tree-like fashion.  You can nest containers within
# containers and it will provide all the tools to browse and parse
# the elements of each container.  You can also easily add elements
# such as new categories, values in the dictionaries, etc.

# List Object: browse_list
# Callback : When an Item is Selected See the ::Browse::Item::Open Procedure
#			 so you can adjust the action that will take place.
# ::Widgets::Meta::BrowseTitle : You may want to change this as it is meant
#								 to list the title of the page to the container
#								 being viewed.  Define whatever you like.

# Set Testing to 1 When Using in TCL Shell or Outside of the Original Script
# it was Designed For.  This will make it so the script won't error out.
set testing 1

namespace eval Browse {
	
	variable History ""
	variable CurrentList [dict create]
	variable CurrentContainer ""
	variable CurrentCategory ""
	variable Stations [dict create]
	variable Refresh 0

	## Provide the Initial Framework for the Containers
	# This will be added to using the procedures below
	# as new content becomes available.

	variable Containers [dict create \
		ccount 1 \
		icount 0 \
		name "Root" \
		type "root" \
		c [dict create \
			0 [dict create \
				name "Music Services" \
				type "category" \
				ccount 0 \
				icount 0 \
			] \
		] \
	]

	variable Sort [dict create \
		ABC "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z" \
	]
	
	# Item Formatting
	# i {0 {name "Item Name" type "Item Type" id "Item ID"}}

	proc Menu {i} {
		## Opens the Initial Container Desired
		# 0 = iTunes Radio
		# 1 = Purchased Music
		# 2 = Purchased Movies
		# 3 = Purchased TV Shows
		LOG "User Begins Browsing: $i"
		set ::Browse::CurrentCategory $i
		::Browse::Container::Open $i	
	}

	proc Start {} {
		## Called to Startup the Browse System
		LOG "::Browse::Start"
		::Browse::List::Reset
		::Browse::Sources::Update
		
	}

	proc AddToPath {contents args} {
		# Used to place $contents at the path specified by $args
		# will replace any data with the same keys but will not affect keys that
		# are not included in $contents (it will unpack contents and set them one-by-one)
		# ccount and icount will be ignored if provided as a safety measure, the script
		# will count the items provided and validate before adding to the container
		LOG "::Containers::AddToPath $args"
		set path [::Browse::Container::Path $args]
		if {[dict exists $::Browse::Containers {*}$path]} {
			if {[catch {dict keys $contents}]} {
				LOG "ERROR: $contents is not a Valid Dictionary"
				return
			}
			dict for {k v} $contents {
				switch -- $k {
					"c" {
						if {[catch {dict keys $v}]} {
							LOG "ERROR: Provided Container Value is Not a Valid Dictionary"
							continue
						}
						set ccount [llength [dict keys $v]]
						dict set ::Browse::Containers {*}$path c $v
						dict set ::Browse::Containers {*}$path ccount $ccount
					}
					"i" {
						if {[catch {dict keys $v}]} {
							LOG "ERROR: Provided Items Value is Not a Valid Dictionary"
							continue
						}
						set icount [llength [dict keys $v]]
						dict set ::Browse::Containers {*}$path i $v
						dict set ::Browse::Containers {*}$path icount $icount
					}
					"ccount" {LOG "IGNORING PROVIDED CCOUNT!"; continue}
					"icount" {LOG "IGNORING PROVIDED ICOUNT!"; continue}
					default {
						# If Not a c or i value, we will add it to the path for manual use
						
						# Check if Replacing Current Data
						if {[dict exists $::Browse::Containers {*}$path $k]} {
							LOG "REPLACING CURRENT DATA at $path $k!"	
						}
						dict set ::Browse::Containers {*}$path $k $v
					}
				}
			}
		} else {
			LOG "ERROR: Provided Path $args Does Not Exist!"
		}
	}

	namespace eval Container {

		proc Back {args} {
			## Navigate Back One Container
			LOG "::Browse::Container::Back $args"
			if {$args == ""} {
				if {[dict exists $::Browse::Containers {*}$::Browse::CurrentContainer]} {
					LOG "----- DELETING A CONTAINER $::Browse::CurrentContainer!!!! -----" 
					if {[dict exists $::Browse::Containers {*}$::Browse::CurrentContainer i]} {
						dict set ::Browse::Containers {*}$::Browse::CurrentContainer icount 0
						dict unset ::Browse::Containers {*}$::Browse::CurrentContainer i
					}
					if {[dict exists $::Browse::Containers {*}$::Browse::CurrentContainer c]} {
						dict set ::Browse::Containers {*}$::Browse::CurrentContainer ccount 0
						dict unset ::Browse::Containers {*}$::Browse::CurrentContainer c
					}
				}
				set ::Browse::Refresh 1
				::Browse::Container::Open {*}[::Browse::Container::GetParent]
			} else {
				## TODO: Use Args to Define Multiple Jump Backs
			}
		}

		proc Refresh {} {
			## Refreshes the Currently Viewed Container List
			# - Generally this Means New Data Arrived for the Viewed Container
			LOG "::Browse::Container::Refresh"
			set ::Browse::Refresh 1
			::Browse::Container::Open {*}$::Browse::CurrentContainer
		}

		proc Open {args} {
			## ::Browse::Container::Open <path>
			# Opens and Displays the Given Container by first parsing all
			# container items (folders) then
			LOG "::Browse::Container::Open $args"
			set path [::Browse::Container::Path $args]
			set ::Browse::CurrentContainer $path
			LOG "Open Container: $path"
			::Browse::List::Reset
			if {[dict exists $::Browse::Containers {*}$path]} {
				set d [dict get $::Browse::Containers {*}$path]
				set type [dict get $d type]
				set containerName [dict get $d name]
				LOG "Container Name: $containerName"
				if {$::Browse::Refresh == 0} {
					if {[dict exists $d c]} {set ds $d; dict unset ds c} else {set ds $d}
					::Widgets::Browse::Container {*}$ds
				} else {set ::Browse::Refresh 0}
				switch -glob -nocase -- $type {
					co* {
						# Container is a Container
						set ilist [::Browse::List::GetIcon ocontainer]
						set clist $containerName
						::Browse::List::Add [list $clist] $ilist
						set ifCategory 0
					}
					ca* {
						# Container is a Category Container
						set ifCategory 1
					}
					default {set ifCategory 0}
				}
				if {[dict exists $d ccount] && [dict get $d ccount] > 0} {
					set c [dict get $d c]	
					set clist ""; # Container Name List
					set ilist ""; # Icon List
					dict for {k v} $c {
						set type [dict get $v type]
						if {!$ifCategory} {
							lappend clist "  [dict get $v name]"
							lappend ilist " [::Browse::List::GetIcon $type]"
						} else {
							lappend clist [dict get $v name]
							lappend ilist [::Browse::List::GetIcon $type]
						}
					}
					::Browse::List::Add $clist $ilist
				}
				if {[dict exists $d icount] && [dict get $d icount] > 0} {
					set i [dict get $d i]
					set clist ""; # Item List
					set ilist ""; # Icon List
					dict for {k v} $i {
						set type [dict get $v type]
						if {!$ifCategory} {
							lappend clist "  [dict get $v name]"
							lappend ilist " [::Browse::List::GetIcon $type]"
						} else {
							lappend clist [dict get $v name]
							lappend ilist [::Browse::List::GetIcon $type]
						}
					}
					::Browse::List::Add $clist $ilist
				}
			}
		}

		proc Add {name items params args} {
			## ::Browse::Container::Add 
			# Adds a Container to the Container List for Browsing and Selection
			# ::Browse::Container::Add "Apple Stations" $itemDict 0
			## Parent Container Must Exist
			# Automatically increments the count values
			# Params will add params for the container such as ID's or anything else
			# into the params key
			LOG "::Browse::Container::Add -name $name -items $items -path $args"
			set args [string map {"c " ""} $args]
			set path c
			foreach p $args {
				lappend path $p
				if {![dict exists $::Browse::Containers {*}$path]} {
					LOG "Parent Container Doesn't Exist, Create it First!"
					LOG "Current Path: $path"
					return
				}
				lappend path c
			}
			LOG "Final Container Path: $path"
			set cPath [lrange $path 0 end-1]
			set check -1
			if {[catch {dict keys $items}]} {
				puts "ERROR: Items Not Dictionary"
				return
			}
			if {[dict exists $::Browse::Containers {*}$cPath ccount]} {
				set ccount [dict get $::Browse::Containers {*}$cPath ccount]
				if {$ccount > 0} {
					## If Container Already Exists, Replace
					set d [dict get $::Browse::Containers {*}$cPath c]
					dict for {k v} $d {
						if {[dict exists $v name] && [dict get $v name] == $name} {
							set check $k
							break
						}
					}
				}
				if {$check == -1} {
					incr ccount
					dict set ::Browse::Containers {*}$cPath ccount $ccount
					set index [expr {$ccount - 1}]
				} else {
					set index $check
				}
				set icount [llength [dict keys $items]]
				dict set ::Browse::Containers {*}$path $index name $name
				dict set ::Browse::Containers {*}$path $index params [dict create {*}$params index $index]
				dict set ::Browse::Containers {*}$path $index type container
				if {![dict exists $::Browse::Containers {*}$path $index ccount]} {
					dict set ::Browse::Containers {*}$path $index ccount 0
				}
				dict set ::Browse::Containers {*}$path $index icount $icount
				if {$icount > 0} {
					dict set ::Browse::Containers {*}$path $index i $items
				}
				# Return the Path to the Procedure that Called it
				LOG "New Containers Value:\n$::Browse::Containers"
				return "$path $index"
			}
		}

		proc Multiple {names args} {
			## Creates Multiple Containers at path identified by $args
			# Containers Will be Empty, All Containers at path will be replaced
			# Example Call:
			# ::Browse::Container::Multiple [list Songs Artists Albums Genres Composers] 1
			LOG "::Browse::Container::Multiple $names $args"
			set path [::Browse::Container::Path $args]
			if {[dict exists $::Browse::Containers {*}$path]} {
				set ccount [llength $names]
				dict set ::Browse::Containers {*}$path ccount $ccount
				set i 0
				set c [dict create]
				foreach name $names {
					# 0 {name {Apple Stations} type container ccount 0 icount 0}
					dict set c $i [dict create name $name type container ccount 0 icount 0]
					incr i
				}
				dict set ::Browse::Containers {*}$path c $c
				LOG "New Container: [dict get $::Browse::Containers {*}$path]"
			} else {
				LOG "ERROR: Given Path $path Does Not Exist"
			}
		}

		proc lappend {containers args} {
			## Adds the provided $containers to the path identified by $args
			# Index provided for items will be ignored and replaced by proper
			# values.  No check will be done so be careful not to provide
			# duplicate data
			set path [::Browse::Container::Path $args]
			if {[dict exists $::Browse::Containers {*}$path]} {
				set ccount [dict get $::Browse::Containers {*}$path ccount]
				dict for {k v} $items {
					dict set ::Browse::Containers {*}$path c $ccount $v
					incr ccount
				}
				dict set ::Browse::Containers {*}$path ccount $ccount
				LOG "New Container: [dict get $::Browse::Containers {*}$path]"
			} else {
				LOG "ERROR: Given Path $path Does Not Exist"
			}

		}

		proc Replace {containers args} {
			# Replaces Containers at Location Specified by args - should be
			# properly formatted containers and items inside of them (if applicable)
			# Will automatically adjust ccount and any items in the container
			# will be left alone
			set args [string map {"c " ""} $args]
			set path c
			foreach p $args {
				lappend path $p
				if {![dict exists $::Browse::Containers {*}$path]} {
					LOG "Parent Container Doesn't Exist, Create it First!"
					LOG "Current Path: $path"
					return
				}
				lappend path c
			}
			LOG "Final Container Path: $path"
			set cPath [lrange $path 0 end-1]
			set check -1
			if {[catch {dict keys $containers}]} {
				puts "ERROR: Containers Not Dictionary"
				return
			}
			if {[dict exists $::Browse::Containers {*}$cPath ccount]} {
				set ccount [dict get $::Browse::Containers {*}$cPath ccount]
				if {$ccount > 0} {LOG "Replacing Previous Containers"}	
			}
			set ccount [llength [dict keys $containers]]
			dict set ::Browse::Containers {*}$cPath ccount $ccount
			dict set ::Browse::Containers {*}$path $containers
			LOG "Containers at $path Replaced!"
			LOG "[dict get $::Browse::Containers {*}$cPath]"
		}


		proc GetIndex {name args} {
			# Get the Index of the Container at the Given Path by Name
			# ::Browse::Container::GetIndex "Apple Stations" 0
			# -> 0 c 0
		}

		proc GetParent {args} {
			## Get the Parent Container of the Container at args
			# If Left Blank, Returns Parent of Current Container
			if {$args == ""} {
				set path $::Browse::CurrentContainer
			} else {
				set path $args
			}
			set parent [lrange $path 0 end-2]
			puts "Parent Container: $parent"
			set name [dict get $::Browse::Containers {*}$parent name]
			puts "Parent Name: $name"
			if {$name == "Root"} {
				LOG "Already at Root Level Returning $path"
				set rootPath [string map {"c " ""} $args]
				return $rootPath
			} else {
				set parentPath [string map {"c " ""} $parent]
				return $parentPath
			}
		}

		proc Path {args} {
			set args [string map {"c " ""} $args]
			set path c
			foreach p {*}$args {
				lappend path $p
				if {![dict exists $::Browse::Containers {*}$path]} {
					puts "Parent Container Doesn't Exist, Create it First!"
					puts "Current Path: $path"
					return
				}
				lappend path c
			}
			if {[lindex $path end] eq "c"} {set path [lrange $path 0 end-1]}
			return $path
		}
	}

	namespace eval Item {

		proc Send {args} {
			## Sends the Item to the System Script to Be Executed
			LOG "::Browse::Item::Send $args"
			::device_proc "::ATV::Control::PlayItem $args"
		}
		
		proc Open {args} {
			## Open an Item at the Given Path
			# ::Browse::Item::Open 0 0 2 <- Item 3 in Container 0 -> 0
			LOG "::Browse::Item::Open $args"
			set args [string map {"c " ""} $args]
			set itemIndex [lindex $args end]
			set containerPath [lrange $args 0 end-1]
			set cPath [::Browse::Container::Path $containerPath]
			if {[dict exists $::Browse::Containers {*}$cPath i $itemIndex]} {
				## Item Exists at Given Path!
				set d [dict get $::Browse::Containers {*}$cPath i $itemIndex]
				::Browse::Item::Send $d
			}
		}
		
		proc Add {type name id params args} {
			## Adds a single Item to the Given Container found at $args
			LOG "::Browse::Item::Add $type $name $id $args"
			set path [::Browse::Container::Path $args]
			if {[dict exists $::Browse::Containers {*}$path]} {
				set icount [dict get $::Browse::Containers {*}$path icount]
				incr icount
				dict set ::Browse::Containers {*}$path icount $icount
				set index [expr {$icount - 1}]
				dict set ::Browse::Containers {*}$path i $index \
					[dict create type $type name $name id $id params $params]
				LOG "New Container: [dict get $::Browse::Containers {*}$path]"
			} else {
				LOG "ERROR: Given Path $path Does Not Exist"
			}
		}

		proc Multiple {items args} {
			## Adds Multiple Items to the Given Container found at $args
			# Items Must Be Formatted as a List of Items Properly Formatted.  Their Index
			# will be converted automatically.
			set path [::Browse::Container::Path $args]
			if {[dict exists $::Browse::Containers {*}$path]} {
				set icount [dict get $::Browse::Containers {*}$path icount]
				dict for {k v} $items {
					dict set ::Browse::Containers {*}$path i $icount $v
					incr icount
				}
				dict set ::Browse::Containers {*}$path icount $icount
				LOG "New Container: [dict get $::Browse::Containers {*}$path]"
			} else {
				LOG "ERROR: Given Path $path Does Not Exist"
			}
		}

		proc Replace {items args} {
			## Replaces Items at $args with $items
			# The item dict should be properly formatted
			# 0 {type "Item Type" name "Item Name" id "ID"} 1 ...
			LOG "::Browse::Item::Multiple $items $args"
			set path [::Browse::Container::Path $args]
			if {[catch {dict keys $items}]} {LOG "$items is not a Dictionary"; return}
			if {[dict exists $::Browse::Containers {*}$path]} {
				set icount [llength [dict keys $items]]
				dict set ::Browse::Containers {*}$path icount $icount
				dict set ::Browse::Containers {*}$path i $items
				LOG "New Container: [dict get $::Browse::Containers {*}$path]"
			} else {
				LOG "ERROR: Given Path $path Does Not Exist"
			}
		}
	}

	namespace eval List {
		proc Reset {} {
			::clear_list "browse_list"
		}

		proc Select {index} {
			# User Selects an Item in the List
			LOG "::Browse::List::Select $index"
			set c [dict get $::Browse::Containers {*}$::Browse::CurrentContainer]
			dict with c {}
			switch -glob -- $type {
				co* {
					# Browsing Container
					if {$index == 0} {
						# Navigate Back One
						::Browse::Container::Back
						return
					} else {
						set index [expr {$index - 1}]
					}
				}
				ca* {
					# Browsing Category
				}
				default {
					LOG "ERROR: Browsing Unspecified Category - $type!"
					return
				}
			}
			set ifItem 0
			if {$ccount > 0} {
				set cindex [expr {$ccount - 1}]
				if {$index > $cindex} {
					set ifItem 1
				} else {
					LOG "Container Number: $index"
					::Browse::Container::Open {*}$::Browse::CurrentContainer $index
				}
			} else {set ifItem 1}
			if {$ifItem && $icount > 0} {
				set index [expr {$index - $ccount}]
				LOG "Item Number: $index"
				::Browse::Item::Open {*}$::Browse::CurrentContainer $index
			}
		}

		proc Add {l i} {
			## List Containers
			::add_to_list "browse_list" $l $i
		}

		proc GetIcon {t} {
			# Set the Icon to Return for Given Item or Container Types
			switch -nocase -glob -- $t {
				So* -
				St* {return s; # Station					}
				Co* {return $; # Container					}
				Oc* {return %; # Ocontainer (Open Container)}
			}
		}
	}
}

if {$testing == 1} {
	proc LOG {t} {puts $t}
	namespace eval Widgets {
		namespace eval Meta {
			proc BrowseTitle {args} {puts "Simulating Browse Title to $args"}
		}

               namespace eval Browse {
                        proc Container {args} {puts "User is Browsing Container $args"}
               }
	}
	
	proc add_to_list {args} {puts "Simulating Add to List $args"}

	proc clear_list {args} {puts "Simulating Clear List $args"}
}