]> err.no Git - sope/blob - sopex/SOPEX/SOPEXBrowserController.m
SOPE:X 2.x
[sope] / sopex / SOPEX / SOPEXBrowserController.m
1 /*
2   Copyright (C) 2004 Marcus Mueller <znek@mulle-kybernetik.com>
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #import "SOPEXBrowserController.h"
23 #import <WebKit/WebFrame.h>
24 #import <WebKit/WebBackForwardList.h>
25 #import <WebKit/WebDocument.h>
26 #import <WebKit/WebDataSource.h>
27 #import "SOPEXAppController.h"
28 #import "SOPEXToolbarController.h"
29 #import "SOPEXWebConnection.h"
30 #import "WebView+Ext.h"
31 #import "SOPEXBrowserWindow.h"
32 #import "SOPEXDocument.h"
33 #import "SOPEXWOXDocument.h"
34 #import "SOPEXWODocument.h"
35 #import "SOPEXSheetRunner.h"
36 #include "common.h"
37
38 #define DNC [NSNotificationCenter defaultCenter]
39 #define UD [NSUserDefaults standardUserDefaults]
40
41
42 @interface SOPEXBrowserController (PrivateAPI)
43 - (void)setStatus:(NSString *)_msg;
44 - (void)setStatus:(NSString *)_msg isError:(BOOL)isError;
45 - (void)flushDocument;
46 - (void)createDocumentFromResponse;
47 - (void)_selectTabWithIdentifier:(NSString *)identifier;
48 @end
49
50
51 @implementation SOPEXBrowserController
52
53 NSString *SOPEXApplicationTabIdentifier = @"application";
54 NSString *SOPEXWOTabIdentifier          = @"wo";
55 NSString *SOPEXWOXTabIdentifier         = @"wox";
56 NSString *SOPEXHTMLTabIdentifier        = @"html";
57 NSString *SOPEXHTTPTabIdentifier        = @"http";
58
59 static NGLogger *logger = nil;
60
61 + (void)initialize {
62   NGLoggerManager *lm;
63   static BOOL     didInit = NO;
64   
65   if(didInit) return;
66   didInit = YES;
67   lm      = [NGLoggerManager defaultLoggerManager];
68   logger  = [lm loggerForDefaultKey:@"SOPEXDebugEnabled"];
69 }
70
71
72 /* init & dealloc */
73
74 - (id)init {
75   self = [super init];
76   if(self) {
77     [NSBundle loadNibNamed:@"SOPEXBrowserController" owner:self];
78     NSAssert(self->mainWindow != nil,
79              @"Problem loading SOPEXBrowserController.nib!");
80   }
81   return self;
82 }
83
84 - (oneway void)release {
85 #warning !! FIXME
86   /* This seems to be triggered by a bug in WebKit by the resource load
87      delegate, after a successful load - it might be another problem,
88      though.
89   */
90 #if 0
91    [self errorWithFormat:@"%s THIS SHOULD NEVER HAPPEN!!", __PRETTY_FUNCTION__];
92 #endif
93 }
94
95 - (void)dealloc
96 {
97   [self->responseHeaderValues release];
98   [self->connection release];
99   [self->toolbarController release];
100   [self->document release];
101   [super dealloc];
102 }
103
104
105 /* setup */
106
107 - (void)awakeFromNib {
108   [self->webView setGroupName:@"WebUI"];
109   self->responseHeaderValues = [[NSMutableArray alloc] initWithCapacity:20];
110
111   self->toolbarController = [[SOPEXToolbarController alloc]
112     initWithIdentifier:@"SOPEXWebUI"
113     target:self];
114
115   [self setStatus:nil];
116   [self viewApplication:nil];
117 }
118
119
120 /* accessors */
121
122 - (void)setWebConnection:(SOPEXWebConnection *)_conn {
123   ASSIGN(self->connection, _conn);
124   if(logger)
125     [self debugWithFormat:@"%s connection: %@",
126                             __PRETTY_FUNCTION__,
127                             self->connection];
128 }
129
130
131 /* actions */
132
133 - (IBAction)orderFront:(id)sender {
134   [self->mainWindow makeKeyAndOrderFront:sender];
135 }
136
137 - (IBAction)reload:(id)sender {
138   if(sender == nil) {
139     NSURLRequest *rq;
140     
141     rq = [NSURLRequest requestWithURL:[self->connection url] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0];
142     if(logger)
143       [self debugWithFormat:@"%s request: %@", __PRETTY_FUNCTION__, rq];
144     [[self->webView mainFrame] loadRequest:rq];
145   }
146   else {
147     [self->webView reload:self];
148   }
149   [self viewApplication:sender];
150 }
151
152 - (IBAction)back:(id)sender {
153   [self->webView goBack];
154 }
155
156 - (IBAction)viewApplication:(id)sender {
157   [self _selectTabWithIdentifier:SOPEXApplicationTabIdentifier];
158 }
159
160 - (IBAction)viewSource:(id)sender {
161   NSString *componentName;
162   
163   componentName = [[self->document path] lastPathComponent];
164   if([componentName hasSuffix:@"wo"]) {
165     [self->woComponentNameField setStringValue:componentName];
166     [self _selectTabWithIdentifier:SOPEXWOTabIdentifier];
167   }
168   else {
169     [self->woxNameField setStringValue:componentName];
170     [self _selectTabWithIdentifier:SOPEXWOXTabIdentifier];
171   }
172 }
173
174 - (IBAction)viewHTML:(id)sender {
175   WebDataSource *dataSource;
176   id <WebDocumentRepresentation> representation;
177   NSString *source;
178   
179   dataSource = [[self->webView mainFrame] dataSource];
180   NSAssert(dataSource != nil, @"dataSource not yet committed?!");
181   NSAssert([dataSource isLoading] == NO, @"dataSource not finished loading?!");
182   
183   representation = [dataSource representation];
184   
185   if([representation canProvideDocumentSource])
186     source = [representation documentSource];
187   else
188     source = @"";
189   
190   [self->htmlView setString:source];
191   [self _selectTabWithIdentifier:SOPEXHTMLTabIdentifier];
192 }
193
194 - (IBAction)viewHTTP:(id)sender {
195   WebDataSource *dataSource;
196   NSHTTPURLResponse *response;
197   NSDictionary *headerFields;
198   NSArray *headers;
199   int count, i;
200   
201   dataSource = [[self->webView mainFrame] dataSource];
202   response = (NSHTTPURLResponse *)[dataSource response];
203   
204   headerFields = [response allHeaderFields];
205   headers = [headerFields allKeys];
206   count = [headers count];
207   
208   [self->responseHeaderValues removeAllObjects];
209   
210   for(i = 0; i < count; i++) {
211     NSString *header, *value;
212     NSDictionary *headerValueInfo;
213     
214     header = [headers objectAtIndex:i];
215     value = [headerFields objectForKey:header];
216     headerValueInfo = [[NSDictionary alloc] initWithObjectsAndKeys:value,
217       @"value", header, @"header", nil];
218     [self->responseHeaderValues addObject:headerValueInfo];
219     [headerValueInfo release];
220   }
221   
222   [self->responseHeaderInfoTableView reloadData];
223   [self _selectTabWithIdentifier:SOPEXHTTPTabIdentifier];
224 }
225
226 - (void)_selectTabWithIdentifier:(NSString *)identifier {
227   [self->tabView selectTabViewItemWithIdentifier:identifier];
228 }
229
230 - (IBAction)saveDocument:(id)sender {
231   if(self->document == nil)
232     return;
233   if([self->document hasChanges]) {
234     if(![self->document performSave]) {
235       NSBeep();
236       return;
237     }
238     [self->mainWindow setDocumentEdited:NO];
239   }
240 }
241
242 - (IBAction)revertDocumentToSaved:(id)sender {
243   [self->document revertChanges];
244   [self->mainWindow setDocumentEdited:NO];
245 }
246
247 - (IBAction)toggleToolbar:(id)sender {
248   if([self->mainWindow toolbar] == nil)
249     [self->toolbarController applyOnWindow:self->mainWindow];
250   else
251     [self->mainWindow setToolbar:nil];
252 }
253
254 - (IBAction)editInXcode:(id)sender {
255   NSString *path;
256   
257   path = [self->document path];
258   [[NSWorkspace sharedWorkspace] openFile:path withApplication:@"Xcode" andDeactivate:YES]; 
259 }
260
261
262 /* menu & toolbar */
263
264 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem {
265   return [self validateMenuItem:(id <NSMenuItem>)theItem];
266 }
267
268 - (BOOL)validateMenuItem:(id <NSMenuItem>)_item {
269   SEL action = [_item action];
270   NSString *tabId;
271
272 #if 0
273   NSLog(@"%s action:%@", __PRETTY_FUNCTION__, NSStringFromSelector(action));
274 #endif
275   if(action == @selector(back:))
276     return [self->webView canGoBack];
277   else if(action == @selector(saveDocument:) ||
278           action == @selector(revertDocumentToSaved:))
279   {
280     return self->document == nil ? NO : [self->document hasChanges];
281   }
282
283   tabId = [[self->tabView selectedTabViewItem] identifier];
284   if(action == @selector(viewApplication:)) {
285     BOOL isOn = [tabId isEqualToString:SOPEXApplicationTabIdentifier];
286     [_item setState:isOn ? NSOnState : NSOffState];
287   }
288   else if(action == @selector(viewSource:)) {
289     BOOL isOn = ([tabId isEqualToString:SOPEXWOXTabIdentifier] ||
290                  [tabId isEqualToString:SOPEXWOTabIdentifier]);
291     [_item setState:isOn ? NSOnState : NSOffState];
292
293     return self->document != nil ? YES : NO;
294   }
295   else if(action == @selector(viewHTML:)) {
296     BOOL isOn;
297     WebDataSource *dataSource;
298
299     isOn = [tabId isEqualToString:SOPEXHTMLTabIdentifier];
300     [_item setState:isOn ? NSOnState : NSOffState];
301     dataSource = [[self->webView mainFrame] dataSource];
302     if(dataSource == nil)
303       return NO;
304     return [dataSource isLoading] == NO;
305   }
306   else if(action == @selector(viewHTTP:)) {
307     BOOL isOn = [tabId isEqualToString:SOPEXHTTPTabIdentifier];
308     [_item setState:isOn ? NSOnState : NSOffState];
309   }
310   return YES;
311 }
312
313
314 /* SOPEXDocumentController PROTOCOL */
315
316 - (NSTextView *)document:(SOPEXDocument *)_document
317   textViewForType:(NSString *)_fileType
318 {
319   if([_document isKindOfClass:[SOPEXWODocument class]]) {
320       if([_fileType isEqualToString:@"html"])
321           return self->woSourceView;
322       return self->woDefinitionView;
323   }
324   return self->woxSourceView;
325 }
326
327 - (void)document:(SOPEXDocument *)document
328   didValidateWithError:(NSError *)error
329   forType:(NSString *)fileType
330 {
331   [self viewSource:self];
332 }
333
334
335 /* private api */
336
337 - (void)setStatus:(NSString *)_msg {
338   [self setStatus:_msg isError:NO];
339 }
340
341 - (void)setStatus:(NSString *)_msg isError:(BOOL)isError {
342   if(_msg == nil)
343       _msg = @"";
344   [self->statusBarTextField setStringValue:_msg];
345 }
346
347 - (void)flushDocument {
348   [self->document release];
349   self->document = nil;
350   [self->mainWindow setDocumentEdited:NO];
351 }
352
353 - (void)createDocumentFromResponse {
354   WebDataSource *dataSource;
355   NSHTTPURLResponse *response;
356   NSDictionary *headerFields;
357   NSString *templatePath;
358   
359   dataSource = [[self->webView mainFrame] dataSource];
360   response = (NSHTTPURLResponse *)[dataSource response];
361   
362   if([response isKindOfClass:[NSHTTPURLResponse class]]) {
363     headerFields = [response allHeaderFields];
364     // NOTE: WebKit cuddly-capses header keys!
365     templatePath = [headerFields objectForKey:@"X-Sope-Template-Path"];
366     if(templatePath == nil)
367       return;
368     
369     if([templatePath hasSuffix:@"wo"])
370       self->document = [[SOPEXWODocument alloc] initWithPath:templatePath
371                                                 controller:self];
372     else
373       self->document = [[SOPEXWOXDocument alloc] initWithPath:templatePath
374                                                  controller:self];
375   }
376 }
377
378
379 /* window delegate */
380
381
382 - (BOOL)windowShouldClose:(id)sender {
383 #if 0
384   if(logger)
385     [self debugWithFormat:@"%s sender:%@", __PRETTY_FUNCTION__, sender];
386 #endif
387   
388   if(sender != self->mainWindow)
389     return YES;
390   
391   if(self->document != nil && [self->document hasChanges]) {
392     id panel;
393     int rc;
394     
395     panel = NSGetAlertPanel(
396       NSLocalizedString(@"Do you want to save changes to the source code before closing?", "Title of the alert sheet when window should close but changes are still not saved"),
397       NSLocalizedString(@"If you don\\u2019t save, your changes will be lost.", "Message of the alert sheet when unsaved changes are about to be lost"),
398       NSLocalizedString(@"Save", "Default button text for the alert sheet"),
399       NSLocalizedString(@"Don\\u2019t save", "Alternate button text for the alert sheet"),
400       NSLocalizedString(@"Cancel", "Other button text for the alert sheet")
401       );
402     
403     rc = SOPEXRunSheetModalForWindow(panel, self->mainWindow);
404     NSReleaseAlertPanel(panel);
405     
406     // NSAlertOtherReturn == Cancel
407     // NSAlertAlternateReturn == Don't save
408     // NSAlertDefaultReturn == Save
409     
410     if(rc == NSAlertOtherReturn)
411       return NO;
412     if(rc == NSAlertDefaultReturn)
413       [self saveDocument:self];
414     [self flushDocument];
415   }
416   return YES;
417 }
418
419 /* tableview datasource */
420
421 - (int)numberOfRowsInTableView:(NSTableView *)_tableView {
422   return [self->responseHeaderValues count];
423 }
424
425 - (id)tableView:(NSTableView *)_tableView
426   objectValueForTableColumn:(NSTableColumn *)_tableColumn
427   row:(int)_rowIndex
428 {
429   return [[self->responseHeaderValues objectAtIndex:_rowIndex]
430                                       objectForKey:[_tableColumn identifier]];
431 }
432
433
434 /* WebResourceLoadDelegate */
435
436 - (id)webView:(WebView *)_sender 
437   identifierForInitialRequest:(NSURLRequest *)_rq 
438   fromDataSource:(WebDataSource *)_ds
439 {
440   return [[_rq URL] absoluteString];
441 }
442
443 - (NSURLRequest *)webView:(WebView *)_sender 
444   resource:(id)_id
445   willSendRequest:(NSURLRequest *)_rq
446   redirectResponse:(NSURLResponse *)redirectResponse 
447   fromDataSource:(WebDataSource *)_ds
448 {
449   /* use that to patch resource requests to local files ;-) */
450   NSURL    *url, *rurl;
451   
452   url = [_rq URL];
453   if(logger)
454     [self debugWithFormat:@"%s: %@ request: %@ url: %@",
455                             __PRETTY_FUNCTION__,
456                             _id,
457                             _rq,
458                             url];
459
460   if (![self->connection shouldRewriteRequestURL:url])
461     return _rq;
462   
463   if ((rurl = [self->connection rewriteRequestURL:url]) == nil)
464     return _rq;
465   if ([rurl isEqual:url])
466     return _rq;
467   
468   return [NSURLRequest requestWithURL:rurl
469                        cachePolicy:NSURLRequestUseProtocolCachePolicy
470                        timeoutInterval:5.0];
471 }
472
473 - (void)webView:(WebView *)_sender
474   resource:(id)_rid 
475   didReceiveContentLength:(unsigned)_length 
476   fromDataSource:(WebDataSource *)_ds
477 {
478     //NSLog(@"%s: %@ len: %d", __PRETTY_FUNCTION__, _rid, _length);
479 }
480
481 - (void)webView:(WebView *)_sender
482   resource:(id)_rid 
483   didFinishLoadingFromDataSource:(WebDataSource *)_ds
484 {
485   NSURLResponse *r = [_ds response];
486   
487   if(logger) {
488     [self debugWithFormat:@"%s: %@ ds: %@\n  data-len: %i\n  response: %@\n "
489                             @"type: %@\n  enc: %@", 
490                             __PRETTY_FUNCTION__, _rid, _ds,
491                             [[_ds data] length], r, [r MIMEType],
492                             [r textEncodingName]];
493   }
494   [self->connection processResponse:[_ds response] data:[_ds data]];
495 }
496
497 - (void)webView:(WebView *)_sender
498   resource:(id)_rid
499   didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)_c 
500   fromDataSource:(WebDataSource *)_ds
501 {
502   if(logger)
503     [self debugWithFormat:@"%s: %@ ds: %@", __PRETTY_FUNCTION__, _rid, _ds];
504 }
505
506 - (void)webView:(WebView *)_sender 
507   resource:(id)_identifier 
508   didReceiveResponse:(NSURLResponse *)_response 
509   fromDataSource:(WebDataSource *)_ds
510 {
511   if (logger) {
512     [self debugWithFormat:@"%s: view: %@\n  resource: %@\n  received: %@\n"
513                             @"  datasource: %@\n  data-len: %i%s",
514                             __PRETTY_FUNCTION__,
515                             _sender, _identifier, _response, _ds, 
516                             [[_ds data] length],
517                             [_ds isLoading]? " LOADING" : ""];
518   }
519 }
520
521
522 /* WebFrameLoadDelegate */
523
524
525 - (void)webView:(WebView *)sender
526   didStartProvisionalLoadForFrame:(WebFrame *)frame
527 {
528   if(self->document != nil && [self->document hasChanges]) {
529     id panel;
530     int rc;
531     
532     panel = NSGetAlertPanel(
533       NSLocalizedString(@"Do you want to save changes to the source code before proceeding?", "Title of the alert sheet when user wants to proceed but changes are still not saved"),
534       NSLocalizedString(@"If you don\\u2019t save, your changes will be lost.", "Message of the alert sheet when unsaved changes are about to be lost"),
535       NSLocalizedString(@"Save", "Default button text for the alert sheet"),
536       NSLocalizedString(@"Don\\u2019t save", "Alternate button text for the alert sheet"),
537       NULL);
538     
539     rc = SOPEXRunSheetModalForWindow(panel, self->mainWindow);
540     NSReleaseAlertPanel(panel);
541     
542     // NSAlertOtherReturn     == Cancel
543     // NSAlertAlternateReturn == Don't save
544     // NSAlertDefaultReturn   == Save
545
546     if(rc == NSAlertDefaultReturn)
547       [self saveDocument:self];
548   }
549   [self flushDocument];
550   [self->progressIndicator startAnimation:self];
551 }
552
553 - (void)webView:(WebView *)sender
554   didReceiveTitle:(NSString *)_title
555   forFrame:(WebFrame *)_frame
556 {
557   [self->mainWindow setTitle:_title];
558 }
559
560
561 - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
562   [self->progressIndicator stopAnimation:self];
563   
564 #if 0
565 #warning ** setFavIcon enabled ... doesnt really make sense
566   [self->mainWindow setFavIcon:[sender pageIcon]];
567 #endif
568   [self createDocumentFromResponse];
569 }
570
571 - (void)webView:(WebView *)sender
572   didFailLoadWithError:(NSError *)error
573   forFrame:(WebFrame *)frame
574 {
575   [self->progressIndicator stopAnimation:self];
576   [self setStatus:[error localizedDescription] isError:YES];
577 }
578
579 - (void)webView:(WebView *)sender
580   didFailProvisionalLoadWithError:(NSError *)error
581   forFrame:(WebFrame *)frame
582 {
583   [self webView:sender didFailLoadWithError:error forFrame:frame];
584 }
585
586
587 /* WebView UI delegate */
588
589 - (BOOL)webViewIsStatusBarVisible:(WebView *)sender {
590   return YES;
591 }
592
593 - (void)webView:(WebView *)sender
594   mouseDidMoveOverElement:(NSDictionary *)_info
595   modifierFlags:(unsigned int)_flags
596 {
597   NSURL *url;
598   
599 #if 0
600   NSLog(@"%s _info:%@", __PRETTY_FUNCTION__, _info);
601 #endif
602   
603   
604   url = [_info objectForKey:WebElementImageURLKey];
605   if(url != nil) {
606     NSString *altString;
607     NSRect imageRect;
608     NSImage *image;
609     NSMutableString *status;
610     
611     altString = [_info objectForKey:WebElementImageAltStringKey];
612     imageRect = [[_info objectForKey:WebElementImageRectKey] rectValue];
613     image     = [_info objectForKey:WebElementImageKey];
614     
615     status = [NSMutableString string];
616     
617     if(altString == nil)
618       altString = [url absoluteString];
619     
620     url = [_info objectForKey:WebElementLinkURLKey];
621     if(url != nil)
622       [status appendFormat:@"%@    ", [url absoluteString]];
623     
624     [status appendFormat:@"[%@]   (w:%.0f h:%.0f)",
625                            altString,
626                            imageRect.size.width, imageRect.size.height];
627     if(NSEqualSizes([image size], imageRect.size) == NO) {
628       NSSize size = [image size];
629       [status appendFormat:@" -> scaled from (w:%.0f h:%.0f)!", size.width, size.height];
630     }
631     
632     [self setStatus:status];
633     return;
634   }
635   
636   url = [_info objectForKey:WebElementLinkURLKey];
637   if(url != nil) {
638     [self setStatus:[url absoluteString]];
639     return;
640   }
641   
642   [self setStatus:nil];
643 }
644
645 /* Logging */
646
647 - (id)debugLogger {
648   return logger;
649 }
650
651 @end