Commits

Jens Alfke  committed ab710ac

Got Project window working. (Fixes #21)

  • Participants
  • Parent commits e4a7447

Comments (0)

Files changed (9)

File English.lproj/Projects.xib

 		<string key="IBDocument.HIToolboxVersion">353.00</string>
 		<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
 			<bool key="EncodedWithXMLCoder">YES</bool>
-			<integer value="3"/>
+			<integer value="2"/>
 		</object>
 		<object class="NSArray" key="IBDocument.PluginDependencies">
 			<bool key="EncodedWithXMLCoder">YES</bool>
 															<int key="NSColorSpace">6</int>
 															<string key="NSCatalogName">System</string>
 															<string key="NSColorName">controlBackgroundColor</string>
-															<object class="NSColor" key="NSColor">
+															<object class="NSColor" key="NSColor" id="455245759">
 																<int key="NSColorSpace">3</int>
 																<bytes key="NSWhite">MC42NjY2NjY2OQA</bytes>
 															</object>
 											<int key="NSDraggingSourceMaskForLocal">15</int>
 											<int key="NSDraggingSourceMaskForNonLocal">0</int>
 											<bool key="NSAllowsTypeSelect">YES</bool>
+											<bool key="NSOutlineViewAutosaveExpandedItemsKey">YES</bool>
 										</object>
 									</object>
 									<string key="NSFrame">{{1, 1}, {270, 326}}</string>
 									<double key="NSPercent">5.714286e-01</double>
 								</object>
 							</object>
-							<string key="NSFrame">{{0, 18}, {272, 328}}</string>
+							<string key="NSFrame">{{0, 19}, {272, 328}}</string>
 							<reference key="NSSuperview" ref="1006"/>
 							<reference key="NSNextKeyView" ref="1037672051"/>
 							<int key="NSsFlags">530</int>
 								<int key="NSPeriodicInterval">75</int>
 							</object>
 						</object>
+						<object class="NSTextField" id="1071444777">
+							<reference key="NSNextResponder" ref="1006"/>
+							<int key="NSvFlags">-2147483350</int>
+							<string key="NSFrame">{{17, 120}, {238, 126}}</string>
+							<reference key="NSSuperview" ref="1006"/>
+							<bool key="NSEnabled">YES</bool>
+							<object class="NSTextFieldCell" key="NSCell" id="29968862">
+								<int key="NSCellFlags">67239424</int>
+								<int key="NSCellFlags2">138412032</int>
+								<string type="base64-UTF8" key="NSContents">ZHJhZyByZXBvc2l0b3J5IGZvbGRlcnMgb3IgVVJMcyBoZXJlIGZvciBxdWljayBhY2Nlc3MKCmNyZWF0
+ZSBmb2xkZXJzIGZvciBncm91cGluZw</string>
+								<object class="NSFont" key="NSSupport">
+									<string key="NSName">LucidaGrande</string>
+									<double key="NSSize">1.600000e+01</double>
+									<int key="NSfFlags">16</int>
+								</object>
+								<reference key="NSControlView" ref="1071444777"/>
+								<object class="NSColor" key="NSBackgroundColor">
+									<int key="NSColorSpace">6</int>
+									<string key="NSCatalogName">System</string>
+									<string key="NSColorName">controlColor</string>
+									<reference key="NSColor" ref="455245759"/>
+								</object>
+								<object class="NSColor" key="NSTextColor">
+									<int key="NSColorSpace">1</int>
+									<bytes key="NSRGB">MC44MjYwODY5NCAwLjgyNjA4Njk0IDAuODI2MDg2OTQAA</bytes>
+								</object>
+							</object>
+						</object>
 					</object>
 					<string key="NSFrameSize">{272, 346}</string>
 					<reference key="NSSuperview"/>
 					</object>
 					<int key="connectionID">42</int>
 				</object>
+				<object class="IBConnectionRecord">
+					<object class="IBOutletConnection" key="connection">
+						<string key="label">_introMessage</string>
+						<reference key="source" ref="1001"/>
+						<reference key="destination" ref="1071444777"/>
+					</object>
+					<int key="connectionID">45</int>
+				</object>
 			</object>
 			<object class="IBMutableOrderedSet" key="objectRecords">
 				<object class="NSArray" key="orderedObjects">
 						<reference key="object" ref="1006"/>
 						<object class="NSMutableArray" key="children">
 							<bool key="EncodedWithXMLCoder">YES</bool>
-							<reference ref="467887392"/>
 							<reference ref="155112105"/>
 							<reference ref="490372094"/>
+							<reference ref="467887392"/>
+							<reference ref="1071444777"/>
 						</object>
 						<reference key="parent" ref="1005"/>
 					</object>
 						<reference key="object" ref="441850038"/>
 						<reference key="parent" ref="191438930"/>
 					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">43</int>
+						<reference key="object" ref="1071444777"/>
+						<object class="NSMutableArray" key="children">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<reference ref="29968862"/>
+						</object>
+						<reference key="parent" ref="1006"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">44</int>
+						<reference key="object" ref="29968862"/>
+						<reference key="parent" ref="1071444777"/>
+					</object>
 				</object>
 			</object>
 			<object class="NSMutableDictionary" key="flattenedProperties">
 					<string>11.IBPluginDependency</string>
 					<string>16.IBPluginDependency</string>
 					<string>2.IBPluginDependency</string>
+					<string>21.IBAttributePlaceholdersKey</string>
 					<string>21.IBPluginDependency</string>
 					<string>22.IBPluginDependency</string>
+					<string>23.IBAttributePlaceholdersKey</string>
 					<string>23.IBPluginDependency</string>
 					<string>24.IBPluginDependency</string>
 					<string>27.IBPluginDependency</string>
 					<string>3.IBPluginDependency</string>
 					<string>30.IBPluginDependency</string>
 					<string>4.IBPluginDependency</string>
+					<string>43.IBPluginDependency</string>
+					<string>44.IBPluginDependency</string>
 					<string>5.IBPluginDependency</string>
 					<string>6.IBPluginDependency</string>
 					<string>8.IBPluginDependency</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
-					<string>{{62, 477}, {272, 346}}</string>
+					<string>{{57, 485}, {272, 346}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
-					<string>{{62, 477}, {272, 346}}</string>
+					<string>{{57, 485}, {272, 346}}</string>
 					<integer value="0"/>
 					<string>{196, 240}</string>
 					<string>{{83, 495}, {272, 346}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<object class="NSMutableDictionary">
+						<string key="NS.key.0">ToolTip</string>
+						<object class="IBToolTipAttribute" key="NS.object.0">
+							<string key="name">ToolTip</string>
+							<reference key="object" ref="155112105"/>
+							<string key="toolTip">Create a project folder</string>
+						</object>
+					</object>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<object class="NSMutableDictionary">
+						<string key="NS.key.0">ToolTip</string>
+						<object class="IBToolTipAttribute" key="NS.object.0">
+							<string key="name">ToolTip</string>
+							<reference key="object" ref="490372094"/>
+							<string key="toolTip">Remove selected item from the list</string>
+						</object>
+					</object>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 				</object>
 			</object>
 			<object class="NSMutableDictionary" key="unlocalizedProperties">
 				</object>
 			</object>
 			<nil key="sourceID"/>
-			<int key="maxID">42</int>
+			<int key="maxID">45</int>
 		</object>
 		<object class="IBClassDescriber" key="IBDocument.Classes">
 			<object class="NSMutableArray" key="referencedPartialClassDescriptions">
 					</object>
 				</object>
 				<object class="IBPartialClassDescription">
+					<string key="className">NSWindow</string>
+					<object class="IBClassDescriptionSource" key="sourceIdentifier">
+						<string key="majorKey">IBProjectSource</string>
+						<string key="minorKey">../MYUtilities/MYWindowUtils.h</string>
+					</object>
+				</object>
+				<object class="IBPartialClassDescription">
 					<string key="className">ProjectsController</string>
 					<string key="superclassName">NSWindowController</string>
 					<object class="NSMutableDictionary" key="actions">
 						<bool key="EncodedWithXMLCoder">YES</bool>
 						<object class="NSMutableArray" key="dict.sortedKeys">
 							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>_introMessage</string>
 							<string>_outline</string>
 							<string>_tree</string>
 						</object>
 						<object class="NSMutableArray" key="dict.values">
 							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>NSTextField</string>
 							<string>NSOutlineView</string>
 							<string>NSTreeController</string>
 						</object>

File Murky.xcodeproj/project.pbxproj

 		27C657AA0FAEA82300CFB909 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27C657A70FAEA82300CFB909 /* MainMenu.xib */; };
 		27D124F60C8F501B0075446A /* URLFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D124F50C8F501B0075446A /* URLFormatter.m */; };
 		27D918810C890F5500D53A8D /* xmlminimal.style in Resources */ = {isa = PBXBuildFile; fileRef = 27D918800C890F5500D53A8D /* xmlminimal.style */; };
+		27E771CC0FB01124006504EF /* BitbucketFavIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 27E771CB0FB01124006504EF /* BitbucketFavIcon.png */; };
+		27E772930FB09EA7006504EF /* MYWindowUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 27E772920FB09EA7006504EF /* MYWindowUtils.m */; };
 		8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
 		8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
 /* End PBXBuildFile section */
 		27D124F40C8F501B0075446A /* URLFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = URLFormatter.h; sourceTree = "<group>"; };
 		27D124F50C8F501B0075446A /* URLFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLFormatter.m; sourceTree = "<group>"; };
 		27D918800C890F5500D53A8D /* xmlminimal.style */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = xmlminimal.style; sourceTree = "<group>"; };
