📜 ⬆️ ⬇️

Xcode: plugins plugins



Interested in publishing "Writing your Xcode plugin" decided to write a simple time tracker for Xcode. The process I went through is the essence of this article. In it, we will analyze a few plugins that will help write other plugins faster and more efficiently.

The main idea of ​​any plug-in with the interface is that it integrates into the Xcode UI and looks as native as possible. But as soon as we look at the Xcode window, the question immediately arises: “How to understand where is the object and how can we integrate into the one we need?” So the first plugin appears on our way. We will write a simple plugin that will be loaded into Xcode and say where the object is located.

First plugin


First, install the template for the plugins and create the plugin. Then everything is simple: in order to understand what Xcode consists of, it is necessary to output its objects to the log. To do this, you can write logs to a file or display them in dialogs and close them each time. Oh, how it would be convenient to display this information directly to the Xcode console, you say? Well, nothing, we will solve this problem with our second plugin, but more on that a little bit later. In the meantime, in order not to understand the location of the objects from the logs, we will take a screenshot of the Xcode window with the filled area of ​​objects and save it all to a file.
')
Our first plugin will consist of only one method, which will run through all the 'NSView', color them and make a screenshot of the current window. It sounds simple, but in practice there is one small nuance: some of the 'NSView' objects in Xcode can have nothing but a single 'contentView' and we cannot add our own to it. But it does not matter, we will simply ignore such objects and delve into them.

