Keith Vetter 2008-07-09 : I've been doing a lot of programming in Microsoft Visual Studio recently. It has one minor mis-feature which has bugged me for a while and I finally decided to fix it.
On its start page Visual Studio shows a list of most recently used projects. BUT there's no way of editing it. I often create test projects to try out some idea and these just clutter up the list. The only work around is to go into the registry and fix it by hand.
So I decided to write this tool which lets you edit the Visual Studio's MRU list. For every installed version of Visual Studio, it lists the MRU list. You can rearrange, delete and even add projects to the list. Two nice additional features are Kill Zombies which deletes all projects which no longer exist and Explorer which opens Windows Explorer at that project.
The only missing piece is to get tklib's tooltip to work on listbox items.
##+########################################################################## # # VSProjectListEditor.tcl -- allows you to edit Visual Studio's most recent # project list (which is stored in the registry). # by Keith Vetter, July 2008 # package require Tk 8.5 package require registry set RKEY {HKEY_CURRENT_USER\Software\Microsoft\VisualStudio} array set VSPretty {7.0 "Visual Studio 2002" 7.1 "Visual Studio 2003" 8.0 "Visual Studio 2005" 8.0Exp "Visual Studio 2005 Express" 9.0 "Visual Studio 2008"} array set S {title "VS Project List Editor" modified 0} proc DoDisplay {} { global S image create bitmap ::bmp::up -data { #define up_width 11 #define up_height 11 static char up_bits = { 0x00, 0x00, 0x20, 0x00, 0x70, 0x00, 0xf8, 0x00, 0xfc, 0x01, 0xfe, 0x03, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00 } } image create bitmap ::bmp::down -data { #define down_width 11 #define down_height 11 static char down_bits = { 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0xfe, 0x03, 0xfc, 0x01, 0xf8, 0x00, 0x70, 0x00, 0x20, 0x00, 0x00, 0x00 } } image create bitmap ::bmp::x -data { #define X_width 11 #define X_height 11 static char X_bits = { 0x04, 0x01, 0x8e, 0x03, 0xdf, 0x07, 0xfe, 0x03, 0xfc, 0x01, 0xf8, 0x00, 0xfc, 0x01, 0xfe, 0x03, 0xdf, 0x07, 0x8e, 0x03, 0x04, 0x01 } } image create bitmap ::bmp::plus -data { #define plus_width 11 #define plus_height 11 static char plus_bits = { 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0xff, 0xFF, 0xff, 0xFF, 0xff, 0xFF, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00 } } image create bitmap ::bmp::quest -data { #define quest_width 11 #define quest_height 11 static char quest_bits = { 0xf0, 0x00, 0xf8, 0x01, 0x9c, 0x03, 0x9c, 0x03, 0xc0, 0x03, 0xe0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00 } } wm title . $S(title) ::ttk::frame .top ::ttk::combobox .cb -values $::vsInfo(vsVersions) -state readonly \ -textvariable ::S(version) -exportselection 0 ::ttk::button .about -image ::bmp::quest -command About ::ttk::frame .middle -borderwidth 2 -relief ridge listbox .lb -listvariable ::S(mru) -height 20 -width 60 -exportselection 0 \ -bd 0 -highlightthickness 0 ::ttk::frame .right ::ttk::button .right.up -image ::bmp::up -command Up ::ttk::button .right.x -image ::bmp::x -command Delete ::ttk::button .right.add -image ::bmp::plus -command Add ::ttk::button .right.down -image ::bmp::down -command Down ::ttk::frame .buttons -borderwidth 2 -relief ridge ;# -pady .1i ::ttk::button .buttons.zombie -text "Kill Zombies" -command KillZombies ::ttk::button .buttons.explorer -text "Explorer..." -command Explorer ::ttk::button .buttons.save -text "Save" -command Save pack .top -side top -fill both pack .cb -in .top -side left pack .about -in .top -side right pack .buttons -side bottom -fill x -ipady .1i pack .middle -side top -fill both -expand 1 pack .right -in .middle -side right pack .lb -in .middle -side left -fill both -expand 1 pack {*}[winfo child .right] -side top pack {*}[winfo child .buttons] -side left -expand 1 foreach t [trace info variable ::S(version)] { trace remove variable ::S(version) write FillListBox } trace add variable ::S(version) write FillListBox set ::S(version) [lindex $::vsInfo(vsVersions) end] bind all <F2> {console show} AddToolTips } ##+########################################################################## # # AddToolTips -- Adds tooltip using tklib's tooltip package # proc AddToolTips {} { set n [catch {package require tooltip}] if {$n} return foreach {w txt} { .right.up "Move project up in the list" .right.down "Move project down in the list" .right.x "Delete project from the list" .right.add "Add a new project to the list" .buttons.zombie "Delete all non-existant projects" .buttons.explorer "Open Windows Explorer at this project" .buttons.save "Save changes back into the registry" .about "About this tool" } { if {[winfo exists $w]} { ::tooltip::tooltip $w $txt } else { puts "bad window: '$w'" } } } ##+########################################################################## # # ToggleButtons -- Enables buttons based on data in listbox # proc ToggleButtons {} { global S set how [expr {$S(mru) ne "" ? "normal" : "disabled"}] foreach w [winfo child .right] { $w config -state $how } .buttons.explorer config -state $how .buttons.zombie config -state $how .buttons.save config -state [expr {$S(modified) ? "normal" : "disabled"}] } ##+########################################################################## # # Up -- Moves selected item up # proc Up {} { set idx [.lb curselection] if {$idx eq ""} return if {$idx eq 0} return set value [.lb get $idx] .lb delete $idx incr idx -1 .lb insert $idx $value .lb selection clear 0 end .lb selection set $idx incr ::S(modified) ToggleButtons } ##+########################################################################## # # Down -- Moves selected item down # proc Down {} { set idx [.lb curselection] if {$idx eq ""} return if {$idx+1 == [.lb index end]} return set value [.lb get $idx] .lb delete $idx incr idx 1 .lb insert $idx $value .lb selection clear 0 end .lb selection set $idx incr ::S(modified) ToggleButtons } ##+########################################################################## # # Delete -- Deletes selected item # proc Delete {} { set idx [.lb curselection] if {$idx eq ""} return .lb delete $idx incr ::S(modified) .lb selection set $idx if {[.lb curselection] eq ""} { .lb selection set [incr idx -1] } ToggleButtons } ##+########################################################################## # # Add -- Adds a new project to the list # proc Add {} { global S vsInfo set types {{{All VS Files} {.sln .csproj .vbproj}} {{All Files} *}} set pName [tk_getOpenFile -filetypes $types] if {$pName eq ""} return set v $S(version) set pretty [GetProjectPrettyName $v $pName] lappend S(mru) " $pretty" set value "File[llength $S(mru)]" set vsInfo($v,$pretty) $pName incr S(modified) ToggleButtons } ##+########################################################################## # # FillListBox -- Fills listbox with projects for this version of Visual Studio # proc FillListBox {var1 var2 op} { global S vsInfo if {$S(modified)} { set ans [tk_messageBox -icon question -type yesnocancel \ -message "Save changes"] if {$ans eq "cancel"} { set S(version) $S(oldVersion) return } } set S(modified) 0 set S(mru) $vsInfo($S(version),fileList) .lb select clear 0 end .lb selection set 0 set S(oldVersion) $S(version) ToggleButtons } ##+########################################################################## # # GetVSInfo -- Gets info about all Visual Studio projects # proc GetVSInfo {} { global RKEY vsInfo unset -nocomplain vsInfo set n [catch { set keys [registry keys $RKEY] }] if {$n || $keys eq ""} { set emsg "No version of Visual Studio found." wm withdraw . tk_messageBox -icon error -message $emsg exit } # vsInfo(vsVersions) => all VS versions in pretty format # vsInfo($v,key) => registry key to this vs version ProjectMRUList # vsInfo($v,fileList) => list of pretty project names as in the registry # vsInfo($v,$pretty) => where $pretty lives foreach key [registry keys $RKEY] { set key2 "$RKEY\\$key" set key3 "$RKEY\\$key\\ProjectMRUList" set mru [registry keys $key2 ProjectMRUList] if {$mru eq {}} continue set v [GetVSPrettyName $key $key2] lappend vsInfo(vsVersions) $v set vsInfo($v,key) $key3 set vsInfo($v,fileList) {} set plist [lsort -dictionary [registry values $key3]] foreach value $plist { set raw [registry get $key3 $value] set pretty [GetProjectPrettyName $v $raw] lappend vsInfo($v,fileList) " $pretty" set vsInfo($v,$pretty) $raw } } } ##+########################################################################## # # GetVSPrettyName -- Gets the common name for a Visual Studio version # proc GetVSPrettyName {key key2} { global VSPretty if {[info exists VSPretty($key)]} { return $VSPretty($key) } set VSPretty($key) "Version #$key" set key2 "$RKEY\\$key" set where [registry get $key2 "DefaultOpenSolutionLocation"] set value [lsearch -glob -inline [file split $where] "Visual*"] if {$value ne ""} { set VSPretty($key) "Version $key" } return $VSPretty($key) } ##+########################################################################## # # GetProjectPrettyName -- Gets unique pretty name for a project # proc GetProjectPrettyName {vsPretty pName} { global vsInfo regsub {\|.*$} $pName {} pName set pName [file rootname [file tail $pName]] if {! [info exists vsInfo($vsPretty,$pName)]} { return $pName } # Name not unique, add version number to it for {set i 1} {$i < 999} {incr i} { set pName2 "$pName ($i)" if {! [info exists vsInfo($vsPretty,$pName2)]} { return $pName2 } } error "Cannot create a unique project name for '$pName'" return $pName } ##+########################################################################## # # Explorer -- Opens Windows Explorer at this project # proc Explorer {} { global S vsInfo env set idx [.lb curselection] if {$idx eq ""} return set who [string range [.lb get $idx] 1 end] if {$who eq ""} return set raw [ResolveProject $who] if {! [file exists $raw]} { set msg "Error: project is a zombie\n'$raw'" tk_messageBox -icon error -message $msg return } set cmd "/e,/select,$raw" exec explorer $cmd 2>@1 & } ##+########################################################################## # # ResolveProject -- Returns absolute path to a project (with no spaces) # proc ResolveProject {pretty} { global S vsInfo env set raw $vsInfo($S(version),$pretty) regsub {\|.*$} $raw {} raw; regsub -all {%(.*?)%} $raw {$env(\1)} raw set raw [subst -nobackslashes -nocommands $raw] if {[file exists $raw]} { set raw [file nativename [file attribute $raw -shortname]] } return $raw } ##+########################################################################## # # KillZombies -- deletes all missing projects # proc KillZombies {} { global S .lb selection clear 0 end set max [expr {[.lb index end] - 1}] for {set i $max} {$i >= 0} {incr i -1} { set pretty [string range [.lb get $i] 1 end] set raw [ResolveProject $pretty] if {! [file exists $raw]} { .lb delete $i incr S(modified) } } ToggleButtons } ##+########################################################################## # # About -- Does the about box # proc About {} { set msg "Visual Studio Project List Editor\n" append msg "by Keith Vetter, July 2008\n\n" append msg "Visual Studio's start page shows a list of\n" append msg "recently used projects. It stores this data\n" append msg "in the registry and provides no way to edit it.\n\n" append msg "This tool lets you edit--delete, rearrange and\n" append msg "add--projects in the list. It works even if you\n" append msg "have multiple versions of Visual Studio installed." tk_messageBox -icon info -title About -message $msg } ##+########################################################################## # # Save -- Saves user changes back into the registry # proc Save {} { global S vsInfo set dirty 0 set v $S(version) set key $vsInfo($v,key) set idx 0 foreach was $vsInfo($v,fileList) now $S(mru) { incr idx if {$now eq ""} break; ;# End of new list if {$was eq $now} continue; ;# It's correct set value "File$idx" set raw $vsInfo($v,[string range $now 1 end]) registry set $key $value $raw incr dirty } # Delete now unused entries from the registry set idx [llength $S(mru)] while {[incr idx] <= [llength $vsInfo($v,fileList)]} { set value "File$idx" registry delete $key $value incr dirty } set S(modified) 0 if {$dirty} { GetVSInfo set ::S(version) $::S(version) } ToggleButtons } ################################################################ GetVSInfo DoDisplay return