+		27E771CB0FB01124006504EF /* BitbucketFavIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = BitbucketFavIcon.png; path = Resources/BitbucketFavIcon.png; sourceTree = "<group>"; };
+		27E772910FB09EA7006504EF /* MYWindowUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MYWindowUtils.h; sourceTree = "<group>"; };
+		27E772920FB09EA7006504EF /* MYWindowUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MYWindowUtils.m; sourceTree = "<group>"; };
 		29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
 		29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
 		32CA4F630368D1EE00C91783 /* Murky_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Murky_Prefix.pch; sourceTree = "<group>"; };
 				27AA81BB0FADF65500D4FCBB /* Test.m */,
 				27AA81C50FADF83000D4FCBB /* MYUtilities_Debug.xcconfig */,
 				27AA81C60FADF83000D4FCBB /* MYUtilities_Release.xcconfig */,
+				27E772910FB09EA7006504EF /* MYWindowUtils.h */,
+				27E772920FB09EA7006504EF /* MYWindowUtils.m */,
 			);
 			name = MYUtilities;
 			sourceTree = MYUtilities;
 				089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
 				27C656FB0FAE0DC100CFB909 /* Credits.rtf */,
 				27C6572C0FAE743800CFB909 /* TextEditors.plist */,
+				27E771CB0FB01124006504EF /* BitbucketFavIcon.png */,
 			);
 			name = Resources;
 			sourceTree = "<group>";
 				27C657A10FAEA7E800CFB909 /* Projects.xib in Resources */,
 				27C657A60FAEA80D00CFB909 /* Repo.xib in Resources */,
 				27C657AA0FAEA82300CFB909 /* MainMenu.xib in Resources */,
