VS Project List Editor

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 vdelete ::S(version) w FillListBox
    }
    trace variable ::S(version) w 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