This is IKBrowser.m in view mode; [Download] [Up]
/* File IKBrowser.m Release 1.2, 7 June 1994 Copyright (C) 1994 by H. Scott Roy This code is part of IconKit, a general toolbox for drag-and-drop applications. IconKit is free for noncommercial use, but costs money for a commercial license. You should have received a copy of the license agreement with this file. If not, a copy of the license and the complete source of IconKit can be obtained from the author: H. Scott Roy 2573 Stowe Ct. Northbrook, IL 60062-8103 iconkit@cs.stanford.edu For your editing convenience, this file is best viewed using an editor that automatically wraps long lines, in a fixed point font at 80 columns, with tabs every 4 spaces. */ /* ========================================================================== */ /* An IKBrowser works like the Workspace file viewer. Selections are displayed in an icon path, from which they can be dragged or inspected. */ #import <appkit/appkit.h> #import <apps/InterfaceBuilder.h> #import <math.h> #import "iconkit.h" @implementation IKBrowser /* ========================================================================== */ #define max(a,b) ((a) > (b) ? (a):(b)) #define LEFT 0xAC #define UP 0xAD #define RIGHT 0xAE #define DOWN 0xAF #define BUFSIZE 1000 /* ========================================================================== */ /* The Matrix getSelectedCells method breaks in InterfaceBuilder test mode. The IKIconPath class implements a correct version, so the workaround is to use that method whenever running in IB test mode. Note that it would be undesirable to use the IKIconPath implementation exclusively, since programmers might conceivably create an IKBrowser with a custom matrix class that overrides getSelectedCells:. */ static IMP getSelectedCells; + initialize { if (self == [IKBrowser class]) { getSelectedCells = [IKIconPath instanceMethodFor: @selector(getSelectedCells:)]; } return self; } /* ========================================================================== */ /* The IKBrowser is designed to work in conjunction with a ScrollView containing an IKIconPath. Accordingly, the defaults for an IKBrowser are a little bit different than a normal NXBrowser. Various aspects of the scrollView and iconPath must be initialized when an IKBrowser awakes. In particular, the scroller on the scrollView must be set to pass its messages through the browser, so that the browser can detect when it should scroll to a new selection. The iconPath must also be told to send its double clicks to the browser, so that the browser can treat them just like double clicks in the browser itself. */ - initFrame: (const NXRect *) frameRect { [super initFrame: frameRect]; [[[[[[[[[self setHorizontalScrollButtonsEnabled: NO] setHorizontalScrollerEnabled: NO] setMultipleSelectionEnabled: YES] acceptArrowKeys: YES andSendActionMessages: YES] setAutoSynchronize: YES] useScrollButtons: YES] reuseColumns: YES] setCellClass: [IKBrowserCell class]] setTitled: NO]; return self; } - awake { id scroller; [self sizeCells]; if (!scroll) { scroll = [scroller = [scrollView horizScroller] action]; [[scroller setAction: @selector(scrolling:)] setTarget: self]; } [iconPath setDoubleAction: @selector(doDoubleClick:)]; [iconPath setBackgroundGray: NX_LTGRAY]; [window makeFirstResponder: self]; return self; } /* ========================================================================== */ /* Goodness! The awake method gets called quite a bit--three times when unarchiving from a nib file. Alas, I've not really found a better way. I had originally intended not to include the awake call in setIconPath: and setScrollView:, but that plan was foiled when I realized InterfaceBuilder doesn't send out any awakeFromNib messages in test mode. */ - (const char *) getInspectorClassName { return "IKBrowserInspector"; } - iconPath { return iconPath; } - scrollView { return scrollView; } - setIconPath: theIconPath { iconPath = theIconPath; [self awake]; return self; } - setScrollView: theScrollView { scrollView = theScrollView; [self awake]; return self; } /* ========================================================================== */ /* Among other things, the archiving methods allow an IKBrowser to be correctly initialized from an InterfaceBuilder palette. */ - read: (NXTypedStream *) stream { [super read: stream]; iconPath = NXReadObject (stream); scrollView = NXReadObject (stream); NXReadTypes (stream, "c", &autoSynchronize); return self; } - write: (NXTypedStream *) stream { [super write: stream]; NXWriteObject (stream, iconPath); NXWriteObject (stream, scrollView); NXWriteTypes (stream, "c", &autoSynchronize); return self; } /* ========================================================================== */ /* The icon path should always show an integral number of icons. Hence, whenever the ScrollView resizes, the cell size must also change. This code is a slight hack, since I assume that the scrollView never changes size unless the browser does so as well. */ - sizeTo: (NXCoord) width : (NXCoord) height { int first = [self firstVisibleColumn]; [super sizeTo: width : height]; [self scrollColumnToVisible: first]; [self sizeCells]; return self; } - sizeCells { NXSize size; [scrollView getContentSize: &size]; size.width = floor(size.width / [self numVisibleColumns]); [iconPath setCellSize: &size]; [self synchronizePath]; [[scrollView setLineScroll: size.width] setPageScroll: 0.0]; return self; } /* ========================================================================== */ /* The iconPath should always reflect the current state of the browser. The scrollView should display the same number of columns as are visible in the browser, but the iconPath matrix itself is sometimes larger to avoid spurious scrolling. The first icon visible always corresponds to the first visible column. */ - synchronizePath { id clipView; NXPoint origin; NXSize size, contentSize; int cells, n; float excess; if (autoSynchronize) { [scrollView getContentSize: &contentSize]; [iconPath getCellSize: &size]; excess = contentSize.width - size.width * [self numVisibleColumns]; cells = [self lastColumn] + 1; n = max(cells, [self firstVisibleColumn] + [self numVisibleColumns]); [iconPath renewRows: 1 cols: cells]; [iconPath sizeTo: n * size.width + excess : size.height]; [[iconPath cellAt: 0 : cells - 2] setBranch: YES]; [[iconPath cellAt: 0 : cells - 1] setBranch: NO]; [iconPath selectCellAt: 0 : cells - 1]; [iconPath update]; origin.x = floor(size.width) * [self firstVisibleColumn]; origin.y = 0.0; [clipView = [iconPath superview] rawScroll: &origin]; [scrollView reflectScroll: clipView]; } return self; } - setAutoSynchronize: (BOOL) flag; { autoSynchronize = flag; return self; } /* ========================================================================== */ /* This is the method an IKBrowser calls whenever there is a new selection. It sends a request to the delegate to install an icon to represent the new selection; then it redraws the affected cell. */ - newSelection { if (autoSynchronize) { if ([delegate respondsTo: @selector(browser:setColumnIcon:for:)]) [delegate browser: self setColumnIcon: [iconPath selectedCell] for: [self lastColumn]]; } return self; } /* ========================================================================== */ /* The browser can update itself in three ways: mouse clicks or arrow movements within the browser, clicks inside the icon path, and calls to setPath:. The beginUpdate and endUpdate methods provide the common glue at the start and end of these operations. We achieve maximum performance when we disable displaying the icon path until the update is complete, while drawing the browser as things arrive. */ - beginUpdate { [window disableFlushWindow]; [[iconPath setAutodisplay: NO] endEditing]; return self; } - endUpdate { [[iconPath setAutodisplay: YES] displayIfNeeded]; if ([delegate respondsTo: @selector(browserWillFinishChange:)]) [delegate browserWillFinishChange: self]; [[[window makeFirstResponder: self] reenableFlushWindow] flushWindow]; if ([delegate respondsTo: @selector(browserDidChange:)]) [delegate browserDidChange: self]; return self; } /* ========================================================================== */ /* The iconPath must synchronize after any event that affects the browser. The most significant times are when a new column is added and when the number of loaded columns is decreased. The setLastColumn: method alerts the browser delegate that columns are about to disappear so that it can properly clear out all the cells. -WARNING- It's essentially impossible to clean things up at the right time if the browser doesn't reuse its columns. Moreover, NXBrowser doesn't provide any way to check whether it reuses its columns or not. So we disallow reuseColumns: NO. */ - addColumn { [[[super addColumn] synchronizePath] newSelection]; if ([self lastColumn] == 0) [iconPath editCellAt: 0 : 0]; return self; } - setLastColumn: (int) n { id columnMatrices = [[List alloc] init], pathCells = [[List alloc] init]; int i = n, m = [self lastColumn]; while (i++ < m) { [columnMatrices addObject: [self matrixInColumn: i]]; [pathCells addObject: [iconPath cellAt: 0 : i]]; } [[super setLastColumn: n] synchronizePath]; i = [columnMatrices count]; while (i--) { if ([delegate respondsTo: @selector(browser:emptyMatrix:inColumn:)]) [delegate browser: self emptyMatrix: [columnMatrices objectAt: i] inColumn: n + i + 1]; if ([delegate respondsTo: @selector(browser:removeColumnIcon:for:)]) [delegate browser: self removeColumnIcon: [pathCells objectAt: i] for: n + i + 1]; } [columnMatrices free]; [pathCells free]; return self; } - reuseColumns: (BOOL) flag { if (flag == YES) [super reuseColumns: flag]; return self; } /* ========================================================================== */ /* Under the right circumstances, the drawing performance of setPath: can be improved by disabling window drawing entirely. With drawing disabled, one needs to redraw all the visible columns, but one avoids drawing columns and then scrolling them away. */ - (BOOL) _disableDrawingForPath: (const char *) path { const unsigned char * p = path; unsigned char currentPath [10000], separator [2], * q = currentPath; int common = 0, rest = 0; [self getPath: currentPath toColumn: [self lastColumn]]; separator[0] = pathSeparator; separator[1] = '\0'; strcat (q, separator); while (*q && *q == *p) if (p++, *q++ == pathSeparator) common++; while (*p) if (*p++ == pathSeparator) rest++; return rest > [self numVisibleColumns]; } /* ========================================================================== */ /* When setting the path, the iconPath is not synchronized until after the complete path is loaded. The performance improvement is significant. */ - setPath: (const char *) path { int disable = [self _disableDrawingForPath: path], n; id current = nil; [[self setAutoSynchronize: NO] beginUpdate]; if (disable) [window disableDisplay]; [super setPath: path]; if ([self lastColumn] == [self selectedColumn]) [self addColumn]; [[self setAutoSynchronize: YES] synchronizePath]; for (n = 0; n <= [self lastColumn]; n++) { current = [iconPath cellAt: 0 : n]; [current setBranch: YES]; if ([delegate respondsTo: @selector(browser:setColumnIcon:for:)]) [delegate browser: self setColumnIcon: current for: n]; } [current setBranch: NO]; [iconPath update]; if (disable) { [[scrollView horizScroller] update]; [[window reenableDisplay] displayIfNeeded]; } [self endUpdate]; return self; } /* ========================================================================== */ /* The action method below allows controls to set the browser path. Nothing in the IconKit currently uses it, since requests to set the path typically pass through an IKBrowserManager. */ - takePathFrom: sender { [self setPath: [sender stringValue]]; return self; } /* ========================================================================== */ /* The method below determines how the browser repsonds when a column changes. After the new data is loaded, the column either flushes everything to the right of the changed column or tries to reset itself to the indicated path. The usual practice is for this path to be the one that was present before the column changed. */ - resetColumn: (int) n usingPath: (const char *) path { [window disableFlushWindow]; [self reloadColumn: n]; if (path) [self setPath: path]; else [[[[[self beginUpdate] setLastColumn: n] matrixInColumn: n] selectCellAt: -1 : -1] endUpdate]; [[window reenableFlushWindow] flushWindow]; return self; } /* ========================================================================== */ /* Scroll messages pass through the IKBrowser so that it can respond accordingly. When the scroll knob is dragged, the browser waits until the mouseUp event before it shifts to its new location. The scroller is also reset so that the scrollView exactly encloses the current set of path icons. Note that the NXBrowser fails to respond to page scrolling. NeXT should probably fix this at some point. The iconSelected: message is sent by the iconPath whenever an icon is clicked. It scrolls the browser to the indicated column and sends out the browser's action message. */ - scrolling: sender { if ([sender hitPart] != NX_KNOB) { [window disableFlushWindow]; [iconPath setAutodisplay: NO]; [[self scrollViaScroller: sender] synchronizePath]; [iconPath setAutodisplay: YES]; [[window reenableFlushWindow] flushWindow]; } else [scrollView perform: scroll with: sender]; return self; } - iconSelected: sender { [[[self setAutoSynchronize: NO] beginUpdate] setLastColumn: [iconPath selectedCol]]; [[self matrixInColumn: [self lastColumn]] selectCellAt: -1 : -1]; [[[[self setAutoSynchronize: YES] synchronizePath] endUpdate] sendAction]; return self; } /* ========================================================================== */ /* A selected leaf node gets its own column in an IKBrowser. AutoSynchronize is disabled to avoid setting the icon path twice. The delegate must be smart enough to insert the right icon and leave the column empty. */ - doClick: sender { [[self setAutoSynchronize: NO] beginUpdate]; [super doClick: sender]; if ([self lastColumn] == [self selectedColumn]) [self addColumn]; [[[[self setAutoSynchronize: YES] synchronizePath] newSelection] endUpdate]; return self; } /* ========================================================================== */ /* The IKBrowser seems not to handle the arrow keys well using the standard NXBrowser methods, so keyDown: and keyUp: get new implementations that try to keep everything in sync. Updating the browser is delayed after up and down arrow keys to allow rapid fire scrolling within browser columns. */ - keyDown: (NXEvent *) theEvent { int r, c = [self selectedColumn], n, m; id matrix; BOOL clickNow; switch (theEvent->data.key.charCode) { case LEFT: matrix = [self matrixInColumn: c - 1]; r = [matrix selectedRow]; clickNow = YES; break; case RIGHT: matrix = [self matrixInColumn: c + 1]; r = 0; clickNow = YES; break; case UP: [matrix = [self matrixInColumn: c] getNumRows: &n numCols: &m]; r = ([matrix selectedRow] + n - 1) % n; clickNow = NO; break; case DOWN: [matrix = [self matrixInColumn: c] getNumRows: &n numCols: &m]; r = ([matrix selectedRow] + 1) % n; clickNow = NO; break; default: return nil; break; } [[matrix scrollCellToVisible: r : 0] selectCellAt: r : 0]; if (matrix && ([matrix selectedRow] != -1) && clickNow) [self doClick: matrix]; return self; } - keyUp: (NXEvent *) theEvent { id matrix; if ((theEvent->data.key.charCode == UP) || (theEvent->data.key.charCode == DOWN)) { matrix = [self matrixInColumn: [self selectedColumn]]; if (matrix && ([matrix selectedRow] != -1)) [self doClick: matrix]; } return self; } /* ========================================================================== */ /* The empty extra column that an IKBrowser puts in for a leaf selection messes up the selectAll: message, so that gets fixed here. */ - selectAll: sender { int r, c; [window disableFlushWindow]; [[self matrixInColumn: [self lastColumn]] getNumRows: &r numCols: &c]; if (r == 0) [self setLastColumn: [self lastColumn] - 1]; [super selectAll: sender]; if ([self lastColumn] == [self selectedColumn]) [self addColumn]; [window reenableFlushWindow]; [window flushWindow]; return self; } /* ========================================================================== */ /* The method below facilitates finding the selected objects in a browser. It extracts the selected cell delegates in the indicated column. */ - getSelectionInColumn: (int) column { id selection = nil, matrix = [self matrixInColumn: column]; int i; if (matrix != nil) { selection = [NXApp conformsTo: @protocol(IB)] ? getSelectedCells (matrix, @selector(getSelectedCells:), nil): [matrix getSelectedCells: nil]; for (i = 0; i < [selection count]; i++) [selection replaceObjectAt: i with: [[selection objectAt: i] delegate]]; } return selection; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.