+				27E771CC0FB01124006504EF /* BitbucketFavIcon.png in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 				27C656880FAE01A200CFB909 /* MYErrorUtils.m in Sources */,
 				27C656890FAE01A200CFB909 /* MYTask.m in Sources */,
 				27C656B90FAE046B00CFB909 /* MYDirectoryWatcher.m in Sources */,
+				27E772930FB09EA7006504EF /* MYWindowUtils.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

File Resources/BitbucketFavIcon.png

Added
New image

File Resources/BitbucketLogo.png

Added
New image

File Resources/mercurial-logo-official.icns

Binary file modified.

File Source/HgProject.h

     NSString *_name;
     NSMutableArray *_children;      // array of child HgProjects
     NSURL *_repositoryURL;          // repository URL (if I'm a leaf)
+    NSURL *_browseURL;              // Web URL for browsing repo
     HgRepository *_repository;      // repository object (instantiated lazily)
+    NSString *_uuid;                // Unique ID (only for folders)
 }
 
 + (HgProject*) projectWithName: (NSString*)name;
 
 @property (copy) NSString *name;
 @property (copy) NSArray *children;
+@property (readonly) NSString *uuid;
 
 - (BOOL) addChild: (HgProject*)child;
 - (BOOL) addChild: (HgProject*)child atIndex: (unsigned)index;
-- (void) removeChild: (HgProject*)child;
+- (BOOL) addChild: (HgProject*)child afterChild: (HgProject*)afterChild;
+- (BOOL) removeChild: (HgProject*)child;
+- (BOOL) removeDescendent: (HgProject*)descendent;
+
+- (BOOL) containsProject: (HgProject*)descendent;
 
 @property (readonly) BOOL isLeaf, isRepository, isLocal;
-@property (assign) NSURL* repositoryURL;
+@property (assign) NSURL* repositoryURL, *browseURL;
 @property (readonly) HgRepository* repository;
 
 @property (readonly) NSImage *icon;

File Source/HgProject.m

 @implementation HgProject
 
 
-- (id) _initWithName: (NSString*)name children: (NSArray*)children repositoryURL: (NSURL*)repositoryURL
+- (id) _initWithName: (NSString*)name 
+            children: (NSArray*)children 
+       repositoryURL: (NSURL*)repositoryURL
+                uuid: (NSString*)uuid
 {
     Assert(name);
     self = [super init];
         _name = name;
         if( repositoryURL )
             _repositoryURL = repositoryURL;
-        else if( children )
-            _children = [children mutableCopy];
-        else
-            _children = [NSMutableArray array];
+        else {
+            if( children )
+                _children = [children mutableCopy];
+            else
+                _children = [NSMutableArray array];
+            _uuid = uuid;
+        }
+        if (!_uuid) {
+            CFUUIDRef uuidRef = CFUUIDCreate(NULL);
+            _uuid = (id)CFMakeCollectable(CFUUIDCreateString(NULL,uuidRef));
+            CFRelease(uuidRef);
+        }
     }
     return self;
 }
 
 - (id)initWithCoder:(NSCoder *)decoder
 {
-    return [self _initWithName: [decoder decodeObjectForKey: @"name"]
+    self = [self _initWithName: [decoder decodeObjectForKey: @"name"]
                       children: [decoder decodeObjectForKey: @"children"]
-                 repositoryURL: [decoder decodeObjectForKey: @"url"]];
+                 repositoryURL: [decoder decodeObjectForKey: @"url"]
+                          uuid: [decoder decodeObjectForKey: @"uuid"]];
+    if (self) {
+        _browseURL = [decoder decodeObjectForKey: @"browseurl"];
+    }
+    return self;
 }
 
 - (void)encodeWithCoder:(NSCoder *)coder
     [coder encodeObject: _name forKey: @"name"];
     [coder encodeObject: _children forKey: @"children"];
     [coder encodeObject: _repositoryURL forKey: @"url"];
+    [coder encodeObject: _browseURL forKey: @"browseurl"];
+    [coder encodeObject: _uuid forKey: @"uuid"];
 }
 
 
 + (HgProject*) projectWithName: (NSString*)name
 {
-    return [[self alloc] _initWithName: name children: nil repositoryURL: nil];
+    return [[self alloc] _initWithName: name children: nil repositoryURL: nil uuid: nil];
 }
 
 + (HgProject*) projectWithName: (NSString*)name repositoryURL: (NSURL*)url
 {
     Assert(url);
-    return [[self alloc] _initWithName: name children: nil repositoryURL: url];
+    HgProject *project = [[self alloc] _initWithName: name children: nil repositoryURL: url uuid: nil];
+    if (0==[url.host caseInsensitiveCompare: @"bitbucket.org"]) {
+        project.browseURL = [[NSURL alloc] initWithScheme: @"http" host: @"bitbucket.org" path: url.path];
+    } else if ([url.scheme hasPrefix: @"http"]) {
+        project.browseURL = url;
+    }
+    return project;
 }
 
 + (HgProject*) projectWithRepositoryURL: (NSURL*)url
 {
-    Assert(url);
-    // Heuristics to make up a name from the last URL path component:
+    if (!url)
+        return nil;
+    NSString *scheme = url.scheme;
     NSString *path = url.path;
-    NSString *name = url.path.lastPathComponent;
-    if( [name isEqualToString: @"/"] ) {
-        path = [path stringByDeletingLastPathComponent];
-        name = url.path.lastPathComponent;
-        if( [name isEqualToString: @"/"] )
-            name = nil;
+    if (![$array(@"file",@"http",@"https",@"ssh") containsObject: scheme])
+        return nil;
+    if ([scheme isEqualToString: @"file"]) {
+        // If it's a file, locate the root of the repo:
+        path = [HgRepository findRootOf: path localPath: nil];
+        if( ! path )
+            return nil;
+        url = [NSURL fileURLWithPath: path];
     }
-    if( name && ! [name.pathExtension isEqualToString: @"cgi"] )
-        name = [name stringByAppendingFormat: @" @ %@",url.host];
-    else
-        name = url.host;
-    return [[self alloc] _initWithName: name children: nil repositoryURL: url];
+    
+    // Heuristics to make up a name:
+    NSString *name;
+    if( 0 == [url.host caseInsensitiveCompare: @"bitbucket.org"] ) {
+        NSArray *comps = [path componentsSeparatedByString: @"/"];
+        if (comps.count >= 3)
+            name = $sprintf(@"%@ / %@", [comps objectAtIndex: 1], [comps objectAtIndex:2]);
+        else
+            name = path;
+    } else {
+        name = path.lastPathComponent;
+        if( [name isEqualToString: @"/"] ) {
+            path = [path stringByDeletingLastPathComponent];
+            name = url.path.lastPathComponent;
+            if( [name isEqualToString: @"/"] )
+                name = nil;
+        }
+        if( name && ! [name.pathExtension isEqualToString: @"cgi"] ) {
+            if (!url.isFileURL)
+                name = [name stringByAppendingFormat: @" @ %@",url.host];
+        } else
+            name = url.host;
+    }
+    return [self projectWithName: name repositoryURL: url];
 }
 
 
 }
 
 
-@synthesize name=_name, repositoryURL=_repositoryURL;
+- (NSString*) description {
+    return $sprintf(@"%@[%@]", [self class], _name);
+}
+
+
+@synthesize name=_name, repositoryURL=_repositoryURL, browseURL=_browseURL;
 
 - (BOOL) isLeaf         {return _children==nil;}
 - (BOOL) isRepository   {return _repositoryURL==nil;}
 - (BOOL) isLocal        {return _repositoryURL.isFileURL;}
+- (id) uuid             {return _uuid;}
 
 
 - (NSArray*) children               {return _children;}
 - (void) setChildren: (NSArray*)c   {[_children setArray: c];}
 
+
+- (BOOL) containsProject: (HgProject*)descendent
+{
+    if (self==descendent)
+        return YES;
+    else {
+        for (HgProject *child in _children)
+            if (child==descendent || [child containsProject: descendent])
+                return YES;
+        return NO;
+    }
+}
+
+
 - (BOOL) addChild: (HgProject*)child
 {
     return [self addChild: child atIndex: _children.count];
 
 - (BOOL) addChild: (HgProject*)child atIndex: (unsigned)index
 {
-    if( ! [_children containsObject: child] ) {
+    if( _children && [_children indexOfObjectIdenticalTo: child]==NSNotFound ) {
         NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index];
         [self willChange: NSKeyValueChangeInsertion valuesAtIndexes: indexes forKey: @"children"];
         [_children insertObject: child atIndex: index];
         return NO;
 }
 
+- (BOOL) addChild: (HgProject*)child afterChild: (HgProject*)afterChild
+{
+    unsigned index;
+    if (afterChild) {
+        index = [_children indexOfObjectIdenticalTo: afterChild];
+        if (index==NSNotFound)
+            return NO;
+        index++;
+    } else
+        index = _children.count;
+    return [self addChild: child atIndex: index];
+}
 
-- (void) removeChild: (HgProject*)child
+
+- (BOOL) removeChild: (HgProject*)child
 {
-    unsigned index = [_children indexOfObject: child];
-    if( index != NSNotFound ) {
-        NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index];
-        [self willChange: NSKeyValueChangeInsertion valuesAtIndexes: indexes forKey: @"children"];
-        [_children removeObjectAtIndex: index];
-        [self didChange: NSKeyValueChangeInsertion valuesAtIndexes: indexes forKey: @"children"];
+    if (_children) {
+        unsigned index = [_children indexOfObjectIdenticalTo: child];
+        if( index != NSNotFound ) {
+            NSIndexSet *indexes = [NSIndexSet indexSetWithIndex: index];
+            [self willChange: NSKeyValueChangeInsertion valuesAtIndexes: indexes forKey: @"children"];
+            [_children removeObjectAtIndex: index];
+            [self didChange: NSKeyValueChangeInsertion valuesAtIndexes: indexes forKey: @"children"];
+            return YES;
+        } 
+    }
+    return NO;
+}
+
+- (BOOL) removeDescendent: (HgProject*)descendent
+{
+    if ([self removeChild: descendent])
+        return YES;
+    else {
+        for (HgProject *child in _children)
+            if ([child removeDescendent: descendent])
+                return YES;
+        return NO;
     }
 }
-
-
+        
 
 - (HgRepository*) repository
 {
         return nil;
     else if( _repositoryURL.isFileURL ) {
         return [[NSWorkspace sharedWorkspace] iconForFile: _repositoryURL.path];
+    } else if( 0 == [_repositoryURL.host caseInsensitiveCompare: @"bitbucket.org"] ) {
+        return [NSImage imageNamed: @"BitbucketFavIcon.png"];
     } else if( [_repositoryURL.scheme hasPrefix: @"http"] ) {
         // Safari bookmark icon:
         return [[NSWorkspace sharedWorkspace] iconForFileType: NSFileTypeForHFSTypeCode('tSts')];

File Source/ProjectsController.h

 {
     IBOutlet NSOutlineView *_outline;
     IBOutlet NSTreeController *_tree;
+    IBOutlet NSTextField *_introMessage;
     
     HgProject *_root;
+    BOOL _changed, _exists;
 }
 
 + (ProjectsController*) sharedInstance;

File Source/ProjectsController.m

 #import "MercurialApp.h"
 #import "HgProject.h"
 #import "HgRepository.h"
+#import "MYWindowUtils.h"
+#import "ExceptionUtils.h"
 
 
 @implementation ProjectsController
 
 
+#define kProjectsPboardType @"MurkySerializedHgProjects"
+
+
 static NSArray *kDropTypes;
 
 
 + (void) initialize
 {
     if( ! kDropTypes )
-        kDropTypes = [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil];
+        kDropTypes = $array(kProjectsPboardType, NSFilenamesPboardType, NSURLPboardType);
 }
 
 
     self = [super initWithWindowNibName: @"Projects"];
     if( self ) {
         _root = [NSKeyedUnarchiver unarchiveObjectWithFile: [[self class] saveFile]];
-        if( ! _root ) {
-            _root = [HgProject projectWithName: @"Projects"];
+        if( _root ) {
+            _exists = YES;
+        } else {
+            _root = [HgProject projectWithName: @"Projects"];   // not itself visible
+            [_root addChild: [HgProject projectWithName: @"untitled project"]];
         }
-        
+                
         [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(save)
                                                      name: NSApplicationWillTerminateNotification
                                                    object: NSApp];
 
 - (void) save
 {
-    NSError *error;
-    [[NSKeyedArchiver archivedDataWithRootObject: _root] writeToFile: [[self class] saveFile]
-                                                             options: NSAtomicWrite
-                                                               error: &error];
+    if (_changed) {
+        NSError *error;
+        [[NSKeyedArchiver archivedDataWithRootObject: _root] writeToFile: [[self class] saveFile]
+                                                                 options: NSAtomicWrite
+                                                                   error: &error];
+        Log(@"Saved project tree");
+        _changed = NO;
+    }
+}
+
+- (void) changed {
+    if (!_changed) {
+        _changed = YES;
+        [self performSelector: @selector(save) withObject: nil afterDelay: 5.0];
+        _exists = YES;
+        [_introMessage setHidden: YES];
+    }
+}
+
+
+- (NSArray*) selection {
+    NSMutableArray *sel = $marray();
+    NSIndexSet *indexes = _outline.selectedRowIndexes;
+    for (NSInteger row=indexes.firstIndex; row!=NSNotFound; row=[indexes indexGreaterThanIndex: row])
+        [sel addObject: [[_outline itemAtRow: row] representedObject]];
+    return sel;
 }
 
 
 {
     _outline.doubleAction = @selector(openRepository:);
     [_outline registerForDraggedTypes: kDropTypes];
+    if (!_exists)
+        [_introMessage setHidden: NO];
+    [self.window my_setTitleBarIcon: [NSImage imageNamed: @"mercurial-logo-official.icns"]];
     [self.window setExcludedFromWindowsMenu: YES];
 }
 
 
 + (void) applicationLaunched
 {
-    if( [[NSUserDefaults standardUserDefaults] boolForKey: @"ProjectsShowing"] )
+    if( ! [[NSUserDefaults standardUserDefaults] boolForKey: @"ProjectsHidden"] )
         [[self sharedInstance] showWindow: self];
 }
 
 - (void)windowWillClose:(NSNotification *)notification
 {
-    [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"ProjectsShowing"];
+    [[NSUserDefaults standardUserDefaults] setBool: YES forKey: @"ProjectsHidden"];
 }
 
 - (void)windowDidBecomeKey:(NSNotification *)notification
 {
-    [[NSUserDefaults standardUserDefaults] setBool: YES forKey: @"ProjectsShowing"];
+    [[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"ProjectsHidden"];
+}
+
+
+- (id)outlineView:(NSOutlineView *)outlineView itemForPersistentObject:(id)object {
+    NSIndexPath *path = [NSKeyedUnarchiver unarchiveObjectWithData: object];
+    return [_tree.arrangedObjects descendantNodeAtIndexPath: path];
+    
+}
+
+- (id)outlineView:(NSOutlineView *)outlineView persistentObjectForItem:(id)item {
+    NSIndexPath *path = [(NSTreeNode*)item indexPath];
+    return [NSKeyedArchiver archivedDataWithRootObject: path];
 }
 
 
 
 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
 {
-    // Double click should not begin editing table text.
-    return tableColumn.isEditable && [[NSApp currentEvent] clickCount]<2;
+    // Double click should not begin editing title of a leaf.
+    HgProject *project = [item representedObject];
+    return tableColumn.isEditable && (!project.isLeaf || [[NSApp currentEvent] clickCount]<2);
 }
 
 
 #pragma mark DRAGGING:
 
 
+static NSMutableArray *sDraggingProjects;
+
+
 - (int) _typeOfDrag: (id<NSDraggingInfo>)info
 {
     NSString *type = [[info draggingPasteboard] availableTypeFromArray: kDropTypes];
         return NSNotFound;
 }
 
-- (NSArray*) _urlsFromDrag: (id <NSDraggingInfo>)info
+- (NSArray*) _projectsFromDrag: (id <NSDraggingInfo>)info
 {
     NSPasteboard *pb = [info draggingPasteboard];
     switch( [self _typeOfDrag: info] ) {
         case 0: {
-            NSMutableArray *urls = [NSMutableArray array];
-            for( NSString *path in [pb propertyListForType: NSFilenamesPboardType] ) {
-                path = [HgRepository findRootOf: path localPath: nil];
-                if( ! path )
-                    return nil;
-                [urls addObject: [NSURL fileURLWithPath: path]];
-            }
-            return urls;
+            return sDraggingProjects;
         }
         case 1: {
+            NSMutableArray *projects = [NSMutableArray array];
+            for( NSString *path in [pb propertyListForType: NSFilenamesPboardType] ) {
+                NSURL *url = [NSURL fileURLWithPath: path];
+                HgProject *project = [HgProject projectWithRepositoryURL: url];
+                if (project)
+                    [projects addObject: project];
+            }
+            if (projects.count==0)
+                projects = nil;
+            return projects;
+        }
+        case 2: {
             NSURL *url = [NSURL URLFromPasteboard: pb];
-            return url ?[NSArray arrayWithObject: url] :nil;
+            HgProject *project = [HgProject projectWithRepositoryURL: url];
+            return project ?[NSArray arrayWithObject: project] :nil;
         }
         default:
             return nil;
 
 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
 {
-    if( [self _typeOfDrag: info] != NSNotFound ) {
-        HgProject *project = [item representedObject];
-        if( project!=_root )
-            return NSDragOperationLink;
-    }
+    @try{
+        NSArray *draggedProjects = [self _projectsFromDrag: info];
+        if (draggedProjects == nil )
+            return NSDragOperationNone;
+        
+        HgProject *project = item ?[item representedObject] :_root;
+        //Log(@"Drag: index=%i, item=%@, project=%@", index,item,project);//TEMP
+        if (index==NSOutlineViewDropOnItemIndex) {
+            if (project==_root) {
+                // Drag to empty space in window appends to root:
+                index = project.children.count;
+            } else if (project.isLeaf) {
+                // Drag onto a leaf item turns into drag above it:
+                item = [item parentNode];
+                NSArray *siblings = [[item representedObject] valueForKey: @"children"];
+                if (siblings) {
+                    index = [siblings indexOfObjectIdenticalTo: project];
+                    if (index==NSNotFound) {
+                        Warn(@"Couldn't find %@ in parent %@",project,item);
+                        index = 0;
+                    }
+                }
+                if (![item parentNode])
+                    item = nil;
+            }
+            //Log(@"  now index=%i, item=%@", index,item);//TEMP
+            [outlineView setDropItem: item dropChildIndex: index];
+        }
+        
+        // Prevent dragging into a folder that's being dragged:
+        if (draggedProjects == sDraggingProjects) {
+            for (HgProject *draggedProject in draggedProjects) {
+                if ([draggedProject containsProject: project])
+                    return NSDragOperationNone;
+            }
+        }
+        
+        return NSDragOperationLink;
+    }catchAndReport(@"Exception handling drag");
     return NSDragOperationNone;
 }
 
 
-- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
+- (BOOL)outlineView:(NSOutlineView *)outlineView 
+         acceptDrop:(id <NSDraggingInfo>)info 
+               item:(id)item 
+         childIndex:(NSInteger)index
 {
-    NSArray *urls = [self _urlsFromDrag: info];
-    if( urls ) {
-        HgProject *project = [item representedObject];
-        if( project!=_root ) {
-            for( NSURL *url in urls ) {
-                HgProject *child = [HgProject projectWithRepositoryURL: url];
-                if( child && [project addChild: child atIndex: index] )
-                    index++;
-            }
-            return YES;
+    NSArray *projects = [self _projectsFromDrag: info];
+    Log(@"acceptDrop: %@", projects);
+    if( projects ) {
+        HgProject *dstParent = item ?[item representedObject] :_root;
+        if (index==NSOutlineViewDropOnItemIndex) index=0;
+        
+        for( HgProject *child in projects ) {
+            Log(@"    add %@ to %@ at %i", child,dstParent,index);
+            if ([dstParent.children indexOfObjectIdenticalTo: child] < (unsigned)index)
+                index--;
+            [_root removeDescendent: child];
+            if( [dstParent addChild: child atIndex: index] )
+                index++;
         }
+        [self changed];
+        return YES;
     }
     return NO;
 }
 
 
+- (BOOL)outlineView:(NSOutlineView *)outlineView 
+         writeItems:(NSArray *)items 
+       toPasteboard:(NSPasteboard *)pasteboard;
+{
+    sDraggingProjects = $marray();
+    for (id item in items)
+        [sDraggingProjects addObject: [item representedObject]];
+    Log(@"Dragging %@ ...", sDraggingProjects);
+    
+    [pasteboard declareTypes: $array(kProjectsPboardType) owner: self];
+    return YES;
+}
+
+- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type
+{
+    Log(@"provideDataForType");
+    [sender setData: [NSData data] forType: type];
+}
+
+- (void)pasteboardChangedOwner:(NSPasteboard *)sender
+{
+    Log(@"pasteboardChangedOwner");
+    sDraggingProjects = nil;
+}
+
+
+
 
 #pragma mark -
 #pragma mark ACTIONS:
 
 - (IBAction) addProject: (id)sender
 {
-    [_root addChild: [HgProject projectWithName: @""]];
+    [_root addChild: [HgProject projectWithName: @"untitled project"]];
+    [self changed];
 }
 
 
 - (IBAction) removeProject: (id)sender
 {
     [_tree remove: sender];
+    [self changed];
+}
+
+- (IBAction) delete: (id)sender
+{
+    [self removeProject: self];
 }
 
 
     NSURL *url = self.selectedProject.repositoryURL;
     if( url.isFileURL )
         [gMercurialApp openRepository: url.path remember: YES];
-    else
-        if( ! [[NSWorkspace sharedWorkspace] openURL: url] )
+    else {
+        url = self.selectedProject.browseURL;
+        if( !url || ! [[NSWorkspace sharedWorkspace] openURL: url] )
             NSBeep();
+    }
 }
 
 
 }
 
 
+- (void) keyDown: (NSEvent*)ev {
+    NSString *chars = [ev charactersIgnoringModifiers];
+    if ( [chars length] == 1 ) {
+        unichar ch = [chars characterAtIndex:0];
+        switch (ch) {
+            case 0x7F:
+            case NSDeleteFunctionKey:
+                [self removeProject: self];
+                break;
+        }
+    }
+}
+
 
 @end