├── .gitattributes ├── ClassLibrary ├── ClassLibrary.csproj ├── Config.cs ├── P4Command.cs ├── SolutionConfigForm.Designer.cs ├── SolutionConfigForm.cs └── SolutionConfigForm.resx ├── LICENSE.txt ├── P4SimpleScc.sln ├── P4SimpleScc_2019 ├── AsyncPackageHelpers │ ├── AsyncPackageRegistrationAttribute.cs │ ├── ExtensionMethods.cs │ └── ProvideAutoLoadAttribute.cs ├── P4SimpleScc_2019.csproj ├── Properties │ └── AssemblyInfo.cs └── source.extension.vsixmanifest ├── P4SimpleScc_2022 ├── AsyncPackageHelpers │ ├── AsyncPackageRegistrationAttribute.cs │ ├── ExtensionMethods.cs │ └── ProvideAutoLoadAttribute.cs ├── P4SimpleScc_2022.csproj ├── Properties │ └── AssemblyInfo.cs └── source.extension.vsixmanifest ├── README.md ├── ReadMe └── ReadMe.txt ├── SharedFiles ├── P4SimpleSccPackage.vsct └── Resources │ └── Command.png ├── SharedProject ├── ActionProviderFactory.cs ├── FileContextProviderFactory.cs ├── Guids.cs ├── ProvideSolutionProps.cs ├── ProvideSourceControlProvider.cs ├── SccProvider.cs ├── SccProviderService.cs ├── SharedProject.projitems └── SharedProject.shproj └── img ├── CheckOutDocument.png ├── CheckOutFile.png ├── FileCheckedOut.png ├── OutputWindow.png ├── SolutionConfigurationDialog.png ├── SolutionConfigurationMenu.png └── Tools_Options_SourceControl.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /ClassLibrary/ClassLibrary.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF} 8 | Library 9 | ClassLibrary 10 | ClassLibrary 11 | v4.7.2 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Form 55 | 56 | 57 | SolutionConfigForm.cs 58 | 59 | 60 | 61 | 62 | SolutionConfigForm.cs 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /ClassLibrary/Config.cs: -------------------------------------------------------------------------------- 1 |  2 | // 3 | // Copyright 2022 - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | using System.IO; 11 | using System.Drawing; 12 | 13 | namespace ClassLibrary 14 | { 15 | public static class Config 16 | { 17 | public enum KEY // everything is 'int' unless otherwise noted 18 | { 19 | SolutionConfigType, 20 | SolutionConfigCheckOutOnEdit, // bool 21 | SolutionConfigPromptForCheckout, // bool 22 | SolutionConfigDialogPosX, 23 | SolutionConfigDialogPosY, 24 | SolutionConfigDialogP4Port, // string 25 | SolutionConfigDialogP4User, // string 26 | SolutionConfigDialogP4Client, // string 27 | SolutionConfigVerboseOutput, // bool 28 | SolutionConfigOutputEnabled, // bool 29 | }; 30 | 31 | private static Dictionary ConfigDictionary = new Dictionary(); 32 | 33 | public static void Load(string InputConfig) 34 | { 35 | ConfigDictionary = new Dictionary(); 36 | 37 | string line; 38 | 39 | try 40 | { 41 | byte[] buffer = new byte[InputConfig.Length]; 42 | System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); 43 | encoding.GetBytes(InputConfig, 0, InputConfig.Length, buffer, 0); 44 | 45 | MemoryStream memory_stream = new MemoryStream(buffer); 46 | 47 | using (StreamReader sr = new StreamReader(memory_stream)) 48 | { 49 | while (sr.Peek() >= 0) 50 | { 51 | line = sr.ReadLine().Trim(); 52 | 53 | if (line.Length > 0) // not a blank line? 54 | { 55 | int pos = line.IndexOf("="); 56 | 57 | if ((pos > 0) && (pos < (line.Length-1))) // '=' must not be the first or last character of the line 58 | { 59 | string key = line.Substring(0, pos); 60 | string value = line.Substring(pos+1, (line.Length-pos)-1); 61 | 62 | ConfigDictionary.Add(key, value); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | catch(Exception) 69 | { 70 | } 71 | } 72 | 73 | public static void Save(out string OutputString) 74 | { 75 | OutputString = ""; 76 | 77 | try 78 | { 79 | foreach (KEY key in Enum.GetValues(typeof(KEY))) 80 | { 81 | string key_string = key.ToString(); 82 | if (ConfigDictionary.ContainsKey(key_string)) 83 | { 84 | OutputString += string.Format("{0}={1}", key_string, ConfigDictionary[key_string]) + "\n"; 85 | } 86 | } 87 | } 88 | catch(Exception) 89 | { 90 | } 91 | } 92 | 93 | public static bool Get(KEY key, ref string value) 94 | { 95 | string key_string = key.ToString(); 96 | if (ConfigDictionary.ContainsKey(key_string)) 97 | { 98 | value = ConfigDictionary[key_string]; 99 | return true; 100 | } 101 | return false; 102 | } 103 | 104 | public static void Set(KEY key, string value) 105 | { 106 | string key_string = key.ToString(); 107 | 108 | if (ConfigDictionary.ContainsKey(key_string)) // if the key/value pair exists... 109 | { 110 | ConfigDictionary.Remove(key_string); // ...remove the old key/value pair 111 | } 112 | 113 | ConfigDictionary.Add(key_string, value); // add the key/value pair to the dictionary 114 | } 115 | 116 | 117 | public static bool Get(KEY key, ref int value) 118 | { 119 | string key_string = key.ToString(); 120 | if (ConfigDictionary.ContainsKey(key_string)) 121 | { 122 | value = 0; 123 | Int32.TryParse(ConfigDictionary[key_string], out value); 124 | return true; 125 | } 126 | return false; 127 | } 128 | 129 | public static void Set(KEY key, int value) 130 | { 131 | Set(key, value.ToString()); 132 | } 133 | 134 | 135 | public static bool Get(KEY key, ref bool value) 136 | { 137 | string key_string = key.ToString(); 138 | if (ConfigDictionary.ContainsKey(key_string)) 139 | { 140 | value = ConfigDictionary[key_string] == "True"; 141 | return true; 142 | } 143 | return false; 144 | } 145 | 146 | public static void Set(KEY key, bool value) 147 | { 148 | Set(key, value.ToString()); // set to 'False' or 'True' 149 | } 150 | 151 | 152 | public static bool Get(KEY key, ref List value) 153 | { 154 | value = new List(); 155 | 156 | int count = 1; 157 | bool found = false; 158 | do 159 | { 160 | string key_string = string.Format("{0}{1}", key.ToString(), count); 161 | found = ConfigDictionary.ContainsKey(key_string); 162 | if (found) 163 | { 164 | value.Add(ConfigDictionary[key_string]); 165 | } 166 | count++; 167 | } while(found); 168 | 169 | return (value.Count() > 0); 170 | } 171 | 172 | public static void Set(KEY key, List StringList) 173 | { 174 | int count = 1; 175 | bool found = false; 176 | // remove ALL old key/value pairs from the dictionary (since the list parameter passed in can be different size than what's currently in dictionary) 177 | do 178 | { 179 | string key_string = string.Format("{0}{1}", key.ToString(), count); 180 | found = ConfigDictionary.ContainsKey(key_string); 181 | if (found) 182 | { 183 | ConfigDictionary.Remove(key_string); 184 | } 185 | count++; 186 | } while(found); 187 | 188 | // add the new key/value pairs to the dictionary 189 | for (int index = 0; index < StringList.Count; index++) 190 | { 191 | string key_string = string.Format("{0}{1}", key.ToString(), index+1); 192 | ConfigDictionary.Add(key_string, StringList[index]); 193 | } 194 | } 195 | 196 | 197 | public static bool Get(KEY key, ref List value) 198 | { 199 | value = new List(); 200 | 201 | List StringList = new List(); 202 | 203 | Get(key, ref StringList); 204 | 205 | for(int index = 0; index < StringList.Count; index++) 206 | { 207 | value.Add(Convert.ToInt32(StringList[index])); 208 | } 209 | 210 | return (value.Count() > 0); 211 | } 212 | 213 | public static void Set(KEY key, List IntList) 214 | { 215 | List StringList = new List(); 216 | 217 | for(int index = 0; index < IntList.Count; index++) 218 | { 219 | StringList.Add(IntList[index].ToString()); 220 | } 221 | 222 | Set(key, StringList); 223 | } 224 | 225 | 226 | public static bool Get(KEY key, ref Font value) 227 | { 228 | string FontString = ""; 229 | if (Get(key, ref FontString)) 230 | { 231 | string[] fields = FontString.Split(','); 232 | 233 | if (fields.Length == 4) 234 | { 235 | if ((fields[0].Substring(0,1) == "\"") && (fields[0].Substring(fields[0].Length - 1,1) == "\"")) 236 | { 237 | fields[0] = fields[0].Substring(1, fields[0].Length - 2); // chop off the leading and trailing double quote 238 | } 239 | 240 | FontFamily font_family = new FontFamily(fields[0]); 241 | 242 | float size = float.Parse(fields[1]); 243 | int int_style = int.Parse(fields[2]); 244 | 245 | FontStyle style = FontStyle.Regular; 246 | if ((int_style & 1) != 0) 247 | { 248 | style = style | FontStyle.Bold; 249 | } 250 | if ((int_style & 2) != 0) 251 | { 252 | style = style | FontStyle.Italic; 253 | } 254 | 255 | GraphicsUnit graphics_unit = (GraphicsUnit) Enum.Parse(typeof(GraphicsUnit), fields[3]); 256 | 257 | value = new Font(font_family, size, style, graphics_unit); 258 | return true; 259 | } 260 | } 261 | 262 | return false; 263 | } 264 | 265 | public static void Set(KEY key, Font value) 266 | { 267 | int Style = 0; // Regular 268 | if (value.Style.HasFlag(FontStyle.Bold)) 269 | { 270 | Style |= 1; 271 | } 272 | if (value.Style.HasFlag(FontStyle.Italic)) 273 | { 274 | Style |= 2; 275 | } 276 | 277 | int GraphicsUnit = (int)value.Unit; 278 | 279 | Set(key, String.Format("\"{0}\",{1},{2},{3}", value.FontFamily.Name, value.SizeInPoints, Style, GraphicsUnit)); 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /ClassLibrary/P4Command.cs: -------------------------------------------------------------------------------- 1 |  2 | // 3 | // Copyright 2022 - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | using System.Text; 8 | 9 | using System.Diagnostics; 10 | using System.Threading; 11 | using System.IO; 12 | 13 | namespace ClassLibrary 14 | { 15 | public class P4Command 16 | { 17 | private static string port; 18 | private static string user; 19 | private static string workspace; 20 | 21 | private StringBuilder stdoutBuilder; 22 | private StringBuilder stderrBuilder; 23 | private StringBuilder verboseBuilder; 24 | 25 | private bool StdOutDone; // wait until OnOutputDataReceived receives e.Data == null to know that stdout has terminated 26 | private bool StdErrDone; // wait until OnErrorDataReceived receives e.Data == null to know that stderr has terminated 27 | 28 | public enum CheckOutStatus 29 | { 30 | FileCheckedOut, // the file was successfully checked out of source control 31 | FileAlreadyCheckedOut, // the file was already checked out of source control 32 | FileNotInSourceControl, // the file does not exist in source control 33 | ErrorCheckingOutFile, // the file could not be checked out of source control 34 | } 35 | 36 | public P4Command() 37 | { 38 | } 39 | 40 | public static void SetEnv(string in_port, string in_user, string in_workspace) // set the default port, user and workspace 41 | { 42 | port = in_port; 43 | user = in_user; 44 | workspace = in_workspace; 45 | } 46 | 47 | public void Run(string command, out string stdout, out string stderr, out string verbose) // use the current port, user and workspace for the command 48 | { 49 | Run(command, port, user, workspace, out stdout, out stderr, out verbose); 50 | } 51 | 52 | public void Run(string command, string in_port, string in_user, string in_workspace, out string stdout, out string stderr, out string verbose) // use the specified port, user and workspace for the command 53 | { 54 | stdoutBuilder = new StringBuilder(); 55 | stderrBuilder = new StringBuilder(); 56 | verboseBuilder = new StringBuilder(); 57 | 58 | bool bTimedOut = false; 59 | 60 | try 61 | { 62 | Process proc = new Process(); 63 | 64 | StdOutDone = false; 65 | StdErrDone = false; 66 | 67 | ProcessStartInfo startInfo = new ProcessStartInfo("cmd.exe"); 68 | 69 | string arguments = "p4.exe"; 70 | 71 | if (in_port != null && in_port.Length > 0) 72 | { 73 | arguments += String.Format(" -p {0}", in_port); 74 | } 75 | 76 | if (in_user != null && in_user.Length > 0) 77 | { 78 | arguments += String.Format(" -u {0}", in_user); 79 | } 80 | 81 | if (in_workspace != null && in_workspace.Length > 0) 82 | { 83 | arguments += String.Format(" -c {0}", in_workspace); 84 | } 85 | 86 | startInfo.UseShellExecute = false; 87 | startInfo.CreateNoWindow = true; 88 | 89 | startInfo.RedirectStandardInput = false; 90 | startInfo.RedirectStandardOutput = true; 91 | startInfo.RedirectStandardError = true; 92 | 93 | startInfo.Arguments = "/C " + arguments + " " + command; 94 | verboseBuilder.Append("command: " + arguments + " " + command + "\n"); 95 | 96 | proc.StartInfo = startInfo; 97 | proc.EnableRaisingEvents = true; 98 | 99 | proc.OutputDataReceived += OnOutputDataReceived; 100 | proc.ErrorDataReceived += OnErrorDataReceived; 101 | 102 | proc.Start(); 103 | proc.BeginOutputReadLine(); 104 | proc.BeginErrorReadLine(); 105 | 106 | if (!proc.WaitForExit(5 * 60 * 1000)) // wait with a 5 minute timeout (in milliseconds) 107 | { 108 | bTimedOut = true; 109 | proc.Kill(); 110 | } 111 | 112 | int output_timeout = 100; 113 | while (!StdOutDone || !StdErrDone) // wait until the output and error streams have been flushed (or a 1 second (1000 ms) timeout is reached) 114 | { 115 | Thread.Sleep(10); 116 | 117 | if (--output_timeout == 0) 118 | { 119 | break; 120 | } 121 | } 122 | 123 | proc.Close(); 124 | } 125 | catch (Exception ex) 126 | { 127 | string message = String.Format("P4Command.Run exception: {0}", ex.Message); 128 | System.Console.WriteLine(message); 129 | } 130 | 131 | stdout = stdoutBuilder.ToString(); 132 | stderr = stderrBuilder.ToString(); 133 | 134 | verboseBuilder.Append("response: " + stdout); 135 | if (!stdout.EndsWith("\n")) 136 | { 137 | verboseBuilder.Append("\n"); 138 | } 139 | 140 | verboseBuilder.Append("error: " + stderr); 141 | if (!stderr.EndsWith("\n")) 142 | { 143 | verboseBuilder.Append("\n"); 144 | } 145 | 146 | verbose = verboseBuilder.ToString(); 147 | 148 | if (bTimedOut) // did the P4 command take too long and time out? 149 | { 150 | stderr = "Perforce command timed out after waiting for 5 minutes. Check your Solution Configuration settings to make sure they are correct. You may want to try to manually check out or add the file in P4V.\n"; 151 | } 152 | } 153 | 154 | private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) 155 | { 156 | if (e.Data != null) 157 | { 158 | stdoutBuilder.AppendLine(e.Data); 159 | } 160 | else 161 | { 162 | StdOutDone = true; 163 | } 164 | } 165 | 166 | private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) 167 | { 168 | if (e.Data != null) 169 | { 170 | stderrBuilder.AppendLine(e.Data); 171 | } 172 | else 173 | { 174 | StdErrDone = true; 175 | } 176 | } 177 | 178 | private bool GetField(string input, string fieldname, out string fieldvalue) // scans input looking for field and returns value of field if found 179 | { 180 | fieldvalue = ""; 181 | 182 | if (fieldname == null || fieldname.Length == 0) 183 | { 184 | return false; 185 | } 186 | 187 | if (input == null || input.Length == 0) 188 | { 189 | return false; 190 | } 191 | 192 | string line; 193 | 194 | try 195 | { 196 | byte[] buffer = new byte[input.Length]; 197 | System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); 198 | encoding.GetBytes(input, 0, input.Length, buffer, 0); 199 | 200 | MemoryStream memory_stream = new MemoryStream(buffer); 201 | 202 | using (StreamReader sr = new StreamReader(memory_stream)) 203 | { 204 | while (sr.Peek() >= 0) 205 | { 206 | line = sr.ReadLine().Trim(); 207 | 208 | if (line.Length > 0) // not a blank line? 209 | { 210 | if (line[0] == '#') 211 | { 212 | continue; // skip comment lines 213 | } 214 | 215 | int pos = line.IndexOf(fieldname); 216 | 217 | if (pos == 0) // found a match? 218 | { 219 | fieldvalue = line.Substring(fieldname.Length, line.Length - fieldname.Length).Trim(); 220 | 221 | return true; 222 | } 223 | } 224 | } 225 | } 226 | } 227 | catch(Exception) 228 | { 229 | } 230 | 231 | return false; 232 | } 233 | 234 | public void RunP4Set(string solutionDirectory, out string P4Port, out string P4User, out string P4Client, out string verbose) 235 | { 236 | string P4Config; 237 | RunP4Set(solutionDirectory, out P4Port, out P4User, out P4Client, out P4Config, out verbose); 238 | } 239 | 240 | 241 | public void RunP4Set(string solutionDirectory, out string P4Port, out string P4User, out string P4Client, out string P4Config, out string verbose) 242 | { 243 | P4Port = ""; 244 | P4User = ""; 245 | P4Client = ""; 246 | P4Config = ""; 247 | verbose = ""; 248 | 249 | if (solutionDirectory == null || solutionDirectory == "") 250 | { 251 | return; 252 | } 253 | 254 | SetEnv(P4Port, P4User, P4Client); 255 | 256 | // the "p4.exe set" command must be run from the solution directory (to get the proper settings from the .p4config file) 257 | string CurrentDirectory = Directory.GetCurrentDirectory(); // save the current directory 258 | Directory.SetCurrentDirectory(solutionDirectory); 259 | 260 | P4Command p4cmd = new ClassLibrary.P4Command(); 261 | 262 | p4cmd.Run("set", out string stdout, out string stderr, out verbose); // note: "p4 set" does not require a port, user, password or workspace. 263 | 264 | Directory.SetCurrentDirectory(CurrentDirectory); // restore the saved current directory 265 | 266 | if (stdout != null && stdout.Length > 0) 267 | { 268 | StringReader reader = new StringReader(stdout); 269 | string line; 270 | while ((line = reader.ReadLine()) != null) 271 | { 272 | if (line.Contains("P4PORT=")) 273 | { 274 | int pos = line.IndexOf(" ("); 275 | if (pos >= 0) // if the line contains " (" then strip it and the following text 276 | { 277 | line = line.Remove(pos).Trim(); 278 | } 279 | 280 | P4Port = line.Substring(7); 281 | } 282 | else if (line.Contains("P4USER=")) 283 | { 284 | int pos = line.IndexOf(" ("); 285 | if (pos >= 0) // if the line contains " (" then strip it and the following text 286 | { 287 | line = line.Remove(pos).Trim(); 288 | } 289 | 290 | P4User = line.Substring(7); 291 | } 292 | else if (line.Contains("P4CLIENT=")) 293 | { 294 | int pos = line.IndexOf(" ("); 295 | if (pos >= 0) // if the line contains " (" then strip it and the following text 296 | { 297 | line = line.Remove(pos).Trim(); 298 | } 299 | 300 | P4Client = line.Substring(9); 301 | } 302 | else if (line.Contains("P4CONFIG=")) 303 | { 304 | int pos = line.IndexOf(" ("); 305 | if (pos >= 0) // if the line contains " (" then strip it and the following text 306 | { 307 | line = line.Remove(pos).Trim(); 308 | } 309 | 310 | P4Config = line.Substring(9); 311 | } 312 | } 313 | } 314 | } 315 | 316 | public void ServerConnect(out string stdout, out string stderr, out string verbose, out bool bIsNotAllWrite) 317 | { 318 | verbose = ""; 319 | bIsNotAllWrite = true; 320 | 321 | Run("info -s", port, "", "", out stdout, out stderr, out string info_verbose); // 'info' needs to run on the specified server (to make sure the server is valid) 322 | verbose += info_verbose; 323 | 324 | if (stderr != null && stderr.Length > 0) 325 | { 326 | return; 327 | } 328 | 329 | string command = String.Format("users {0}", user); 330 | Run(command, port, user, "", out stdout, out stderr, out string users_verbose); // get the user information from the specified server (to make sure the user is valid) 331 | verbose += users_verbose; 332 | 333 | if (stderr != null && stderr.Length > 0) 334 | { 335 | return; 336 | } 337 | 338 | command = String.Format("client -o {0}", workspace); // get the user's workspace from the specified server and then validate it 339 | Run(command, port, user, "", out stdout, out stderr, out string client_verbose); 340 | verbose += client_verbose; 341 | 342 | if (stderr != null && stderr.Length > 0) 343 | { 344 | return; 345 | } 346 | 347 | // Parse output looking for "Access:" and "Root:" to validate the workspace settings on this machine 348 | string Access = ""; 349 | if (!GetField(stdout, "Access:", out Access)) 350 | { 351 | stderr = String.Format("P4CLIENT workspace '{0}' is not valid on server.\n", workspace); 352 | return; 353 | } 354 | 355 | string Root = ""; 356 | if (!GetField(stdout, "Root:", out Root)) 357 | { 358 | stderr = String.Format("P4CLIENT workspace '{0}' is not valid on server.\n", workspace); 359 | return; 360 | } 361 | 362 | string Options = ""; 363 | if (!GetField(stdout, "Options:", out Options)) 364 | { 365 | stderr = String.Format("P4CLIENT workspace '{0}' is not valid on server.\n", workspace); 366 | return; 367 | } 368 | 369 | bIsNotAllWrite = Options.Contains("noallwrite") ? true : false; 370 | 371 | // check if Root directory exists on this machine 372 | if (!Directory.Exists(Root)) 373 | { 374 | stderr = String.Format("workspace Root: folder '{0}' does not exist on this machine.\n", Root); 375 | return; 376 | } 377 | } 378 | 379 | public bool IsCheckedOut(string Filename, out string stdout, out string stderr, out string verbose) 380 | { 381 | // see if the file is checked out (for edit, not for integrate) 382 | string command = String.Format("fstat -T \"action\" \"{0}\"", Filename); 383 | Run(command, out stdout, out stderr, out verbose); 384 | 385 | if (stderr == null || stderr.Length == 0) // ignore errors here, if no error, check if file is open for edit 386 | { 387 | string action = ""; 388 | GetField(stdout, "... action", out action); 389 | 390 | if (action == "edit") // if already open for edit then we don't need to do anything 391 | { 392 | return true; 393 | } 394 | else if (action == "add") // if open for add then we don't need to do anything (GitHub issue #4) 395 | { 396 | return true; 397 | } 398 | } 399 | 400 | return false; 401 | } 402 | 403 | public CheckOutStatus CheckOutFile(string Filename, out string stdout, out string stderr, out string verbose) 404 | { 405 | verbose = ""; 406 | 407 | // see if the file exists in source control 408 | string command = String.Format("fstat -T \"clientFile\" \"{0}\"", Filename); 409 | Run(command, out stdout, out stderr, out string clientFile_verbose); 410 | verbose += clientFile_verbose; 411 | 412 | if (stderr != null && stderr.Length > 0) 413 | { 414 | if (stderr.Contains("is not under client's root") || stderr.Contains("not in client view") || stderr.Contains("no such file")) // if file is outside client's workspace, or file does not exist in source control... 415 | { 416 | return CheckOutStatus.FileNotInSourceControl; 417 | } 418 | else 419 | { 420 | return CheckOutStatus.ErrorCheckingOutFile; 421 | } 422 | } 423 | 424 | // see if the file is already checked out 425 | command = String.Format("fstat -T \"action\" \"{0}\"", Filename); 426 | Run(command, out stdout, out stderr, out string action_verbose); 427 | verbose += action_verbose; 428 | 429 | if (stderr == null || stderr.Length == 0) // ignore errors here, if no error, check if file is open for edit 430 | { 431 | string action = ""; 432 | GetField(stdout, "... action", out action); 433 | 434 | if (action == "edit") // if already open for edit then we don't need to do anything 435 | { 436 | return CheckOutStatus.FileAlreadyCheckedOut; 437 | } 438 | } 439 | 440 | command = String.Format("edit \"{0}\"", Filename); 441 | Run(command, out stdout, out stderr, out string edit_verbose); 442 | verbose += edit_verbose; 443 | 444 | if (stderr != null && stderr.Length > 0) 445 | { 446 | return CheckOutStatus.ErrorCheckingOutFile; 447 | } 448 | 449 | return CheckOutStatus.FileCheckedOut; 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /ClassLibrary/SolutionConfigForm.Designer.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ClassLibrary 3 | { 4 | partial class SolutionConfigForm 5 | { 6 | /// 7 | /// Required designer variable. 8 | /// 9 | private System.ComponentModel.IContainer components = null; 10 | 11 | /// 12 | /// Clean up any resources being used. 13 | /// 14 | /// true if managed resources should be disposed; otherwise, false. 15 | protected override void Dispose(bool disposing) 16 | { 17 | if (disposing && (components != null)) 18 | { 19 | components.Dispose(); 20 | } 21 | base.Dispose(disposing); 22 | } 23 | 24 | #region Windows Form Designer generated code 25 | 26 | /// 27 | /// Required method for Designer support - do not modify 28 | /// the contents of this method with the code editor. 29 | /// 30 | private void InitializeComponent() 31 | { 32 | this.panel1 = new System.Windows.Forms.Panel(); 33 | this.textBoxP4Client = new System.Windows.Forms.TextBox(); 34 | this.textBoxP4User = new System.Windows.Forms.TextBox(); 35 | this.textBoxP4Port = new System.Windows.Forms.TextBox(); 36 | this.label7 = new System.Windows.Forms.Label(); 37 | this.label6 = new System.Windows.Forms.Label(); 38 | this.label5 = new System.Windows.Forms.Label(); 39 | this.label4 = new System.Windows.Forms.Label(); 40 | this.label3 = new System.Windows.Forms.Label(); 41 | this.label2 = new System.Windows.Forms.Label(); 42 | this.label1 = new System.Windows.Forms.Label(); 43 | this.radioButtonManual = new System.Windows.Forms.RadioButton(); 44 | this.radioButtonAutomatic = new System.Windows.Forms.RadioButton(); 45 | this.radioButtonDisabled = new System.Windows.Forms.RadioButton(); 46 | this.panel2 = new System.Windows.Forms.Panel(); 47 | this.radioButtonOnSave = new System.Windows.Forms.RadioButton(); 48 | this.radioButtonOnModify = new System.Windows.Forms.RadioButton(); 49 | this.buttonOK = new System.Windows.Forms.Button(); 50 | this.buttonCancel = new System.Windows.Forms.Button(); 51 | this.checkBoxPromptForCheckout = new System.Windows.Forms.CheckBox(); 52 | this.checkBoxVerboseOutput = new System.Windows.Forms.CheckBox(); 53 | this.checkBoxOutputEnabled = new System.Windows.Forms.CheckBox(); 54 | this.panel1.SuspendLayout(); 55 | this.panel2.SuspendLayout(); 56 | this.SuspendLayout(); 57 | // 58 | // panel1 59 | // 60 | this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; 61 | this.panel1.Controls.Add(this.textBoxP4Client); 62 | this.panel1.Controls.Add(this.textBoxP4User); 63 | this.panel1.Controls.Add(this.textBoxP4Port); 64 | this.panel1.Controls.Add(this.label7); 65 | this.panel1.Controls.Add(this.label6); 66 | this.panel1.Controls.Add(this.label5); 67 | this.panel1.Controls.Add(this.label4); 68 | this.panel1.Controls.Add(this.label3); 69 | this.panel1.Controls.Add(this.label2); 70 | this.panel1.Controls.Add(this.label1); 71 | this.panel1.Controls.Add(this.radioButtonManual); 72 | this.panel1.Controls.Add(this.radioButtonAutomatic); 73 | this.panel1.Controls.Add(this.radioButtonDisabled); 74 | this.panel1.Location = new System.Drawing.Point(12, 12); 75 | this.panel1.Name = "panel1"; 76 | this.panel1.Size = new System.Drawing.Size(506, 261); 77 | this.panel1.TabIndex = 0; 78 | // 79 | // textBoxP4Client 80 | // 81 | this.textBoxP4Client.Location = new System.Drawing.Point(76, 223); 82 | this.textBoxP4Client.Name = "textBoxP4Client"; 83 | this.textBoxP4Client.ReadOnly = true; 84 | this.textBoxP4Client.Size = new System.Drawing.Size(416, 20); 85 | this.textBoxP4Client.TabIndex = 5; 86 | // 87 | // textBoxP4User 88 | // 89 | this.textBoxP4User.Location = new System.Drawing.Point(76, 197); 90 | this.textBoxP4User.Name = "textBoxP4User"; 91 | this.textBoxP4User.ReadOnly = true; 92 | this.textBoxP4User.Size = new System.Drawing.Size(416, 20); 93 | this.textBoxP4User.TabIndex = 4; 94 | // 95 | // textBoxP4Port 96 | // 97 | this.textBoxP4Port.Location = new System.Drawing.Point(76, 171); 98 | this.textBoxP4Port.Name = "textBoxP4Port"; 99 | this.textBoxP4Port.ReadOnly = true; 100 | this.textBoxP4Port.Size = new System.Drawing.Size(416, 20); 101 | this.textBoxP4Port.TabIndex = 3; 102 | // 103 | // label7 104 | // 105 | this.label7.AutoSize = true; 106 | this.label7.Location = new System.Drawing.Point(9, 226); 107 | this.label7.Name = "label7"; 108 | this.label7.Size = new System.Drawing.Size(61, 13); 109 | this.label7.TabIndex = 12; 110 | this.label7.Text = "P4CLIENT:"; 111 | // 112 | // label6 113 | // 114 | this.label6.AutoSize = true; 115 | this.label6.Location = new System.Drawing.Point(9, 200); 116 | this.label6.Name = "label6"; 117 | this.label6.Size = new System.Drawing.Size(53, 13); 118 | this.label6.TabIndex = 11; 119 | this.label6.Text = "P4USER:"; 120 | // 121 | // label5 122 | // 123 | this.label5.AutoSize = true; 124 | this.label5.Location = new System.Drawing.Point(9, 174); 125 | this.label5.Name = "label5"; 126 | this.label5.Size = new System.Drawing.Size(53, 13); 127 | this.label5.TabIndex = 10; 128 | this.label5.Text = "P4PORT:"; 129 | // 130 | // label4 131 | // 132 | this.label4.AutoSize = true; 133 | this.label4.Location = new System.Drawing.Point(9, 140); 134 | this.label4.Name = "label4"; 135 | this.label4.Size = new System.Drawing.Size(306, 13); 136 | this.label4.TabIndex = 9; 137 | this.label4.Text = "Enter the P4PORT, P4USER and P4CLIENT settings manually."; 138 | // 139 | // label3 140 | // 141 | this.label3.AutoSize = true; 142 | this.label3.Location = new System.Drawing.Point(9, 94); 143 | this.label3.Name = "label3"; 144 | this.label3.Size = new System.Drawing.Size(382, 13); 145 | this.label3.TabIndex = 8; 146 | this.label3.Text = "environment, or from previous \'p4 set\' settings, or from the .p4config file setti" + 147 | "ngs."; 148 | // 149 | // label2 150 | // 151 | this.label2.AutoSize = true; 152 | this.label2.Location = new System.Drawing.Point(9, 78); 153 | this.label2.Name = "label2"; 154 | this.label2.Size = new System.Drawing.Size(394, 13); 155 | this.label2.TabIndex = 7; 156 | this.label2.Text = "Uses \'p4 set\' to get P4PORT, P4USER and P4CLIENT settings from the Windows"; 157 | // 158 | // label1 159 | // 160 | this.label1.AutoSize = true; 161 | this.label1.Location = new System.Drawing.Point(9, 32); 162 | this.label1.Name = "label1"; 163 | this.label1.Size = new System.Drawing.Size(184, 13); 164 | this.label1.TabIndex = 6; 165 | this.label1.Text = "Disable P4SimpleScc for this solution."; 166 | // 167 | // radioButtonManual 168 | // 169 | this.radioButtonManual.AutoSize = true; 170 | this.radioButtonManual.Location = new System.Drawing.Point(12, 120); 171 | this.radioButtonManual.Name = "radioButtonManual"; 172 | this.radioButtonManual.Size = new System.Drawing.Size(101, 17); 173 | this.radioButtonManual.TabIndex = 2; 174 | this.radioButtonManual.Text = "Manual Settings"; 175 | this.radioButtonManual.UseVisualStyleBackColor = true; 176 | this.radioButtonManual.CheckedChanged += new System.EventHandler(this.radioButtonManual_CheckedChanged); 177 | // 178 | // radioButtonAutomatic 179 | // 180 | this.radioButtonAutomatic.AutoSize = true; 181 | this.radioButtonAutomatic.Location = new System.Drawing.Point(12, 58); 182 | this.radioButtonAutomatic.Name = "radioButtonAutomatic"; 183 | this.radioButtonAutomatic.Size = new System.Drawing.Size(72, 17); 184 | this.radioButtonAutomatic.TabIndex = 1; 185 | this.radioButtonAutomatic.Text = "Automatic"; 186 | this.radioButtonAutomatic.UseVisualStyleBackColor = true; 187 | this.radioButtonAutomatic.CheckedChanged += new System.EventHandler(this.radioButtonAutomatic_CheckedChanged); 188 | // 189 | // radioButtonDisabled 190 | // 191 | this.radioButtonDisabled.AutoSize = true; 192 | this.radioButtonDisabled.Location = new System.Drawing.Point(12, 12); 193 | this.radioButtonDisabled.Name = "radioButtonDisabled"; 194 | this.radioButtonDisabled.Size = new System.Drawing.Size(66, 17); 195 | this.radioButtonDisabled.TabIndex = 0; 196 | this.radioButtonDisabled.TabStop = true; 197 | this.radioButtonDisabled.Text = "Disabled"; 198 | this.radioButtonDisabled.UseVisualStyleBackColor = true; 199 | this.radioButtonDisabled.CheckedChanged += new System.EventHandler(this.radioButtonDisabled_CheckedChanged); 200 | // 201 | // panel2 202 | // 203 | this.panel2.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; 204 | this.panel2.Controls.Add(this.radioButtonOnSave); 205 | this.panel2.Controls.Add(this.radioButtonOnModify); 206 | this.panel2.Location = new System.Drawing.Point(12, 288); 207 | this.panel2.Name = "panel2"; 208 | this.panel2.Size = new System.Drawing.Size(506, 66); 209 | this.panel2.TabIndex = 1; 210 | // 211 | // radioButtonOnSave 212 | // 213 | this.radioButtonOnSave.AutoSize = true; 214 | this.radioButtonOnSave.Location = new System.Drawing.Point(12, 35); 215 | this.radioButtonOnSave.Name = "radioButtonOnSave"; 216 | this.radioButtonOnSave.Size = new System.Drawing.Size(136, 17); 217 | this.radioButtonOnSave.TabIndex = 1; 218 | this.radioButtonOnSave.Text = "Check out files on save"; 219 | this.radioButtonOnSave.UseVisualStyleBackColor = true; 220 | this.radioButtonOnSave.CheckedChanged += new System.EventHandler(this.radioButtonOnSave_CheckedChanged); 221 | // 222 | // radioButtonOnModify 223 | // 224 | this.radioButtonOnModify.AutoSize = true; 225 | this.radioButtonOnModify.Location = new System.Drawing.Point(12, 12); 226 | this.radioButtonOnModify.Name = "radioButtonOnModify"; 227 | this.radioButtonOnModify.Size = new System.Drawing.Size(143, 17); 228 | this.radioButtonOnModify.TabIndex = 0; 229 | this.radioButtonOnModify.TabStop = true; 230 | this.radioButtonOnModify.Text = "Check out files on modify"; 231 | this.radioButtonOnModify.UseVisualStyleBackColor = true; 232 | this.radioButtonOnModify.CheckedChanged += new System.EventHandler(this.radioButtonOnModify_CheckedChanged); 233 | // 234 | // buttonOK 235 | // 236 | this.buttonOK.Location = new System.Drawing.Point(325, 422); 237 | this.buttonOK.Name = "buttonOK"; 238 | this.buttonOK.Size = new System.Drawing.Size(79, 29); 239 | this.buttonOK.TabIndex = 4; 240 | this.buttonOK.Text = "OK"; 241 | this.buttonOK.UseVisualStyleBackColor = true; 242 | this.buttonOK.Click += new System.EventHandler(this.buttonOK_Click); 243 | // 244 | // buttonCancel 245 | // 246 | this.buttonCancel.Location = new System.Drawing.Point(421, 422); 247 | this.buttonCancel.Name = "buttonCancel"; 248 | this.buttonCancel.Size = new System.Drawing.Size(79, 29); 249 | this.buttonCancel.TabIndex = 5; 250 | this.buttonCancel.Text = "Cancel"; 251 | this.buttonCancel.UseVisualStyleBackColor = true; 252 | this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); 253 | // 254 | // checkBoxPromptForCheckout 255 | // 256 | this.checkBoxPromptForCheckout.AutoSize = true; 257 | this.checkBoxPromptForCheckout.Location = new System.Drawing.Point(25, 369); 258 | this.checkBoxPromptForCheckout.Name = "checkBoxPromptForCheckout"; 259 | this.checkBoxPromptForCheckout.Size = new System.Drawing.Size(248, 17); 260 | this.checkBoxPromptForCheckout.TabIndex = 2; 261 | this.checkBoxPromptForCheckout.Text = "Prompt for permission before checking out files."; 262 | this.checkBoxPromptForCheckout.UseVisualStyleBackColor = true; 263 | this.checkBoxPromptForCheckout.CheckedChanged += new System.EventHandler(this.checkBoxPromptForCheckout_CheckedChanged); 264 | // 265 | // checkBoxVerboseOutput 266 | // 267 | this.checkBoxVerboseOutput.AutoSize = true; 268 | this.checkBoxVerboseOutput.Location = new System.Drawing.Point(144, 399); 269 | this.checkBoxVerboseOutput.Name = "checkBoxVerboseOutput"; 270 | this.checkBoxVerboseOutput.Size = new System.Drawing.Size(226, 17); 271 | this.checkBoxVerboseOutput.TabIndex = 3; 272 | this.checkBoxVerboseOutput.Text = "Verbose Output ( for debugging purposes )"; 273 | this.checkBoxVerboseOutput.UseVisualStyleBackColor = true; 274 | this.checkBoxVerboseOutput.CheckedChanged += new System.EventHandler(this.checkBoxVerboseOutput_CheckedChanged); 275 | // 276 | // checkBoxOutputEnabled 277 | // 278 | this.checkBoxOutputEnabled.AutoSize = true; 279 | this.checkBoxOutputEnabled.Location = new System.Drawing.Point(25, 399); 280 | this.checkBoxOutputEnabled.Name = "checkBoxOutputEnabled"; 281 | this.checkBoxOutputEnabled.Size = new System.Drawing.Size(100, 17); 282 | this.checkBoxOutputEnabled.TabIndex = 6; 283 | this.checkBoxOutputEnabled.Text = "Output Enabled"; 284 | this.checkBoxOutputEnabled.UseVisualStyleBackColor = true; 285 | this.checkBoxOutputEnabled.CheckedChanged += new System.EventHandler(this.checkBoxOutputEnabled_CheckedChanged); 286 | // 287 | // SolutionConfigForm 288 | // 289 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 290 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 291 | this.ClientSize = new System.Drawing.Size(531, 469); 292 | this.Controls.Add(this.checkBoxOutputEnabled); 293 | this.Controls.Add(this.checkBoxVerboseOutput); 294 | this.Controls.Add(this.checkBoxPromptForCheckout); 295 | this.Controls.Add(this.buttonCancel); 296 | this.Controls.Add(this.buttonOK); 297 | this.Controls.Add(this.panel2); 298 | this.Controls.Add(this.panel1); 299 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; 300 | this.MaximizeBox = false; 301 | this.MinimizeBox = false; 302 | this.Name = "SolutionConfigForm"; 303 | this.Text = "Solution Configuration"; 304 | this.Shown += new System.EventHandler(this.OnShown); 305 | this.Move += new System.EventHandler(this.OnMove); 306 | this.panel1.ResumeLayout(false); 307 | this.panel1.PerformLayout(); 308 | this.panel2.ResumeLayout(false); 309 | this.panel2.PerformLayout(); 310 | this.ResumeLayout(false); 311 | this.PerformLayout(); 312 | 313 | } 314 | 315 | #endregion 316 | 317 | private System.Windows.Forms.Panel panel1; 318 | private System.Windows.Forms.RadioButton radioButtonManual; 319 | private System.Windows.Forms.RadioButton radioButtonAutomatic; 320 | private System.Windows.Forms.RadioButton radioButtonDisabled; 321 | private System.Windows.Forms.Panel panel2; 322 | private System.Windows.Forms.RadioButton radioButtonOnSave; 323 | private System.Windows.Forms.RadioButton radioButtonOnModify; 324 | private System.Windows.Forms.Label label4; 325 | private System.Windows.Forms.Label label3; 326 | private System.Windows.Forms.Label label2; 327 | private System.Windows.Forms.Label label1; 328 | private System.Windows.Forms.TextBox textBoxP4Client; 329 | private System.Windows.Forms.TextBox textBoxP4User; 330 | private System.Windows.Forms.TextBox textBoxP4Port; 331 | private System.Windows.Forms.Label label7; 332 | private System.Windows.Forms.Label label6; 333 | private System.Windows.Forms.Label label5; 334 | private System.Windows.Forms.Button buttonOK; 335 | private System.Windows.Forms.Button buttonCancel; 336 | private System.Windows.Forms.CheckBox checkBoxPromptForCheckout; 337 | private System.Windows.Forms.CheckBox checkBoxVerboseOutput; 338 | private System.Windows.Forms.CheckBox checkBoxOutputEnabled; 339 | } 340 | } -------------------------------------------------------------------------------- /ClassLibrary/SolutionConfigForm.cs: -------------------------------------------------------------------------------- 1 |  2 | // 3 | // Copyright 2022 - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | using System.Drawing; 8 | using System.Windows.Forms; 9 | 10 | namespace ClassLibrary 11 | { 12 | public partial class SolutionConfigForm : Form 13 | { 14 | private bool bWindowInitComplete; // set when form window is done initializing 15 | 16 | public int PosX = -1; 17 | public int PosY = -1; 18 | 19 | private string solutionDirectory = ""; 20 | 21 | public int SolutionConfigType = 0; 22 | public bool bCheckOutOnEdit = true; 23 | public bool bPromptForCheckout = false; 24 | public bool bVerboseOutput = false; 25 | public bool bOutputEnabled = false; 26 | 27 | public string P4Port = ""; 28 | public string P4User = ""; 29 | public string P4Client = ""; 30 | 31 | // save the manual settings that are passed in so we can display them (if needed) when switching solution configurations 32 | private string ManualP4Port = ""; 33 | private string ManualP4User = ""; 34 | private string ManualP4Client = ""; 35 | 36 | public delegate void VerboseOutputDelegate(string message); 37 | private VerboseOutputDelegate VerboseOutput = null; 38 | 39 | public SolutionConfigForm(int InPosX, int InPosY, string InSolutionDirectory, int InSolutionConfigType, bool bInCheckOutOnEdit, bool bInPromptForCheckout, bool bInVerboseOutput, bool bInOutputEnabled, string InP4Port, string InP4User, string InP4Client, VerboseOutputDelegate InVerboseOutput) 40 | { 41 | bWindowInitComplete = false; // we aren't done initializing the window yet 42 | 43 | InitializeComponent(); 44 | 45 | VerboseOutput = new VerboseOutputDelegate(InVerboseOutput); 46 | 47 | solutionDirectory = InSolutionDirectory; 48 | SolutionConfigType = InSolutionConfigType; 49 | bCheckOutOnEdit = bInCheckOutOnEdit; 50 | bPromptForCheckout = bInPromptForCheckout; 51 | bVerboseOutput = bInVerboseOutput; 52 | bOutputEnabled = bInOutputEnabled; 53 | 54 | P4Port = InP4Port; 55 | P4User = InP4User; 56 | P4Client = InP4Client; 57 | 58 | if (SolutionConfigType == 0) // disabled 59 | { 60 | radioButtonDisabled.Checked = true; 61 | } 62 | else if (SolutionConfigType == 1) // automatic 63 | { 64 | radioButtonAutomatic.Checked = true; 65 | } 66 | else if (SolutionConfigType == 2) // manual 67 | { 68 | ManualP4Port = P4Port; 69 | ManualP4User = P4User; 70 | ManualP4Client = P4Client; 71 | 72 | radioButtonManual.Checked = true; 73 | } 74 | 75 | if (bCheckOutOnEdit) 76 | { 77 | radioButtonOnModify.Checked = true; 78 | } 79 | else 80 | { 81 | radioButtonOnSave.Checked = true; 82 | } 83 | 84 | checkBoxPromptForCheckout.Checked = bPromptForCheckout; 85 | checkBoxVerboseOutput.Checked = bVerboseOutput; 86 | checkBoxOutputEnabled.Checked = bOutputEnabled; 87 | 88 | checkBoxVerboseOutput.Enabled = bOutputEnabled; 89 | 90 | PosX = InPosX; 91 | PosY = InPosY; 92 | 93 | if ((PosX != -1) && (PosY != -1)) 94 | { 95 | this.StartPosition = FormStartPosition.Manual; 96 | this.Location = new Point(PosX, PosY); 97 | } 98 | else // otherwise, center the window on the parent form 99 | { 100 | this.StartPosition = FormStartPosition.CenterParent; 101 | } 102 | } 103 | 104 | private void OnShown(object sender, EventArgs e) 105 | { 106 | bWindowInitComplete = true; // window initialization is complete 107 | } 108 | 109 | private void OnMove(object sender, EventArgs e) 110 | { 111 | if (bWindowInitComplete) 112 | { 113 | PosX = Location.X; 114 | PosY = Location.Y; 115 | } 116 | } 117 | 118 | private void TextBoxEnable(bool bEnable) 119 | { 120 | textBoxP4Port.ReadOnly = !bEnable; 121 | textBoxP4User.ReadOnly = !bEnable; 122 | textBoxP4Client.ReadOnly = !bEnable; 123 | } 124 | 125 | private void SetTextBoxText() 126 | { 127 | textBoxP4Port.Text = P4Port; 128 | textBoxP4User.Text = P4User; 129 | textBoxP4Client.Text = P4Client; 130 | } 131 | 132 | private void radioButtonDisabled_CheckedChanged(object sender, EventArgs e) 133 | { 134 | if (radioButtonDisabled.Checked) 135 | { 136 | SolutionConfigType = 0; // disabled 137 | 138 | P4Port = ""; 139 | P4User = ""; 140 | P4Client = ""; 141 | 142 | TextBoxEnable(false); 143 | SetTextBoxText(); 144 | } 145 | } 146 | 147 | private void radioButtonAutomatic_CheckedChanged(object sender, EventArgs e) 148 | { 149 | if (radioButtonAutomatic.Checked) 150 | { 151 | SolutionConfigType = 1; // automatic 152 | 153 | P4Command p4 = new P4Command(); 154 | p4.RunP4Set(solutionDirectory, out P4Port, out P4User, out P4Client, out string verbose); 155 | 156 | // if we have enabled the 'bVerboseOutput' setting locally in this dialog box, we need to output the results of the "p4 set" command here... 157 | if ((VerboseOutput != null) && bOutputEnabled && bVerboseOutput) 158 | { 159 | VerboseOutput(verbose); 160 | } 161 | 162 | TextBoxEnable(false); 163 | SetTextBoxText(); 164 | } 165 | } 166 | 167 | private void radioButtonManual_CheckedChanged(object sender, EventArgs e) 168 | { 169 | if (radioButtonManual.Checked) 170 | { 171 | SolutionConfigType = 2; // manual settings 172 | 173 | // default to the previously set manual settings 174 | P4Port = ManualP4Port; 175 | P4User = ManualP4User; 176 | P4Client = ManualP4Client; 177 | 178 | TextBoxEnable(true); 179 | SetTextBoxText(); 180 | } 181 | else 182 | { 183 | // save the previous manual settings 184 | ManualP4Port = textBoxP4Port.Text; 185 | ManualP4User = textBoxP4User.Text; 186 | ManualP4Client = textBoxP4Client.Text; 187 | } 188 | } 189 | 190 | private void radioButtonOnModify_CheckedChanged(object sender, EventArgs e) 191 | { 192 | if (!bWindowInitComplete) 193 | { 194 | return; 195 | } 196 | 197 | if (radioButtonOnModify.Checked) 198 | { 199 | bCheckOutOnEdit = true; 200 | } 201 | } 202 | 203 | private void radioButtonOnSave_CheckedChanged(object sender, EventArgs e) 204 | { 205 | if (!bWindowInitComplete) 206 | { 207 | return; 208 | } 209 | 210 | if (radioButtonOnSave.Checked) 211 | { 212 | bCheckOutOnEdit = false; 213 | } 214 | } 215 | 216 | private void checkBoxPromptForCheckout_CheckedChanged(object sender, EventArgs e) 217 | { 218 | bPromptForCheckout = checkBoxPromptForCheckout.Checked; 219 | } 220 | 221 | private void checkBoxOutputEnabled_CheckedChanged(object sender, EventArgs e) 222 | { 223 | bOutputEnabled = checkBoxOutputEnabled.Checked; 224 | checkBoxVerboseOutput.Enabled = bOutputEnabled; 225 | } 226 | 227 | private void checkBoxVerboseOutput_CheckedChanged(object sender, EventArgs e) 228 | { 229 | bVerboseOutput = checkBoxVerboseOutput.Checked; 230 | } 231 | 232 | private void buttonOK_Click(object sender, EventArgs e) 233 | { 234 | if (SolutionConfigType != 0) 235 | { 236 | if (textBoxP4Port.Text == "" || textBoxP4User.Text == "" || textBoxP4Client.Text == "") 237 | { 238 | string message = "P4PORT, P4USER or P4CLIENT is blank. These must not be blank for things to work properly. If you are using Windows environment variables, you will need to restart Visual Studio after changing them. Changes via 'p4 set' or from the .p4config file do not require restarting Visual Studio."; 239 | string caption = "Invalid Settings"; 240 | MessageBox.Show(message, caption, MessageBoxButtons.OK); 241 | 242 | return; 243 | } 244 | } 245 | 246 | if (SolutionConfigType == 2) // manual settings? 247 | { 248 | P4Port = textBoxP4Port.Text; 249 | P4User = textBoxP4User.Text; 250 | P4Client = textBoxP4Client.Text; 251 | } 252 | 253 | this.DialogResult = DialogResult.OK; 254 | 255 | Close(); 256 | } 257 | 258 | private void buttonCancel_Click(object sender, EventArgs e) 259 | { 260 | this.DialogResult = DialogResult.Cancel; 261 | 262 | Close(); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /ClassLibrary/SolutionConfigForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2022 Jeffrey "botman" Broome 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /P4SimpleScc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.32002.261 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "P4SimpleScc_2019", "P4SimpleScc_2019\P4SimpleScc_2019.csproj", "{6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "P4SimpleScc_2022", "P4SimpleScc_2022\P4SimpleScc_2022.csproj", "{A739EBFE-8820-4E3E-B9B5-23A1202506A6}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary", "ClassLibrary\ClassLibrary.csproj", "{147CB2FB-A361-49EA-A025-5E1A37AA12EF}" 11 | EndProject 12 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedProject", "SharedProject\SharedProject.shproj", "{64CD1D5B-0E68-4A05-9535-BF12013DD743}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SharedFiles", "SharedFiles", "{E48A1386-AF13-48F4-8E50-8BC6D2C31097}" 15 | ProjectSection(SolutionItems) = preProject 16 | SharedFiles\P4SimpleSccPackage.vsct = SharedFiles\P4SimpleSccPackage.vsct 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{785ABA1B-4A2A-4D94-884F-4D585E4C0D50}" 20 | ProjectSection(SolutionItems) = preProject 21 | SharedFiles\Resources\Command.png = SharedFiles\Resources\Command.png 22 | EndProjectSection 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReadMe", "ReadMe", "{406A1291-CEC3-4255-90FC-1CB714ED57DC}" 25 | ProjectSection(SolutionItems) = preProject 26 | ReadMe\ReadMe.txt = ReadMe\ReadMe.txt 27 | EndProjectSection 28 | EndProject 29 | Global 30 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 31 | SharedProject\SharedProject.projitems*{64cd1d5b-0e68-4a05-9535-bf12013dd743}*SharedItemsImports = 13 32 | SharedProject\SharedProject.projitems*{6b141b6a-8ace-4b1e-93fb-dde01c2f6bad}*SharedItemsImports = 4 33 | SharedProject\SharedProject.projitems*{a739ebfe-8820-4e3e-b9b5-23a1202506a6}*SharedItemsImports = 4 34 | EndGlobalSection 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Debug|x86 = Debug|x86 38 | Release|Any CPU = Release|Any CPU 39 | Release|x86 = Release|x86 40 | EndGlobalSection 41 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 42 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Debug|x86.ActiveCfg = Debug|x86 45 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Debug|x86.Build.0 = Debug|x86 46 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Release|x86.ActiveCfg = Release|x86 49 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD}.Release|x86.Build.0 = Release|x86 50 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Debug|x86.ActiveCfg = Debug|x86 53 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Debug|x86.Build.0 = Debug|x86 54 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Release|x86.ActiveCfg = Release|x86 57 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6}.Release|x86.Build.0 = Release|x86 58 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Debug|x86.ActiveCfg = Debug|Any CPU 61 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Debug|x86.Build.0 = Debug|Any CPU 62 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Release|x86.ActiveCfg = Release|Any CPU 65 | {147CB2FB-A361-49EA-A025-5E1A37AA12EF}.Release|x86.Build.0 = Release|Any CPU 66 | EndGlobalSection 67 | GlobalSection(SolutionProperties) = preSolution 68 | HideSolutionNode = FALSE 69 | EndGlobalSection 70 | GlobalSection(NestedProjects) = preSolution 71 | {785ABA1B-4A2A-4D94-884F-4D585E4C0D50} = {E48A1386-AF13-48F4-8E50-8BC6D2C31097} 72 | EndGlobalSection 73 | GlobalSection(ExtensibilityGlobals) = postSolution 74 | SolutionGuid = {312D0A8F-D1B9-4EAF-B353-4D9F9DC82E53} 75 | EndGlobalSection 76 | EndGlobal 77 | -------------------------------------------------------------------------------- /P4SimpleScc_2019/AsyncPackageHelpers/AsyncPackageRegistrationAttribute.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation. All rights reserved. 4 | // 5 | //------------------------------------------------------------------------------ 6 | 7 | namespace Microsoft.VisualStudio.AsyncPackageHelpers { 8 | 9 | using System; 10 | using System.Globalization; 11 | using System.IO; 12 | using System.ComponentModel; 13 | using System.ComponentModel.Design; 14 | using Microsoft.VisualStudio.Shell; 15 | using Microsoft.VisualStudio.Shell.Interop; 16 | 17 | /// 18 | /// This attribute is defined on a package to get it to be registered. It 19 | /// is internal because packages are meant to be registered, so it is 20 | /// implicit just by having a package in the assembly. 21 | /// 22 | [AttributeUsage(AttributeTargets.Class, Inherited=true, AllowMultiple=false)] 23 | public sealed class AsyncPackageRegistrationAttribute : RegistrationAttribute 24 | { 25 | private RegistrationMethod registrationMethod = RegistrationMethod.Default; 26 | private bool useManagedResources = false; 27 | private bool allowsBackgroundLoad = false; 28 | private string satellitePath = null; 29 | 30 | /// 31 | /// Select between specifying the Codebase entry or the Assembly entry in the registry. 32 | /// This can be overriden during registration 33 | /// 34 | public RegistrationMethod RegisterUsing 35 | { 36 | get 37 | { 38 | return registrationMethod; 39 | } 40 | set 41 | { 42 | registrationMethod = value; 43 | } 44 | } 45 | 46 | /// 47 | /// For managed resources, there should not be a native ui dll registered. 48 | /// 49 | public bool UseManagedResourcesOnly 50 | { 51 | get { return useManagedResources; } 52 | set { useManagedResources = value; } 53 | } 54 | 55 | /// 56 | /// Package is safe to load on a background thread. 57 | /// 58 | public bool AllowsBackgroundLoading 59 | { 60 | get { return allowsBackgroundLoad; } 61 | set { allowsBackgroundLoad = value; } 62 | } 63 | 64 | /// 65 | /// To specify a resource dll located in a different location then the default, 66 | /// set this property. This can be useful if your package is installed in the GAC. 67 | /// If this is not set, the directory where the package is located will be use. 68 | /// 69 | /// Note that the dll should be located at the following path: 70 | /// SatellitePath\lcid\PackageDllNameUI.dll 71 | /// 72 | public string SatellitePath 73 | { 74 | get { return satellitePath; } 75 | set { satellitePath = value; } 76 | } 77 | 78 | private string RegKeyName(RegistrationContext context) 79 | { 80 | return String.Format(CultureInfo.InvariantCulture, "Packages\\{0}", context.ComponentType.GUID.ToString("B")); 81 | } 82 | 83 | /// 84 | /// Called to register this attribute with the given context. The context 85 | /// contains the location where the registration information should be placed. 86 | /// it also contains such as the type being registered, and path information. 87 | /// 88 | /// This method is called both for registration and unregistration. The difference is 89 | /// that unregistering just uses a hive that reverses the changes applied to it. 90 | /// 91 | /// 92 | /// Contains the location where the registration information should be placed. 93 | /// It also contains other information such as the type being registered 94 | /// and path of the assembly. 95 | /// 96 | public override void Register(RegistrationContext context) { 97 | Type t = context.ComponentType; 98 | 99 | Key packageKey = null; 100 | try 101 | { 102 | packageKey = context.CreateKey(RegKeyName(context)); 103 | 104 | //use a friendly description if it exists. 105 | DescriptionAttribute attr = TypeDescriptor.GetAttributes(t)[typeof(DescriptionAttribute)] as DescriptionAttribute; 106 | if (attr != null && !String.IsNullOrEmpty(attr.Description)) { 107 | packageKey.SetValue(string.Empty, attr.Description); 108 | } 109 | else { 110 | packageKey.SetValue(string.Empty, t.Name); 111 | } 112 | 113 | packageKey.SetValue("InprocServer32", context.InprocServerPath); 114 | packageKey.SetValue("Class", t.FullName); 115 | 116 | // If specified on the command line, let the command line option override 117 | if (context.RegistrationMethod != RegistrationMethod.Default) 118 | { 119 | registrationMethod = context.RegistrationMethod; 120 | } 121 | 122 | // Select registration method 123 | switch (registrationMethod) 124 | { 125 | case RegistrationMethod.Assembly: 126 | case RegistrationMethod.Default: 127 | packageKey.SetValue("Assembly", t.Assembly.FullName); 128 | break; 129 | 130 | case RegistrationMethod.CodeBase: 131 | packageKey.SetValue("CodeBase", context.CodeBase); 132 | break; 133 | } 134 | 135 | Key childKey = null; 136 | if (!useManagedResources) 137 | { 138 | try 139 | { 140 | childKey = packageKey.CreateSubkey("SatelliteDll"); 141 | 142 | // Register the satellite dll 143 | string satelliteDllPath; 144 | if (SatellitePath != null) 145 | { 146 | // Use provided path 147 | satelliteDllPath = context.EscapePath(SatellitePath); 148 | } 149 | else 150 | { 151 | // Default to package path 152 | satelliteDllPath = context.ComponentPath; 153 | } 154 | childKey.SetValue("Path", satelliteDllPath); 155 | childKey.SetValue("DllName", String.Format(CultureInfo.InvariantCulture, "{0}UI.dll", Path.GetFileNameWithoutExtension(t.Assembly.ManifestModule.Name))); 156 | } 157 | finally 158 | { 159 | if (childKey != null) 160 | childKey.Close(); 161 | } 162 | } 163 | 164 | if (allowsBackgroundLoad) 165 | { 166 | packageKey.SetValue("AllowsBackgroundLoad", true); 167 | } 168 | 169 | if (typeof(IVsPackageDynamicToolOwner).IsAssignableFrom(context.ComponentType) || 170 | typeof(IVsPackageDynamicToolOwnerEx).IsAssignableFrom(context.ComponentType)) 171 | { 172 | packageKey.SetValue("SupportsDynamicToolOwner", Microsoft.VisualStudio.PlatformUI.Boxes.BooleanTrue); 173 | } 174 | } 175 | finally 176 | { 177 | if (packageKey != null) 178 | packageKey.Close(); 179 | } 180 | } 181 | 182 | /// 183 | /// Unregister this package. 184 | /// 185 | /// 186 | public override void Unregister(RegistrationContext context) 187 | { 188 | context.RemoveKey(RegKeyName(context)); 189 | } 190 | 191 | } 192 | } 193 | 194 | -------------------------------------------------------------------------------- /P4SimpleScc_2019/AsyncPackageHelpers/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | //using System.Runtime.InteropServices; 3 | //using Microsoft.VisualStudio.Shell.Interop; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.Shell; 6 | 7 | namespace Microsoft.VisualStudio.AsyncPackageHelpers 8 | { 9 | public static class ExtensionMethods 10 | { 11 | /// 12 | /// Helper method to use async/await with IAsyncServiceProvider implementation 13 | /// 14 | /// IAsyncServciceProvider instance 15 | /// Type of the Visual Studio service requested 16 | /// Service object as type of T 17 | public static async Task GetServiceAsync(this Shell.Interop.IAsyncServiceProvider asyncServiceProvider, Type serviceType) where T : class 18 | { 19 | T returnValue = null; 20 | 21 | await ThreadHelper.JoinableTaskFactory.RunAsync(async () => 22 | { 23 | object serviceInstance = null; 24 | Guid serviceTypeGuid = serviceType.GUID; 25 | serviceInstance = await asyncServiceProvider.QueryServiceAsync(ref serviceTypeGuid); 26 | 27 | // We have to make sure we are on main UI thread before trying to cast as underlying implementation 28 | // can be an STA COM object and doing a cast would require calling QueryInterface/AddRef marshaling 29 | // to main thread via COM. 30 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 31 | returnValue = serviceInstance as T; 32 | }); 33 | 34 | return returnValue; 35 | } 36 | 37 | /// 38 | /// Gets if async package is supported in the current instance of Visual Studio 39 | /// 40 | /// an IServiceProvider instance, usually a Package instance 41 | /// true if async packages are supported 42 | public static bool IsAsyncPackageSupported(this IServiceProvider serviceProvider) 43 | { 44 | Shell.Interop.IAsyncServiceProvider asyncServiceProvider = serviceProvider.GetService(typeof(Shell.Interop.SAsyncServiceProvider)) as Shell.Interop.IAsyncServiceProvider; 45 | return asyncServiceProvider != null; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /P4SimpleScc_2019/AsyncPackageHelpers/ProvideAutoLoadAttribute.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation, All rights reserved. 4 | // This code sample is provided "AS IS" without warranty of any kind, 5 | // it is not recommended for use in a production environment. 6 | // 7 | //------------------------------------------------------------------------------ 8 | 9 | namespace Microsoft.VisualStudio.AsyncPackageHelpers 10 | { 11 | 12 | using System; 13 | using System.Globalization; 14 | using Microsoft.VisualStudio.Shell; 15 | 16 | [Flags] 17 | public enum PackageAutoLoadFlags 18 | { 19 | /// 20 | /// Indicates no special auto-load behavior. This is the default flag value if not specified. 21 | /// 22 | None = 0x0, 23 | 24 | /// 25 | /// When set package will not auto load in newer Visual Studio versions with rule based UI contexts 26 | /// 27 | SkipWhenUIContextRulesActive = 0x1, 28 | 29 | /// 30 | /// When set, if the associated package is marked as allowing background loads (via the ), 31 | /// then the package will be loaded asynchronously, in the background, when the associated UI context is triggered. 32 | /// 33 | BackgroundLoad = 0x2 34 | } 35 | 36 | /// 37 | /// This attribute registers the package as an extender. The GUID passed in determines 38 | /// what is being extended. The attributes on a package do not control the behavior of 39 | /// the package, but they can be used by registration tools to register the proper 40 | /// information with Visual Studio. 41 | /// 42 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 43 | public sealed class ProvideAutoLoadAttribute : RegistrationAttribute 44 | { 45 | 46 | private Guid loadGuid = Guid.Empty; 47 | 48 | /// 49 | /// Specify that the package should get loaded when this context is active. 50 | /// 51 | /// Context which should trigger the loading of your package. 52 | public ProvideAutoLoadAttribute(string cmdUiContextGuid) : this(cmdUiContextGuid, PackageAutoLoadFlags.None) 53 | { 54 | } 55 | 56 | /// 57 | /// Specify that the package should get loaded when this context is active. 58 | /// 59 | /// Context which should trigger the loading of your package. 60 | public ProvideAutoLoadAttribute(string cmdUiContextGuid, PackageAutoLoadFlags flags = PackageAutoLoadFlags.None) 61 | { 62 | loadGuid = new Guid(cmdUiContextGuid); 63 | Flags = flags; 64 | } 65 | 66 | /// 67 | /// Context Guid which triggers the loading of the package. 68 | /// 69 | public Guid LoadGuid 70 | { 71 | get 72 | { 73 | return loadGuid; 74 | } 75 | } 76 | 77 | /// 78 | /// Specifies the options for package auto load entry 79 | /// 80 | public PackageAutoLoadFlags Flags 81 | { 82 | get; 83 | private set; 84 | } 85 | 86 | /// 87 | /// The reg key name of this AutoLoad. 88 | /// 89 | private string RegKeyName 90 | { 91 | get 92 | { 93 | return string.Format(CultureInfo.InvariantCulture, "AutoLoadPackages\\{0}", loadGuid.ToString("B")); 94 | } 95 | } 96 | 97 | /// 98 | /// Called to register this attribute with the given context. The context 99 | /// contains the location where the registration information should be placed. 100 | /// it also contains such as the type being registered, and path information. 101 | /// 102 | public override void Register(RegistrationContext context) 103 | { 104 | using (Key childKey = context.CreateKey(RegKeyName)) 105 | { 106 | childKey.SetValue(context.ComponentType.GUID.ToString("B"), (int)Flags); 107 | } 108 | } 109 | 110 | /// 111 | /// Unregister this AutoLoad specification. 112 | /// 113 | public override void Unregister(RegistrationContext context) 114 | { 115 | context.RemoveValue(RegKeyName, context.ComponentType.GUID.ToString("B")); 116 | } 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /P4SimpleScc_2019/P4SimpleScc_2019.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 16.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | Debug 10 | AnyCPU 11 | 2.0 12 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 13 | {6B141B6A-8ACE-4B1E-93FB-DDE01C2F6BAD} 14 | Library 15 | Properties 16 | P4SimpleScc_2019 17 | P4SimpleScc_2019 18 | v4.7.2 19 | true 20 | true 21 | true 22 | false 23 | false 24 | true 25 | true 26 | Program 27 | $(DevEnvDir)devenv.exe 28 | /rootsuffix Exp 29 | 30 | 31 | true 32 | full 33 | false 34 | bin\Debug\ 35 | TRACE;DEBUG 36 | prompt 37 | 4 38 | 39 | 40 | pdbonly 41 | true 42 | bin\Release\ 43 | TRACE 44 | prompt 45 | 4 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Designer 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | P4SimplePackage.vsct 67 | Menus.ctmenu 68 | 69 | 70 | 71 | 72 | LICENSE.txt 73 | Always 74 | true 75 | 76 | 77 | 78 | 79 | {147cb2fb-a361-49ea-a025-5e1a37aa12ef} 80 | ClassLibrary 81 | 82 | 83 | 84 | 85 | 16.10.31321.278 86 | 87 | 88 | 16.10.31321.278 89 | 90 | 91 | 16.10.26 92 | 93 | 94 | 15.0.392 95 | 96 | 97 | 15.0.392 98 | 99 | 100 | 101 | 102 | 103 | 110 | -------------------------------------------------------------------------------- /P4SimpleScc_2019/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("P4SimpleScc")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("P4SimpleScc")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("2.2.0.0")] 33 | [assembly: AssemblyFileVersion("2.2.0.0")] 34 | -------------------------------------------------------------------------------- /P4SimpleScc_2019/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | P4SimpleScc 6 | P4SimpleScc source code control provider 7 | LICENSE.txt 8 | https://github.com/botman99/P4SimpleScc 9 | https://github.com/botman99/P4SimpleScc/releases 10 | Perforce; Scc; source; control 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /P4SimpleScc_2022/AsyncPackageHelpers/AsyncPackageRegistrationAttribute.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation. All rights reserved. 4 | // 5 | //------------------------------------------------------------------------------ 6 | 7 | namespace Microsoft.VisualStudio.AsyncPackageHelpers { 8 | 9 | using System; 10 | using System.Globalization; 11 | using System.IO; 12 | using System.ComponentModel; 13 | using System.ComponentModel.Design; 14 | using Microsoft.VisualStudio.Shell; 15 | using Microsoft.VisualStudio.Shell.Interop; 16 | 17 | /// 18 | /// This attribute is defined on a package to get it to be registered. It 19 | /// is internal because packages are meant to be registered, so it is 20 | /// implicit just by having a package in the assembly. 21 | /// 22 | [AttributeUsage(AttributeTargets.Class, Inherited=true, AllowMultiple=false)] 23 | public sealed class AsyncPackageRegistrationAttribute : RegistrationAttribute 24 | { 25 | private RegistrationMethod registrationMethod = RegistrationMethod.Default; 26 | private bool useManagedResources = false; 27 | private bool allowsBackgroundLoad = false; 28 | private string satellitePath = null; 29 | 30 | /// 31 | /// Select between specifying the Codebase entry or the Assembly entry in the registry. 32 | /// This can be overriden during registration 33 | /// 34 | public RegistrationMethod RegisterUsing 35 | { 36 | get 37 | { 38 | return registrationMethod; 39 | } 40 | set 41 | { 42 | registrationMethod = value; 43 | } 44 | } 45 | 46 | /// 47 | /// For managed resources, there should not be a native ui dll registered. 48 | /// 49 | public bool UseManagedResourcesOnly 50 | { 51 | get { return useManagedResources; } 52 | set { useManagedResources = value; } 53 | } 54 | 55 | /// 56 | /// Package is safe to load on a background thread. 57 | /// 58 | public bool AllowsBackgroundLoading 59 | { 60 | get { return allowsBackgroundLoad; } 61 | set { allowsBackgroundLoad = value; } 62 | } 63 | 64 | /// 65 | /// To specify a resource dll located in a different location then the default, 66 | /// set this property. This can be useful if your package is installed in the GAC. 67 | /// If this is not set, the directory where the package is located will be use. 68 | /// 69 | /// Note that the dll should be located at the following path: 70 | /// SatellitePath\lcid\PackageDllNameUI.dll 71 | /// 72 | public string SatellitePath 73 | { 74 | get { return satellitePath; } 75 | set { satellitePath = value; } 76 | } 77 | 78 | private string RegKeyName(RegistrationContext context) 79 | { 80 | return String.Format(CultureInfo.InvariantCulture, "Packages\\{0}", context.ComponentType.GUID.ToString("B")); 81 | } 82 | 83 | /// 84 | /// Called to register this attribute with the given context. The context 85 | /// contains the location where the registration information should be placed. 86 | /// it also contains such as the type being registered, and path information. 87 | /// 88 | /// This method is called both for registration and unregistration. The difference is 89 | /// that unregistering just uses a hive that reverses the changes applied to it. 90 | /// 91 | /// 92 | /// Contains the location where the registration information should be placed. 93 | /// It also contains other information such as the type being registered 94 | /// and path of the assembly. 95 | /// 96 | public override void Register(RegistrationContext context) { 97 | Type t = context.ComponentType; 98 | 99 | Key packageKey = null; 100 | try 101 | { 102 | packageKey = context.CreateKey(RegKeyName(context)); 103 | 104 | //use a friendly description if it exists. 105 | DescriptionAttribute attr = TypeDescriptor.GetAttributes(t)[typeof(DescriptionAttribute)] as DescriptionAttribute; 106 | if (attr != null && !String.IsNullOrEmpty(attr.Description)) { 107 | packageKey.SetValue(string.Empty, attr.Description); 108 | } 109 | else { 110 | packageKey.SetValue(string.Empty, t.Name); 111 | } 112 | 113 | packageKey.SetValue("InprocServer32", context.InprocServerPath); 114 | packageKey.SetValue("Class", t.FullName); 115 | 116 | // If specified on the command line, let the command line option override 117 | if (context.RegistrationMethod != RegistrationMethod.Default) 118 | { 119 | registrationMethod = context.RegistrationMethod; 120 | } 121 | 122 | // Select registration method 123 | switch (registrationMethod) 124 | { 125 | case RegistrationMethod.Assembly: 126 | case RegistrationMethod.Default: 127 | packageKey.SetValue("Assembly", t.Assembly.FullName); 128 | break; 129 | 130 | case RegistrationMethod.CodeBase: 131 | packageKey.SetValue("CodeBase", context.CodeBase); 132 | break; 133 | } 134 | 135 | Key childKey = null; 136 | if (!useManagedResources) 137 | { 138 | try 139 | { 140 | childKey = packageKey.CreateSubkey("SatelliteDll"); 141 | 142 | // Register the satellite dll 143 | string satelliteDllPath; 144 | if (SatellitePath != null) 145 | { 146 | // Use provided path 147 | satelliteDllPath = context.EscapePath(SatellitePath); 148 | } 149 | else 150 | { 151 | // Default to package path 152 | satelliteDllPath = context.ComponentPath; 153 | } 154 | childKey.SetValue("Path", satelliteDllPath); 155 | childKey.SetValue("DllName", String.Format(CultureInfo.InvariantCulture, "{0}UI.dll", Path.GetFileNameWithoutExtension(t.Assembly.ManifestModule.Name))); 156 | } 157 | finally 158 | { 159 | if (childKey != null) 160 | childKey.Close(); 161 | } 162 | } 163 | 164 | if (allowsBackgroundLoad) 165 | { 166 | packageKey.SetValue("AllowsBackgroundLoad", true); 167 | } 168 | 169 | if (typeof(IVsPackageDynamicToolOwner).IsAssignableFrom(context.ComponentType) || 170 | typeof(IVsPackageDynamicToolOwnerEx).IsAssignableFrom(context.ComponentType)) 171 | { 172 | packageKey.SetValue("SupportsDynamicToolOwner", Microsoft.VisualStudio.PlatformUI.Boxes.BooleanTrue); 173 | } 174 | } 175 | finally 176 | { 177 | if (packageKey != null) 178 | packageKey.Close(); 179 | } 180 | } 181 | 182 | /// 183 | /// Unregister this package. 184 | /// 185 | /// 186 | public override void Unregister(RegistrationContext context) 187 | { 188 | context.RemoveKey(RegKeyName(context)); 189 | } 190 | 191 | } 192 | } 193 | 194 | -------------------------------------------------------------------------------- /P4SimpleScc_2022/AsyncPackageHelpers/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | //using System.Runtime.InteropServices; 3 | //using Microsoft.VisualStudio.Shell.Interop; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.Shell; 6 | 7 | namespace Microsoft.VisualStudio.AsyncPackageHelpers 8 | { 9 | public static class ExtensionMethods 10 | { 11 | /// 12 | /// Helper method to use async/await with IAsyncServiceProvider implementation 13 | /// 14 | /// IAsyncServciceProvider instance 15 | /// Type of the Visual Studio service requested 16 | /// Service object as type of T 17 | public static async Task GetServiceAsync(this Shell.Interop.COMAsyncServiceProvider.IAsyncServiceProvider asyncServiceProvider, Type serviceType) where T : class 18 | { 19 | T returnValue = null; 20 | 21 | await ThreadHelper.JoinableTaskFactory.RunAsync(async () => 22 | { 23 | object serviceInstance = null; 24 | Guid serviceTypeGuid = serviceType.GUID; 25 | serviceInstance = await asyncServiceProvider.QueryServiceAsync(ref serviceTypeGuid); 26 | 27 | // We have to make sure we are on main UI thread before trying to cast as underlying implementation 28 | // can be an STA COM object and doing a cast would require calling QueryInterface/AddRef marshaling 29 | // to main thread via COM. 30 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 31 | returnValue = serviceInstance as T; 32 | }); 33 | 34 | return returnValue; 35 | } 36 | 37 | /// 38 | /// Gets if async package is supported in the current instance of Visual Studio 39 | /// 40 | /// an IServiceProvider instance, usually a Package instance 41 | /// true if async packages are supported 42 | public static bool IsAsyncPackageSupported(this IServiceProvider serviceProvider) 43 | { 44 | Shell.Interop.COMAsyncServiceProvider.IAsyncServiceProvider asyncServiceProvider = serviceProvider.GetService(typeof(Shell.Interop.SAsyncServiceProvider)) as Shell.Interop.COMAsyncServiceProvider.IAsyncServiceProvider; 45 | return asyncServiceProvider != null; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /P4SimpleScc_2022/AsyncPackageHelpers/ProvideAutoLoadAttribute.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation, All rights reserved. 4 | // This code sample is provided "AS IS" without warranty of any kind, 5 | // it is not recommended for use in a production environment. 6 | // 7 | //------------------------------------------------------------------------------ 8 | 9 | namespace Microsoft.VisualStudio.AsyncPackageHelpers 10 | { 11 | 12 | using System; 13 | using System.Globalization; 14 | using Microsoft.VisualStudio.Shell; 15 | 16 | [Flags] 17 | public enum PackageAutoLoadFlags 18 | { 19 | /// 20 | /// Indicates no special auto-load behavior. This is the default flag value if not specified. 21 | /// 22 | None = 0x0, 23 | 24 | /// 25 | /// When set package will not auto load in newer Visual Studio versions with rule based UI contexts 26 | /// 27 | SkipWhenUIContextRulesActive = 0x1, 28 | 29 | /// 30 | /// When set, if the associated package is marked as allowing background loads (via the ), 31 | /// then the package will be loaded asynchronously, in the background, when the associated UI context is triggered. 32 | /// 33 | BackgroundLoad = 0x2 34 | } 35 | 36 | /// 37 | /// This attribute registers the package as an extender. The GUID passed in determines 38 | /// what is being extended. The attributes on a package do not control the behavior of 39 | /// the package, but they can be used by registration tools to register the proper 40 | /// information with Visual Studio. 41 | /// 42 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 43 | public sealed class ProvideAutoLoadAttribute : RegistrationAttribute 44 | { 45 | 46 | private Guid loadGuid = Guid.Empty; 47 | 48 | /// 49 | /// Specify that the package should get loaded when this context is active. 50 | /// 51 | /// Context which should trigger the loading of your package. 52 | public ProvideAutoLoadAttribute(string cmdUiContextGuid) : this(cmdUiContextGuid, PackageAutoLoadFlags.None) 53 | { 54 | } 55 | 56 | /// 57 | /// Specify that the package should get loaded when this context is active. 58 | /// 59 | /// Context which should trigger the loading of your package. 60 | public ProvideAutoLoadAttribute(string cmdUiContextGuid, PackageAutoLoadFlags flags = PackageAutoLoadFlags.None) 61 | { 62 | loadGuid = new Guid(cmdUiContextGuid); 63 | Flags = flags; 64 | } 65 | 66 | /// 67 | /// Context Guid which triggers the loading of the package. 68 | /// 69 | public Guid LoadGuid 70 | { 71 | get 72 | { 73 | return loadGuid; 74 | } 75 | } 76 | 77 | /// 78 | /// Specifies the options for package auto load entry 79 | /// 80 | public PackageAutoLoadFlags Flags 81 | { 82 | get; 83 | private set; 84 | } 85 | 86 | /// 87 | /// The reg key name of this AutoLoad. 88 | /// 89 | private string RegKeyName 90 | { 91 | get 92 | { 93 | return string.Format(CultureInfo.InvariantCulture, "AutoLoadPackages\\{0}", loadGuid.ToString("B")); 94 | } 95 | } 96 | 97 | /// 98 | /// Called to register this attribute with the given context. The context 99 | /// contains the location where the registration information should be placed. 100 | /// it also contains such as the type being registered, and path information. 101 | /// 102 | public override void Register(RegistrationContext context) 103 | { 104 | using (Key childKey = context.CreateKey(RegKeyName)) 105 | { 106 | childKey.SetValue(context.ComponentType.GUID.ToString("B"), (int)Flags); 107 | } 108 | } 109 | 110 | /// 111 | /// Unregister this AutoLoad specification. 112 | /// 113 | public override void Unregister(RegistrationContext context) 114 | { 115 | context.RemoveValue(RegKeyName, context.ComponentType.GUID.ToString("B")); 116 | } 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /P4SimpleScc_2022/P4SimpleScc_2022.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 16.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | Debug 10 | AnyCPU 11 | 2.0 12 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 13 | {A739EBFE-8820-4E3E-B9B5-23A1202506A6} 14 | Library 15 | Properties 16 | P4SimpleScc_2022 17 | P4SimpleScc_2022 18 | v4.7.2 19 | true 20 | true 21 | true 22 | false 23 | false 24 | true 25 | true 26 | Program 27 | $(DevEnvDir)devenv.exe 28 | /rootsuffix Exp 29 | 30 | 31 | true 32 | full 33 | false 34 | bin\Debug\ 35 | DEBUG;TRACE 36 | prompt 37 | 4 38 | 39 | 40 | pdbonly 41 | true 42 | bin\Release\ 43 | TRACE 44 | prompt 45 | 4 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Designer 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | P4SimplePackage.vsct 74 | Menus.ctmenu 75 | 76 | 77 | 78 | 79 | LICENSE.txt 80 | Always 81 | true 82 | 83 | 84 | 85 | 86 | {147cb2fb-a361-49ea-a025-5e1a37aa12ef} 87 | ClassLibrary 88 | 89 | 90 | 91 | 92 | 17.11.20 93 | 94 | 95 | 17.11.40262 96 | 97 | 98 | 15.0.392 99 | 100 | 101 | 15.0.392 102 | 103 | 104 | 105 | 106 | 107 | 114 | -------------------------------------------------------------------------------- /P4SimpleScc_2022/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("P4SimpleScc_2022")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("P4SimpleScc_2022")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("2.2.0.0")] 33 | [assembly: AssemblyFileVersion("2.2.0.0")] 34 | -------------------------------------------------------------------------------- /P4SimpleScc_2022/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | P4SimpleScc 6 | P4SimpleScc source code control provider 7 | LICENSE.txt 8 | https://github.com/botman99/P4SimpleScc 9 | https://github.com/botman99/P4SimpleScc/releases 10 | Perforce; Scc; source; control 11 | 12 | 13 | 14 | amd64 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # P4SimpleScc 2 | 3 | **P4SimpleScc** is a Visual Studio Extension Source Control Provider for Perforce (Helix Core). 4 | 5 | The "simple" part of this extension is that it only does **one** thing. It checks files out from a Perforce server when they are modified (or when they are saved). It doesn't do anything else. It doesn't show status of checked out files. It doesn't submit changelists of files. It doesn't diff files. It doesn't revert files. It doesn't shelve files. It doesn't show history or time-lapse view of files. It doesn't show a revision graph of files. All of those functions can be performed in P4V instead of implementing that same interface in Visual Studio. 6 | 7 | If all you want is to check out files when modified, and you want something small, fast and efficient, then P4SimpleScc does that task well. 8 | 9 | To use P4SimpleScc, just install the extension (there's one for Visual Studio 2019 and one for Visual Studio 2022), then start up Visual Studio and click on "Tools -> Options" from the main menu. Expand the 'Source Control' group and select 'P4SimpleScc' from the 'Current source control plug-in:' drop down and click "OK". That will enable the P4SimpleScc provider. 10 | 11 | ![SourceControl](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/Tools_Options_SourceControl.png) 12 | 13 | While the P4SimpleScc provider is active, you can click on 'Extensions' in the main menu and you should see a 'P4SimpleScc' menu (this menu is hidden if P4SimpleScc is not the active source control provider). If a solution is loaded, you can then click on the 'Solution Configuration' menu item to enable source control on the currently loaded solution. 14 | 15 | ![SolutionConfigurationMenu](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/SolutionConfigurationMenu.png) 16 | 17 | The solution configuration dialog looks like this: 18 | 19 | ![SolutionConfigurationDialog](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/SolutionConfigurationDialog.png) 20 | 21 | There are three choices at the top, 'Disabled, 'Automatic' and 'Manual'. 'Disable' will disable P4SimpleScc provider control for this solution. 'Automatic' will use the console command "p4 set" to automatically gather the settings for P4PORT, P4USER and P4CLIENT which can come from the Windows environment, from a previous 'p4 set' command, or from a .p4config file (as long as the P4CONFIG setting has previously been set using 'p4 set P4CONFIG=' followed by the .p4config file name). You can also use 'Manual' to set the 'P4PORT', 'P4USER' and 'P4CLIENT' settings manually. Each of these settings must be non-blank and will be validated with the Perforce (Helix Core) server to make sure they are correct. P4PORT will be the server name and port number (separated by a ':'). P4USER will be your username on the Perforce server. P4CLIENT will be your workspace name on the Perforce server for where you have the solution and files (although the solution .sln file doesn't have to be under Perforce control). 22 | 23 | You can also select whether you want to check out files when they are modified or wait and check them out when the changes to the file have been saved. Waiting until the file is saved means that you can "undo" changes to a file if you modified it by mistake and it won't be checked out from Perforce until you save it, so you won't have to revert that file later. If you had selected 'Check out files on modify' it would have been checked out as soon as you accidentally modified it. 24 | 25 | There is an option to prompt you with a dialog to verify that it is okay to check out each file before doing so (otherwise, the file will automatically be checked out upon modify/save). 26 | 27 | There is also an option to enable or disable the 'P4SimpleScc' Output pane in the Output Window. By default, this output is disabled. If you enable output, you can also enable 'Verbose Output' which outputs the response of all P4 commands that are issued to the server by the P4SimpleScc extension. The 'Verbose Output' is normally not needed unless you are trying to debug why some operation in P4SimpleScc is failing. 28 | 29 | If you have 'Output Enabled' checked in the Solution Configuration, when the P4SimpleScc provider is enabled, you can see status messages from it by opening the Output window ("View -> Output" from the main menu, or Ctrl-Alt-O). Then select 'P4SimpleScc' in the 'Show output from:' drop down. 30 | 31 | ![OutputWindow](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/OutputWindow.png) 32 | 33 | You can manually check out file by selecting one or more files in the Solution Explorer view, right clicking and selecting "Check Out File". 34 | 35 | ![CheckOutFile](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/CheckOutFile.png) 36 | 37 | You can also open a file in the document editor and right click on the filename tab and select "Check Out File" from there. 38 | 39 | ![CheckOutDocument](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/CheckOutDocument.png) 40 | 41 | If you right click on a file and the file is already checked out, it will show "Check Out File" as being disabled. 42 | 43 | ![FileCheckedOut](https://raw.githubusercontent.com/botman99/P4SimpleScc/master/img/FileCheckedOut.png) 44 | 45 | **NOTE:** If you have the P4VS extension installed, you may need to disable it (you don't need to uninstall, just disable) since the P4VS extension seems to want to make itself the active source control provider even if another provider was active or is controlling that solution. 46 | 47 | See the [Releases](https://github.com/botman99/P4SimpleScc/releases) page to download the latest release. 48 | 49 | * Author: Jeffrey "botman" Broome 50 | * License: [MIT](http://opensource.org/licenses/mit-license.php) 51 | -------------------------------------------------------------------------------- /ReadMe/ReadMe.txt: -------------------------------------------------------------------------------- 1 | 2 | This Visual Studio extension was created using shared projects as described here: 3 | 4 | https://docs.microsoft.com/en-us/visualstudio/extensibility/migration/update-visual-studio-extension?view=vs-2022#use-shared-projects-for-multi-targeting 5 | 6 | To build (and debug) the VS2019 extension, open the .sln file using Visual Studio 2019. 7 | To build (and debug) the VS2022 extension, open the .sln file using Visual Studio 2022. 8 | 9 | 10 | The ClassLibrary was originally created as a Windows Forms App (.NET Framework) Project and then 11 | the 'Output type:' in the Properties was changed to "Class Library". This was necessary because 12 | both of the VISX projects need access to the Windows Forms Dialog(s) and Visual Studio won't let 13 | you add Windows Forms Dialogs (using .NET Framework) to the SharedProject. 14 | 15 | After being switched from "Windows Forms App" to "Class Library", things like 'Program.cs' and 16 | 'App.config' were removed from the new project (since these aren't needed for a class library). 17 | 18 | Any additional classes that are needed by both the VISX project's classes and by the Windows 19 | Forms classes (such as P4Command.cs) need to also be added to the ClassLibrary so that these are 20 | accessible by both projects. 21 | 22 | 23 | The origin of the Source Control Provider code came from the VSSDK sample here: 24 | https://github.com/microsoft/VSSDK-Extensibility-Samples/tree/master/ArchivedSamples/Source_Code_Control_Provider 25 | 26 | The AsyncPackageHelpers code came from the 'Backwards Compatible AsyncPackage 2013 code here: 27 | https://github.com/microsoft/VSSDK-Extensibility-Samples/tree/master/Backwards_Compatible_AsyncPackage_2013/BackwardsCompatibleAsyncPackage/AsyncPackageHelpers 28 | 29 | This was needed to get the extension to auto-load and auto-execute on load so that this 30 | extension could detect when a solution was loaded that was using P4SimpleScc as the source 31 | control provider. 32 | 33 | See this page for some migration issues from VS2019 and VS2022: 34 | 35 | https://docs.microsoft.com/en-us/visualstudio/extensibility/migration/breaking-api-list?view=vs-2022 36 | 37 | -------------------------------------------------------------------------------- /SharedFiles/P4SimpleSccPackage.vsct: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | P4SimpleScc 40 | 41 | 42 | 43 | 44 | 45 | 46 | Sou&rce Control Provider 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 85 | 94 | 95 | 106 | 107 | 117 | 118 | 119 | 120 | 121 | 122 | 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 | -------------------------------------------------------------------------------- /SharedFiles/Resources/Command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/SharedFiles/Resources/Command.png -------------------------------------------------------------------------------- /SharedProject/ActionProviderFactory.cs: -------------------------------------------------------------------------------- 1 |  2 | // 3 | // Copyright - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.ComponentModel.Design; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.VisualStudio.Workspace; 12 | using Microsoft.VisualStudio.Workspace.Extensions.VS; 13 | 14 | 15 | namespace P4SimpleScc 16 | { 17 | [ExportFileContextActionProvider((FileContextActionProviderOptions)VsCommandActionProviderOptions.SupportVsCommands, ProviderType, ProviderPriority.Normal, GuidList.SourceFileContextType)] 18 | public class ActionProviderFactory : IWorkspaceProviderFactory, IVsCommandActionProvider 19 | { 20 | // Unique Guid for WordCountActionProvider. 21 | private const string ProviderType = "0DD39C9C-3DE4-4B9C-BE19-7D011341A65B"; 22 | 23 | private static readonly Guid ProviderCommandGroup = GuidList.GuidOpenFolderExtensibilityPackageCmdSet; 24 | 25 | public IFileContextActionProvider CreateProvider(IWorkspace workspaceContext) 26 | { 27 | return new ActionProvider(workspaceContext); 28 | } 29 | 30 | public IReadOnlyCollection GetSupportedVsCommands() 31 | { 32 | return new List 33 | { 34 | new CommandID(GuidList.GuidOpenFolderExtensibilityPackageCmdSet, SccProvider.icmdWorkspaceCheckOutFile) 35 | }; 36 | } 37 | 38 | internal class ActionProvider : IFileContextActionProvider 39 | { 40 | private IWorkspace workspaceContext; 41 | 42 | internal ActionProvider(IWorkspace workspaceContext) 43 | { 44 | this.workspaceContext = workspaceContext; 45 | } 46 | 47 | public Task> GetActionsAsync(string filePath, FileContext fileContext, CancellationToken cancellationToken) 48 | { 49 | bool bIsCheckedOut = SccProvider.IsCheckedOut(filePath, out string stderr); 50 | 51 | if (!bIsCheckedOut) // if the file is not already checked out, then add the menu item to check out the file 52 | { 53 | return Task.FromResult>(new IFileContextAction[] 54 | { 55 | new MyContextAction( 56 | fileContext, 57 | new Tuple(ProviderCommandGroup, SccProvider.icmdWorkspaceCheckOutFile), 58 | "My Action", 59 | async (fCtxt, progress, ct) => 60 | { 61 | await DoWorkAsync(workspaceContext, fileContext, filePath); 62 | } 63 | ) 64 | }); 65 | } 66 | else // if the file is already checked out, we return an empty list, SccProvider.QueryStatus will intercept the menu command and disable it (grey it out) 67 | { 68 | return Task.FromResult>(new IFileContextAction[] 69 | { 70 | }); 71 | } 72 | } 73 | 74 | internal static async Task DoWorkAsync(IWorkspace workspaceContext, FileContext fileContext, string filePath) 75 | { 76 | await Microsoft.VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 77 | 78 | SccProvider.CheckOutFile(filePath); 79 | 80 | // invoke the FileContextChanged event to force the menu to be recreated (since the action above will change the state of the file) 81 | await fileContext.OnFileContextChanged.InvokeAsync(null, new EventArgs()); 82 | } 83 | 84 | internal class MyContextAction : IFileContextAction, IVsCommandItem 85 | { 86 | private Func, CancellationToken, Task> executeAction; 87 | 88 | internal MyContextAction( 89 | FileContext fileContext, 90 | Tuple command, 91 | string displayName, 92 | Func, CancellationToken, Task> executeAction) 93 | { 94 | this.executeAction = executeAction; 95 | this.Source = fileContext; 96 | this.CommandGroup = command.Item1; 97 | this.CommandId = command.Item2; 98 | this.DisplayName = displayName; 99 | } 100 | 101 | public Guid CommandGroup { get; } 102 | public uint CommandId { get; } 103 | public string DisplayName { get; } 104 | public FileContext Source { get; } 105 | 106 | public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) 107 | { 108 | await this.executeAction(this.Source, progress, cancellationToken); 109 | return new FileContextActionResult(true); 110 | } 111 | 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /SharedProject/FileContextProviderFactory.cs: -------------------------------------------------------------------------------- 1 |  2 | // 3 | // Copyright - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.VisualStudio.Workspace; 11 | using Task = System.Threading.Tasks.Task; 12 | 13 | namespace P4SimpleScc 14 | { 15 | /// 16 | /// File context provider for TXT files. 17 | /// 18 | [ExportFileContextProvider(ProviderType, GuidList.SourceFileContextType)] 19 | public class FileContextProviderFactory : IWorkspaceProviderFactory 20 | { 21 | // Unique Guid for FileContextProvider. 22 | private const string ProviderType = "F0970AFD-11A4-41BF-8BEE-EF3FA288FB97"; 23 | 24 | /// 25 | public IFileContextProvider CreateProvider(IWorkspace workspaceContext) 26 | { 27 | return new FileContextProvider(workspaceContext); 28 | } 29 | 30 | private class FileContextProvider : IFileContextProvider 31 | { 32 | private IWorkspace workspaceContext; 33 | 34 | internal FileContextProvider(IWorkspace workspaceContext) 35 | { 36 | this.workspaceContext = workspaceContext; 37 | } 38 | 39 | /// 40 | public async Task> GetContextsForFileAsync(string filePath, CancellationToken cancellationToken) 41 | { 42 | var fileContexts = new List(); 43 | 44 | fileContexts.Add(new FileContext( 45 | new Guid(ProviderType), 46 | new Guid(GuidList.SourceFileContextType), 47 | filePath + "\n", 48 | Array.Empty())); 49 | 50 | return await Task.FromResult(fileContexts.ToArray()); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SharedProject/Guids.cs: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | 8 | namespace P4SimpleScc 9 | { 10 | /// 11 | /// This class is used only to expose the list of Guids used by this package. 12 | /// This list of guids must match the set of Guids used inside the VSCT file. 13 | /// 14 | public static class GuidList 15 | { 16 | // Now define the list of guids as public static members. 17 | 18 | // Unique ID of the source control provider; this is also used as the command UI context to show/hide the pacakge UI 19 | public static readonly Guid guidSccProvider = new Guid("{B205A1B6-0000-4A1C-8680-97FD2219C692}"); 20 | // The guid of the source control provider service (implementing IVsSccProvider interface) 21 | public static readonly Guid guidSccProviderService = new Guid("{B205A1B6-1000-4A1C-8680-97FD2219C692}"); 22 | // The guid of the source control provider package (implementing IVsPackage interface) 23 | public static readonly Guid guidSccProviderPkg = new Guid("{B205A1B6-2000-4A1C-8680-97FD2219C692}"); 24 | 25 | // Other guids for menus and commands 26 | public static readonly Guid guidSccProviderCmdSet = new Guid("{B205A1B6-9463-474A-807D-17F40BCFBB17}"); 27 | 28 | public static readonly Guid GuidOpenFolderExtensibilityPackageCmdSet = new Guid("e37cc989-b956-4a50-9515-b0395b288e4a"); 29 | 30 | // Guid to associate file action factories. 31 | public const string SourceFileContextType = "2C4D13FF-FEA9-4AEC-A48E-17FD9D70E594"; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /SharedProject/ProvideSolutionProps.cs: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF 5 | ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY 6 | IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR 7 | PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. 8 | 9 | ***************************************************************************/ 10 | 11 | using System; 12 | using System.Globalization; 13 | using Microsoft.VisualStudio.Shell; 14 | 15 | namespace P4SimpleScc 16 | { 17 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 18 | internal sealed class ProvideSolutionProps : RegistrationAttribute 19 | { 20 | private string _propName; 21 | 22 | public ProvideSolutionProps(string propName) 23 | { 24 | _propName = propName; 25 | } 26 | 27 | public override void Register(RegistrationContext context) 28 | { 29 | context.Log.WriteLine(string.Format(CultureInfo.InvariantCulture, "ProvideSolutionProps: ({0} = {1})", context.ComponentType.GUID.ToString("B"), PropName)); 30 | 31 | Key childKey = null; 32 | 33 | try 34 | { 35 | childKey = context.CreateKey(string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", "SolutionPersistence", PropName)); 36 | 37 | childKey.SetValue(string.Empty, context.ComponentType.GUID.ToString("B")); 38 | } 39 | finally 40 | { 41 | if (childKey != null) childKey.Close(); 42 | } 43 | } 44 | 45 | public override void Unregister(RegistrationContext context) 46 | { 47 | context.RemoveKey(string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", "SolutionPersistence", PropName)); 48 | } 49 | 50 | public string PropName { get { return _propName; } } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SharedProject/ProvideSourceControlProvider.cs: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF 5 | ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY 6 | IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR 7 | PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. 8 | 9 | ***************************************************************************/ 10 | 11 | using System; 12 | using System.Globalization; 13 | using MsVsShell = Microsoft.VisualStudio.Shell; 14 | 15 | namespace P4SimpleScc 16 | { 17 | /// 18 | /// This attribute registers the source control provider. 19 | /// 20 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 21 | public sealed class ProvideSourceControlProvider : MsVsShell.RegistrationAttribute 22 | { 23 | private string _regName = null; 24 | private string _uiName = null; 25 | 26 | /// 27 | /// Constructor 28 | /// 29 | public ProvideSourceControlProvider(string regName, string uiName) 30 | { 31 | _regName = regName; 32 | _uiName = uiName; 33 | } 34 | 35 | /// 36 | /// Get the friendly name of the provider (written in registry) 37 | /// 38 | public string RegName 39 | { 40 | get { return _regName; } 41 | } 42 | 43 | /// 44 | /// Get the unique guid identifying the provider 45 | /// 46 | public Guid RegGuid 47 | { 48 | get { return GuidList.guidSccProvider; } 49 | } 50 | 51 | /// 52 | /// Get the UI name of the provider (string resource ID) 53 | /// 54 | public string UIName 55 | { 56 | get { return _uiName; } 57 | } 58 | 59 | /// 60 | /// Get the package containing the UI name of the provider 61 | /// 62 | public Guid UINamePkg 63 | { 64 | get { return GuidList.guidSccProviderPkg; } 65 | } 66 | 67 | /// 68 | /// Get the guid of the provider's service 69 | /// 70 | public Guid SccProviderService 71 | { 72 | get { return GuidList.guidSccProviderService; } 73 | } 74 | 75 | /// 76 | /// Called to register this attribute with the given context. The context 77 | /// contains the location where the registration inforomation should be placed. 78 | /// It also contains other information such as the type being registered and path information. 79 | /// 80 | public override void Register(RegistrationContext context) 81 | { 82 | // Write to the context's log what we are about to do 83 | context.Log.WriteLine(string.Format(CultureInfo.CurrentCulture, "P4SimpleSccProvider:\t\t{0}\n", RegName)); 84 | 85 | // Declare the source control provider, its name, the provider's service 86 | // and aditionally the packages implementing this provider 87 | using (Key sccProviders = context.CreateKey("SourceControlProviders")) 88 | { 89 | using (Key sccProviderKey = sccProviders.CreateSubkey(RegGuid.ToString("B"))) 90 | { 91 | sccProviderKey.SetValue("", RegName); 92 | sccProviderKey.SetValue("Service", SccProviderService.ToString("B")); 93 | 94 | using (Key sccProviderNameKey = sccProviderKey.CreateSubkey("Name")) 95 | { 96 | sccProviderNameKey.SetValue("", UIName); 97 | sccProviderNameKey.SetValue("Package", UINamePkg.ToString("B")); 98 | 99 | sccProviderNameKey.Close(); 100 | } 101 | 102 | // Additionally, you can create a "Packages" subkey where you can enumerate the dll 103 | // that are used by the source control provider, something like "Package1"="P4SimpleSccProvider.dll" 104 | // but this is not a requirement. 105 | sccProviderKey.Close(); 106 | } 107 | 108 | sccProviders.Close(); 109 | } 110 | } 111 | 112 | /// 113 | /// Unregister the source control provider 114 | /// 115 | /// The Registration Context to be unregistered 116 | public override void Unregister(RegistrationContext context) 117 | { 118 | context.RemoveKey("SourceControlProviders\\" + GuidList.guidSccProviderPkg.ToString("B")); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SharedProject/SccProvider.cs: -------------------------------------------------------------------------------- 1 |  2 | // 3 | // Copyright - Jeffrey "botman" Broome 4 | // 5 | 6 | using System; 7 | using System.IO; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Globalization; 11 | using System.Runtime.InteropServices; 12 | using System.ComponentModel.Design; 13 | using Microsoft.VisualStudio.Shell.Interop; 14 | using Microsoft.VisualStudio.OLE.Interop; 15 | using Microsoft.VisualStudio; 16 | using Microsoft.VisualStudio.Settings; 17 | using Microsoft.VisualStudio.Shell.Settings; 18 | using PackageAutoLoadFlags = Microsoft.VisualStudio.AsyncPackageHelpers.PackageAutoLoadFlags; 19 | 20 | using MsVsShell = Microsoft.VisualStudio.Shell; 21 | 22 | using ClassLibrary; 23 | 24 | namespace P4SimpleScc 25 | { 26 | /// 27 | /// This is the class that implements the package exposed by this assembly. 28 | /// 29 | /// 30 | /// 31 | /// The minimum requirement for a class to be considered a valid package for Visual Studio 32 | /// is to implement the IVsPackage interface and register itself with the shell. 33 | /// This package uses the helper classes defined inside the Managed Package Framework (MPF) 34 | /// to do it: it derives from the Package class that provides the implementation of the 35 | /// IVsPackage interface and uses the registration attributes defined in the framework to 36 | /// register itself and its components with the shell. These attributes tell the pkgdef creation 37 | /// utility what data to put into .pkgdef file. 38 | /// 39 | /// 40 | /// To get loaded into VS, the package must be referred by <Asset Type="Microsoft.VisualStudio.VsPackage" ...> in .vsixmanifest file. 41 | /// 42 | /// 43 | // Declare that resources for the package are to be found in the managed assembly resources, and not in a satellite dll 44 | [MsVsShell.PackageRegistration(UseManagedResourcesOnly = true)] 45 | // Register the resource ID of the CTMENU section (generated from compiling the VSCT file), so the IDE will know how to merge this package's menus with the rest of the IDE when "devenv /setup" is run 46 | // The menu resource ID needs to match the ResourceName number defined in the csproj project file in the VSCTCompile section 47 | // Everytime the version number changes VS will automatically update the menus on startup; if the version doesn't change, you will need to run manually "devenv /setup /rootsuffix:Exp" to see VSCT changes reflected in IDE 48 | [MsVsShell.ProvideMenuResource("Menus.ctmenu", 1)] 49 | // Register the source control provider's service (implementing IVsScciProvider interface) 50 | [MsVsShell.ProvideService(typeof(SccProviderService), ServiceName = "P4SimpleScc")] 51 | // Register the source control provider to be visible in Tools/Options/SourceControl/Plugin dropdown selector 52 | [@ProvideSourceControlProvider("P4SimpleScc", "#100")] 53 | 54 | // Pre-load the package when the command UI context is asserted (the provider will be automatically loaded after restarting the shell if it was active last time the shell was shutdown) 55 | [Microsoft.VisualStudio.AsyncPackageHelpers.ProvideAutoLoad("B205A1B6-0000-4A1C-8680-97FD2219C692",PackageAutoLoadFlags.BackgroundLoad)] 56 | [Microsoft.VisualStudio.AsyncPackageHelpers.AsyncPackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 57 | [Microsoft.VisualStudio.AsyncPackageHelpers.ProvideAutoLoad(VSConstants.UICONTEXT.NoSolution_string, PackageAutoLoadFlags.BackgroundLoad)] 58 | [Microsoft.VisualStudio.AsyncPackageHelpers.ProvideAutoLoad(VSConstants.UICONTEXT.SolutionOpening_string, PackageAutoLoadFlags.BackgroundLoad)] 59 | 60 | // Register the key used for persisting solution properties, so the IDE will know to load the source control package when opening a controlled solution containing properties written by this package 61 | [ProvideSolutionProps(_strSolutionPersistanceKey)] 62 | // Declare the package guid 63 | [Guid("B205A1B6-2000-4A1C-8680-97FD2219C692")] // this is GuidList.guidSccProviderPkg 64 | public sealed class SccProvider : MsVsShell.Package, 65 | IOleCommandTarget, 66 | IVsPersistSolutionProps // We'll write properties in the solution file to track when solution is controlled; the interface needs to be implemented by the package object 67 | { 68 | // The service provider implemented by the package 69 | private SccProviderService sccService = null; 70 | 71 | // The name of this provider (to be written in solution and project files) 72 | // As a best practice, to be sure the provider has an unique name, a guid like the provider guid can be used as a part of the name 73 | private const string _strProviderName = "P4SimpleScc:{B205A1B6-2000-4A1C-8680-97FD2219C692}"; // this is GuidList.guidSccProviderPkg (same as this class's Guid) 74 | 75 | // The name of the solution section used to persist provider options (should be unique) 76 | private const string _strSolutionPersistanceKey = "P4SimpleSccProviderSolutionProperties"; 77 | 78 | // The name of the section in the solution user options file used to persist user-specific options (should be unique, shorter than 31 characters and without dots) 79 | private const string _strSolutionUserOptionsKey = "P4SimpleSccProvider"; 80 | 81 | // The names of the properties stored by the provider in the solution file 82 | private const string _strSolutionControlledProperty = "SolutionIsControlled"; 83 | private const string _strSolutionBindingsProperty = "SolutionBindings"; 84 | 85 | private static MsVsShell.Package package; 86 | 87 | public static Guid OutputPaneGuid = Guid.Empty; 88 | 89 | public string solutionDirectory = ""; 90 | public string solutionFile = ""; 91 | public string solutionUserOptions = ""; 92 | 93 | public bool bSolutionLoadedOutputDone = false; 94 | 95 | public bool bIsWorkspace = false; // 'true' if "Open Folder" was used to open this project, 'false' if project was opened using a Solution file 96 | public string WindowActivatedFilename = ""; 97 | 98 | public bool bReadUserOptionsCalled = false; // this allows us to determine if user settings were attempted to be loaded 99 | public bool bUserSettingsWasEmpty = false; // previously, if P4SimpleScc was disabled, settings were removed from the .sou file in the .vs folder, this bool will be 'true' if the settings are missing 100 | 101 | // P4SimpleScc solution configuration settings... 102 | public static int SolutionConfigType = 0; // 0 = disabled, 1 = automatic, 2 = manual settings 103 | public bool bCheckOutOnEdit = true; 104 | public bool bPromptForCheckout = false; 105 | public string P4Port = ""; 106 | public string P4User = ""; 107 | public string P4Client = ""; 108 | public static bool bVerboseOutput = false; 109 | public static bool bOutputEnabled = false; 110 | 111 | public static bool P4SimpleSccConfigDirty = false; // has the solution configuration for this solution been modified (and needs to be saved)? 112 | 113 | public static bool bIsNotAllWrite = true; 114 | 115 | private static object OpututQueueLock = new object(); 116 | private static List OutputQueueList = new List(); 117 | private System.Windows.Threading.DispatcherTimer OutputQueueTimer = null; 118 | 119 | private static char[] InvalidChars; 120 | private List FilenameList; 121 | 122 | private CommandID menuCommandId; 123 | private CommandID menuCheckOutFileCommandId; 124 | 125 | /// 126 | /// Command ID. 127 | /// 128 | public const int icmdSolutionConfiguration = 0x0100; 129 | public const int icmdCheckOutFile = 0x101; 130 | public const int icmdWorkspaceCheckOutFile = 0x0501; 131 | 132 | 133 | /// 134 | /// Constructor 135 | /// 136 | public SccProvider() 137 | { 138 | // The provider implements the IVsPersistSolutionProps interface which is derived from IVsPersistSolutionOpts, 139 | // The base class MsVsShell.Package also implements IVsPersistSolutionOpts, so we're overriding its functionality 140 | // Therefore, to persist user options in the suo file we will not use the set of AddOptionKey/OnLoadOptions/OnSaveOptions 141 | // set of functions, but instead we'll use the IVsPersistSolutionProps functions directly. 142 | } 143 | 144 | #region Package Members 145 | 146 | /// 147 | /// Get an instance of the desired service type. (Calls Package.GetService) 148 | /// 149 | /// The type of service to retrieve 150 | /// An instance of the requested service, or null if the service could not be found 151 | public new object GetService(Type serviceType) 152 | { 153 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 154 | 155 | return base.GetService(serviceType); 156 | } 157 | 158 | /// 159 | /// Initialize the Provider 160 | /// 161 | protected override void Initialize() 162 | { 163 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 164 | 165 | package = this; 166 | 167 | base.Initialize(); 168 | 169 | InvalidChars = Path.GetInvalidPathChars(); // get characters not allowed in file paths 170 | 171 | // Proffer the source control service implemented by the provider 172 | sccService = new SccProviderService(this); 173 | ((IServiceContainer)this).AddService(typeof(SccProviderService), sccService, true); 174 | 175 | var dte = this.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE; 176 | dte.Events.WindowEvents.WindowActivated += WindowEventsOnWindowActivated; 177 | 178 | // Add our command handlers for menu (commands must exist in the .vsct file) 179 | MsVsShell.OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as MsVsShell.OleMenuCommandService; 180 | if (mcs != null) 181 | { 182 | // ToolWindow Command 183 | menuCommandId = new CommandID(GuidList.guidSccProviderCmdSet, icmdSolutionConfiguration); 184 | MenuCommand menuCmd = new MenuCommand(new EventHandler(Exec_menuCommand), menuCommandId); 185 | mcs.AddCommand(menuCmd); 186 | 187 | menuCheckOutFileCommandId = new CommandID(GuidList.guidSccProviderCmdSet, icmdCheckOutFile); 188 | MenuCommand menuCheckOutFileCmd = new MenuCommand(new EventHandler(Exec_menuCheckOutFileCommand), menuCheckOutFileCommandId); 189 | mcs.AddCommand(menuCheckOutFileCmd); 190 | } 191 | 192 | bool bP4SimpleSccEnabled = false; 193 | 194 | SettingsManager settingsManager = new ShellSettingsManager(this); 195 | if (settingsManager != null) 196 | { 197 | WritableSettingsStore store = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); 198 | if (store.CollectionExists(@"P4SimpleScc")) 199 | { 200 | bP4SimpleSccEnabled = store.GetBoolean("P4SimpleScc", "bP4SimpleSccEnabled"); 201 | } 202 | } 203 | 204 | if (bP4SimpleSccEnabled) // only enable this source control provider by default if it was enabled the last time Visual Studio was shut down 205 | { 206 | // Register the provider with the source control manager 207 | // If the package is to become active, this will also callback on OnActiveStateChange and the menu commands will be enabled 208 | IVsRegisterScciProvider rscp = (IVsRegisterScciProvider)GetService(typeof(IVsRegisterScciProvider)); 209 | if (rscp != null) 210 | { 211 | rscp.RegisterSourceControlProvider(GuidList.guidSccProvider); 212 | } 213 | } 214 | 215 | if (GetSolutionFileName() != null) // this is the path that will be taken when the solution is opened by double clicking or being specified as a commandline argument when launching Visual Studio 216 | { 217 | if (solutionDirectory != null && solutionDirectory.Length > 0) // was solution already opened by the time we were loaded? 218 | { 219 | IVsSolutionPersistence solPersistence = (IVsSolutionPersistence)GetService(typeof(IVsSolutionPersistence)); 220 | if (solPersistence != null) 221 | { 222 | // see if there are User Opts for this solution (this will load the Config settings and enable this provider if this solution is controlled by this provider 223 | solPersistence.LoadPackageUserOpts(this, _strSolutionUserOptionsKey); 224 | } 225 | 226 | if (!bSolutionLoadedOutputDone && (solutionDirectory != null) && (solutionDirectory.Length > 0)) 227 | { 228 | string message = String.Format("Loaded solution: {0}\n", solutionFile); 229 | SccProvider.P4SimpleSccQueueOutput(message); 230 | 231 | bSolutionLoadedOutputDone = true; 232 | } 233 | 234 | AfterOpenSolutionOrFolder(); 235 | } 236 | } 237 | 238 | OutputQueueTimer = new System.Windows.Threading.DispatcherTimer(); 239 | OutputQueueTimer.Interval = TimeSpan.FromMilliseconds(100); 240 | OutputQueueTimer.Tick += OnTimerEvent; 241 | OutputQueueTimer.Start(); 242 | } 243 | 244 | /// 245 | /// Unregister from receiving Solution Events and Project Documents, then release the resources used by the Package object 246 | /// 247 | /// true if the object is being disposed, false if it is being finalized 248 | protected override void Dispose(bool disposing) 249 | { 250 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 251 | 252 | sccService.Dispose(); 253 | 254 | base.Dispose(disposing); 255 | } 256 | 257 | /// 258 | /// Checks whether the provider is invoked in command line mode 259 | /// 260 | public bool InCommandLineMode() 261 | { 262 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 263 | 264 | IVsShell shell = (IVsShell)GetService(typeof(SVsShell)); 265 | if (shell != null) 266 | { 267 | object pvar; 268 | if (shell.GetProperty((int)__VSSPROPID.VSSPROPID_IsInCommandLineMode, out pvar) == VSConstants.S_OK && 269 | (bool)pvar) 270 | { 271 | return true; 272 | } 273 | } 274 | 275 | return false; 276 | } 277 | 278 | /// 279 | /// This function is called by the IVsSccProvider service implementation when the active state of the provider changes 280 | /// If the package needs to refresh UI or perform other tasks, this is a good place to add the code 281 | /// 282 | public void OnActiveStateChange(bool bIsEnabled) 283 | { 284 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 285 | 286 | MsVsShell.OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as MsVsShell.OleMenuCommandService; 287 | if (mcs != null) 288 | { 289 | var menuCommand = mcs.FindCommand(menuCommandId); 290 | menuCommand.Enabled = bIsEnabled; 291 | menuCommand.Visible = bIsEnabled; 292 | } 293 | 294 | // store user settings to indicate if this source control provider was last enabled or disabled when Visual Studio shut down (so we can read it when it starts up again) 295 | SettingsManager settingsManager = new ShellSettingsManager(this); 296 | if (settingsManager != null) 297 | { 298 | WritableSettingsStore store = settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); 299 | if (!store.CollectionExists(@"P4SimpleScc")) 300 | { 301 | store.CreateCollection(@"P4SimpleScc"); 302 | } 303 | 304 | store.SetBoolean("P4SimpleScc", "bP4SimpleSccEnabled", bIsEnabled); 305 | } 306 | 307 | if (bIsEnabled) 308 | { 309 | IVsRegisterScciProvider rscp = (IVsRegisterScciProvider)GetService(typeof(IVsRegisterScciProvider)); 310 | if (rscp != null) 311 | { 312 | rscp.RegisterSourceControlProvider(GuidList.guidSccProvider); 313 | } 314 | } 315 | } 316 | 317 | /// 318 | /// Returns the name of the source control provider 319 | /// 320 | public string ProviderName 321 | { 322 | get { return _strProviderName; } 323 | } 324 | 325 | #endregion // Package Members 326 | 327 | 328 | #region Source Control Utility Functions 329 | 330 | /// 331 | /// Returns the filename of the solution 332 | /// 333 | public string GetSolutionFileName() 334 | { 335 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 336 | 337 | IVsSolution sol = (IVsSolution)GetService(typeof(SVsSolution)); 338 | if (sol != null) 339 | { 340 | if (sol.GetSolutionInfo(out solutionDirectory, out solutionFile, out solutionUserOptions) == VSConstants.S_OK) 341 | { 342 | return solutionFile; 343 | } 344 | } 345 | 346 | return null; 347 | } 348 | 349 | #endregion // Source Control Utility Functions 350 | 351 | 352 | //-------------------------------------------------------------------------------- 353 | // IVsPersistSolutionProps specific functions 354 | //-------------------------------------------------------------------------------- 355 | #region IVsPersistSolutionProps interface functions 356 | 357 | public int SaveUserOptions(IVsSolutionPersistence pPersistence) 358 | { 359 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 360 | 361 | // The shell will create a stream for the section of interest, and will call back the provider on 362 | // IVsPersistSolutionProps.WriteUserOptions() to save specific options under the specified key. 363 | if (P4SimpleSccConfigDirty) 364 | { 365 | pPersistence.SavePackageUserOpts(this, _strSolutionUserOptionsKey); 366 | } 367 | 368 | return VSConstants.S_OK; 369 | } 370 | 371 | public int LoadUserOptions(IVsSolutionPersistence pPersistence, uint grfLoadOpts) 372 | { 373 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 374 | 375 | // Note this can be during opening a new solution, or may be during merging of 2 solutions. 376 | // The provider calls the shell back to let it know which options keys from the suo file were written by this provider. 377 | // If the shell will find in the suo file a section that belong to this package, it will create a stream, 378 | // and will call back the provider on IVsPersistSolutionProps.ReadUserOptions() to read specific options 379 | // under that option key. 380 | pPersistence.LoadPackageUserOpts(this, _strSolutionUserOptionsKey); 381 | 382 | return VSConstants.S_OK; 383 | } 384 | 385 | public int WriteUserOptions(IStream pOptionsStream, string pszKey) 386 | { 387 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 388 | 389 | // This function gets called by the shell to let the package write user options under the specified key. 390 | // The key was declared in SaveUserOptions(), when the shell started saving the suo file. 391 | Debug.Assert(pszKey.CompareTo(_strSolutionUserOptionsKey) == 0, "The shell called to read an key that doesn't belong to this package"); 392 | 393 | if (pszKey == _strSolutionUserOptionsKey) 394 | { 395 | Config.Set(Config.KEY.SolutionConfigType, SolutionConfigType); 396 | 397 | Config.Save(out string config_string); 398 | 399 | byte[] buffer = new byte[config_string.Length]; 400 | uint BytesWritten = 0; 401 | 402 | System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); 403 | encoding.GetBytes(config_string, 0, config_string.Length, buffer, 0); 404 | 405 | pOptionsStream.Write(buffer, (uint)config_string.Length, out BytesWritten); 406 | 407 | P4SimpleSccConfigDirty = false; 408 | } 409 | 410 | return VSConstants.S_OK; 411 | } 412 | 413 | public int ReadUserOptions(IStream pOptionsStream, string pszKey) 414 | { 415 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 416 | 417 | // This function is called by the shell if the _strSolutionUserOptionsKey section declared 418 | // in LoadUserOptions() as being written by this package has been found in the suo file. 419 | // Note this can be during opening a new solution, or may be during merging of 2 solutions. 420 | // A good source control provider may need to persist this data until OnAfterOpenSolution or OnAfterMergeSolution is called 421 | 422 | bReadUserOptionsCalled = true; 423 | 424 | string config_string = ""; 425 | 426 | if (pszKey == _strSolutionUserOptionsKey) 427 | { 428 | uint buffer_size = 4096; // allocate enough space for longest possible configuration string 429 | byte[] buffer = new byte[buffer_size]; 430 | uint BytesRead = 0; 431 | 432 | pOptionsStream.Read(buffer, buffer_size, out BytesRead); 433 | 434 | config_string = System.Text.Encoding.UTF8.GetString(buffer).TrimEnd('\0'); // trim nulls from end of string 435 | 436 | if (config_string != "") // process the config settings 437 | { 438 | Config.Load(config_string); 439 | 440 | Config.Get(Config.KEY.SolutionConfigType, ref SolutionConfigType); 441 | Config.Get(Config.KEY.SolutionConfigCheckOutOnEdit, ref bCheckOutOnEdit); 442 | Config.Get(Config.KEY.SolutionConfigPromptForCheckout, ref bPromptForCheckout); 443 | 444 | // NOTE: We don't initialize P4Port, P4User or P4Client here as these are handled in SetP4SettingsForSolution based on SolutionConfigType setting 445 | 446 | Config.Get(Config.KEY.SolutionConfigVerboseOutput, ref bVerboseOutput); 447 | Config.Get(Config.KEY.SolutionConfigOutputEnabled, ref bOutputEnabled); 448 | } 449 | else 450 | { 451 | bUserSettingsWasEmpty = true; // no settings in the .suo file means P4SimpleScc was previously disabled (prior to verson 2.1) 452 | } 453 | 454 | if (bOutputEnabled) 455 | { 456 | CreateOutputPane(); 457 | } 458 | 459 | GetSolutionFileName(); 460 | 461 | if (!bSolutionLoadedOutputDone && (solutionDirectory != null) && (solutionDirectory.Length > 0)) 462 | { 463 | string message = String.Format("Loaded solution: {0}\n", solutionFile); 464 | SccProvider.P4SimpleSccQueueOutput(message); 465 | 466 | bSolutionLoadedOutputDone = true; 467 | } 468 | 469 | if (SolutionConfigType != 0) 470 | { 471 | IVsRegisterScciProvider rscp = (IVsRegisterScciProvider)GetService(typeof(IVsRegisterScciProvider)); 472 | rscp.RegisterSourceControlProvider(GuidList.guidSccProvider); 473 | 474 | SetP4SettingsForSolution(solutionDirectory); 475 | 476 | ServerConnect(); 477 | } 478 | } 479 | 480 | return VSConstants.S_OK; 481 | } 482 | 483 | public int QuerySaveSolutionProps(IVsHierarchy pHierarchy, VSQUERYSAVESLNPROPS[] pqsspSave) 484 | { 485 | // we don't use the solution props as they modify the .sln file (which may or may not be under source control) 486 | return VSConstants.S_OK; 487 | } 488 | 489 | public int SaveSolutionProps(IVsHierarchy pHierarchy, IVsSolutionPersistence pPersistence) 490 | { 491 | // we don't use the solution props as they modify the .sln file (which may or may not be under source control) 492 | return VSConstants.S_OK; 493 | } 494 | 495 | public int WriteSolutionProps(IVsHierarchy pHierarchy, string pszKey, IPropertyBag pPropBag) 496 | { 497 | // we don't use the solution props as they modify the .sln file (which may or may not be under source control) 498 | return VSConstants.S_OK; 499 | } 500 | 501 | public int ReadSolutionProps(IVsHierarchy pHierarchy, string pszProjectName, string pszProjectMk, string pszKey, int fPreLoad, IPropertyBag pPropBag) 502 | { 503 | // we don't use the solution props as they modify the .sln file (which may or may not be under source control) 504 | return VSConstants.S_OK; 505 | } 506 | 507 | public int OnProjectLoadFailure(IVsHierarchy pStubHierarchy, string pszProjectName, string pszProjectMk, string pszKey) 508 | { 509 | return VSConstants.S_OK; 510 | } 511 | 512 | #endregion // IVsPersistSolutionProps 513 | 514 | public void AfterOpenSolutionOrFolder() 515 | { 516 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 517 | 518 | if (bReadUserOptionsCalled && bUserSettingsWasEmpty) // dirty hack to handle P4SimpleScc being disabled for a solution prior to version 2.1 519 | { 520 | P4SimpleSccConfigDirty = true; // we need to save these settings (so that we save the 'disabled' setting) 521 | } 522 | 523 | if (GetSolutionFileName() != null && !String.IsNullOrEmpty(solutionDirectory)) 524 | { 525 | if (!bReadUserOptionsCalled) // don't automatically change anything if previous solution settings exists 526 | { 527 | // We may have a P4CONFIG file (https://www.perforce.com/manuals/v23.1/cmdref/Content/CmdRef/P4CONFIG.html), 528 | // in which case we want to default the solution config to automatic. 529 | // Start with checking whether P4 is even configured to look for one. 530 | ClassLibrary.P4Command p4 = new ClassLibrary.P4Command(); 531 | p4.RunP4Set(solutionDirectory, out string P4Port, out string P4User, out string P4Client, out string P4Config, out string verbose); 532 | if (!string.IsNullOrEmpty(P4Config)) 533 | { 534 | // The P4CONFIG environment variable is set, now go look for the file. 535 | string currentPath = solutionDirectory; 536 | 537 | // Loop until we reach the root directory. 538 | while (!string.IsNullOrEmpty(currentPath) && Directory.Exists(currentPath)) 539 | { 540 | string filePath = Path.Combine(currentPath, P4Config); 541 | 542 | if (File.Exists(filePath)) 543 | { 544 | // P4CONFIG exists! Start out defaulting to automatic and set up P4 settings. 545 | SolutionConfigType = 1; 546 | SetP4SettingsForSolution(solutionDirectory); 547 | 548 | p4.ServerConnect(out string stdout, out string stderr, out verbose, out bool bIsNotAllWrite); 549 | 550 | if (stderr != null && stderr.Length > 0) // if the P4CONFIG settings were not valid... 551 | { 552 | SolutionConfigType = 0; // revert back to P4SimpleScc being disabled 553 | } 554 | else 555 | { 556 | P4SimpleSccConfigDirty = true; // we need to save these settings 557 | } 558 | 559 | break; 560 | } 561 | 562 | var parentDir = Directory.GetParent(currentPath); 563 | if (parentDir == null) 564 | { 565 | break; 566 | } 567 | currentPath = parentDir.FullName; 568 | } 569 | } 570 | } 571 | } 572 | } 573 | 574 | 575 | #region Source Control Command Enabling 576 | 577 | public int QueryStatus(ref Guid guidCmdGroup, uint cCmds, OLECMD[] prgCmds, System.IntPtr pCmdText) 578 | { 579 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 580 | 581 | Debug.Assert(cCmds == 1, "Multiple commands"); 582 | Debug.Assert(prgCmds != null, "NULL argument"); 583 | 584 | if ((prgCmds == null)) 585 | return VSConstants.E_INVALIDARG; 586 | 587 | // Filter out commands that are not defined by this package 588 | if ((guidCmdGroup != GuidList.guidSccProviderCmdSet) && (guidCmdGroup != GuidList.GuidOpenFolderExtensibilityPackageCmdSet)) 589 | { 590 | return (int)(Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED); ; 591 | } 592 | 593 | OLECMDF cmdf = OLECMDF.OLECMDF_SUPPORTED; 594 | 595 | // All source control commands needs to be hidden and disabled when the provider is not active 596 | if (!sccService.Active) 597 | { 598 | cmdf = cmdf | OLECMDF.OLECMDF_INVISIBLE; 599 | cmdf = cmdf & ~(OLECMDF.OLECMDF_ENABLED); 600 | 601 | prgCmds[0].cmdf = (uint)cmdf; 602 | return VSConstants.S_OK; 603 | } 604 | 605 | // Process our Commands 606 | switch (prgCmds[0].cmdID) 607 | { 608 | case icmdSolutionConfiguration: 609 | cmdf |= OLECMDF.OLECMDF_ENABLED; 610 | break; 611 | 612 | case icmdCheckOutFile: 613 | if (SolutionConfigType == 0) 614 | { 615 | cmdf = cmdf | OLECMDF.OLECMDF_INVISIBLE; 616 | cmdf = cmdf & ~(OLECMDF.OLECMDF_ENABLED); 617 | } 618 | else 619 | { 620 | cmdf |= QueryStatus_icmdCheckOutFile(); 621 | } 622 | break; 623 | 624 | case icmdWorkspaceCheckOutFile: 625 | cmdf |= OLECMDF.OLECMDF_SUPPORTED; 626 | break; 627 | 628 | default: 629 | return (int)(Microsoft.VisualStudio.OLE.Interop.Constants.OLECMDERR_E_NOTSUPPORTED); 630 | } 631 | 632 | prgCmds[0].cmdf = (uint)cmdf; 633 | 634 | return VSConstants.S_OK; 635 | } 636 | 637 | OLECMDF QueryStatus_icmdCheckOutFile() 638 | { 639 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 640 | 641 | if (GetSolutionFileName() == null) 642 | { 643 | return OLECMDF.OLECMDF_INVISIBLE; 644 | } 645 | 646 | bool bAllFilesAreCheckedOut = true; // assume all files are checked out 647 | FilenameList = new List(); 648 | 649 | if (bIsWorkspace) // is a Workspace open (i.e. "Open Folder"), if not, we assume we opened a Solution 650 | { 651 | if (WindowActivatedFilename != "") 652 | { 653 | FilenameList.Add(WindowActivatedFilename); 654 | 655 | bool bIsCheckedOut = IsCheckedOut(WindowActivatedFilename, out string stderr); 656 | 657 | bool bShouldIgnoreStatus = false; 658 | if (stderr.Contains("is not under client's root") || stderr.Contains("not in client view") || stderr.Contains("no such file")) // if file is outside client's workspace, or file does not exist in source control... 659 | { 660 | bShouldIgnoreStatus = true; // don't prevent file from being modified (since not under workspace or not under source control) 661 | } 662 | 663 | if (!bIsCheckedOut && !bShouldIgnoreStatus) 664 | { 665 | bAllFilesAreCheckedOut = false; 666 | } 667 | } 668 | } 669 | else 670 | { 671 | IList sel = GetSelectedNodes(); 672 | 673 | foreach (VSITEMSELECTION item in sel) 674 | { 675 | try 676 | { 677 | string Filename = ""; 678 | 679 | if ((item.pHier == null) || ((item.pHier as IVsSolution) != null)) 680 | { 681 | Filename = GetSolutionFileName(); 682 | } 683 | else if ((IVsProject)item.pHier != null) 684 | { 685 | IVsProject Project = (IVsProject)item.pHier; 686 | 687 | Project.GetMkDocument(item.itemid, out Filename); 688 | } 689 | 690 | if (Filename != "") 691 | { 692 | FilenameList.Add(Filename); 693 | 694 | bool bIsCheckedOut = IsCheckedOut(Filename, out string stderr); 695 | 696 | bool bShouldIgnoreStatus = false; 697 | if (stderr.Contains("is not under client's root") || stderr.Contains("not in client view") || stderr.Contains("no such file")) // if file is outside client's workspace, or file does not exist in source control... 698 | { 699 | bShouldIgnoreStatus = true; // don't prevent file from being modified (since not under workspace or not under source control) 700 | } 701 | 702 | if (!bIsCheckedOut && !bShouldIgnoreStatus) 703 | { 704 | bAllFilesAreCheckedOut = false; 705 | } 706 | } 707 | } 708 | catch (Exception ex) 709 | { 710 | Debug.WriteLine(string.Format(CultureInfo.CurrentUICulture, "Exception: ex.message = {0}", ex.Message)); 711 | } 712 | } 713 | } 714 | 715 | if (bAllFilesAreCheckedOut) 716 | { 717 | return OLECMDF.OLECMDF_SUPPORTED; 718 | } 719 | 720 | return OLECMDF.OLECMDF_ENABLED; 721 | } 722 | 723 | /// 724 | /// Gets the list of directly selected VSITEMSELECTION objects 725 | /// 726 | /// A list of VSITEMSELECTION objects 727 | private IList GetSelectedNodes() 728 | { 729 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 730 | 731 | // Retrieve shell interface in order to get current selection 732 | IVsMonitorSelection monitorSelection = this.GetService(typeof(IVsMonitorSelection)) as IVsMonitorSelection; 733 | Debug.Assert(monitorSelection != null, "Could not get the IVsMonitorSelection object from the services exposed by this project"); 734 | if (monitorSelection == null) 735 | { 736 | throw new InvalidOperationException(); 737 | } 738 | 739 | List selectedNodes = new List(); 740 | IntPtr hierarchyPtr = IntPtr.Zero; 741 | IntPtr selectionContainer = IntPtr.Zero; 742 | try 743 | { 744 | // Get the current project hierarchy, project item, and selection container for the current selection 745 | // If the selection spans multiple hierachies, hierarchyPtr is Zero 746 | uint itemid; 747 | IVsMultiItemSelect multiItemSelect = null; 748 | ErrorHandler.ThrowOnFailure(monitorSelection.GetCurrentSelection(out hierarchyPtr, out itemid, out multiItemSelect, out selectionContainer)); 749 | 750 | if (itemid != VSConstants.VSITEMID_SELECTION) 751 | { 752 | // We only care if there are nodes selected in the tree 753 | if (itemid != VSConstants.VSITEMID_NIL) 754 | { 755 | if (hierarchyPtr == IntPtr.Zero) 756 | { 757 | // Solution is selected 758 | VSITEMSELECTION vsItemSelection; 759 | vsItemSelection.pHier = null; 760 | vsItemSelection.itemid = itemid; 761 | selectedNodes.Add(vsItemSelection); 762 | } 763 | else 764 | { 765 | IVsHierarchy hierarchy = (IVsHierarchy)Marshal.GetObjectForIUnknown(hierarchyPtr); 766 | // Single item selection 767 | VSITEMSELECTION vsItemSelection; 768 | vsItemSelection.pHier = hierarchy; 769 | vsItemSelection.itemid = itemid; 770 | selectedNodes.Add(vsItemSelection); 771 | } 772 | } 773 | } 774 | else 775 | { 776 | if (multiItemSelect != null) 777 | { 778 | // This is a multiple item selection. 779 | 780 | //Get number of items selected and also determine if the items are located in more than one hierarchy 781 | uint numberOfSelectedItems; 782 | int isSingleHierarchyInt; 783 | ErrorHandler.ThrowOnFailure(multiItemSelect.GetSelectionInfo(out numberOfSelectedItems, out isSingleHierarchyInt)); 784 | bool isSingleHierarchy = (isSingleHierarchyInt != 0); 785 | 786 | // Now loop all selected items and add them to the list 787 | Debug.Assert(numberOfSelectedItems > 0, "Bad number of selected itemd"); 788 | if (numberOfSelectedItems > 0) 789 | { 790 | VSITEMSELECTION[] vsItemSelections = new VSITEMSELECTION[numberOfSelectedItems]; 791 | ErrorHandler.ThrowOnFailure(multiItemSelect.GetSelectedItems(0, numberOfSelectedItems, vsItemSelections)); 792 | foreach (VSITEMSELECTION vsItemSelection in vsItemSelections) 793 | { 794 | selectedNodes.Add(vsItemSelection); 795 | } 796 | } 797 | } 798 | } 799 | } 800 | finally 801 | { 802 | if (hierarchyPtr != IntPtr.Zero) 803 | { 804 | Marshal.Release(hierarchyPtr); 805 | } 806 | if (selectionContainer != IntPtr.Zero) 807 | { 808 | Marshal.Release(selectionContainer); 809 | } 810 | } 811 | 812 | return selectedNodes; 813 | } 814 | 815 | #endregion // Source Control Command Enabling 816 | 817 | 818 | #region Source Control Commands Execution 819 | 820 | private void Exec_menuCommand(object sender, EventArgs e) 821 | { 822 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 823 | 824 | GetSolutionFileName(); 825 | 826 | // see if we have a solution loaded or not 827 | if (solutionDirectory == null || solutionDirectory == "") 828 | { 829 | string message = "There is no solution loaded. You need to load a solution before applying settings."; 830 | MsVsShell.VsShellUtilities.ShowMessageBox(package, message, "P4SimpleScc Warning", OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); 831 | 832 | return; 833 | } 834 | 835 | int pos_x = -1; 836 | Config.Get(Config.KEY.SolutionConfigDialogPosX, ref pos_x); 837 | int pos_y = -1; 838 | Config.Get(Config.KEY.SolutionConfigDialogPosY, ref pos_y); 839 | 840 | SolutionConfigForm Dialog = new SolutionConfigForm(pos_x, pos_y, solutionDirectory, SolutionConfigType, bCheckOutOnEdit, bPromptForCheckout, bVerboseOutput, bOutputEnabled, P4Port, P4User, P4Client, VerboseOutput); 841 | 842 | System.Windows.Forms.DialogResult result = Dialog.ShowDialog(); 843 | 844 | if (Dialog.PosX != pos_x || Dialog.PosY != pos_y) 845 | { 846 | Config.Set(Config.KEY.SolutionConfigDialogPosX, Dialog.PosX); 847 | Config.Set(Config.KEY.SolutionConfigDialogPosY, Dialog.PosY); 848 | 849 | P4SimpleSccConfigDirty = true; // we need to save these settings 850 | } 851 | 852 | if (result == System.Windows.Forms.DialogResult.OK) 853 | { 854 | // set the global configuration settings 855 | SolutionConfigType = Dialog.SolutionConfigType; 856 | bCheckOutOnEdit = Dialog.bCheckOutOnEdit; 857 | bPromptForCheckout = Dialog.bPromptForCheckout; 858 | bVerboseOutput = Dialog.bVerboseOutput; 859 | bOutputEnabled = Dialog.bOutputEnabled; 860 | 861 | if (bOutputEnabled && (OutputPaneGuid == Guid.Empty)) // if output is enabled and there's no output pane, then create one 862 | { 863 | CreateOutputPane(); 864 | } 865 | else if (!bOutputEnabled && (OutputPaneGuid != Guid.Empty)) // else if output is not enabled and there is an output pane, then remove it 866 | { 867 | RemoveOutputPane(); 868 | } 869 | 870 | if (Dialog.SolutionConfigType == 2) // manual settings 871 | { 872 | P4Port = Dialog.P4Port; 873 | P4User = Dialog.P4User; 874 | P4Client = Dialog.P4Client; 875 | } 876 | else 877 | { 878 | P4Port = ""; 879 | P4User = ""; 880 | P4Client = ""; 881 | } 882 | 883 | // save all the config settings 884 | Config.Set(Config.KEY.SolutionConfigType, SolutionConfigType); 885 | Config.Set(Config.KEY.SolutionConfigCheckOutOnEdit, bCheckOutOnEdit); 886 | Config.Set(Config.KEY.SolutionConfigPromptForCheckout, bPromptForCheckout); 887 | 888 | // these will be blank for all configurations except 'manual settings' (SetP4SettingsForSolution will re-initialize them at runtime) 889 | Config.Set(Config.KEY.SolutionConfigDialogP4Port, P4Port); 890 | Config.Set(Config.KEY.SolutionConfigDialogP4User, P4User); 891 | Config.Set(Config.KEY.SolutionConfigDialogP4Client, P4Client); 892 | 893 | Config.Set(Config.KEY.SolutionConfigVerboseOutput, bVerboseOutput); 894 | Config.Set(Config.KEY.SolutionConfigOutputEnabled, bOutputEnabled); 895 | 896 | P4SimpleSccConfigDirty = true; // we need to write these to the solution .suo file 897 | 898 | SetP4SettingsForSolution(solutionDirectory); // update the settings based on the new SolutionConfigType 899 | 900 | ServerConnect(); // attempt to connect to the server using the new settings 901 | } 902 | 903 | } 904 | 905 | private void Exec_menuCheckOutFileCommand(object sender, EventArgs e) 906 | { 907 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 908 | 909 | foreach (string Filename in FilenameList) 910 | { 911 | CheckOutFile(Filename); 912 | } 913 | } 914 | 915 | #endregion // Source Control Commands Execution 916 | 917 | 918 | public void SetP4SettingsForSolution(string solutionDirectory) 919 | { 920 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 921 | 922 | if (solutionDirectory != null && solutionDirectory.Length > 0) // only set the P4 settings if we have a valid solution directory 923 | { 924 | // clear previous settings and load everything from the config settings for this solution 925 | P4Port = ""; 926 | P4User = ""; 927 | P4Client = ""; 928 | 929 | if (SolutionConfigType == 1) // if automatic settings 930 | { 931 | P4Command p4 = new P4Command(); 932 | p4.RunP4Set(solutionDirectory, out P4Port, out P4User, out P4Client, out string verbose); 933 | 934 | if (bVerboseOutput) // we don't need to worry about stderr output here since "p4 set" should never fail to run (as long as P4V is installed) 935 | { 936 | if (!verbose.EndsWith("\n")) 937 | { 938 | verbose += "\n"; 939 | } 940 | P4SimpleSccQueueOutput(verbose); 941 | } 942 | } 943 | else if (SolutionConfigType == 2) // if manual settings 944 | { 945 | // always override all settings with the manual settings (from the Config class) 946 | Config.Get(Config.KEY.SolutionConfigDialogP4Port, ref P4Port); 947 | Config.Get(Config.KEY.SolutionConfigDialogP4User, ref P4User); 948 | Config.Get(Config.KEY.SolutionConfigDialogP4Client, ref P4Client); 949 | } 950 | 951 | string message = ""; 952 | 953 | if (SolutionConfigType == 0) 954 | { 955 | message = String.Format("P4SimpleScc is disabled for this solution.\n"); 956 | } 957 | else if (SolutionConfigType == 1) 958 | { 959 | message = String.Format("Using automatic settings: P4PORT={0}, P4USER={1}, P4CLIENT={2}\n", P4Port, P4User, P4Client); 960 | } 961 | else if (SolutionConfigType == 2) 962 | { 963 | message = String.Format("Using manual settings: P4PORT={0}, P4USER={1}, P4CLIENT={2}\n", P4Port, P4User, P4Client); 964 | } 965 | 966 | if (message != "") 967 | { 968 | P4SimpleSccQueueOutput(message); 969 | } 970 | 971 | // set the P4Command environment variables for future "p4" commands (like "p4 edit", etc.) 972 | P4Command.SetEnv(P4Port, P4User, P4Client); 973 | } 974 | } 975 | 976 | public void ServerConnect() 977 | { 978 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 979 | 980 | if (SolutionConfigType != 0) // not disabled? 981 | { 982 | P4Command p4 = new P4Command(); 983 | 984 | p4.ServerConnect(out string stdout, out string stderr, out string verbose, out bIsNotAllWrite); 985 | 986 | if (bVerboseOutput) 987 | { 988 | if (!verbose.EndsWith("\n")) 989 | { 990 | verbose += "\n"; 991 | } 992 | P4SimpleSccQueueOutput(verbose); 993 | } 994 | 995 | if (stderr != null && stderr.Length > 0) 996 | { 997 | P4SimpleSccQueueOutput("Connection to server failed!\n"); 998 | P4SimpleSccQueueOutput(stderr); 999 | 1000 | string message = "Connection to server failed.\n" + stderr; 1001 | MsVsShell.VsShellUtilities.ShowMessageBox(package, message, "P4SimpleScc Error", OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); 1002 | } 1003 | else 1004 | { 1005 | P4SimpleSccQueueOutput("Connection to server successful.\n"); 1006 | } 1007 | } 1008 | } 1009 | 1010 | public static bool IsCheckedOut(string Filename, out string stderr) 1011 | { 1012 | stderr = ""; 1013 | bool status = false; 1014 | 1015 | if (SolutionConfigType != 0) // not disabled? 1016 | { 1017 | if (bIsNotAllWrite) 1018 | { 1019 | FileInfo info = new FileInfo(Filename); 1020 | 1021 | if (!info.IsReadOnly) 1022 | { 1023 | return true; 1024 | } 1025 | } 1026 | 1027 | P4Command p4 = new P4Command(); 1028 | 1029 | status = p4.IsCheckedOut(Filename, out string stdout, out stderr, out string verbose); // ignore stderr here since failure will be treated as if file is not checked out 1030 | 1031 | if (bVerboseOutput) 1032 | { 1033 | if (!verbose.EndsWith("\n")) 1034 | { 1035 | verbose += "\n"; 1036 | } 1037 | P4SimpleSccQueueOutput(verbose); 1038 | } 1039 | } 1040 | 1041 | return status; 1042 | } 1043 | 1044 | public static bool CheckOutFile(string Filename) 1045 | { 1046 | if (SolutionConfigType != 0) // not disabled? 1047 | { 1048 | if (File.Exists(Filename)) // if the file doesn't exist, then it can't be in source control (i.e. brand new file) 1049 | { 1050 | P4Command p4 = new P4Command(); 1051 | 1052 | P4Command.CheckOutStatus status = p4.CheckOutFile(Filename, out string stdout, out string stderr, out string verbose); 1053 | 1054 | if (bVerboseOutput) 1055 | { 1056 | if (!verbose.EndsWith("\n")) 1057 | { 1058 | verbose += "\n"; 1059 | } 1060 | P4SimpleSccQueueOutput(verbose); 1061 | } 1062 | 1063 | // if file already checked out, or file not in client's root (outside workspace), or file is not in source control (brand new file, not yet added to source control) 1064 | if (status == P4Command.CheckOutStatus.FileAlreadyCheckedOut || status == P4Command.CheckOutStatus.FileNotInSourceControl) 1065 | { 1066 | return true; 1067 | } 1068 | 1069 | if (stderr != null && stderr.Length > 0) 1070 | { 1071 | string message = String.Format("Check out of '{0}' failed.\n", Filename); 1072 | P4SimpleSccQueueOutput(message); 1073 | P4SimpleSccQueueOutput(stderr); 1074 | 1075 | string dialog_message = message + stderr; 1076 | MsVsShell.VsShellUtilities.ShowMessageBox(package, dialog_message, "P4SimpleScc Error", OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST); 1077 | 1078 | return false; 1079 | } 1080 | else 1081 | { 1082 | string message = String.Format("Check out of '{0}' successful.\n", Filename); 1083 | P4SimpleSccQueueOutput(message); 1084 | 1085 | return true; 1086 | } 1087 | } 1088 | } 1089 | 1090 | return true; // return good status if P4SimpleScc is disabled for this solution 1091 | } 1092 | 1093 | public void VerboseOutput(string message) 1094 | { 1095 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 1096 | 1097 | if (OutputPaneGuid == Guid.Empty) 1098 | { 1099 | CreateOutputPane(); 1100 | } 1101 | 1102 | string output = message; 1103 | 1104 | if (!message.EndsWith("\n")) // add newline character at end if there isn't one 1105 | { 1106 | output += "\n"; 1107 | } 1108 | 1109 | P4SimpleSccQueueOutput(output, true); 1110 | } 1111 | 1112 | /// 1113 | /// This function will create the P4SimpleScc Output window (for logging purposes for P4 commands) 1114 | /// 1115 | private void CreateOutputPane() 1116 | { 1117 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 1118 | 1119 | if (OutputPaneGuid != Guid.Empty) 1120 | { 1121 | return; 1122 | } 1123 | 1124 | // Create a new output pane. 1125 | IVsOutputWindow output = GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow; 1126 | 1127 | OutputPaneGuid = Guid.NewGuid(); 1128 | 1129 | bool visible = true; 1130 | bool clearWithSolution = false; 1131 | output.CreatePane(ref OutputPaneGuid, "P4SimpleScc", Convert.ToInt32(visible), Convert.ToInt32(clearWithSolution)); 1132 | } 1133 | 1134 | /// 1135 | /// This function will remove the P4SimpleScc Output window (for logging purposes for P4 commands) 1136 | /// 1137 | private void RemoveOutputPane() 1138 | { 1139 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 1140 | 1141 | IVsOutputWindow output = GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow; 1142 | 1143 | // Remove the existing output pane 1144 | output.DeletePane(ref OutputPaneGuid); 1145 | 1146 | OutputPaneGuid = Guid.Empty; 1147 | } 1148 | 1149 | /// 1150 | /// This function will output a line of text to the P4SimpleScc Output window (for logging purposes for P4 commands) 1151 | /// 1152 | /// text string to be output to the P4SimpleScc Output window (user must include \n character for newline at the end of the text string) 1153 | public static void P4SimpleSccQueueOutput(string text, bool bForceOutput = false) // user must include \n newline characters 1154 | { 1155 | if (bOutputEnabled || bForceOutput) 1156 | { 1157 | string OutputMessage = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss - ") + text; 1158 | 1159 | lock (OpututQueueLock) 1160 | { 1161 | OutputQueueList.Add(OutputMessage); 1162 | } 1163 | } 1164 | } 1165 | 1166 | private void OnTimerEvent(object sender, EventArgs e) 1167 | { 1168 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 1169 | 1170 | lock (OpututQueueLock) 1171 | { 1172 | IVsOutputWindow output = GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow; 1173 | 1174 | if (output != null) 1175 | { 1176 | // Retrieve the output pane. 1177 | output.GetPane(ref OutputPaneGuid, out IVsOutputWindowPane OutputPane); 1178 | 1179 | if (OutputPane != null) 1180 | { 1181 | foreach( string message in OutputQueueList) 1182 | { 1183 | OutputPane.OutputString(message); 1184 | } 1185 | } 1186 | } 1187 | 1188 | OutputQueueList.Clear(); 1189 | } 1190 | } 1191 | 1192 | private void WindowEventsOnWindowActivated(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) 1193 | { 1194 | MsVsShell.ThreadHelper.ThrowIfNotOnUIThread(); 1195 | 1196 | if (gotFocus.Kind != "Document") 1197 | { 1198 | return; //It's not a document (e.g. it's a tool window) 1199 | } 1200 | 1201 | WindowActivatedFilename = gotFocus.Document.FullName; 1202 | } 1203 | } 1204 | } 1205 | -------------------------------------------------------------------------------- /SharedProject/SccProviderService.cs: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF 5 | ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY 6 | IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR 7 | PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. 8 | 9 | ***************************************************************************/ 10 | 11 | // SccProviderService.cs : Implementation of Sample Source Control Provider Service 12 | 13 | using System; 14 | using System.IO; 15 | using System.Diagnostics; 16 | using System.Globalization; 17 | using System.Runtime.InteropServices; 18 | using Microsoft.VisualStudio.Shell.Interop; 19 | using Microsoft.VisualStudio; 20 | using Microsoft.VisualStudio.Shell; 21 | 22 | namespace P4SimpleScc 23 | { 24 | [Guid("B205A1B6-1000-4A1C-8680-97FD2219C692")] 25 | public class SccProviderService : 26 | IVsSccProvider, // Required for provider registration with source control manager 27 | IVsSccManager2, // Base source control functionality interface 28 | IVsSolutionEvents, // We'll register for solution events, these are useful for source control 29 | IVsSolutionEvents7, // We'll register for folder events, these are useful for source control 30 | IVsQueryEditQuerySave2, // Required to allow editing of controlled files 31 | IDisposable 32 | { 33 | // Whether the provider is active or not 34 | private bool _active = false; 35 | // The service and source control provider 36 | private SccProvider _sccProvider = null; 37 | // The cookie for solution events 38 | private uint _vsSolutionEventsCookie; 39 | 40 | 41 | #region SccProvider Service initialization/unitialization 42 | 43 | public SccProviderService(SccProvider sccProvider) 44 | { 45 | ThreadHelper.ThrowIfNotOnUIThread(); 46 | 47 | Debug.Assert(null != sccProvider); 48 | _sccProvider = sccProvider; 49 | 50 | // Subscribe to solution events 51 | IVsSolution sol = (IVsSolution)_sccProvider.GetService(typeof(SVsSolution)); 52 | sol.AdviseSolutionEvents(this, out _vsSolutionEventsCookie); 53 | Debug.Assert(VSConstants.VSCOOKIE_NIL != _vsSolutionEventsCookie); 54 | } 55 | 56 | /// 57 | /// Unregister from receiving Solution Events and Project Documents 58 | /// 59 | public void Dispose() 60 | { 61 | ThreadHelper.ThrowIfNotOnUIThread(); 62 | 63 | // Unregister from receiving solution events 64 | if (VSConstants.VSCOOKIE_NIL != _vsSolutionEventsCookie) 65 | { 66 | IVsSolution sol = (IVsSolution)_sccProvider.GetService(typeof(SVsSolution)); 67 | sol.UnadviseSolutionEvents(_vsSolutionEventsCookie); 68 | _vsSolutionEventsCookie = VSConstants.VSCOOKIE_NIL; 69 | } 70 | } 71 | 72 | #endregion // SccProvider 73 | 74 | 75 | //---------------------------------------------------------------------------- 76 | // IVsSccProvider specific functions 77 | //-------------------------------------------------------------------------------- 78 | #region IVsSccProvider interface functions 79 | 80 | public int SetActive() 81 | { 82 | ThreadHelper.ThrowIfNotOnUIThread(); 83 | 84 | Debug.WriteLine(string.Format(CultureInfo.CurrentUICulture, "The source control provider is now active")); 85 | 86 | _active = true; 87 | _sccProvider.OnActiveStateChange(true); 88 | 89 | return VSConstants.S_OK; 90 | } 91 | 92 | public int SetInactive() 93 | { 94 | ThreadHelper.ThrowIfNotOnUIThread(); 95 | 96 | Debug.WriteLine(string.Format(CultureInfo.CurrentUICulture, "The source control provider is now inactive")); 97 | 98 | _active = false; 99 | _sccProvider.OnActiveStateChange(false); 100 | 101 | return VSConstants.S_OK; 102 | } 103 | 104 | public int AnyItemsUnderSourceControl(out int pfResult) 105 | { 106 | if (!_active) 107 | { 108 | pfResult = 0; 109 | } 110 | else 111 | { 112 | // Although the parameter is an int, it's in reality a BOOL value, so let's return 0/1 values 113 | pfResult = (SccProvider.SolutionConfigType != 0) ? 1 : 0; 114 | } 115 | 116 | return VSConstants.S_OK; 117 | } 118 | 119 | #endregion // IVsSccProvider 120 | 121 | 122 | //---------------------------------------------------------------------------- 123 | // IVsSccManager2 specific functions 124 | //-------------------------------------------------------------------------------- 125 | #region IVsSccManager2 interface functions 126 | 127 | public int RegisterSccProject(IVsSccProject2 pscp2Project, string pszSccProjectName, string pszSccAuxPath, string pszSccLocalPath, string pszProvider) 128 | { 129 | ThreadHelper.ThrowIfNotOnUIThread(); 130 | 131 | if (pszProvider.CompareTo(_sccProvider.ProviderName)!=0) 132 | { 133 | // If the provider name controlling this project is not our provider, the user may be adding to a 134 | // solution controlled by this provider an existing project controlled by some other provider. 135 | // We'll deny the registration with scc in such case. 136 | return VSConstants.E_FAIL; 137 | } 138 | 139 | if (pscp2Project == null) 140 | { 141 | Debug.WriteLine(string.Format(CultureInfo.CurrentUICulture, "Solution {0} is registering with source control", _sccProvider.GetSolutionFileName())); 142 | } 143 | 144 | return VSConstants.S_OK; 145 | } 146 | 147 | public int UnregisterSccProject(IVsSccProject2 pscp2Project) 148 | { 149 | return VSConstants.S_OK; 150 | } 151 | 152 | public int GetSccGlyph(int cFiles, string[] rgpszFullPaths, VsStateIcon[] rgsiGlyphs, uint[] rgdwSccStatus) 153 | { 154 | return VSConstants.S_OK; 155 | } 156 | 157 | public int GetSccGlyphFromStatus(uint dwSccStatus, VsStateIcon[] psiGlyph) 158 | { 159 | psiGlyph[0] = VsStateIcon.STATEICON_BLANK; 160 | return VSConstants.S_OK; 161 | } 162 | 163 | public int IsInstalled(out int pbInstalled) 164 | { 165 | // All source control packages should always return S_OK and set pbInstalled to nonzero 166 | pbInstalled = 1; 167 | return VSConstants.S_OK; 168 | } 169 | 170 | public int BrowseForProject(out string pbstrDirectory, out int pfOK) 171 | { 172 | // Obsolete method 173 | pbstrDirectory = null; 174 | pfOK = 0; 175 | return VSConstants.E_NOTIMPL; 176 | } 177 | 178 | public int CancelAfterBrowseForProject() 179 | { 180 | // Obsolete method 181 | return VSConstants.E_NOTIMPL; 182 | } 183 | 184 | #endregion // IVsSccManager2 185 | 186 | 187 | //---------------------------------------------------------------------------- 188 | // IVsSolutionEvents specific functions 189 | //-------------------------------------------------------------------------------- 190 | #region IVsSolutionEvents interface functions 191 | 192 | public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) 193 | { 194 | return VSConstants.S_OK; 195 | } 196 | 197 | public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) 198 | { 199 | return VSConstants.S_OK; 200 | } 201 | 202 | public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) 203 | { 204 | return VSConstants.S_OK; 205 | } 206 | 207 | public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) 208 | { 209 | return VSConstants.S_OK; 210 | } 211 | 212 | public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) 213 | { 214 | return VSConstants.S_OK; 215 | } 216 | 217 | public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) 218 | { 219 | return VSConstants.S_OK; 220 | } 221 | 222 | public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) 223 | { 224 | ThreadHelper.ThrowIfNotOnUIThread(); 225 | _sccProvider.AfterOpenSolutionOrFolder(); 226 | 227 | _sccProvider.bIsWorkspace = false; // we opened a solution, not a folder 228 | 229 | return VSConstants.S_OK; 230 | } 231 | 232 | public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) 233 | { 234 | return VSConstants.S_OK; 235 | } 236 | 237 | public int OnBeforeCloseSolution(object pUnkReserved) 238 | { 239 | ThreadHelper.ThrowIfNotOnUIThread(); 240 | return OnBeforeCloseSolutionOrFolder(); 241 | } 242 | 243 | public int OnAfterCloseSolution(object pUnkReserved) 244 | { 245 | return VSConstants.S_OK; 246 | } 247 | 248 | #endregion // IVsSolutionEvents 249 | 250 | 251 | //---------------------------------------------------------------------------- 252 | // IVsSolutionEvents7 specific functions 253 | //-------------------------------------------------------------------------------- 254 | #region IVsSolutionEvents7 interface functions 255 | public void OnAfterOpenFolder(string folderPath) 256 | { 257 | ThreadHelper.ThrowIfNotOnUIThread(); 258 | _sccProvider.AfterOpenSolutionOrFolder(); 259 | 260 | _sccProvider.bIsWorkspace = true; // we opened a folder, not a solution 261 | } 262 | 263 | public void OnQueryCloseFolder(string folderPath, ref int pfCancel) 264 | { 265 | } 266 | 267 | public void OnBeforeCloseFolder(string folderPath) 268 | { 269 | ThreadHelper.ThrowIfNotOnUIThread(); 270 | OnBeforeCloseSolutionOrFolder(); 271 | } 272 | 273 | public void OnAfterCloseFolder(string folderPath) 274 | { 275 | } 276 | 277 | [Obsolete("This API is no longer supported by Visual Studio.")] 278 | public void OnAfterLoadAllDeferredProjects() 279 | { } 280 | #endregion // IVsSolutionEvents7 281 | 282 | //---------------------------------------------------------------------------- 283 | // IVsQueryEditQuerySave2 specific functions 284 | //-------------------------------------------------------------------------------- 285 | #region IVsQueryEditQuerySave2 interface functions 286 | 287 | public int QueryEditFiles(uint rgfQueryEdit, int cFiles, string[] rgpszMkDocuments, uint[] rgrgf, VSQEQS_FILE_ATTRIBUTE_DATA[] rgFileInfo, out uint pfEditVerdict, out uint prgfMoreInfo) 288 | { 289 | ThreadHelper.ThrowIfNotOnUIThread(); 290 | 291 | // Initialize output variables 292 | pfEditVerdict = (uint)tagVSQueryEditResult.QER_EditOK; 293 | prgfMoreInfo = 0; 294 | 295 | if ((rgfQueryEdit & (uint)tagVSQueryEditFlags.QEF_ReportOnly) != 0) // ignore any report query 296 | { 297 | return VSConstants.S_OK; 298 | } 299 | 300 | // In non-UI mode just allow the edit, because the user cannot be asked what to do with the file 301 | if (_sccProvider.InCommandLineMode()) 302 | { 303 | return VSConstants.S_OK; 304 | } 305 | 306 | if (SccProvider.SolutionConfigType != 0) // if not disabled... 307 | { 308 | if (_sccProvider.bCheckOutOnEdit) 309 | { 310 | try 311 | { 312 | //Iterate through all the files 313 | for (int iFile = 0; iFile < cFiles; iFile++) 314 | { 315 | uint fEditVerdict = (uint)tagVSQueryEditResult.QER_EditNotOK; // assume not okay until known otherwise 316 | uint fMoreInfo = 0; 317 | 318 | bool fileExists = File.Exists(rgpszMkDocuments[iFile]); 319 | 320 | if (!fileExists) // if the file doesn't exist then it's okay to edit as this is a brand new file (not saved yet) 321 | { 322 | fEditVerdict = (uint)tagVSQueryEditResult.QER_EditOK; 323 | } 324 | else 325 | { 326 | bool bIsCheckoutOkay = true; 327 | bool bIsCheckedOut = SccProvider.IsCheckedOut(rgpszMkDocuments[iFile], out string stderr); 328 | 329 | bool bShouldIgnoreStatus = false; 330 | if (stderr.Contains("is not under client's root") || stderr.Contains("not in client view") || stderr.Contains("no such file")) // if file is outside client's workspace, or file does not exist in source control... 331 | { 332 | bShouldIgnoreStatus = true; // don't prevent file from being modified (since not under workspace or not under source control) 333 | } 334 | 335 | if (_sccProvider.bPromptForCheckout && !bIsCheckedOut && !bShouldIgnoreStatus) // if prompt for permission to check out and file is not checked out... 336 | { 337 | IVsUIShell uiShell = ServiceProvider.GlobalProvider.GetService(typeof(SVsUIShell)) as IVsUIShell; 338 | 339 | string message = String.Format("Do you want to check out: '{0}'?", rgpszMkDocuments[iFile]); 340 | if (!VsShellUtilities.PromptYesNo(message, "Warning", OLEMSGICON.OLEMSGICON_WARNING, uiShell)) 341 | { 342 | bIsCheckoutOkay = false; 343 | 344 | pfEditVerdict = (uint)tagVSQueryEditResult.QER_NoEdit_UserCanceled; 345 | prgfMoreInfo = (uint)tagVSQueryEditResultFlags.QER_CheckoutCanceledOrFailed; 346 | } 347 | } 348 | 349 | if (bIsCheckoutOkay) 350 | { 351 | if (bIsCheckedOut || bShouldIgnoreStatus || SccProvider.CheckOutFile(rgpszMkDocuments[iFile])) // if file is already checked out or if we were able to check out the file, then edit is okay 352 | { 353 | fEditVerdict = (uint)tagVSQueryEditResult.QER_EditOK; 354 | fMoreInfo = (uint)tagVSQueryEditResultFlags.QER_MaybeCheckedout; 355 | } 356 | else 357 | { 358 | // if we couldn't check out the file, it's okay to edit it in memory, when the file is saved, it will try to check it out and dislay a "Save As" dialog if it couldn't be checked out 359 | fEditVerdict = (uint)tagVSQueryEditResult.QER_EditOK; 360 | fMoreInfo = (uint)tagVSQueryEditResultFlags.QER_InMemoryEdit; 361 | } 362 | } 363 | } 364 | 365 | // It's a bit unfortunate that we have to return only one set of flags for all the files involved in the operation 366 | // The edit can continue if all the files were approved for edit 367 | prgfMoreInfo |= fMoreInfo; 368 | pfEditVerdict |= fEditVerdict; 369 | } 370 | } 371 | catch(Exception) 372 | { 373 | // If an exception was caught, do not allow the edit 374 | pfEditVerdict = (uint)tagVSQueryEditResult.QER_EditNotOK; 375 | prgfMoreInfo = (uint)tagVSQueryEditResultFlags.QER_EditNotPossible; 376 | } 377 | } 378 | } 379 | 380 | return VSConstants.S_OK; 381 | } 382 | 383 | public int QuerySaveFiles(uint rgfQuerySave, int cFiles, string[] rgpszMkDocuments, uint[] rgrgf, VSQEQS_FILE_ATTRIBUTE_DATA[] rgFileInfo, out uint pdwQSResult) 384 | { 385 | ThreadHelper.ThrowIfNotOnUIThread(); 386 | 387 | // Initialize output variables 388 | // It's a bit unfortunate that we have to return only one set of flags for all the files involved in the operation 389 | // The last file will win setting this flag 390 | pdwQSResult = (uint)tagVSQuerySaveResult.QSR_SaveOK; 391 | 392 | // In non-UI mode attempt to silently flip the attributes of files or check them out 393 | // and allow the save, because the user cannot be asked what to do with the file 394 | if (_sccProvider.InCommandLineMode()) 395 | { 396 | rgfQuerySave = rgfQuerySave | (uint)tagVSQuerySaveFlags.QSF_SilentMode; 397 | } 398 | 399 | if (SccProvider.SolutionConfigType != 0) // if not disabled... 400 | { 401 | if (!_sccProvider.bCheckOutOnEdit) 402 | { 403 | try 404 | { 405 | //Iterate through all the files 406 | for (int iFile = 0; iFile < cFiles; iFile++) 407 | { 408 | bool fileExists = File.Exists(rgpszMkDocuments[iFile]); // if the file doesn't exist, then we don't need to check it out (it's a brand new file) 409 | 410 | if (fileExists) 411 | { 412 | bool bIsCheckoutOkay = true; 413 | bool bIsCheckedOut = SccProvider.IsCheckedOut(rgpszMkDocuments[iFile], out string stderr); 414 | 415 | bool bShouldIgnoreStatus = false; 416 | if (stderr.Contains("is not under client's root") || stderr.Contains("not in client view") || stderr.Contains("no such file")) // if file is outside client's workspace, or file does not exist in source control... 417 | { 418 | bShouldIgnoreStatus = true; // don't prevent file from being modified (since not under workspace or not under source control) 419 | } 420 | 421 | if (_sccProvider.bPromptForCheckout && !bIsCheckedOut && !bShouldIgnoreStatus) // if prompt for permission to check out and file is not checked out... 422 | { 423 | IVsUIShell uiShell = ServiceProvider.GlobalProvider.GetService(typeof(SVsUIShell)) as IVsUIShell; 424 | 425 | string message = String.Format("Do you want to check out: '{0}'?", rgpszMkDocuments[iFile]); 426 | if (!VsShellUtilities.PromptYesNo(message, "Warning", OLEMSGICON.OLEMSGICON_WARNING, uiShell)) 427 | { 428 | bIsCheckoutOkay = false; 429 | pdwQSResult = (uint)tagVSQuerySaveResult.QSR_NoSave_UserCanceled; 430 | } 431 | } 432 | 433 | if (bIsCheckoutOkay && !bIsCheckedOut && !bShouldIgnoreStatus && !SccProvider.CheckOutFile(rgpszMkDocuments[iFile])) // if file exists and we couldn't check it out, then it's not okay to save the file 434 | { 435 | pdwQSResult = (uint)tagVSQuerySaveResult.QSR_ForceSaveAs; // force a "Save As" dialog to save the file 436 | } 437 | } 438 | } 439 | } 440 | catch (Exception) 441 | { 442 | // If an exception was caught, do not allow the save 443 | pdwQSResult = (uint)tagVSQuerySaveResult.QSR_NoSave_Cancel; 444 | } 445 | } 446 | } 447 | 448 | return VSConstants.S_OK; 449 | } 450 | 451 | public int QuerySaveFile(string pszMkDocument, uint rgf, VSQEQS_FILE_ATTRIBUTE_DATA[] pFileInfo, out uint pdwQSResult) 452 | { 453 | ThreadHelper.ThrowIfNotOnUIThread(); 454 | 455 | // Delegate to the other QuerySave function 456 | string[] rgszDocuements = new string[1]; 457 | uint[] rgrgf = new uint[1]; 458 | rgszDocuements[0] = pszMkDocument; 459 | rgrgf[0] = rgf; 460 | 461 | return QuerySaveFiles(((uint)tagVSQuerySaveFlags.QSF_DefaultOperation), 1, rgszDocuements, rgrgf, pFileInfo, out pdwQSResult); 462 | } 463 | 464 | public int DeclareReloadableFile(string pszMkDocument, uint rgf, VSQEQS_FILE_ATTRIBUTE_DATA[] pFileInfo) 465 | { 466 | return VSConstants.S_OK; 467 | } 468 | 469 | public int DeclareUnreloadableFile(string pszMkDocument, uint rgf, VSQEQS_FILE_ATTRIBUTE_DATA[] pFileInfo) 470 | { 471 | return VSConstants.S_OK; 472 | } 473 | 474 | public int OnAfterSaveUnreloadableFile(string pszMkDocument, uint rgf, VSQEQS_FILE_ATTRIBUTE_DATA[] pFileInfo) 475 | { 476 | return VSConstants.S_OK; 477 | } 478 | 479 | public int IsReloadable(string pszMkDocument, out int pbResult) 480 | { 481 | // Since we're not tracking which files are reloadable and which not, consider everything reloadable 482 | pbResult = 1; 483 | 484 | return VSConstants.S_OK; 485 | } 486 | 487 | public int BeginQuerySaveBatch() 488 | { 489 | return VSConstants.S_OK; 490 | } 491 | 492 | public int EndQuerySaveBatch() 493 | { 494 | return VSConstants.S_OK; 495 | } 496 | 497 | #endregion // IVsQueryEditQuerySave2 498 | 499 | private int OnBeforeCloseSolutionOrFolder() 500 | { 501 | ThreadHelper.ThrowIfNotOnUIThread(); 502 | 503 | if (_sccProvider.GetSolutionFileName() != null) 504 | { 505 | if (_sccProvider.solutionDirectory != null && _sccProvider.solutionDirectory.Length > 0) 506 | { 507 | string message = String.Format("Unloading solution: {0}\n", _sccProvider.solutionFile); 508 | SccProvider.P4SimpleSccQueueOutput(message); 509 | } 510 | } 511 | 512 | // set all configuration settings back to uninitialized 513 | SccProvider.SolutionConfigType = 0; 514 | _sccProvider.bCheckOutOnEdit = true; 515 | _sccProvider.bPromptForCheckout = false; 516 | 517 | _sccProvider.P4Port = ""; 518 | _sccProvider.P4User = ""; 519 | _sccProvider.P4Client = ""; 520 | 521 | _sccProvider.solutionDirectory = ""; 522 | _sccProvider.solutionFile = ""; 523 | 524 | _sccProvider.bSolutionLoadedOutputDone = false; 525 | 526 | _sccProvider.bIsWorkspace = false; 527 | _sccProvider.WindowActivatedFilename = ""; 528 | 529 | _sccProvider.bReadUserOptionsCalled = false; 530 | _sccProvider.bUserSettingsWasEmpty = false; 531 | 532 | return VSConstants.S_OK; 533 | } 534 | 535 | /// 536 | /// Returns whether this source control provider is the active scc provider. 537 | /// 538 | public bool Active 539 | { 540 | get { return _active; } 541 | } 542 | 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /SharedProject/SharedProject.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 64cd1d5b-0e68-4a05-9535-bf12013dd743 7 | 8 | 9 | SharedProject 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SharedProject/SharedProject.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64cd1d5b-0e68-4a05-9535-bf12013dd743 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /img/CheckOutDocument.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/CheckOutDocument.png -------------------------------------------------------------------------------- /img/CheckOutFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/CheckOutFile.png -------------------------------------------------------------------------------- /img/FileCheckedOut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/FileCheckedOut.png -------------------------------------------------------------------------------- /img/OutputWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/OutputWindow.png -------------------------------------------------------------------------------- /img/SolutionConfigurationDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/SolutionConfigurationDialog.png -------------------------------------------------------------------------------- /img/SolutionConfigurationMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/SolutionConfigurationMenu.png -------------------------------------------------------------------------------- /img/Tools_Options_SourceControl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botman99/P4SimpleScc/29c5785b53b7e03bf085e71f39e10ef0de6fb997/img/Tools_Options_SourceControl.png --------------------------------------------------------------------------------