Method text
- (void)viewSnap:(NSView *)view { static int i = 0; // text field    NSTextField *name = [[NSTextField alloc] initWithFrame:view.bounds]; name.backgroundColor = [NSColor colorWithDeviceRed:rand()%255/255.f green:rand()%255/255.f blue:rand()%255/255.f alpha:0.3]; name.textColor = [NSColor blackColor]; NSString *string = view.className?:NSStringFromClass(view.class); name.stringValue = string?:@"unknown"; if (![view respondsToSelector:@selector(contentView)]) {//    text field? [view addSubview:name]; //      NSImage *captureImage = [[NSImage alloc] initWithData:[[NSApp keyWindow].contentView dataWithPDFInsideRect:[[NSApp keyWindow].contentView bounds]]]; [[captureImage TIFFRepresentation] writeToFile:[NSString stringWithFormat:@"%@%d.png", self.dirPath, i++] atomically:YES]; [name removeFromSuperview]; } for (NSView *v in view.subviews) { if ([v respondsToSelector:@selector(contentView)]) { NSView *vv = [v performSelector:@selector(contentView) withObject:nil]; [self viewSnap:vv]; } else { [self viewSnap:v]; } } } 


And the method call:
 - (void)doMenuAction { NSWindow * window = [NSApp keyWindow]; srand(time(NULL)); [self viewSnap:window.contentView]; } 


After that, you can open the folder where you saved the pictures and admire. It is worth playing around with the size of the text depending on the size of the 'NSView'.

Here is the result:




But the results are more beautiful and after manual processing:

Some pictures
image
image
image
image
image

image
image
image

Immediately proceed to the second plugin. We will display information from the plugins in the Xcode console.

Second plugin


From the first plugin, we learned that the console in Xcode is the 'IDEConsoleTextView' class. But what kind of class is it and what methods does it have? To learn this, there are several ways:
1. Write a plugin that finds the console in the window and displays all its methods in a file.
2. Using class-dump, pull all the drivers from private frameworks and try to find this class there.
3. Go to the page of the project XVim and take all private headers there.

Absolutely no matter which way you go, the main thing is that you find out that the console is a subclass from 'NSTextView' and that it contains the following methods: insertText:, insertNewLine:. Great, now we can find the console in the window and write down the lines of information we need there.

Now we need to add a button that is responsible for the logging mode and get information from other plugins.

After the first plug-in, we know that next to the console there is a 'DVTScopeBarView' containing the controls. There we put our button. We look at the 'DVTScopeBarView' header and see that the class contains the addViewOnRight: method. Very good, so we can add our button to the bar and not worry about the position of other elements.

Search IDEConsoleTextView and DVTScopeBarView
 - (IDEConsoleTextView *)consoleViewInMainView:(NSView *)mainView { for (NSView *childView in mainView.subviews) { if ([childView isKindOfClass:NSClassFromString(@"IDEConsoleTextView")]) { return (IDEConsoleTextView *)childView; } else { NSView *v = [self consoleViewInMainView:childView]; if ([v isKindOfClass:NSClassFromString(@"IDEConsoleTextView")]) { return (IDEConsoleTextView *)v; } } } return nil; } - (DVTScopeBarView *)scopeBarViewInView:(NSView *)view { for (NSView *childView in view.subviews) { if ([childView isKindOfClass:NSClassFromString(@"DVTScopeBarView")]) { return (DVTScopeBarView *)childView; } else { NSView *v = [self scopeBarViewInView:childView]; if ([v isKindOfClass:NSClassFromString(@"DVTScopeBarView")]) { return (DVTScopeBarView *)v; } } } return nil; } - (void)someMethod { NSWindow *window = [NSApp keyWindow]; NSView *contentView = window.contentView; IDEConsoleTextView *console = [self consoleViewInMainView:contentView];//  DVTScopeBarView *scopeBar = nil; NSView *parent = console.superview; while (!scopeBar) { if (!parent) break; scopeBar = [self scopeBarViewInView:parent]; parent = parent.superview; } //...     } 


Now we have added a button to the bar and we can find a console on the window. It remains to somehow get information from other plugins and display it. The easiest option: use 'NSNotificationCenter'. Since plugins are loaded on Xcode and I can catch notifications from it, you can send and catch them between plugins. Just subscribe to the notifications we need and tell the console to display the log. To do this, we create a function in client files (files that other plugins will use) that will send the notifications we need and catch them in our plugin.

Log functions and console display
 void PluginLogWithName(NSString *pluginName, NSString *format, ...) { NSString *name = @""; if (pluginName.length) { name = pluginName; } va_list argumentList; va_start(argumentList, format); NSString *string = [NSString stringWithFormat:@"%@ Plugin Console %@: ", [NSDate date], name]; NSString* msg = [[NSString alloc] initWithFormat:[NSString stringWithFormat:@"%@%@",string, format] arguments:argumentList]; NSMutableAttributedString *logString = [[NSMutableAttributedString alloc] initWithString:msg attributes:nil]; [logString setAttributes:[NSDictionary dictionaryWithObject:[NSFont fontWithName:@"Helvetica-Bold" size:15.f] forKey:NSFontAttributeName] range:NSMakeRange(0, string.length)]; [[NSNotificationCenter defaultCenter] postNotificationName:PluginLoggerShouldLogNotification object:logString]; va_end(argumentList); } - (void)addLog:(NSNotification *)notification {//  for (NSWindow *window in [NSApp windows]) {//     NSView *contentView = window.contentView; IDEConsoleTextView *console = [self consoleViewInMainView:contentView];//  console.logMode = 1;//     [console insertText:notification.object];//  [console insertNewline:@""];//     } } 

As you can see, the log can be output in absolutely any font.


So the second plugin is ready. Full source code can be viewed here . The plugin looks like this:



Third plugin


Eh, it would be nice if the plugins were near and access to them was the same as the sections in the left pane of Xcode ...
Let's add our own panel in Xcode so that anyone can add their own plugin without thinking about integrating the plugin with the Xcode window.

Both previous plugins will be useful here. At least they were useful to me. You do not have to suffer with logs, catch endless crashes, understand them and delve into the endless files of headers. I'll just tell you about the results.

We have a window 'NSToolbar', where we will add a button. The most difficult thing is that the toolbar does not have methods to directly add an element. Its elements are the "taxis" delegate, which we, of course, cannot override. The only method that has a toolbar is to add items: insertItemWithItemIdentifier: atIndex:, but the item itself generates a delegate. The only way out is to see who is the delegate. Maybe there are some approaches to it? We deduce the delegate class and get the class 'IDEToolbarDelegate'. Ok, now we go to private leaders, which we received by class-dump or from XVim, and we are looking for this class there. Immediately we see the properties of interest to us in this class: toolbarItemProviders and allowedItemIdentifiers . Presumably, our delegate contains a dictionary of objects that provide the elements. Print the current content of toolbarItemProviders in the logs and see something like this dictionary:

 { "some_id":<IDEToolbarItemProxy class>, "some_other_id":<IDEToolbarItemProxy class>, } 

Great, now we have another clue - this is the 'IDEToolbarItemProxy' class. We also look at its interface in the header and see that it is initialized with an identifier (most likely the element identifier in 'NSToolbar') and has the providerClass property. But what kind of providerClass and how do we implement it? To understand what a given class should contain, there are two ways:
1. Derive these classes and their methods from all providers from the dictionary toolbarItemProviders ;
2. To write an empty class, add it to the dictionary and catch the crashes from Xcode, telling us which methods are missing.

I went the second way, although the first, I think more correct. But when I wrote this plugin, for some reason this idea did not come to me.

So, create a class and add it to our delegate:

Code
 IDEToolbarDelegate *delegate = (IDEToolbarDelegate *)window.toolbar.delegate;//    if ([delegate isKindOfClass:NSClassFromString(@"IDEToolbarDelegate")]) { IDEToolbarItemProxy * proxy = [[NSClassFromString(@"IDEToolbarItemProxy") alloc] initWithItemIdentifier:PluginButtonIdentifier];//      proxy.providerClass = [PluginButtonProvider class];//    ( ) NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:delegate.toolbarItemProviders];//    [d setObject:proxy forKey:proxy.toolbarItemIdentifier];//   delegate.toolbarItemProviders = d;//   NSMutableArray *ar = [NSMutableArray arrayWithArray:delegate.allowedItemIdentifiers];//     [ar addObject:proxy.toolbarItemIdentifier]; delegate.allowedItemIdentifiers = ar; [window.toolbar insertItemWithItemIdentifier:PluginButtonIdentifier atIndex:window.toolbar.items.count];//      } 


Install the plugin, restart Xcode and immediately catch the crash. We look at the logs and understand that our class needs the + (id) itemForItemIdentifier method: (id) arg1 forToolbarInWindow: (id) arg2 . This method is described in the 'IDEToolbarItemProvider' protocol. Remove the plugin, run Xcode and add this method. By the name of the method, it is clear that at the entrance we get an identifier and a window, and at the output we must receive an object. By similar manipulations, namely by trial and error, through the N-th number of kreshy and Xcode restarts, you can find out that this is an object of the class 'DVTViewControllerToolbarItem'. And it in turn is initialized with the class 'DVTGenericButtonViewController'. The object 'DVTGenericButtonViewController' itself has this initialization:
Prior to version 6 of Xcode: initWithButton: actionBlock: itemIdentifier: window:
From the 6th version: initWithButton: actionBlock: setupTeardownBlock: itemIdentifier: window:
By the name of the method it is clear that he needs a button, a block that is called when it is pressed, an identifier and a window.

Create a simple button and initialize the controllers we need:

Code droplet
 DVTGenericButtonViewController *bvc = [(DVTGenericButtonViewController*)[NSClassFromString(@"DVTGenericButtonViewController") alloc] initWithButton:button actionBlock:^(NSButton *sender){} setupTeardownBlock:nil itemIdentifier:PluginButtonIdentifier window:arg2]; DVTViewControllerToolbarItem *c = [ NSClassFromString(@"DVTViewControllerToolbarItem") toolbarItemWithViewController:bvc]; 


Install the plugin and restart Xcode. Now our button is added to Xcode. It remains to write a handler for our button. When you click on a button, we want the right panel to open if it is not open, and our object is added to this panel. Open the right panel and run our first plugin. After viewing its results, it will become clear that the panel is a 'DVTSplitView' object. In addition, it is necessary to determine how to programmatically open the right panel, if it is hidden. To do this, output all 'NSToolbarItem' from the toolbar of our window to the log. We know that the object that we need is the last one (if our button has not yet been added). We take the required 'NSToolbarItem' and see who controls it, that is, we look at the 'target' property. The target of our 'NSToolbarItem' is an object of the class '_IDEWorkspacePartsVisibilityToolbarViewController'. We do not need to look at its interface, since we only need it in order to find the necessary 'NSToolbarItem' in the window in the future (all of a sudden they will be placed in another sort or someone will add an item to us). All preparations are ready, now we can display the right panel, find it in the window and add our object to it.

Button processing
 NSWindow *window = arg2; NSToolbarItem *item = nil; for (NSToolbarItem *it in [[window toolbar] items]) {//     if ([it.target isMemberOfClass:NSClassFromString(@"_IDEWorkspacePartsVisibilityToolbarViewController")]) { item = it; break; } } NSSegmentedControl *control = (NSSegmentedControl *)item.view;//     if ([sender state] == NSOnState) {//   if (![control isSelectedForSegment:2]) {//    [control setSelected:YES forSegment:2];//     [item.target performSelector:item.action withObject:control];//   } DVTSplitView *splitView = [PluginButtonProvider splitViewForWindow:window];//   PanelView *myView = [[PluginPanel sharedPlugin] myViewForWindow:window];///     myView.frame = splitView.bounds;//  [myView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; for (NSView *sub in splitView.subviews) {//      [sub setHidden:YES]; } [splitView addSubview:myView];//   } else { DVTSplitView *splitView = [PluginButtonProvider splitViewForWindow:window];//  PanelView *myView = [[PluginPanel sharedPlugin] myViewForWindow:window];///   [myView removeFromSuperview];//     for (NSView *sub in splitView.subviews) {//      [sub setHidden:NO]; } } 


Our object will be the object 'NSView', which will contain 'DVTChooserView' and the usual 'NSView', to which the content of the plugin will be added. Why 'DVTChooserView'? I would like the panel to be as close as possible to the Xcode window. To do this, run the first plugin, look at the left panel and find that 'DVTChooserView' is just what we need. 'DVTChooserView' contains 'NSMatrix' with buttons and a good delegate that allows us to determine when this or that button was turned on / off. Also, this object accepts 'DVTChoice' objects as input and manipulates them. This is most convenient, given that 'DVTChoice' contains an icon, a signature and an object that will process this object.

Our object and adding items
 //   DVTChooserView _chooserView = [[NSClassFromString(@"DVTChooserView") alloc] initWithFrame:NSZeroRect]; _chooserView.allowsEmptySelection = NO; _chooserView.allowsMultipleSelection = NO; _chooserView.delegate = self; //  - (void)chooserView:(DVTChooserView *)view userWillSelectChoices:(NSArray *)choices { DVTChoice *choice = [choices lastObject];//   self.contentView = [[choice representedObject] view];//   } // DVTChoice    DVTChoice *plugin = note.object;//    if (plugin) { NSWindow *window = [[note userInfo] objectForKey:PluginPanelWindowNotificationKey];//     PanelView *panel = [self myViewForWindow:window];//     [panel.chooserView.mutableChoices addObject:plugin];//   DVTChooserView if (!panel.contentView) { panel.contentView = [[[[panel.chooserView mutableChoices] lastObject] representedObject] view];//   ,    } } 


That's all. We walked through the most interesting places of our third plugin. All sources are here .

Add a plugin to our panel


We have just added a whole panel to Xcode. Now let's fill it up with something.

Due to the fact that we do not need to understand the intricacies of Xcode, we can add our plugin to the panel with just three lines of code.

Three magic lines
 NSImage *image = [[NSImage alloc] initWithContentsOfFile:[[NSBundle bundleForClass:[self class]] pathForImageResource:@"plugin_icon"]];//     // ,    . 1-  TPViewController *c = [[TPViewController alloc] initWithNibName:@"TPView" bundle:[NSBundle bundleForClass:self.class]]; // DVTChoice    . 2-  DVTChoice *choice = [[NSClassFromString(@"DVTChoice") alloc] initWithTitle:@"Time" toolTip:@"Time management plugin" image:image representedObject:c]; //   ,        . 3-  PluginPanelAddPlugin(choice, [[note userInfo] objectForKey:PluginPanelWindowNotificationKey]); 


Now we have our own panel in the Xcode window and we can add any plugin to it. Now part of the plug-ins can be located in one place.

Finally - an example of using the panel - a simple time tracker for Xcode.

Timeplugin

Source: https://habr.com/ru/post/256015/


All Articles