├── .gitignore ├── LITabControl.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── mark.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LITabControl ├── Base.lproj │ └── MainMenu.xib ├── Classes │ ├── LIPullDownTemplate.pdf │ ├── LITabCell.h │ ├── LITabCell.m │ ├── LITabControl.h │ ├── LITabControl.m │ ├── LITabLeftTemplate.pdf │ ├── LITabPlusTemplate.pdf │ ├── LITabRightTemplate.pdf │ ├── NSImage+LITabControl.h │ └── NSImage+LITabControl.m ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── LIAppDelegate.h ├── LIAppDelegate.m ├── LITabControl-Info.plist ├── LITabControl-Prefix.pch ├── en.lproj │ ├── Credits.rtf │ └── InfoPlist.strings └── main.m ├── LITabControlTests ├── LITabControlTests-Info.plist ├── LITabControlTests.m └── en.lproj │ └── InfoPlist.strings └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | -------------------------------------------------------------------------------- /LITabControl.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4D1176FF183A0BEE0055CEB0 /* LIPullDownTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4D1176FE183A0BEE0055CEB0 /* LIPullDownTemplate.pdf */; }; 11 | 4D846D5D183909D8003573EF /* LITabCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D846D5C183909D8003573EF /* LITabCell.m */; }; 12 | 4DC2D65418322F8300FA57A8 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DC2D65318322F8300FA57A8 /* Cocoa.framework */; }; 13 | 4DC2D65E18322F8300FA57A8 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4DC2D65C18322F8300FA57A8 /* InfoPlist.strings */; }; 14 | 4DC2D66018322F8300FA57A8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC2D65F18322F8300FA57A8 /* main.m */; }; 15 | 4DC2D66418322F8300FA57A8 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 4DC2D66218322F8300FA57A8 /* Credits.rtf */; }; 16 | 4DC2D66718322F8300FA57A8 /* LIAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC2D66618322F8300FA57A8 /* LIAppDelegate.m */; }; 17 | 4DC2D66A18322F8300FA57A8 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4DC2D66818322F8300FA57A8 /* MainMenu.xib */; }; 18 | 4DC2D66C18322F8300FA57A8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4DC2D66B18322F8300FA57A8 /* Images.xcassets */; }; 19 | 4DC2D67318322F8300FA57A8 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DC2D67218322F8300FA57A8 /* XCTest.framework */; }; 20 | 4DC2D67418322F8300FA57A8 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DC2D65318322F8300FA57A8 /* Cocoa.framework */; }; 21 | 4DC2D67C18322F8300FA57A8 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4DC2D67A18322F8300FA57A8 /* InfoPlist.strings */; }; 22 | 4DC2D67E18322F8300FA57A8 /* LITabControlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC2D67D18322F8300FA57A8 /* LITabControlTests.m */; }; 23 | 4DC2D68A18322FE700FA57A8 /* LITabControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC2D68918322FE700FA57A8 /* LITabControl.m */; }; 24 | 4DC2D68C1832309E00FA57A8 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DC2D68B1832309E00FA57A8 /* QuartzCore.framework */; }; 25 | 4DC2D69B1833A0E600FA57A8 /* NSImage+LITabControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC2D69A1833A0E600FA57A8 /* NSImage+LITabControl.m */; }; 26 | 4DD302B31833BEA10084A037 /* LITabLeftTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4DD302B01833BEA10084A037 /* LITabLeftTemplate.pdf */; }; 27 | 4DD302B41833BEA10084A037 /* LITabPlusTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4DD302B11833BEA10084A037 /* LITabPlusTemplate.pdf */; }; 28 | 4DD302B51833BEA10084A037 /* LITabRightTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4DD302B21833BEA10084A037 /* LITabRightTemplate.pdf */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | 4DC2D67518322F8300FA57A8 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 4DC2D64818322F8300FA57A8 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 4DC2D64F18322F8300FA57A8; 37 | remoteInfo = LITabControl; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 4D1176FE183A0BEE0055CEB0 /* LIPullDownTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = LIPullDownTemplate.pdf; sourceTree = ""; }; 43 | 4D846D5B183909D8003573EF /* LITabCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LITabCell.h; sourceTree = ""; }; 44 | 4D846D5C183909D8003573EF /* LITabCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LITabCell.m; sourceTree = ""; }; 45 | 4DC2D65018322F8300FA57A8 /* LITabControl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LITabControl.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 4DC2D65318322F8300FA57A8 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 47 | 4DC2D65618322F8300FA57A8 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 48 | 4DC2D65718322F8300FA57A8 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 49 | 4DC2D65818322F8300FA57A8 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 50 | 4DC2D65B18322F8300FA57A8 /* LITabControl-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "LITabControl-Info.plist"; sourceTree = ""; }; 51 | 4DC2D65D18322F8300FA57A8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 52 | 4DC2D65F18322F8300FA57A8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 53 | 4DC2D66118322F8300FA57A8 /* LITabControl-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LITabControl-Prefix.pch"; sourceTree = ""; }; 54 | 4DC2D66318322F8300FA57A8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 55 | 4DC2D66518322F8300FA57A8 /* LIAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LIAppDelegate.h; sourceTree = ""; }; 56 | 4DC2D66618322F8300FA57A8 /* LIAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LIAppDelegate.m; sourceTree = ""; }; 57 | 4DC2D66918322F8300FA57A8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 58 | 4DC2D66B18322F8300FA57A8 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 59 | 4DC2D67118322F8300FA57A8 /* LITabControlTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LITabControlTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 4DC2D67218322F8300FA57A8 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 61 | 4DC2D67918322F8300FA57A8 /* LITabControlTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "LITabControlTests-Info.plist"; sourceTree = ""; }; 62 | 4DC2D67B18322F8300FA57A8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 63 | 4DC2D67D18322F8300FA57A8 /* LITabControlTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LITabControlTests.m; sourceTree = ""; }; 64 | 4DC2D68818322FE700FA57A8 /* LITabControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LITabControl.h; sourceTree = ""; }; 65 | 4DC2D68918322FE700FA57A8 /* LITabControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LITabControl.m; sourceTree = ""; }; 66 | 4DC2D68B1832309E00FA57A8 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 67 | 4DC2D6991833A0E600FA57A8 /* NSImage+LITabControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSImage+LITabControl.h"; sourceTree = ""; }; 68 | 4DC2D69A1833A0E600FA57A8 /* NSImage+LITabControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSImage+LITabControl.m"; sourceTree = ""; }; 69 | 4DD302B01833BEA10084A037 /* LITabLeftTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = LITabLeftTemplate.pdf; sourceTree = ""; }; 70 | 4DD302B11833BEA10084A037 /* LITabPlusTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = LITabPlusTemplate.pdf; sourceTree = ""; }; 71 | 4DD302B21833BEA10084A037 /* LITabRightTemplate.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = LITabRightTemplate.pdf; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | 4DC2D64D18322F8300FA57A8 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | 4DC2D65418322F8300FA57A8 /* Cocoa.framework in Frameworks */, 80 | 4DC2D68C1832309E00FA57A8 /* QuartzCore.framework in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | 4DC2D66E18322F8300FA57A8 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | 4DC2D67418322F8300FA57A8 /* Cocoa.framework in Frameworks */, 89 | 4DC2D67318322F8300FA57A8 /* XCTest.framework in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXFrameworksBuildPhase section */ 94 | 95 | /* Begin PBXGroup section */ 96 | 4D117700183A1C320055CEB0 /* Resources */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 4DD302B01833BEA10084A037 /* LITabLeftTemplate.pdf */, 100 | 4DD302B11833BEA10084A037 /* LITabPlusTemplate.pdf */, 101 | 4DD302B21833BEA10084A037 /* LITabRightTemplate.pdf */, 102 | 4D1176FE183A0BEE0055CEB0 /* LIPullDownTemplate.pdf */, 103 | ); 104 | name = Resources; 105 | sourceTree = ""; 106 | }; 107 | 4DC2D64718322F8300FA57A8 = { 108 | isa = PBXGroup; 109 | children = ( 110 | 4DC2D65918322F8300FA57A8 /* LITabControl */, 111 | 4DC2D67718322F8300FA57A8 /* LITabControlTests */, 112 | 4DC2D65218322F8300FA57A8 /* Frameworks */, 113 | 4DC2D65118322F8300FA57A8 /* Products */, 114 | ); 115 | sourceTree = ""; 116 | }; 117 | 4DC2D65118322F8300FA57A8 /* Products */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 4DC2D65018322F8300FA57A8 /* LITabControl.app */, 121 | 4DC2D67118322F8300FA57A8 /* LITabControlTests.xctest */, 122 | ); 123 | name = Products; 124 | sourceTree = ""; 125 | }; 126 | 4DC2D65218322F8300FA57A8 /* Frameworks */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 4DC2D65318322F8300FA57A8 /* Cocoa.framework */, 130 | 4DC2D68B1832309E00FA57A8 /* QuartzCore.framework */, 131 | 4DC2D67218322F8300FA57A8 /* XCTest.framework */, 132 | 4DC2D65518322F8300FA57A8 /* Other Frameworks */, 133 | ); 134 | name = Frameworks; 135 | sourceTree = ""; 136 | }; 137 | 4DC2D65518322F8300FA57A8 /* Other Frameworks */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 4DC2D65618322F8300FA57A8 /* AppKit.framework */, 141 | 4DC2D65718322F8300FA57A8 /* CoreData.framework */, 142 | 4DC2D65818322F8300FA57A8 /* Foundation.framework */, 143 | ); 144 | name = "Other Frameworks"; 145 | sourceTree = ""; 146 | }; 147 | 4DC2D65918322F8300FA57A8 /* LITabControl */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 4DC2D68718322FA800FA57A8 /* Classes */, 151 | 4DC2D66518322F8300FA57A8 /* LIAppDelegate.h */, 152 | 4DC2D66618322F8300FA57A8 /* LIAppDelegate.m */, 153 | 4DC2D66818322F8300FA57A8 /* MainMenu.xib */, 154 | 4DC2D66B18322F8300FA57A8 /* Images.xcassets */, 155 | 4DC2D65A18322F8300FA57A8 /* Supporting Files */, 156 | ); 157 | path = LITabControl; 158 | sourceTree = ""; 159 | }; 160 | 4DC2D65A18322F8300FA57A8 /* Supporting Files */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 4DC2D65B18322F8300FA57A8 /* LITabControl-Info.plist */, 164 | 4DC2D65C18322F8300FA57A8 /* InfoPlist.strings */, 165 | 4DC2D65F18322F8300FA57A8 /* main.m */, 166 | 4DC2D66118322F8300FA57A8 /* LITabControl-Prefix.pch */, 167 | 4DC2D66218322F8300FA57A8 /* Credits.rtf */, 168 | ); 169 | name = "Supporting Files"; 170 | sourceTree = ""; 171 | }; 172 | 4DC2D67718322F8300FA57A8 /* LITabControlTests */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 4DC2D67D18322F8300FA57A8 /* LITabControlTests.m */, 176 | 4DC2D67818322F8300FA57A8 /* Supporting Files */, 177 | ); 178 | path = LITabControlTests; 179 | sourceTree = ""; 180 | }; 181 | 4DC2D67818322F8300FA57A8 /* Supporting Files */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 4DC2D67918322F8300FA57A8 /* LITabControlTests-Info.plist */, 185 | 4DC2D67A18322F8300FA57A8 /* InfoPlist.strings */, 186 | ); 187 | name = "Supporting Files"; 188 | sourceTree = ""; 189 | }; 190 | 4DC2D68718322FA800FA57A8 /* Classes */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 4DC2D68818322FE700FA57A8 /* LITabControl.h */, 194 | 4DC2D68918322FE700FA57A8 /* LITabControl.m */, 195 | 4D846D5B183909D8003573EF /* LITabCell.h */, 196 | 4D846D5C183909D8003573EF /* LITabCell.m */, 197 | 4DC2D6991833A0E600FA57A8 /* NSImage+LITabControl.h */, 198 | 4DC2D69A1833A0E600FA57A8 /* NSImage+LITabControl.m */, 199 | 4D117700183A1C320055CEB0 /* Resources */, 200 | ); 201 | path = Classes; 202 | sourceTree = ""; 203 | }; 204 | /* End PBXGroup section */ 205 | 206 | /* Begin PBXNativeTarget section */ 207 | 4DC2D64F18322F8300FA57A8 /* LITabControl */ = { 208 | isa = PBXNativeTarget; 209 | buildConfigurationList = 4DC2D68118322F8300FA57A8 /* Build configuration list for PBXNativeTarget "LITabControl" */; 210 | buildPhases = ( 211 | 4DC2D64C18322F8300FA57A8 /* Sources */, 212 | 4DC2D64D18322F8300FA57A8 /* Frameworks */, 213 | 4DC2D64E18322F8300FA57A8 /* Resources */, 214 | ); 215 | buildRules = ( 216 | ); 217 | dependencies = ( 218 | ); 219 | name = LITabControl; 220 | productName = LITabControl; 221 | productReference = 4DC2D65018322F8300FA57A8 /* LITabControl.app */; 222 | productType = "com.apple.product-type.application"; 223 | }; 224 | 4DC2D67018322F8300FA57A8 /* LITabControlTests */ = { 225 | isa = PBXNativeTarget; 226 | buildConfigurationList = 4DC2D68418322F8300FA57A8 /* Build configuration list for PBXNativeTarget "LITabControlTests" */; 227 | buildPhases = ( 228 | 4DC2D66D18322F8300FA57A8 /* Sources */, 229 | 4DC2D66E18322F8300FA57A8 /* Frameworks */, 230 | 4DC2D66F18322F8300FA57A8 /* Resources */, 231 | ); 232 | buildRules = ( 233 | ); 234 | dependencies = ( 235 | 4DC2D67618322F8300FA57A8 /* PBXTargetDependency */, 236 | ); 237 | name = LITabControlTests; 238 | productName = LITabControlTests; 239 | productReference = 4DC2D67118322F8300FA57A8 /* LITabControlTests.xctest */; 240 | productType = "com.apple.product-type.bundle.unit-test"; 241 | }; 242 | /* End PBXNativeTarget section */ 243 | 244 | /* Begin PBXProject section */ 245 | 4DC2D64818322F8300FA57A8 /* Project object */ = { 246 | isa = PBXProject; 247 | attributes = { 248 | CLASSPREFIX = LI; 249 | LastUpgradeCheck = 0500; 250 | ORGANIZATIONNAME = "Mark Onyschuk"; 251 | TargetAttributes = { 252 | 4DC2D67018322F8300FA57A8 = { 253 | TestTargetID = 4DC2D64F18322F8300FA57A8; 254 | }; 255 | }; 256 | }; 257 | buildConfigurationList = 4DC2D64B18322F8300FA57A8 /* Build configuration list for PBXProject "LITabControl" */; 258 | compatibilityVersion = "Xcode 3.2"; 259 | developmentRegion = English; 260 | hasScannedForEncodings = 0; 261 | knownRegions = ( 262 | en, 263 | Base, 264 | ); 265 | mainGroup = 4DC2D64718322F8300FA57A8; 266 | productRefGroup = 4DC2D65118322F8300FA57A8 /* Products */; 267 | projectDirPath = ""; 268 | projectRoot = ""; 269 | targets = ( 270 | 4DC2D64F18322F8300FA57A8 /* LITabControl */, 271 | 4DC2D67018322F8300FA57A8 /* LITabControlTests */, 272 | ); 273 | }; 274 | /* End PBXProject section */ 275 | 276 | /* Begin PBXResourcesBuildPhase section */ 277 | 4DC2D64E18322F8300FA57A8 /* Resources */ = { 278 | isa = PBXResourcesBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | 4DD302B31833BEA10084A037 /* LITabLeftTemplate.pdf in Resources */, 282 | 4DC2D65E18322F8300FA57A8 /* InfoPlist.strings in Resources */, 283 | 4DD302B51833BEA10084A037 /* LITabRightTemplate.pdf in Resources */, 284 | 4DC2D66C18322F8300FA57A8 /* Images.xcassets in Resources */, 285 | 4D1176FF183A0BEE0055CEB0 /* LIPullDownTemplate.pdf in Resources */, 286 | 4DC2D66418322F8300FA57A8 /* Credits.rtf in Resources */, 287 | 4DD302B41833BEA10084A037 /* LITabPlusTemplate.pdf in Resources */, 288 | 4DC2D66A18322F8300FA57A8 /* MainMenu.xib in Resources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | 4DC2D66F18322F8300FA57A8 /* Resources */ = { 293 | isa = PBXResourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | 4DC2D67C18322F8300FA57A8 /* InfoPlist.strings in Resources */, 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | }; 300 | /* End PBXResourcesBuildPhase section */ 301 | 302 | /* Begin PBXSourcesBuildPhase section */ 303 | 4DC2D64C18322F8300FA57A8 /* Sources */ = { 304 | isa = PBXSourcesBuildPhase; 305 | buildActionMask = 2147483647; 306 | files = ( 307 | 4DC2D66018322F8300FA57A8 /* main.m in Sources */, 308 | 4DC2D69B1833A0E600FA57A8 /* NSImage+LITabControl.m in Sources */, 309 | 4DC2D68A18322FE700FA57A8 /* LITabControl.m in Sources */, 310 | 4D846D5D183909D8003573EF /* LITabCell.m in Sources */, 311 | 4DC2D66718322F8300FA57A8 /* LIAppDelegate.m in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | 4DC2D66D18322F8300FA57A8 /* Sources */ = { 316 | isa = PBXSourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | 4DC2D67E18322F8300FA57A8 /* LITabControlTests.m in Sources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | /* End PBXSourcesBuildPhase section */ 324 | 325 | /* Begin PBXTargetDependency section */ 326 | 4DC2D67618322F8300FA57A8 /* PBXTargetDependency */ = { 327 | isa = PBXTargetDependency; 328 | target = 4DC2D64F18322F8300FA57A8 /* LITabControl */; 329 | targetProxy = 4DC2D67518322F8300FA57A8 /* PBXContainerItemProxy */; 330 | }; 331 | /* End PBXTargetDependency section */ 332 | 333 | /* Begin PBXVariantGroup section */ 334 | 4DC2D65C18322F8300FA57A8 /* InfoPlist.strings */ = { 335 | isa = PBXVariantGroup; 336 | children = ( 337 | 4DC2D65D18322F8300FA57A8 /* en */, 338 | ); 339 | name = InfoPlist.strings; 340 | sourceTree = ""; 341 | }; 342 | 4DC2D66218322F8300FA57A8 /* Credits.rtf */ = { 343 | isa = PBXVariantGroup; 344 | children = ( 345 | 4DC2D66318322F8300FA57A8 /* en */, 346 | ); 347 | name = Credits.rtf; 348 | sourceTree = ""; 349 | }; 350 | 4DC2D66818322F8300FA57A8 /* MainMenu.xib */ = { 351 | isa = PBXVariantGroup; 352 | children = ( 353 | 4DC2D66918322F8300FA57A8 /* Base */, 354 | ); 355 | name = MainMenu.xib; 356 | sourceTree = ""; 357 | }; 358 | 4DC2D67A18322F8300FA57A8 /* InfoPlist.strings */ = { 359 | isa = PBXVariantGroup; 360 | children = ( 361 | 4DC2D67B18322F8300FA57A8 /* en */, 362 | ); 363 | name = InfoPlist.strings; 364 | sourceTree = ""; 365 | }; 366 | /* End PBXVariantGroup section */ 367 | 368 | /* Begin XCBuildConfiguration section */ 369 | 4DC2D67F18322F8300FA57A8 /* Debug */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 374 | CLANG_CXX_LIBRARY = "libc++"; 375 | CLANG_ENABLE_OBJC_ARC = YES; 376 | CLANG_WARN_BOOL_CONVERSION = YES; 377 | CLANG_WARN_CONSTANT_CONVERSION = YES; 378 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 379 | CLANG_WARN_EMPTY_BODY = YES; 380 | CLANG_WARN_ENUM_CONVERSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 383 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 384 | COPY_PHASE_STRIP = NO; 385 | GCC_C_LANGUAGE_STANDARD = gnu99; 386 | GCC_DYNAMIC_NO_PIC = NO; 387 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 388 | GCC_OPTIMIZATION_LEVEL = 0; 389 | GCC_PREPROCESSOR_DEFINITIONS = ( 390 | "DEBUG=1", 391 | "$(inherited)", 392 | ); 393 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 394 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 395 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 396 | GCC_WARN_UNDECLARED_SELECTOR = YES; 397 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 398 | GCC_WARN_UNUSED_FUNCTION = YES; 399 | GCC_WARN_UNUSED_VARIABLE = YES; 400 | MACOSX_DEPLOYMENT_TARGET = 10.9; 401 | ONLY_ACTIVE_ARCH = YES; 402 | SDKROOT = macosx; 403 | }; 404 | name = Debug; 405 | }; 406 | 4DC2D68018322F8300FA57A8 /* Release */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_SEARCH_USER_PATHS = NO; 410 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 411 | CLANG_CXX_LIBRARY = "libc++"; 412 | CLANG_ENABLE_OBJC_ARC = YES; 413 | CLANG_WARN_BOOL_CONVERSION = YES; 414 | CLANG_WARN_CONSTANT_CONVERSION = YES; 415 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 416 | CLANG_WARN_EMPTY_BODY = YES; 417 | CLANG_WARN_ENUM_CONVERSION = YES; 418 | CLANG_WARN_INT_CONVERSION = YES; 419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 420 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 421 | COPY_PHASE_STRIP = YES; 422 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 423 | ENABLE_NS_ASSERTIONS = NO; 424 | GCC_C_LANGUAGE_STANDARD = gnu99; 425 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 426 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 427 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 428 | GCC_WARN_UNDECLARED_SELECTOR = YES; 429 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 430 | GCC_WARN_UNUSED_FUNCTION = YES; 431 | GCC_WARN_UNUSED_VARIABLE = YES; 432 | MACOSX_DEPLOYMENT_TARGET = 10.9; 433 | SDKROOT = macosx; 434 | }; 435 | name = Release; 436 | }; 437 | 4DC2D68218322F8300FA57A8 /* Debug */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 441 | COMBINE_HIDPI_IMAGES = YES; 442 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 443 | GCC_PREFIX_HEADER = "LITabControl/LITabControl-Prefix.pch"; 444 | INFOPLIST_FILE = "LITabControl/LITabControl-Info.plist"; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | WRAPPER_EXTENSION = app; 447 | }; 448 | name = Debug; 449 | }; 450 | 4DC2D68318322F8300FA57A8 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 454 | COMBINE_HIDPI_IMAGES = YES; 455 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 456 | GCC_PREFIX_HEADER = "LITabControl/LITabControl-Prefix.pch"; 457 | INFOPLIST_FILE = "LITabControl/LITabControl-Info.plist"; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | WRAPPER_EXTENSION = app; 460 | }; 461 | name = Release; 462 | }; 463 | 4DC2D68518322F8300FA57A8 /* Debug */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/LITabControl.app/Contents/MacOS/LITabControl"; 467 | COMBINE_HIDPI_IMAGES = YES; 468 | FRAMEWORK_SEARCH_PATHS = ( 469 | "$(DEVELOPER_FRAMEWORKS_DIR)", 470 | "$(inherited)", 471 | ); 472 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 473 | GCC_PREFIX_HEADER = "LITabControl/LITabControl-Prefix.pch"; 474 | GCC_PREPROCESSOR_DEFINITIONS = ( 475 | "DEBUG=1", 476 | "$(inherited)", 477 | ); 478 | INFOPLIST_FILE = "LITabControlTests/LITabControlTests-Info.plist"; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | TEST_HOST = "$(BUNDLE_LOADER)"; 481 | WRAPPER_EXTENSION = xctest; 482 | }; 483 | name = Debug; 484 | }; 485 | 4DC2D68618322F8300FA57A8 /* Release */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/LITabControl.app/Contents/MacOS/LITabControl"; 489 | COMBINE_HIDPI_IMAGES = YES; 490 | FRAMEWORK_SEARCH_PATHS = ( 491 | "$(DEVELOPER_FRAMEWORKS_DIR)", 492 | "$(inherited)", 493 | ); 494 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 495 | GCC_PREFIX_HEADER = "LITabControl/LITabControl-Prefix.pch"; 496 | INFOPLIST_FILE = "LITabControlTests/LITabControlTests-Info.plist"; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | TEST_HOST = "$(BUNDLE_LOADER)"; 499 | WRAPPER_EXTENSION = xctest; 500 | }; 501 | name = Release; 502 | }; 503 | /* End XCBuildConfiguration section */ 504 | 505 | /* Begin XCConfigurationList section */ 506 | 4DC2D64B18322F8300FA57A8 /* Build configuration list for PBXProject "LITabControl" */ = { 507 | isa = XCConfigurationList; 508 | buildConfigurations = ( 509 | 4DC2D67F18322F8300FA57A8 /* Debug */, 510 | 4DC2D68018322F8300FA57A8 /* Release */, 511 | ); 512 | defaultConfigurationIsVisible = 0; 513 | defaultConfigurationName = Release; 514 | }; 515 | 4DC2D68118322F8300FA57A8 /* Build configuration list for PBXNativeTarget "LITabControl" */ = { 516 | isa = XCConfigurationList; 517 | buildConfigurations = ( 518 | 4DC2D68218322F8300FA57A8 /* Debug */, 519 | 4DC2D68318322F8300FA57A8 /* Release */, 520 | ); 521 | defaultConfigurationIsVisible = 0; 522 | defaultConfigurationName = Release; 523 | }; 524 | 4DC2D68418322F8300FA57A8 /* Build configuration list for PBXNativeTarget "LITabControlTests" */ = { 525 | isa = XCConfigurationList; 526 | buildConfigurations = ( 527 | 4DC2D68518322F8300FA57A8 /* Debug */, 528 | 4DC2D68618322F8300FA57A8 /* Release */, 529 | ); 530 | defaultConfigurationIsVisible = 0; 531 | defaultConfigurationName = Release; 532 | }; 533 | /* End XCConfigurationList section */ 534 | }; 535 | rootObject = 4DC2D64818322F8300FA57A8 /* Project object */; 536 | } 537 | -------------------------------------------------------------------------------- /LITabControl.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LITabControl.xcodeproj/xcuserdata/mark.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LITabControl.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 4DC2D64F18322F8300FA57A8 16 | 17 | primary 18 | 19 | 20 | 4DC2D67018322F8300FA57A8 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LITabControl/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | Default 522 | 523 | 524 | 525 | 526 | 527 | 528 | Left to Right 529 | 530 | 531 | 532 | 533 | 534 | 535 | Right to Left 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | Default 547 | 548 | 549 | 550 | 551 | 552 | 553 | Left to Right 554 | 555 | 556 | 557 | 558 | 559 | 560 | Right to Left 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | -------------------------------------------------------------------------------- /LITabControl/Classes/LIPullDownTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monyschuk/LITabControl/1ca31290f8b7be36623e675c54234913c34c41c8/LITabControl/Classes/LIPullDownTemplate.pdf -------------------------------------------------------------------------------- /LITabControl/Classes/LITabCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // LITabCell.h 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/17/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef enum { 12 | LIBorderMaskTop = (1<<0), 13 | LIBorderMaskLeft = (1<<1), 14 | LIBorderMaskRight = (1<<2), 15 | LIBorderMaskBottom = (1<<3) 16 | } LIBorderMask; 17 | 18 | @interface LITabCell : NSButtonCell 19 | 20 | @property(nonatomic) BOOL showsMenu; 21 | @property(readonly, nonatomic) BOOL isShowingMenu; 22 | 23 | @property(nonatomic) CGFloat borderWidth; 24 | @property(nonatomic) LIBorderMask borderMask; 25 | @property(nonatomic, copy) NSColor *borderColor; 26 | @property(nonatomic, copy) NSColor *backgroundColor; 27 | 28 | @property(nonatomic, copy) NSColor *titleColor; 29 | @property(nonatomic, copy) NSColor *titleHighlightColor; 30 | 31 | @property(nonatomic) CGFloat minWidth, maxWidth; 32 | 33 | // NOTE: 34 | // Returns an adjusted field editor frame 35 | // used when editing the cell's title. Override 36 | // if cell contents appear to shift while editing 37 | 38 | - (NSRect)editingRectForBounds:(NSRect)rect; 39 | 40 | @end 41 | 42 | @interface LITabButton : NSButton 43 | + (Class)cellClass; 44 | 45 | @property(nonatomic) BOOL showsMenu; 46 | @property(readonly, nonatomic) BOOL isShowingMenu; 47 | 48 | @property(nonatomic) LIBorderMask borderMask; 49 | @property(nonatomic, copy) NSColor *borderColor; 50 | @property(nonatomic, copy) NSColor *backgroundColor; 51 | 52 | @property(nonatomic, copy) NSColor *titleColor; 53 | @property(nonatomic, copy) NSColor *titleHighlightColor; 54 | 55 | @property(nonatomic) CGFloat minWidth, maxWidth; 56 | 57 | @end 58 | 59 | extern BOOL LIRectArrayWithBorderMask(NSRect sourceRect, CGFloat borderWidth, LIBorderMask borderMask, NSRect **rectArray, NSInteger *rectCount); 60 | -------------------------------------------------------------------------------- /LITabControl/Classes/LITabCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // LITabCell.m 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/17/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import "LITabCell.h" 10 | #import "LITabControl.h" 11 | 12 | #import "NSImage+LITabControl.h" 13 | 14 | #define INCH 72.0f 15 | 16 | #define DF_TITLE_COLOR [NSColor colorWithCalibratedWhite:0.25f alpha:1.0f] 17 | #define DF_BORDER_COLOR [NSColor colorWithCalibratedWhite:0.75f alpha:1.0f] 18 | #define DF_HIGHLIGHT_COLOR [NSColor colorWithCalibratedRed:0.119f green:0.399f blue:0.964f alpha:1.0f] 19 | #define DF_BACKGROUND_COLOR [NSColor colorWithCalibratedRed:0.854f green:0.858f blue:0.873f alpha:1.0f] 20 | 21 | #define DF_FONT [NSFont fontWithName:@"HelveticaNeue-Medium" size:13]; 22 | 23 | 24 | @interface LITabButton (Private) 25 | - (void)constrainSizeWithCell:(LITabCell *)cell; 26 | @end 27 | 28 | @implementation LITabCell 29 | 30 | - (id)initTextCell:(NSString *)aString { 31 | if ((self = [super initTextCell:aString])) { 32 | 33 | self.font = DF_FONT; 34 | 35 | _borderWidth = 1; 36 | _borderColor = DF_BORDER_COLOR; 37 | _backgroundColor = DF_BACKGROUND_COLOR; 38 | 39 | _titleColor = DF_TITLE_COLOR; 40 | _titleHighlightColor = DF_HIGHLIGHT_COLOR; 41 | 42 | _minWidth = INCH * 2.75; 43 | _maxWidth = INCH * 2.75; 44 | 45 | [self setBordered:YES]; 46 | [self setBackgroundStyle:NSBackgroundStyleLight]; 47 | 48 | [self setHighlightsBy:NSNoCellMask]; 49 | [self setLineBreakMode:NSLineBreakByTruncatingTail]; 50 | } 51 | return self; 52 | } 53 | 54 | - (id)copyWithZone:(NSZone *)zone { 55 | LITabCell *copy = [super copyWithZone:zone]; 56 | 57 | copy->_minWidth = _minWidth; 58 | copy->_maxWidth = _maxWidth; 59 | 60 | copy->_borderMask = _borderMask; 61 | copy->_borderWidth = _borderWidth; 62 | copy->_borderColor = [_borderColor copyWithZone:zone]; 63 | copy->_backgroundColor = [_backgroundColor copyWithZone:zone]; 64 | 65 | copy->_titleColor = [_titleColor copyWithZone:zone]; 66 | copy->_titleHighlightColor = [_titleHighlightColor copyWithZone:zone]; 67 | 68 | copy->_showsMenu = _showsMenu; 69 | copy->_isShowingMenu = _isShowingMenu; 70 | 71 | return copy; 72 | } 73 | 74 | - (void)setShowsMenu:(BOOL)showsMenu { 75 | if (_showsMenu != showsMenu) { 76 | _showsMenu = showsMenu; 77 | 78 | [self.controlView setNeedsDisplay:YES]; 79 | } 80 | } 81 | 82 | - (void)setBorderWidth:(CGFloat)borderWidth { 83 | if (_borderWidth != borderWidth) { 84 | _borderWidth = borderWidth; 85 | 86 | [self.controlView setNeedsDisplay:YES]; 87 | } 88 | } 89 | - (void)setBorderColor:(NSColor *)borderColor { 90 | if (_borderColor != borderColor) { 91 | _borderColor = borderColor.copy; 92 | 93 | [self.controlView setNeedsDisplay:YES]; 94 | } 95 | } 96 | - (void)setBackgroundColor:(NSColor *)backgroundColor { 97 | if (_backgroundColor != backgroundColor) { 98 | _backgroundColor = backgroundColor.copy; 99 | 100 | [self.controlView setNeedsDisplay:YES]; 101 | } 102 | } 103 | 104 | - (void)setBorderMask:(LIBorderMask)borderMask { 105 | if (_borderMask != borderMask) { 106 | _borderMask = borderMask; 107 | 108 | [self.controlView setNeedsDisplay:YES]; 109 | } 110 | } 111 | 112 | - (void)setMinWidth:(CGFloat)minWidth { 113 | if (_minWidth != minWidth) { 114 | _minWidth = minWidth; 115 | 116 | if ([self.controlView respondsToSelector:@selector(constrainSizeWithCell:)]) { 117 | [(id)self.controlView constrainSizeWithCell:self]; 118 | } 119 | } 120 | } 121 | 122 | - (void)setMaxWidth:(CGFloat)maxWidth { 123 | if (_maxWidth != maxWidth) { 124 | _maxWidth = maxWidth; 125 | 126 | if ([self.controlView respondsToSelector:@selector(constrainSizeWithCell:)]) { 127 | [(id)self.controlView constrainSizeWithCell:self]; 128 | } 129 | } 130 | } 131 | 132 | + (NSImage *)popupImage { 133 | static NSImage *ret = nil; 134 | if (ret == nil) { 135 | ret = [[NSImage imageNamed:@"LIPullDownTemplate"] imageWithTint:[NSColor darkGrayColor]]; 136 | } 137 | return ret; 138 | } 139 | 140 | - (NSSize)cellSizeForBounds:(NSRect)aRect { 141 | NSSize titleSize = [[self attributedTitle] size]; 142 | NSSize popupSize = ([self menu] == nil) ? NSZeroSize : [[LITabCell popupImage] size]; 143 | 144 | return NSMakeSize(titleSize.width + (popupSize.width * 2) + 36, MAX(titleSize.height, popupSize.height)); 145 | } 146 | 147 | - (NSRect)popupRectWithFrame:(NSRect)cellFrame { 148 | NSRect popupRect = NSZeroRect; 149 | popupRect.size = [[LITabCell popupImage] size]; 150 | popupRect.origin = NSMakePoint(NSMaxX(cellFrame) - NSWidth(popupRect) - 8, NSMidY(cellFrame) - NSHeight(popupRect) / 2); 151 | 152 | return popupRect; 153 | } 154 | 155 | - (NSRect)titleRectForBounds:(NSRect)cellFrame { 156 | NSSize titleSize = [[self attributedTitle] size]; 157 | NSRect titleRect = NSMakeRect(NSMinX(cellFrame), floorf(NSMidY(cellFrame) - titleSize.height/2), NSWidth(cellFrame), titleSize.height); 158 | 159 | if (self.menu != nil) { 160 | NSRect popupRect = [self popupRectWithFrame:cellFrame]; 161 | CGFloat titleRectInset = ceilf(NSMaxX(cellFrame) - NSMinX(popupRect)); 162 | 163 | titleRect = NSOffsetRect(titleRect, 0, -1); 164 | titleRect = NSInsetRect(titleRect, titleRectInset, 0); 165 | } 166 | return titleRect; 167 | } 168 | 169 | - (NSRect)editingRectForBounds:(NSRect)rect { 170 | return [self titleRectForBounds:NSOffsetRect(rect, 0, -1)]; 171 | } 172 | 173 | - (LITabControl *)enclosingTabControlInView:(NSView *)controlView { 174 | id view = controlView; 175 | LITabControl *enclosingTabControl = nil; 176 | while ((view = [view superview])) { 177 | if ([view isKindOfClass:[LITabControl class]]) { 178 | enclosingTabControl = view; 179 | break; 180 | } 181 | } 182 | return enclosingTabControl; 183 | } 184 | 185 | - (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)flag { 186 | NSRect popupRect = [self popupRectWithFrame:cellFrame]; 187 | NSPoint location = [controlView convertPoint:[theEvent locationInWindow] fromView:nil]; 188 | if ([self hitTestForEvent:theEvent inRect:[[controlView superview] frame] ofView:[controlView superview]] != NSCellHitNone) { 189 | 190 | NSMenu *menu = [self menuForEvent:theEvent inRect:cellFrame ofView:controlView]; 191 | 192 | if (menu.itemArray.count > 0 && NSPointInRect(location, popupRect)) { 193 | [menu popUpMenuPositioningItem:menu.itemArray[0] atLocation:NSMakePoint(NSMidX(popupRect), NSMaxY(popupRect)) inView:controlView]; 194 | [self setShowsMenu:NO]; 195 | return YES; 196 | 197 | } 198 | } 199 | 200 | return [super trackMouse:theEvent inRect:cellFrame ofView:controlView untilMouseUp:flag]; 201 | 202 | } 203 | 204 | - (NSMenu *)menuForEvent:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)view { 205 | LITabControl *enclosingTabControl = [self enclosingTabControlInView:view]; 206 | 207 | if (enclosingTabControl) { 208 | NSMenu *menu = [enclosingTabControl.dataSource tabControl:enclosingTabControl menuForItem:self.representedObject]; 209 | 210 | if (menu != nil) { 211 | // this following side-effect is a bit hokey 212 | // but it ensures that the receiver of context popup 213 | // actions can determine which tab is associated with 214 | // the action. It also ensures that the dataSource and 215 | // target of the control updates associated views prior 216 | // to context menu display... 217 | 218 | [enclosingTabControl setSelectedItem:self.representedObject]; 219 | [NSApp sendAction:enclosingTabControl.action to:enclosingTabControl.target from:enclosingTabControl]; 220 | [[NSNotificationCenter defaultCenter] postNotificationName:LITabControlSelectionDidChangeNotification object:enclosingTabControl]; 221 | } 222 | 223 | return menu; 224 | } else { 225 | return [super menuForEvent:event inRect:cellFrame ofView:view]; 226 | } 227 | } 228 | 229 | #pragma mark - 230 | #pragma mark Drawing 231 | -(void)drawImage:(NSImage *)image withFrame:(NSRect)frame inView:(NSView *)controlView{ 232 | [super drawImage:[image imageWithTint:self.isHighlighted ? DF_HIGHLIGHT_COLOR : [NSColor darkGrayColor]] withFrame:frame inView:controlView]; 233 | 234 | } 235 | - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { 236 | [super drawWithFrame:cellFrame inView:controlView]; 237 | 238 | if (self.showsMenu) { 239 | [[LITabCell popupImage] drawInRect:[self popupRectWithFrame:cellFrame] fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil]; 240 | } 241 | } 242 | 243 | - (void)drawBezelWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { 244 | [self.backgroundColor set]; 245 | NSRectFill(cellFrame); 246 | 247 | NSRect *borderRects; 248 | NSInteger borderRectCount; 249 | if (LIRectArrayWithBorderMask(cellFrame, self.borderWidth, self.borderMask, &borderRects, &borderRectCount)) { 250 | [self.borderColor set]; 251 | NSRectFillList(borderRects, borderRectCount); 252 | } 253 | } 254 | 255 | - (NSRect)drawTitle:(NSAttributedString*)title withFrame:(NSRect)frame inView:(NSView*)controlView { 256 | NSRect titleRect = [self titleRectForBounds:frame]; 257 | [title drawInRect:titleRect]; 258 | return titleRect; 259 | } 260 | 261 | - (NSAttributedString *)attributedTitle { 262 | NSMutableAttributedString *attributedTitle = [[super attributedTitle] mutableCopy]; 263 | [attributedTitle addAttributes:@{ NSForegroundColorAttributeName : (self.state ? self.titleHighlightColor : self.titleColor) } range:NSMakeRange(0, attributedTitle.length)]; 264 | return attributedTitle; 265 | } 266 | 267 | @end 268 | 269 | @implementation LITabButton { 270 | NSLayoutConstraint *_minWidthConstraint, *_maxWidthConstraint; 271 | } 272 | 273 | + (Class)cellClass { 274 | return [LITabCell class]; 275 | } 276 | 277 | - (BOOL)showsMenu { 278 | return [self.cell showsMenu]; 279 | } 280 | - (void)setShowsMenu:(BOOL)showsMenu { 281 | [self.cell setShowsMenu:showsMenu]; 282 | } 283 | 284 | - (BOOL)isShowingMenu { 285 | return [self.cell isShowingMenu]; 286 | } 287 | 288 | - (LIBorderMask)borderMask { 289 | return [self.cell borderMask]; 290 | } 291 | - (void)setBorderMask:(LIBorderMask)borderMask { 292 | [self.cell setBorderMask:borderMask]; 293 | } 294 | 295 | - (NSColor *)borderColor { 296 | return [self.cell borderColor]; 297 | } 298 | - (void)setBorderColor:(NSColor *)borderColor { 299 | [self.cell setBorderColor:borderColor]; 300 | } 301 | - (NSColor *)backgroundColor { 302 | return [self.cell backgroundColor]; 303 | } 304 | - (void)setBackgroundColor:(NSColor *)backgroundColor { 305 | [self.cell setBackgroundColor:backgroundColor]; 306 | } 307 | 308 | - (NSColor *)titleColor { 309 | return [self.cell titleColor]; 310 | } 311 | - (void)setTitleColor:(NSColor *)titleColor { 312 | [self.cell setTitleColor:titleColor]; 313 | } 314 | - (NSColor *)titleHighlightColor { 315 | return [self.cell titleHighlightColor]; 316 | } 317 | - (void)setTitleHighlightColor:(NSColor *)titleHighlightColor { 318 | [self.cell setTitleHighlightColor:titleHighlightColor]; 319 | } 320 | 321 | - (CGFloat)minWidth { 322 | return [self.cell minWidth]; 323 | } 324 | - (void)setMinWidth:(CGFloat)minWidth { 325 | [self.cell setMinWidth:minWidth]; 326 | } 327 | - (CGFloat)maxWidth { 328 | return [self.cell maxWidth]; 329 | } 330 | - (void)setMaxWidth:(CGFloat)maxWidth { 331 | [self.cell setMaxWidth:maxWidth]; 332 | } 333 | 334 | - (void)setCell:(NSCell *)aCell { 335 | [super setCell:aCell]; 336 | if ([aCell isKindOfClass:[LITabCell class]]) { 337 | [self constrainSizeWithCell:(id)aCell]; 338 | } 339 | } 340 | 341 | - (void)constrainSizeWithCell:(LITabCell *)cell { 342 | if (_minWidthConstraint != nil) { 343 | if (cell.minWidth > 0) { 344 | [_minWidthConstraint setConstant:cell.minWidth]; 345 | } else { 346 | [self removeConstraint:_minWidthConstraint]; 347 | _minWidthConstraint = nil; 348 | } 349 | } else { 350 | if (cell.minWidth > 0) { 351 | _minWidthConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth 352 | relatedBy:NSLayoutRelationGreaterThanOrEqual 353 | toItem:nil attribute:NSLayoutAttributeNotAnAttribute 354 | multiplier:1 constant:cell.minWidth]; 355 | [self addConstraint:_minWidthConstraint]; 356 | } 357 | } 358 | 359 | if (_maxWidthConstraint != nil) { 360 | if (cell.maxWidth > 0) { 361 | [_maxWidthConstraint setConstant:cell.maxWidth]; 362 | } else { 363 | [self removeConstraint:_maxWidthConstraint]; 364 | _maxWidthConstraint = nil; 365 | } 366 | } else { 367 | if (cell.maxWidth > 0) { 368 | _maxWidthConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth 369 | relatedBy:NSLayoutRelationLessThanOrEqual 370 | toItem:nil attribute:NSLayoutAttributeNotAnAttribute 371 | multiplier:1 constant:cell.maxWidth]; 372 | [self addConstraint:_maxWidthConstraint]; 373 | } 374 | } 375 | 376 | } 377 | 378 | @end 379 | 380 | BOOL LIRectArrayWithBorderMask(NSRect sourceRect, CGFloat borderWidth, LIBorderMask borderMask, NSRect **rectArray, NSInteger *rectCount) { 381 | NSInteger outputCount = 0; 382 | static NSRect outputArray[4]; 383 | 384 | NSRect remainderRect; 385 | if (borderMask & LIBorderMaskTop) { 386 | NSDivideRect(sourceRect, &outputArray[outputCount++], &remainderRect, borderWidth, NSMinYEdge); 387 | } 388 | if (borderMask & LIBorderMaskLeft) { 389 | NSDivideRect(sourceRect, &outputArray[outputCount++], &remainderRect, borderWidth, NSMinXEdge); 390 | } 391 | if (borderMask & LIBorderMaskRight) { 392 | NSDivideRect(sourceRect, &outputArray[outputCount++], &remainderRect, borderWidth, NSMaxXEdge); 393 | } 394 | if (borderMask & LIBorderMaskBottom) { 395 | NSDivideRect(sourceRect, &outputArray[outputCount++], &remainderRect, borderWidth, NSMaxYEdge); 396 | } 397 | 398 | if (rectCount) *rectCount = outputCount; 399 | if (rectArray) *rectArray = &outputArray[0]; 400 | 401 | return (outputCount > 0); 402 | } 403 | 404 | -------------------------------------------------------------------------------- /LITabControl/Classes/LITabControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // LITabControl.h 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/12/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import "LITabCell.h" 10 | 11 | @class LITabControl; 12 | 13 | @protocol LITabDataSource 14 | 15 | - (NSUInteger)tabControlNumberOfTabs:(LITabControl *)tabControl; 16 | 17 | - (id)tabControl:(LITabControl *)tabControl itemAtIndex:(NSUInteger)index; 18 | 19 | - (NSMenu *)tabControl:(LITabControl *)tabControl menuForItem:(id)item; 20 | 21 | - (NSString *)tabControl:(LITabControl *)tabControl titleForItem:(id)item; 22 | - (void)tabControl:(LITabControl *)tabControl setTitle:(NSString *)title forItem:(id)item; 23 | 24 | - (BOOL)tabControl:(LITabControl *)tabControl canReorderItem:(id)item; 25 | - (void)tabControlDidReorderItems:(LITabControl *)tabControl orderedItems:(NSArray *)itemArray; 26 | 27 | @optional 28 | - (void)tabControlDidChangeSelection:(NSNotification *)notification; 29 | 30 | - (BOOL)tabControl:(LITabControl *)tabControl canEditItem:(id)item; 31 | - (BOOL)tabControl:(LITabControl *)tabControl canSelectItem:(id)item; 32 | 33 | - (void)tabControl:(LITabControl *)tabControl willDisplayButton:(LITabButton *)button forItem:(id)item; 34 | 35 | @end 36 | 37 | @protocol LITabDelegate 38 | @end 39 | 40 | extern NSString *LITabControlSelectionDidChangeNotification; 41 | 42 | @interface LITabControl : NSControl 43 | 44 | #pragma mark - 45 | #pragma mark Display Properties 46 | 47 | @property(nonatomic) CGFloat borderWidth; 48 | @property(nonatomic, copy) NSColor *borderColor; 49 | @property(nonatomic, copy) NSColor *backgroundColor; 50 | 51 | @property(nonatomic, copy) NSColor *titleColor; 52 | @property(nonatomic, copy) NSColor *titleHighlightColor; 53 | 54 | #pragma mark - 55 | #pragma mark Delegate & Data Source 56 | 57 | @property(nonatomic, weak) id delegate; 58 | @property(nonatomic, weak) id dataSource; 59 | 60 | - (void)reloadData; 61 | 62 | #pragma mark - 63 | #pragma mark Selection 64 | 65 | @property(nonatomic, weak) id selectedItem; 66 | 67 | #pragma mark - 68 | #pragma mark Target/Action 69 | 70 | @property(nonatomic) SEL addAction; 71 | @property(nonatomic, weak) id addTarget; 72 | 73 | #pragma mark - 74 | #pragma mark Editing 75 | 76 | - (void)editItem:(id)item; 77 | 78 | #pragma mark - 79 | #pragma mark Tab Buttons 80 | 81 | + (Class)tabButtonClass; 82 | 83 | - (NSArray *)tabButtons; 84 | - (NSButton *)tabButtonWithItem:(id)item; 85 | 86 | #pragma mark - 87 | #pragma mark Reordering 88 | 89 | @property(nonatomic) BOOL notifiesOnPartialReorder; 90 | 91 | @end 92 | -------------------------------------------------------------------------------- /LITabControl/Classes/LITabControl.m: -------------------------------------------------------------------------------- 1 | // 2 | // LITabControl.m 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/12/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import "LITabControl.h" 10 | #import "LITabCell.h" 11 | 12 | #import 13 | 14 | @interface LITabControl() 15 | 16 | @property(nonatomic, strong) NSArray *items; 17 | 18 | @property(nonatomic, strong) NSScrollView *scrollView; 19 | @property(nonatomic, strong) NSButton *addButton, *scrollLeftButton, *scrollRightButton, *draggingTab; 20 | 21 | @property(nonatomic, strong) NSTextField *editingField; 22 | 23 | @end 24 | 25 | @implementation LITabControl 26 | 27 | + (Class)cellClass { 28 | return [LITabCell class]; 29 | } 30 | 31 | #pragma mark - 32 | #pragma mark Lifecycle 33 | 34 | - (id)initWithFrame:(NSRect)frame { 35 | if ((self = [super initWithFrame:frame])) { 36 | [self configureSubviews]; 37 | } 38 | return self; 39 | } 40 | 41 | - (void)awakeFromNib { 42 | [super awakeFromNib]; 43 | [self configureSubviews]; 44 | } 45 | 46 | - (void)configureSubviews { 47 | if (_scrollView == nil) { 48 | [self setWantsLayer:YES]; 49 | 50 | LITabCell *cell = self.cell; 51 | 52 | cell.title = @""; 53 | cell.borderMask = LIBorderMaskBottom; 54 | 55 | _scrollView = [self viewWithClass:[NSScrollView class]]; 56 | 57 | [_scrollView setDrawsBackground:NO]; 58 | [_scrollView setBackgroundColor:[NSColor redColor]]; 59 | [_scrollView setVerticalScrollElasticity:NSScrollElasticityNone]; 60 | _addButton = [self buttonWithImageNamed:@"LITabPlusTemplate" target:self action:@selector(add:)]; 61 | _scrollLeftButton = [self buttonWithImageNamed:@"LITabLeftTemplate" target:self action:@selector(goLeft:)]; 62 | _scrollRightButton = [self buttonWithImageNamed:@"LITabRightTemplate" target:self action:@selector(goRight:)]; 63 | 64 | [_addButton setMenu:nil]; 65 | [_scrollLeftButton setMenu:nil]; 66 | [_scrollRightButton setMenu:nil]; 67 | 68 | [_scrollLeftButton setContinuous:YES]; 69 | [_scrollRightButton setContinuous:YES]; 70 | 71 | [_scrollLeftButton.cell sendActionOn:NSLeftMouseDownMask|NSPeriodicMask]; 72 | [_scrollRightButton.cell sendActionOn:NSLeftMouseDownMask|NSPeriodicMask]; 73 | 74 | NSDictionary *views = NSDictionaryOfVariableBindings(_scrollView, _addButton, _scrollLeftButton, _scrollRightButton); 75 | 76 | [self setSubviews:@[_scrollView, _addButton, _scrollLeftButton, _scrollRightButton]]; 77 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_addButton]-(-1)-[_scrollView]-(-1)-[_scrollLeftButton][_scrollRightButton]|" options:0 metrics:nil views:views]]; 78 | 79 | for (NSView *view in views.allValues) { 80 | [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": view}]]; 81 | } 82 | 83 | [_addButton addConstraint: 84 | [NSLayoutConstraint constraintWithItem:_addButton attribute:NSLayoutAttributeWidth 85 | relatedBy:NSLayoutRelationEqual 86 | toItem:nil attribute:NSLayoutAttributeNotAnAttribute 87 | multiplier:1 constant:48]]; 88 | 89 | [_scrollLeftButton addConstraint: 90 | [NSLayoutConstraint constraintWithItem:_scrollLeftButton attribute:NSLayoutAttributeWidth 91 | relatedBy:NSLayoutRelationEqual 92 | toItem:nil attribute:NSLayoutAttributeNotAnAttribute 93 | multiplier:1 constant:24]]; 94 | 95 | [_scrollRightButton addConstraint: 96 | [NSLayoutConstraint constraintWithItem:_scrollRightButton attribute:NSLayoutAttributeWidth 97 | relatedBy:NSLayoutRelationEqual 98 | toItem:nil attribute:NSLayoutAttributeNotAnAttribute 99 | multiplier:1 constant:24]]; 100 | 101 | [_addButton.cell setBorderMask:[_addButton.cell borderMask] | LIBorderMaskRight|LIBorderMaskBottom|LIBorderMaskTop]; 102 | [_scrollLeftButton.cell setBorderMask:[_scrollLeftButton.cell borderMask] | LIBorderMaskLeft]; 103 | 104 | [self startObservingScrollView]; 105 | [self updateButtons]; 106 | } 107 | } 108 | 109 | - (void)dealloc { 110 | [self stopObservingScrollView]; 111 | } 112 | 113 | - (void)updateButtons { 114 | // BOOL showAddButton = self.addAction != NULL; 115 | // 116 | // [_addButton setHidden:(showAddButton) ? NO : YES]; 117 | 118 | // [_addButton.constraints.lastObject setConstant:(showAddButton) ? 48 : 0]; 119 | 120 | NSClipView *contentView = self.scrollView.contentView; 121 | 122 | BOOL isDocumentClipped = (contentView.subviews.count > 0) && (NSMaxX([contentView.subviews[0] frame]) > NSWidth(contentView.bounds)); 123 | 124 | if (isDocumentClipped) { 125 | [_scrollLeftButton setHidden:NO]; 126 | [_scrollRightButton setHidden:NO]; 127 | 128 | [_scrollLeftButton setEnabled:([self firstTabLeftOutsideVisibleRect] != nil)]; 129 | [_scrollRightButton setEnabled:([self firstTabRightOutsideVisibleRect] != nil)]; 130 | 131 | } else { 132 | [_scrollLeftButton setHidden:YES]; 133 | [_scrollRightButton setHidden:YES]; 134 | } 135 | } 136 | 137 | - (NSButton *)buttonWithImageNamed:(NSString *)name target:(id)target action:(SEL)action { 138 | NSButton *button = [self viewWithClass:[NSButton class]]; 139 | 140 | [button setCell:[[self cell] copy]]; 141 | 142 | [button setTarget:target]; 143 | [button setAction:action]; 144 | 145 | [button setEnabled:action != NULL]; 146 | 147 | [button setImagePosition:NSImageOnly]; 148 | NSImage* image =[NSImage imageNamed:name]; 149 | [image setTemplate:YES]; 150 | [button setImage:image]; 151 | [button setBezelStyle:NSShadowlessSquareBezelStyle]; 152 | [button setBordered: YES]; 153 | 154 | return button; 155 | } 156 | 157 | - (id)viewWithClass:(Class)clss { 158 | id view = [[clss alloc] initWithFrame:NSZeroRect]; 159 | [view setTranslatesAutoresizingMaskIntoConstraints:NO]; 160 | 161 | return view; 162 | } 163 | 164 | #pragma mark - 165 | #pragma mark ScrollView Observation 166 | 167 | static char LIScrollViewObservationContext; 168 | 169 | - (void)startObservingScrollView { 170 | [self.scrollView addObserver:self forKeyPath:@"frame" options:0 context:&LIScrollViewObservationContext]; 171 | [self.scrollView addObserver:self forKeyPath:@"documentView.frame" options:0 context:&LIScrollViewObservationContext]; 172 | 173 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollViewDidScroll:) name:NSViewBoundsDidChangeNotification object:self.scrollView.contentView]; 174 | } 175 | - (void)stopObservingScrollView { 176 | [self.scrollView removeObserver:self forKeyPath:@"frame" context:&LIScrollViewObservationContext]; 177 | [self.scrollView removeObserver:self forKeyPath:@"documentView.frame" context:&LIScrollViewObservationContext]; 178 | 179 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:self.scrollView.contentView]; 180 | 181 | } 182 | 183 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 184 | if (context == &LIScrollViewObservationContext) { 185 | [self updateButtons]; 186 | } else { 187 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 188 | } 189 | } 190 | 191 | - (void)scrollViewDidScroll:(NSNotification *)notification { 192 | [self updateButtons]; 193 | [self invalidateRestorableState]; 194 | } 195 | 196 | #pragma mark - 197 | #pragma mark Properties 198 | 199 | - (void)setBorderWidth:(CGFloat)borderWidth { 200 | [self.cell setBorderWidth:borderWidth]; 201 | for (id subview in self.subviews) { 202 | if ([subview respondsToSelector:@selector(cell)]) { 203 | id cell = [subview cell]; 204 | if ([cell respondsToSelector:@selector(setBorderWidth:)]) { 205 | [cell setBorderWidth:borderWidth]; 206 | } 207 | } 208 | } 209 | } 210 | 211 | - (void)setBorderColor:(NSColor *)borderColor { 212 | [self.cell setBorderColor:borderColor]; 213 | for (id subview in self.subviews) { 214 | if ([subview respondsToSelector:@selector(cell)]) { 215 | id cell = [subview cell]; 216 | if ([cell respondsToSelector:@selector(setBorderColor:)]) { 217 | [cell setBorderColor:borderColor]; 218 | } 219 | } 220 | } 221 | } 222 | - (void)setBackgroundColor:(NSColor *)backgroundColor { 223 | [self.cell setBackgroundColor:backgroundColor]; 224 | for (id subview in self.subviews) { 225 | if ([subview respondsToSelector:@selector(cell)]) { 226 | id cell = [subview cell]; 227 | if ([cell respondsToSelector:@selector(setBackgroundColor:)]) { 228 | [cell setBackgroundColor:backgroundColor]; 229 | } 230 | } 231 | } 232 | } 233 | - (void)setTitleColor:(NSColor *)titleColor { 234 | [self.cell setTitleColor:titleColor]; 235 | for (id subview in self.subviews) { 236 | if ([subview respondsToSelector:@selector(cell)]) { 237 | id cell = [subview cell]; 238 | if ([cell respondsToSelector:@selector(setTitleColor:)]) { 239 | [cell setTitleColor:titleColor]; 240 | } 241 | } 242 | } 243 | } 244 | - (void)setTitleHighlightColor:(NSColor *)titleHighlightColor { 245 | [self.cell setTitleHighlightColor:titleHighlightColor]; 246 | for (id subview in self.subviews) { 247 | if ([subview respondsToSelector:@selector(cell)]) { 248 | id cell = [subview cell]; 249 | if ([cell respondsToSelector:@selector(setTitleHighlightColor:)]) { 250 | [cell setTitleHighlightColor:titleHighlightColor]; 251 | } 252 | } 253 | } 254 | } 255 | 256 | #pragma mark - 257 | #pragma mark Actions 258 | 259 | - (void)setAddAction:(SEL)addAction { 260 | if (_addAction != addAction) { 261 | _addAction = addAction; 262 | 263 | [self updateButtons]; 264 | } 265 | } 266 | 267 | - (void)add:(id)sender { 268 | [[NSApplication sharedApplication] sendAction:self.addAction to:self.addTarget from:self]; 269 | 270 | [self invalidateRestorableState]; 271 | } 272 | 273 | - (void)goLeft:(id)sender { 274 | NSButton *tab = [self firstTabLeftOutsideVisibleRect]; 275 | 276 | if (tab != nil) { 277 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { 278 | [context setAllowsImplicitAnimation:YES]; 279 | [tab scrollRectToVisible:[tab bounds]]; 280 | } completionHandler:nil]; 281 | } 282 | } 283 | 284 | - (NSButton *)firstTabLeftOutsideVisibleRect { 285 | NSView *tabView = self.scrollView.documentView; 286 | NSRect visibleRect = tabView.visibleRect; 287 | 288 | for (NSButton *button in tabView.subviews) { 289 | if (NSMinX(button.frame) < NSMinX(visibleRect)) { 290 | return button; 291 | } 292 | } 293 | return nil; 294 | } 295 | 296 | - (void)goRight:(id)sender { 297 | NSButton *tab = [self firstTabRightOutsideVisibleRect]; 298 | 299 | if (tab != nil) { 300 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { 301 | [context setAllowsImplicitAnimation:YES]; 302 | [tab scrollRectToVisible:[tab bounds]]; 303 | } completionHandler:nil]; 304 | } 305 | } 306 | 307 | - (NSButton *)firstTabRightOutsideVisibleRect { 308 | NSView *tabView = self.scrollView.documentView; 309 | NSRect visibleRect = tabView.visibleRect; 310 | 311 | for (NSUInteger index = [[tabView subviews] count]; index > 0; index--) { 312 | NSButton *button = [[tabView subviews] objectAtIndex:index - 1]; 313 | 314 | if (NSMaxX(button.frame) > NSMaxX(visibleRect)) { 315 | return button; 316 | } 317 | } 318 | return nil; 319 | } 320 | 321 | - (void)selectTab:(id)sender { 322 | NSButton *selectedButton = sender; 323 | 324 | for (NSButton *button in [self.scrollView.documentView subviews]) { 325 | [button setState:(button == selectedButton) ? 1 : 0]; 326 | } 327 | 328 | [[NSApplication sharedApplication] sendAction:self.action to:self.target from:self]; 329 | [[NSNotificationCenter defaultCenter] postNotificationName:LITabControlSelectionDidChangeNotification object:self]; 330 | 331 | NSEvent *currentEvent = [NSApp currentEvent]; 332 | 333 | if (currentEvent.clickCount > 1) { 334 | // edit on double click... 335 | [self editItem:[[sender cell] representedObject]]; 336 | 337 | } else if ([self.dataSource tabControl:self canReorderItem:[[sender cell] representedObject]]) { 338 | // watch for a drag event and initiate dragging if a drag is found... 339 | if ([self.window nextEventMatchingMask:NSLeftMouseUpMask|NSLeftMouseDraggedMask untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:NO].type == NSLeftMouseDragged) { 340 | [self reorderTab:sender withEvent:currentEvent]; 341 | return; // no autoscroll 342 | } 343 | } 344 | 345 | // scroll to visible if either editing or selecting... 346 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { 347 | [context setAllowsImplicitAnimation:YES]; 348 | [selectedButton.superview scrollRectToVisible:selectedButton.frame]; 349 | } completionHandler:nil]; 350 | 351 | [self invalidateRestorableState]; 352 | } 353 | 354 | #pragma mark - 355 | #pragma mark Reordering 356 | 357 | - (void)reorderTab:(NSButton *)tab withEvent:(NSEvent *)event { 358 | // note existing tabs which will be reordered over 359 | // the course of our drag; while the dragging tab maintains 360 | // its position over the course of the dragging operation 361 | 362 | NSView *tabView = self.scrollView.documentView; 363 | NSMutableArray *orderedTabs = [[NSMutableArray alloc] initWithArray:tabView.subviews.reverseObjectEnumerator.allObjects]; 364 | 365 | // create a dragging tab used to represent our drag, 366 | // and constraint its position and its size; the first 367 | // constraint sets position - we'll be varying this one 368 | // during our drag... 369 | 370 | CGFloat tabX = NSMinX(tab.frame); 371 | NSPoint dragPoint = [tabView convertPoint:event.locationInWindow fromView:nil]; 372 | 373 | 374 | NSButton *draggingTab = [self tabWithItem:[tab.cell representedObject]]; 375 | 376 | NSArray *draggingConstraints = @[[NSLayoutConstraint constraintWithItem:draggingTab attribute:NSLayoutAttributeLeading 377 | relatedBy:NSLayoutRelationEqual 378 | toItem:tabView attribute:NSLayoutAttributeLeading 379 | multiplier:1 constant:tabX], // VARIABLE 380 | 381 | [NSLayoutConstraint constraintWithItem:draggingTab attribute:NSLayoutAttributeTop 382 | relatedBy:NSLayoutRelationEqual 383 | toItem:tabView attribute:NSLayoutAttributeTop 384 | multiplier:1 constant:0], // CONSTANT 385 | [NSLayoutConstraint constraintWithItem:draggingTab attribute:NSLayoutAttributeBottom 386 | relatedBy:NSLayoutRelationEqual 387 | toItem:tabView attribute:NSLayoutAttributeBottom 388 | multiplier:1 constant:0]]; // CONSTANT 389 | 390 | 391 | draggingTab.cell = [tab.cell copy]; 392 | 393 | // cell subclasses may alter drawing based on represented object 394 | [draggingTab.cell setRepresentedObject:[tab.cell representedObject]]; 395 | 396 | // the presence of a menu affects the vertical offset of our title 397 | if ([tab.cell menu] != nil) [draggingTab.cell setMenu:[[NSMenu alloc] init]]; 398 | 399 | 400 | [tabView addSubview:draggingTab]; 401 | [tabView addConstraints:draggingConstraints]; 402 | 403 | [tab setHidden:YES]; 404 | 405 | CGPoint prevPoint = dragPoint; 406 | BOOL dragged = NO, reordered = NO; 407 | 408 | while (1) { 409 | event = [self.window nextEventMatchingMask:NSLeftMouseDraggedMask|NSLeftMouseUpMask]; 410 | 411 | if (event.type == NSLeftMouseUp) break; 412 | 413 | // ensure the dragged tab shows borders on both of its sides when dragging 414 | if (dragged == NO && event.type == NSLeftMouseDragged) { 415 | dragged = YES; 416 | 417 | LITabCell *cell = draggingTab.cell; 418 | cell.borderMask = cell.borderMask | LIBorderMaskLeft | LIBorderMaskRight; 419 | } 420 | 421 | // move the dragged tab 422 | NSPoint nextPoint = [tabView convertPoint:event.locationInWindow fromView:nil]; 423 | 424 | CGFloat nextX = tabX + (nextPoint.x - dragPoint.x); 425 | 426 | BOOL movingLeft = (nextPoint.x < prevPoint.x); 427 | BOOL movingRight = (nextPoint.x > prevPoint.x); 428 | 429 | prevPoint = nextPoint; 430 | 431 | [draggingConstraints[0] setConstant:nextX]; 432 | 433 | // test for reordering... 434 | if (movingLeft && NSMidX(draggingTab.frame) < NSMinX(tab.frame) && tab != orderedTabs.firstObject) { 435 | // shift left 436 | NSUInteger index = [orderedTabs indexOfObject:tab]; 437 | [orderedTabs exchangeObjectAtIndex:index withObjectAtIndex:index - 1]; 438 | 439 | [self layoutTabs:orderedTabs inView:tabView]; 440 | [tabView addConstraints:draggingConstraints]; 441 | 442 | if (self.notifiesOnPartialReorder) { 443 | [self.dataSource tabControlDidReorderItems:self orderedItems:[orderedTabs valueForKeyPath:@"cell.representedObject"]]; 444 | } 445 | 446 | reordered = YES; 447 | 448 | } else if (movingRight && NSMidX(draggingTab.frame) > NSMaxX(tab.frame) && tab != orderedTabs.lastObject) { 449 | // shift right 450 | NSUInteger index = [orderedTabs indexOfObject:tab]; 451 | [orderedTabs exchangeObjectAtIndex:index+1 withObjectAtIndex:index]; 452 | 453 | [self layoutTabs:orderedTabs inView:tabView]; 454 | [tabView addConstraints:draggingConstraints]; 455 | 456 | if (self.notifiesOnPartialReorder) { 457 | [self.dataSource tabControlDidReorderItems:self orderedItems:[orderedTabs valueForKeyPath:@"cell.representedObject"]]; 458 | } 459 | 460 | reordered = YES; 461 | } 462 | 463 | [tabView layoutSubtreeIfNeeded]; 464 | } 465 | 466 | [draggingTab removeFromSuperview]; 467 | draggingTab = nil; 468 | 469 | [tabView removeConstraints:draggingConstraints]; 470 | 471 | [tab setHidden:NO]; 472 | [tab.cell setControlView:tab]; 473 | 474 | if (reordered) { 475 | if (!self.notifiesOnPartialReorder) { 476 | [self.dataSource tabControlDidReorderItems:self orderedItems:[orderedTabs valueForKeyPath:@"cell.representedObject"]]; 477 | } 478 | 479 | [self reloadData]; 480 | 481 | [self setSelectedItem:[tab.cell representedObject]]; 482 | } 483 | } 484 | 485 | #pragma mark - 486 | #pragma mark Selection 487 | 488 | - (id)selectedItem { 489 | for (NSButton *button in [self.scrollView.documentView subviews]) { 490 | if ([button state] == 1) { 491 | return [[button cell] representedObject]; 492 | } 493 | } 494 | return nil; 495 | } 496 | - (void)setSelectedItem:(id)selectedItem { 497 | for (NSButton *button in [self.scrollView.documentView subviews]) { 498 | if ([[[button cell] representedObject] isEqual:selectedItem]) { 499 | [button setState:1]; 500 | 501 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { 502 | [context setAllowsImplicitAnimation:YES]; 503 | [button scrollRectToVisible:[button bounds]]; 504 | } completionHandler:nil]; 505 | 506 | } else { 507 | [button setState:0]; 508 | } 509 | } 510 | 511 | [self invalidateRestorableState]; 512 | } 513 | 514 | #pragma mark - 515 | #pragma mark Data Source 516 | 517 | - (void)setDataSource:(id)dataSource { 518 | if (_dataSource != dataSource) { 519 | 520 | if (_dataSource && [_dataSource respondsToSelector:@selector(tabControlDidChangeSelection:)]) 521 | [[NSNotificationCenter defaultCenter] removeObserver:_dataSource name:LITabControlSelectionDidChangeNotification object:self]; 522 | 523 | _dataSource = dataSource; 524 | 525 | if (_dataSource && [_dataSource respondsToSelector:@selector(tabControlDidChangeSelection:)]) 526 | [[NSNotificationCenter defaultCenter] addObserver:_dataSource selector:@selector(tabControlDidChangeSelection:) name:LITabControlSelectionDidChangeNotification object:self]; 527 | 528 | [self reloadData]; 529 | } 530 | } 531 | 532 | - (void)reloadData { 533 | NSView *tabView = [self viewWithClass:[NSView class]]; 534 | NSMutableArray *newItems = [[NSMutableArray alloc] init]; 535 | 536 | for (NSUInteger i = 0, count = [self.dataSource tabControlNumberOfTabs:self]; i < count; i++) { 537 | [newItems addObject:[self.dataSource tabControl:self itemAtIndex:i]]; 538 | } 539 | 540 | NSMutableArray *newTabs = [[NSMutableArray alloc] init]; 541 | 542 | for (id item in newItems) { 543 | NSButton *button = [self tabWithItem:item]; 544 | LITabCell *buttonCell = [button cell]; 545 | 546 | // NOTE: menus are dynamic, but we indicate their presence by associating a menu 547 | // with the button cell... 548 | 549 | NSMenu *menu = [self.dataSource tabControl:self menuForItem:item]; 550 | if (menu != nil) { 551 | [buttonCell setMenu:[[NSMenu alloc] init]]; 552 | [button addTrackingArea:[[NSTrackingArea alloc] initWithRect:_scrollView.bounds 553 | options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp|NSTrackingInVisibleRect 554 | owner:self 555 | userInfo:@{@"item" : item}]]; 556 | } 557 | 558 | [newTabs addObject:button]; 559 | } 560 | 561 | [tabView setSubviews:newTabs.reverseObjectEnumerator.allObjects]; 562 | [self layoutTabs:newTabs inView:tabView]; 563 | 564 | self.items = newItems; 565 | self.scrollView.documentView = (self.items.count) ? tabView : nil; 566 | [self.scrollView.layer setBorderWidth:1]; 567 | [self.scrollView.layer setBorderColor:[NSColor colorWithCalibratedWhite:0.75f alpha:1.0f].CGColor]; 568 | 569 | if (self.scrollView.documentView) { 570 | NSClipView *clipView = self.scrollView.contentView; 571 | NSView *documentView = self.scrollView.documentView; 572 | [documentView.layer setBorderWidth:1]; 573 | [documentView.layer setBorderColor:[NSColor colorWithCalibratedWhite:0.75f alpha:1.0f].CGColor]; 574 | // document content is as tall as our scrolling area, and at least as wide... 575 | 576 | [clipView addConstraints: 577 | [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[documentView]|" 578 | options:0 579 | metrics:nil 580 | views:@{@"documentView": documentView}]]; 581 | 582 | [clipView addConstraints: 583 | [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[documentView]" 584 | options:0 585 | metrics:nil 586 | views:@{@"documentView": documentView}]]; 587 | 588 | // here's the 'at least as wide' constraint... 589 | 590 | [clipView addConstraint: 591 | [NSLayoutConstraint constraintWithItem:documentView attribute:NSLayoutAttributeRight 592 | relatedBy:NSLayoutRelationGreaterThanOrEqual 593 | toItem:clipView attribute:NSLayoutAttributeRight 594 | multiplier:1 constant:0]]; 595 | } 596 | 597 | [self updateButtons]; 598 | 599 | [self invalidateRestorableState]; 600 | } 601 | 602 | - (void)layoutTabs:(NSArray *)tabs inView:(NSView *)tabView { 603 | // remove old constraints, if any... 604 | [tabView removeConstraints:tabView.constraints]; 605 | 606 | // constrain passed tabs into a horizontal list... 607 | NSButton *prev = nil; 608 | for (NSButton *button in tabs) { 609 | [tabView addConstraints: 610 | [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[button]|" options:0 metrics:nil views:@{@"button":button}]]; 611 | 612 | [tabView addConstraint: 613 | [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeading 614 | relatedBy:NSLayoutRelationEqual 615 | toItem:(prev != nil ? prev : tabView) 616 | attribute:(prev != nil ? NSLayoutAttributeTrailing : NSLayoutAttributeLeading) 617 | multiplier:1 constant:prev!=nil ? -1 : -.5]]; 618 | prev = button; 619 | } 620 | 621 | if (prev) { 622 | NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:prev attribute:NSLayoutAttributeTrailing 623 | relatedBy:NSLayoutRelationLessThanOrEqual 624 | toItem:tabView attribute:NSLayoutAttributeTrailing 625 | multiplier:1 constant:0]; 626 | [trailingConstraint setPriority:NSLayoutPriorityWindowSizeStayPut]; 627 | [tabView addConstraint:trailingConstraint]; 628 | } 629 | 630 | [tabView layoutSubtreeIfNeeded]; 631 | } 632 | 633 | - (LITabButton *)tabWithItem:(id)item { 634 | LITabCell *tabCell = [self.cell copy]; 635 | 636 | tabCell.representedObject = item; 637 | 638 | tabCell.imagePosition = NSNoImage; 639 | tabCell.borderMask = LIBorderMaskLeft | LIBorderMaskRight|LIBorderMaskBottom; 640 | 641 | tabCell.title = [self.dataSource tabControl:self titleForItem:item]; 642 | 643 | tabCell.target = self; 644 | tabCell.action = @selector(selectTab:); 645 | 646 | [tabCell sendActionOn:NSLeftMouseDownMask]; 647 | 648 | LITabButton *tab = [self viewWithClass:[self.class tabButtonClass]]; 649 | 650 | [tab setCell:tabCell]; 651 | 652 | if ([self.dataSource respondsToSelector:@selector(tabControl:canSelectItem:)]) { 653 | [[tab cell] setSelectable:[self.dataSource tabControl:self canSelectItem:item]]; 654 | } 655 | 656 | if ([self.dataSource respondsToSelector:@selector(tabControl:willDisplayButton:forItem:)]) { 657 | [self.dataSource tabControl:self willDisplayButton:tab forItem:item]; 658 | } 659 | 660 | return tab; 661 | } 662 | 663 | #pragma mark - 664 | #pragma mark Tab Buttons 665 | 666 | + (Class)tabButtonClass { 667 | return [LITabButton class]; 668 | } 669 | 670 | - (NSArray *)tabButtons { 671 | NSMutableArray *buttons = @[].mutableCopy; 672 | 673 | for (NSButton *button in [self.scrollView.documentView subviews]) { 674 | if (button != self.draggingTab) { 675 | [buttons addObject:button]; 676 | } 677 | } 678 | return buttons; 679 | } 680 | 681 | - (NSButton *)tabButtonWithItem:(id)item { 682 | for (NSButton *button in [self.scrollView.documentView subviews]) { 683 | if (button != self.draggingTab) { 684 | if ([[[button cell] representedObject] isEqual:item]) { 685 | return button; 686 | } 687 | } 688 | } 689 | return nil; 690 | } 691 | 692 | #pragma mark - 693 | #pragma mark ScrollView Tracking 694 | 695 | - (NSButton *)trackedButtonWithEvent:(NSEvent *)theEvent { 696 | id item = theEvent.trackingArea.userInfo[@"item"]; 697 | return (item != nil) ? [self tabButtonWithItem:item] : nil; 698 | } 699 | 700 | - (void)mouseEntered:(NSEvent *)theEvent { 701 | [[[self trackedButtonWithEvent:theEvent] cell] setShowsMenu:YES]; 702 | } 703 | - (void)mouseExited:(NSEvent *)theEvent { 704 | [[[self trackedButtonWithEvent:theEvent] cell] setShowsMenu:NO]; 705 | } 706 | 707 | #pragma mark - 708 | #pragma mark Editing 709 | 710 | - (void)editItem:(id)item { 711 | NSButton *button = [self tabButtonWithItem:item]; 712 | 713 | // end existing editing, if any... 714 | if (self.editingField != nil) { 715 | [self.window makeFirstResponder:self]; 716 | } 717 | 718 | // layout items if necessary 719 | [self layoutSubtreeIfNeeded]; 720 | 721 | if (button != nil) { 722 | if ([self.dataSource respondsToSelector:@selector(tabControl:canEditItem:)] && [self.dataSource tabControl:self canEditItem:item] == NO) { 723 | return; 724 | } 725 | 726 | LITabCell *cell = button.cell; 727 | NSRect titleRect = [cell editingRectForBounds:button.bounds]; 728 | 729 | self.editingField = [[NSTextField alloc] initWithFrame:titleRect]; 730 | 731 | self.editingField.editable = YES; 732 | self.editingField.font = cell.font; 733 | self.editingField.alignment = cell.alignment; 734 | self.editingField.backgroundColor = cell.backgroundColor; 735 | self.editingField.focusRingType = NSFocusRingTypeNone; 736 | 737 | self.editingField.textColor = [[NSColor darkGrayColor] blendedColorWithFraction:0.5 ofColor:[NSColor blackColor]]; 738 | 739 | NSTextFieldCell *textFieldCell = self.editingField.cell; 740 | 741 | [textFieldCell setBordered:NO]; 742 | [textFieldCell setScrollable:YES]; 743 | 744 | self.editingField.stringValue = button.title; 745 | 746 | [button addSubview:self.editingField]; 747 | 748 | self.editingField.delegate = self; 749 | [self.editingField selectText:self]; 750 | } 751 | } 752 | 753 | #pragma mark - 754 | #pragma mark NSTextFieldDelegate 755 | 756 | - (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor { 757 | BOOL ret = YES; 758 | if ([_delegate respondsToSelector:_cmd]) { 759 | ret = [_delegate control:self textShouldBeginEditing:fieldEditor]; 760 | } 761 | return ret; 762 | } 763 | 764 | - (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor { 765 | BOOL ret = YES; 766 | if ([_delegate respondsToSelector:_cmd]) { 767 | ret = [_delegate control:self textShouldEndEditing:fieldEditor]; 768 | } 769 | return ret; 770 | } 771 | 772 | - (BOOL)control:(NSControl *)control didFailToFormatString:(NSString *)string errorDescription:(NSString *)error { 773 | BOOL ret = YES; 774 | if ([_delegate respondsToSelector:_cmd]) { 775 | ret = [_delegate control:self didFailToFormatString:string errorDescription:error]; 776 | } 777 | return ret; 778 | } 779 | 780 | - (void)control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error { 781 | if ([_delegate respondsToSelector:_cmd]) { 782 | [_delegate control:self didFailToValidatePartialString:string errorDescription:error]; 783 | } 784 | } 785 | 786 | - (BOOL)control:(NSControl *)control isValidObject:(id)obj { 787 | BOOL ret = YES; 788 | if ([_delegate respondsToSelector:_cmd]) { 789 | ret = [_delegate control:self isValidObject:obj]; 790 | } 791 | return ret; 792 | } 793 | 794 | - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { 795 | BOOL ret = NO; 796 | if ([_delegate respondsToSelector:_cmd]) { 797 | ret = [_delegate control:self textView:textView doCommandBySelector:commandSelector]; 798 | } 799 | return ret; 800 | } 801 | 802 | - (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index { 803 | NSArray *ret = nil; 804 | if ([_delegate respondsToSelector:_cmd]) { 805 | 806 | } 807 | return ret; 808 | } 809 | 810 | - (void)controlTextDidChange:(NSNotification *)obj { 811 | [[NSNotificationCenter defaultCenter] postNotificationName:obj.name object:self userInfo:obj.userInfo]; 812 | } 813 | 814 | - (void)controlTextDidBeginEditing:(NSNotification *)obj { 815 | [[NSNotificationCenter defaultCenter] postNotificationName:obj.name object:self userInfo:obj.userInfo]; 816 | } 817 | 818 | - (void)controlTextDidEndEditing:(NSNotification *)obj { 819 | NSString *title = self.editingField.stringValue; 820 | NSButton *button = (id)[self.editingField superview]; 821 | 822 | self.editingField.delegate = nil; 823 | [self.editingField removeFromSuperview]; 824 | self.editingField = nil; 825 | 826 | if (title.length > 0) { 827 | [button setTitle:title]; 828 | 829 | [self.dataSource tabControl:self setTitle:title forItem:[button.cell representedObject]]; 830 | } 831 | 832 | [[NSNotificationCenter defaultCenter] postNotificationName:obj.name object:self userInfo:obj.userInfo]; 833 | } 834 | 835 | #pragma mark - 836 | #pragma mark Drawing 837 | 838 | - (BOOL)isOpaque { 839 | return YES; 840 | } 841 | - (BOOL)isFlipped { 842 | return YES; 843 | } 844 | 845 | #pragma mark - 846 | #pragma mark State Restoration 847 | 848 | // NOTE: to enable state restoration, be sure to either assign an identifier to 849 | // the LITabControl instance within IB or, if the control is created programmatically, 850 | // prior to adding it to your window's view hierarchy. 851 | 852 | #define kScrollXOffsetKey @"scrollOrigin" 853 | #define kSelectedButtonIndexKey @"selectedButtonIndex" 854 | 855 | - (void)encodeRestorableStateWithCoder:(NSCoder *)coder { 856 | [super encodeRestorableStateWithCoder:coder]; 857 | 858 | CGFloat scrollXOffset = 0; 859 | NSUInteger selectedButtonIndex = NSNotFound; 860 | 861 | scrollXOffset = self.scrollView.contentView.bounds.origin.x; 862 | 863 | NSUInteger index = 0; 864 | for (NSButton *button in [self.scrollView.documentView subviews]) { 865 | if (button.state == 1) { 866 | selectedButtonIndex = index; 867 | break; 868 | } 869 | index += 1; 870 | } 871 | 872 | [coder encodeDouble:scrollXOffset forKey:kScrollXOffsetKey]; 873 | [coder encodeInteger:selectedButtonIndex forKey:kSelectedButtonIndexKey]; 874 | } 875 | 876 | - (void)restoreStateWithCoder:(NSCoder *)coder { 877 | [super restoreStateWithCoder:coder]; 878 | 879 | CGFloat scrollXOffset = [coder decodeDoubleForKey:kScrollXOffsetKey]; 880 | NSUInteger selectedButtonIndex = [coder decodeIntegerForKey:kSelectedButtonIndexKey]; 881 | 882 | NSRect bounds = self.scrollView.contentView.bounds; bounds.origin.x = scrollXOffset; 883 | self.scrollView.contentView.bounds = bounds; 884 | 885 | NSUInteger index = 0; 886 | for (NSButton *button in [self.scrollView.documentView subviews]) { 887 | if (index == selectedButtonIndex) { 888 | [button setState:1]; 889 | } else { 890 | [button setState:0]; 891 | } 892 | index += 1; 893 | } 894 | } 895 | 896 | @end 897 | 898 | NSString *LITabControlSelectionDidChangeNotification = @"LITabControlSelectionDidChangeNotification"; 899 | -------------------------------------------------------------------------------- /LITabControl/Classes/LITabLeftTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monyschuk/LITabControl/1ca31290f8b7be36623e675c54234913c34c41c8/LITabControl/Classes/LITabLeftTemplate.pdf -------------------------------------------------------------------------------- /LITabControl/Classes/LITabPlusTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monyschuk/LITabControl/1ca31290f8b7be36623e675c54234913c34c41c8/LITabControl/Classes/LITabPlusTemplate.pdf -------------------------------------------------------------------------------- /LITabControl/Classes/LITabRightTemplate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monyschuk/LITabControl/1ca31290f8b7be36623e675c54234913c34c41c8/LITabControl/Classes/LITabRightTemplate.pdf -------------------------------------------------------------------------------- /LITabControl/Classes/NSImage+LITabControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+LITabControl.h 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/13/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSImage (LITabControl) 12 | 13 | - (NSImage *)imageWithTint:(NSColor *)color; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /LITabControl/Classes/NSImage+LITabControl.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+LITabControl.m 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/13/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import "NSImage+LITabControl.h" 10 | 11 | @implementation NSImage (LITabControl) 12 | 13 | - (NSImage *)imageWithTint:(NSColor *)color { 14 | NSRect imageRect = NSZeroRect; imageRect.size = self.size; 15 | NSImage *highlightImage = [[NSImage alloc] initWithSize:imageRect.size]; 16 | 17 | [highlightImage lockFocus]; 18 | 19 | [self drawInRect:imageRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0]; 20 | 21 | [color set]; 22 | NSRectFillUsingOperation(imageRect, NSCompositeSourceAtop); 23 | 24 | [highlightImage unlockFocus]; 25 | 26 | return highlightImage; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /LITabControl/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /LITabControl/LIAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // LIAppDelegate.h 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/12/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "LITabControl.h" 11 | 12 | @interface LIAppDelegate : NSObject 13 | 14 | @property (assign) IBOutlet NSWindow *window; 15 | @property (assign) IBOutlet LITabControl *tabControl; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /LITabControl/LIAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // LIAppDelegate.m 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/12/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import "LIAppDelegate.h" 10 | 11 | @implementation LIAppDelegate { 12 | NSArray *tabs; 13 | NSMenu *menu; 14 | 15 | } 16 | 17 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 18 | { 19 | [self.window setBackgroundColor:[NSColor whiteColor]]; 20 | 21 | menu = [[NSMenu alloc] init]; 22 | 23 | [menu addItem:[NSMenuItem separatorItem]]; 24 | [[menu addItemWithTitle:NSLocalizedString(@"Delete Sheet...", nil) action:@selector(deleteSheet:) keyEquivalent:@""] setTarget:self]; 25 | 26 | tabs = @[@"Sheet 1", @"Sheet 2", @"Sheet 3", @"Sheet 4", @"Sheet 5"]; 27 | [self.tabControl setDataSource:self]; 28 | } 29 | 30 | - (IBAction)deleteSheet:(id)sender { 31 | NSLog(@"%s", __PRETTY_FUNCTION__); 32 | } 33 | 34 | #pragma mark - 35 | #pragma mark LITabControlDataSource 36 | 37 | - (NSUInteger)tabControlNumberOfTabs:(LITabControl *)tabControl { 38 | return tabs.count; 39 | } 40 | 41 | - (id)tabControl:(LITabControl *)tabControl itemAtIndex:(NSUInteger)index { 42 | return tabs[index]; 43 | } 44 | 45 | - (NSMenu *)tabControl:(LITabControl *)tabControl menuForItem:(id)item { 46 | return menu; 47 | } 48 | 49 | - (NSString *)tabControl:(LITabControl *)tabControl titleForItem:(id)item { 50 | return item; 51 | } 52 | - (void)tabControl:(LITabControl *)tabControl setTitle:(NSString *)title forItem:(id)item { 53 | } 54 | 55 | - (BOOL)tabControl:(LITabControl *)tabControl canReorderItem:(id)item { 56 | return YES; 57 | } 58 | 59 | - (void)tabControlDidReorderItems:(LITabControl *)tabControl orderedItems:(NSArray *)itemArray { 60 | tabs = itemArray; 61 | } 62 | 63 | - (void)tabControlDidChangeSelection:(NSNotification *)notification { 64 | NSLog(@"%s (selected item = %@)", __PRETTY_FUNCTION__, self.tabControl.selectedItem); 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /LITabControl/LITabControl-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.MarkOnyschuk.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2013 Mark Onyschuk. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /LITabControl/LITabControl-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #endif 10 | -------------------------------------------------------------------------------- /LITabControl/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /LITabControl/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /LITabControl/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // LITabControl 4 | // 5 | // Created by Mark Onyschuk on 11/12/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) 12 | { 13 | return NSApplicationMain(argc, argv); 14 | } 15 | -------------------------------------------------------------------------------- /LITabControlTests/LITabControlTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.MarkOnyschuk.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LITabControlTests/LITabControlTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LITabControlTests.m 3 | // LITabControlTests 4 | // 5 | // Created by Mark Onyschuk on 11/12/2013. 6 | // Copyright (c) 2013 Mark Onyschuk. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LITabControlTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation LITabControlTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /LITabControlTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LITabControl 2 | ============ 3 | 4 | An auto-layout based multi-tab control designed to look and behave like the tab control in Apple's Numbers spreadsheet. 5 | 6 | Features 7 | -------- 8 | 9 | LITabControl distinguishes itself from other opensource tab systems with its support for Numbers-style behaviors. It supports arbitrary drop-down menus associated with each tab, an animated scrolling tab area, and animated tab reordering. 10 | 11 | Classes 12 | ------- 13 | 14 | The LITabControl project includes 2 classes: 15 | 16 | 1. **LITabControl** - an NSControl subclass used to display tabs. 17 | 18 | 2. **LITabCell** - an NSButtonCell subclass used to draw the tab area and to draw both text and images in Numbers-style. 19 | 20 | Using LITabControl 21 | ------------------ 22 | 23 | To learn how to use LITabControl, run the LITabControl project in Xcode. LIAppDelegate implements the LITabControlDataSource protocol to display a series of tabs across the top of its associated window.To Add the control to your own project, copy or link assets and classes located in the Classes folder to your project. 24 | 25 | To respond to tab clicks - set LITabControl's target and action properties, or listen for LITabControlSelectionDidChangeNotification notifications. 26 | 27 | To add new tabs, set LITabControl's addTarget and addAction properties. Your method used to add a tab should behave like this: 28 | 29 | ``` 30 | - (IBAction)addTab:(id)sender { 31 | // .. add some model object representing your tab 32 | [sender reloadData]; 33 | } 34 | ``` 35 | 36 | RECENT CHANGES 37 | -------------- 38 | 39 | * 12/28/13 40 | * Tab title editing, double click to edit. 41 | * Fixed vertical text offset in dragged tabs. 42 | * 12/29/13 43 | * Fixed flicker during tab reordering, and bug in reordering first and last tabs. 44 | * 12/30/13 45 | * Replaced static tab menus with dynamic menus, as per original intention 46 | * 02/09/14 47 | * Numerous changes to facilitate subclassing 48 | * Reordered tab views to render front-to-back left to right rather than right to left (eg. tabScroller.subviews.lastObject is leftmost while tabScroller.subviews.firstObject is rightmost). This change in ordering allows subclasses to partially occlude tabs in a visually sensible way. 49 | 50 | LICENSE & NOTES 51 | --------------- 52 | 53 | LITabControl is licensed under the MIT license and hosted on GitHub at https://github.com/monyschuk/LITabControl/ Fork the project and feel free to send pull requests with your changes! 54 | 55 | 56 | --------------------------------------------------------------------------------