Add a tab view and segmented control for selection, for Variables and Breakpoints.
[macgdbp.git] / Source / DebuggerController.m
1 /*
2 * MacGDBp
3 * Copyright (c) 2007 - 2011, Blue Static <http://www.bluestatic.org>
4 *
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
10 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17 #import "DebuggerController.h"
18
19 #import "AppDelegate.h"
20 #import "BSSourceView.h"
21 #import "BreakpointManager.h"
22 #import "DebuggerBackEnd.h"
23 #import "DebuggerModel.h"
24 #import "EvalController.h"
25 #import "PreferenceNames.h"
26 #import "NSXMLElementAdditions.h"
27 #import "StackFrame.h"
28
29 @interface DebuggerController (Private)
30 - (void)updateSourceViewer;
31 - (void)expandVariables;
32 @end
33
34 @implementation DebuggerController
35
36 @synthesize connection, sourceViewer, inspector;
37
38 /**
39 * Initializes the window controller and sets the connection using preference
40 * values
41 */
42 - (id)init
43 {
44 if (self = [super initWithWindowNibName:@"Debugger"])
45 {
46 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
47
48 _model = [[DebuggerModel alloc] init];
49 [_model addObserver:self
50 forKeyPath:@"connected"
51 options:NSKeyValueObservingOptionNew
52 context:nil];
53
54 connection = [[DebuggerBackEnd alloc] initWithPort:[defaults integerForKey:kPrefPort]
55 autoAttach:[defaults boolForKey:kPrefDebuggerAttached]];
56 connection.model = _model;
57 expandedVariables = [[NSMutableSet alloc] init];
58 [[self window] makeKeyAndOrderFront:nil];
59 [[self window] setDelegate:self];
60
61 if ([defaults boolForKey:kPrefInspectorWindowVisible])
62 [inspector orderFront:self];
63 }
64 return self;
65 }
66
67 /**
68 * Dealloc
69 */
70 - (void)dealloc
71 {
72 [connection release];
73 [_model release];
74 [expandedVariables release];
75 [super dealloc];
76 }
77
78 /**
79 * Before the display get's comfortable, set up the NSTextView to scroll horizontally
80 */
81 - (void)awakeFromNib
82 {
83 [[self window] setExcludedFromWindowsMenu:YES];
84 [[self window] setTitle:[NSString stringWithFormat:@"MacGDBp @ %d", [connection port]]];
85 [sourceViewer setDelegate:self];
86 [stackArrayController setSortDescriptors:@[ [[[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES] autorelease] ]];
87 [stackArrayController addObserver:self
88 forKeyPath:@"selectedObjects"
89 options:NSKeyValueObservingOptionNew
90 context:nil];
91 [stackArrayController addObserver:self
92 forKeyPath:@"selection.source"
93 options:NSKeyValueObservingOptionNew
94 context:nil];
95 [[_segmentControl cell] addObserver:self
96 forKeyPath:@"selectedSegment"
97 options:NSKeyValueObservingOptionNew
98 context:nil];
99 self.connection.autoAttach = [attachedCheckbox_ state] == NSOnState;
100
101 [self updateSegmentControl];
102
103 }
104
105 /**
106 * Key-value observation routine.
107 */
108 - (void)observeValueForKeyPath:(NSString*)keyPath
109 ofObject:(id)object
110 change:(NSDictionary<NSString*,id>*)change
111 context:(void*)context {
112 if (object == stackArrayController && [keyPath isEqualToString:@"selectedObjects"]) {
113 for (StackFrame* frame in stackArrayController.selectedObjects)
114 [connection loadStackFrame:frame];
115 } else if (object == stackArrayController && [keyPath isEqualToString:@"selection.source"]) {
116 [self updateSourceViewer];
117 } else if (object == _model && [keyPath isEqualToString:@"connected"]) {
118 if ([change[NSKeyValueChangeNewKey] boolValue])
119 [self debuggerConnected];
120 else
121 [self debuggerDisconnected];
122 } else if (object == self.segmentControl.cell) {
123 [self.tabView selectTabViewItemAtIndex:self.segmentControl.selectedSegment - 1];
124 } else {
125 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
126 }
127 }
128
129 /**
130 * Validates the menu items for the "Debugger" menu
131 */
132 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
133 {
134 SEL action = [anItem action];
135
136 if (action == @selector(stepOut:)) {
137 return _model.connected && _model.stackDepth > 1;
138 } else if (action == @selector(stepIn:) ||
139 action == @selector(stepOver:) ||
140 action == @selector(run:) ||
141 action == @selector(stop:) ||
142 action == @selector(showEvalWindow:)) {
143 return _model.connected;
144 }
145 return [[self window] validateUserInterfaceItem:anItem];
146 }
147
148 /**
149 * Shows the inspector window
150 */
151 - (IBAction)showInspectorWindow:(id)sender
152 {
153 if (![inspector isVisible])
154 [inspector makeKeyAndOrderFront:sender];
155 else
156 [inspector orderOut:sender];
157 }
158
159 /**
160 * Runs the eval window sheet.
161 */
162 - (IBAction)showEvalWindow:(id)sender
163 {
164 // The |controller| will release itself on close.
165 EvalController* controller = [[EvalController alloc] initWithBackEnd:connection];
166 [controller runModalForWindow:[self window]];
167 }
168
169 /**
170 * Resets all the displays to be empty
171 */
172 - (void)resetDisplays
173 {
174 [variablesTreeController setContent:nil];
175 [stackArrayController rearrangeObjects];
176 [[sourceViewer textView] setString:@""];
177 sourceViewer.file = nil;
178 }
179
180 /**
181 * Sets the status to be "Error" and then displays the error message
182 */
183 - (void)setError:(NSString*)anError
184 {
185 [errormsg setStringValue:anError];
186 [errormsg setHidden:NO];
187 }
188
189 /**
190 * Delegate function for GDBpConnection for when the debugger connects.
191 */
192 - (void)debuggerConnected
193 {
194 [errormsg setHidden:YES];
195 if (!self.connection.autoAttach)
196 return;
197 if ([[NSUserDefaults standardUserDefaults] boolForKey:kPrefBreakOnFirstLine])
198 [self stepIn:self];
199 // Do not cache the file between debugger executions.
200 sourceViewer.file = nil;
201 }
202
203 /**
204 * Called once the debugger disconnects.
205 */
206 - (void)debuggerDisconnected
207 {
208 // Invalidate the marked line so we don't look like we're still running.
209 sourceViewer.markedLine = -1;
210 [sourceViewer setNeedsDisplay:YES];
211 }
212
213 /**
214 * Forwards the message to run script execution to the connection
215 */
216 - (IBAction)run:(id)sender
217 {
218 [connection run];
219 }
220
221 - (IBAction)attachedToggled:(id)sender
222 {
223 connection.autoAttach = [sender state] == NSOnState;
224 }
225
226 /**
227 * Forwards the message to "step in" to the connection
228 */
229 - (IBAction)stepIn:(id)sender
230 {
231 if ([[variablesTreeController selectedObjects] count] > 0)
232 selectedVariable = [[variablesTreeController selectedObjects] objectAtIndex:0];
233
234 [connection stepIn];
235 }
236
237 /**
238 * Forwards the message to "step out" to the connection
239 */
240 - (IBAction)stepOut:(id)sender
241 {
242 if ([[variablesTreeController selectedObjects] count] > 0)
243 selectedVariable = [[variablesTreeController selectedObjects] objectAtIndex:0];
244
245 [connection stepOut];
246 }
247
248 /**
249 * Forwards the message to "step over" to the connection
250 */
251 - (IBAction)stepOver:(id)sender
252 {
253 if ([[variablesTreeController selectedObjects] count] > 0)
254 selectedVariable = [[variablesTreeController selectedObjects] objectAtIndex:0];
255
256 [connection stepOver];
257 }
258
259 /**
260 * Forwards the detach/"stop" message to the back end.
261 */
262 - (IBAction)stop:(id)sender
263 {
264 [connection stop];
265 }
266
267 /**
268 * NSTableView delegate method that informs the controller that the stack selection did change and that
269 * we should update the source viewer
270 */
271 - (void)tableViewSelectionDidChange:(NSNotification*)notif
272 {
273 // TODO: This is very, very hacky because it's nondeterministic. The issue
274 // is that calling |-[NSOutlineView expandItem:]| while the table is still
275 // doing its redraw will translate to a no-op. Instead, we need to restructure
276 // this controller so that when everything has been laid out we call
277 // |-expandVariables|; but NSOutlineView doesn't have a |-didFinishDoingCrap:|
278 // method. The other issue is that we need to call this method from
279 // selectionDidChange but ONLY when it was the result of a user-initiated
280 // action and not the stack viewer updating causing a selection change.
281 // If it happens in the latter, then we run into the same issue that causes
282 // this to no-op.
283 [self performSelector:@selector(expandVariables) withObject:nil afterDelay:0.05];
284 }
285
286 /**
287 * Called whenver an item is expanded. This allows us to determine if we need to fetch deeper
288 */
289 - (void)outlineViewItemDidExpand:(NSNotification*)notif
290 {
291 NSTreeNode* node = [[notif userInfo] objectForKey:@"NSObject"];
292 [expandedVariables addObject:[[node representedObject] fullName]];
293
294 [connection loadVariableNode:[node representedObject]
295 forStackFrame:[[stackArrayController selectedObjects] lastObject]];
296 }
297
298 /**
299 * Called when an item was collapsed. This allows us to remove it from the list of expanded items
300 */
301 - (void)outlineViewItemDidCollapse:(NSNotification*)notif
302 {
303 [expandedVariables removeObject:[[[[notif userInfo] objectForKey:@"NSObject"] representedObject] fullName]];
304 }
305
306 #pragma mark Private
307
308 /**
309 * Does the actual updating of the source viewer by reading in the file
310 */
311 - (void)updateSourceViewer
312 {
313 NSArray* selection = [stackArrayController selectedObjects];
314 if (!selection || [selection count] < 1)
315 return;
316 if ([selection count] > 1)
317 NSLog(@"INVALID SELECTION");
318 StackFrame* frame = [selection objectAtIndex:0];
319
320 if (!frame.loaded) {
321 [connection loadStackFrame:frame];
322 return;
323 }
324
325 // Get the filename.
326 NSString* filename = [[NSURL URLWithString:frame.filename] path];
327 if ([filename isEqualToString:@""])
328 return;
329
330 // Replace the source if necessary.
331 if (frame.source && ![sourceViewer.file isEqualToString:filename])
332 {
333 [sourceViewer setString:frame.source asFile:filename];
334
335 NSSet* breakpoints = [NSSet setWithArray:[[BreakpointManager sharedManager] breakpointsForFile:filename]];
336 [sourceViewer setMarkers:breakpoints];
337 }
338
339 [sourceViewer setMarkedLine:frame.lineNumber];
340 [sourceViewer scrollToLine:frame.lineNumber];
341
342 [[sourceViewer textView] setNeedsDisplay:YES];
343 }
344
345 /**
346 * Expands the variables based on the stored set
347 */
348 - (void)expandVariables
349 {
350 NSString* selection = [selectedVariable fullName];
351
352 for (NSInteger i = 0; i < [variablesOutlineView numberOfRows]; i++) {
353 NSTreeNode* node = [variablesOutlineView itemAtRow:i];
354 NSString* fullName = [[node representedObject] fullName];
355
356 // see if it needs expanding
357 if ([expandedVariables containsObject:fullName])
358 [variablesOutlineView expandItem:node];
359
360 // select it if we had it selected before
361 if ([fullName isEqualToString:selection])
362 [variablesTreeController setSelectionIndexPath:[node indexPath]];
363 }
364 }
365
366 /**
367 * Sets the widths of the segmented control.
368 */
369 - (void)updateSegmentControl {
370 NSRect containerFrame = [[_segmentControl superview] frame];
371 CGFloat containerWidth = NSWidth(containerFrame);
372 CGFloat segmentSizes = 0;
373 for (NSInteger i = 1; i < [_segmentControl segmentCount] - 1; ++i) {
374 segmentSizes += [_segmentControl widthForSegment:i];
375 }
376 CGFloat spacerWidth = (containerWidth - segmentSizes) / 2;
377 [_segmentControl setWidth:spacerWidth forSegment:0];
378 [_segmentControl setWidth:spacerWidth forSegment:[_segmentControl segmentCount] - 1];
379
380 [_segmentControl setFrame:NSMakeRect(-1, NSHeight(containerFrame) - 27, containerWidth + 2, 30)];
381 }
382
383 #pragma mark BSSourceView Delegate
384
385 /**
386 * The gutter was clicked, which indicates that a breakpoint needs to be changed
387 */
388 - (void)gutterClickedAtLine:(int)line forFile:(NSString*)file
389 {
390 BreakpointManager* mngr = [BreakpointManager sharedManager];
391
392 if ([mngr hasBreakpointAt:line inFile:file])
393 {
394 [mngr removeBreakpointAt:line inFile:file];
395 }
396 else
397 {
398 Breakpoint* bp = [[Breakpoint alloc] initWithLine:line inFile:file];
399 [mngr addBreakpoint:bp];
400 [bp release];
401 }
402
403 [sourceViewer setMarkers:[NSSet setWithArray:[mngr breakpointsForFile:file]]];
404 [sourceViewer setNeedsDisplay:YES];
405 }
406
407 @end