diff --git a/Cocoa/Document.h b/Cocoa/Document.h index 6a47aa7..9e1e73d 100644 --- a/Cocoa/Document.h +++ b/Cocoa/Document.h @@ -7,7 +7,12 @@ @property (strong) IBOutlet NSPanel *consoleWindow; @property (strong) IBOutlet NSTextField *consoleInput; @property (strong) IBOutlet NSWindow *mainWindow; +@property (strong) IBOutlet NSView *memoryView; +@property (strong) IBOutlet NSPanel *memoryWindow; +-(uint8_t) readMemory:(uint16_t) addr; +-(void) writeMemory:(uint16_t) addr value:(uint8_t)value; +-(void) performAtomicBlock: (void (^)())block; @end diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 3aec220..2a7a89d 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -4,6 +4,9 @@ #include "AppDelegate.h" #include "gb.h" #include "debugger.h" +#include "memory.h" +#include "HexFiend/HexFiend.h" +#include "GBMemoryByteArray.h" @interface Document () { @@ -14,6 +17,7 @@ bool tooMuchLogs; bool fullScreen; bool in_sync_input; + HFController *hex_controller; NSString *lastConsoleInput; } @@ -62,7 +66,7 @@ static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) volatile bool stopping; NSConditionLock *has_debugger_input; NSMutableArray *debugger_input_queue; - bool is_inited; + volatile bool is_inited; } - (instancetype)init { @@ -122,9 +126,12 @@ static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) } andSampleRate:96000]; self.view.mouseHidingEnabled = YES; [self.audioClient start]; + NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; while (running) { GB_run(&gb); } + [hex_timer invalidate]; [self.audioClient stop]; self.audioClient = nil; self.view.mouseHidingEnabled = NO; @@ -154,8 +161,8 @@ static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { bool was_cgb = gb.is_cgb; [self stop]; - GB_free(&gb); is_inited = false; + GB_free(&gb); if (([sender tag] == 0 && was_cgb) || [sender tag] == 2) { [self initCGB]; } @@ -200,6 +207,51 @@ static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) } +- (void) initMemoryView +{ + hex_controller = [[HFController alloc] init]; + [hex_controller setBytesPerColumn:1]; + [hex_controller setFont:[NSFont userFixedPitchFontOfSize:12]]; + [hex_controller setEditMode:HFOverwriteMode]; + + [hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; + + /* Here we're going to make three representers - one for the hex, one for the ASCII, and one for the scrollbar. To lay these all out properly, we'll use a fourth HFLayoutRepresenter. */ + HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init]; + HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init]; + HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init]; + HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init]; + HFLineCountingRepresenter *lineRep = [[HFLineCountingRepresenter alloc] init]; + HFStatusBarRepresenter *statusRep = [[HFStatusBarRepresenter alloc] init]; + + lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal; + + /* Add all our reps to the controller. */ + [hex_controller addRepresenter:layoutRep]; + [hex_controller addRepresenter:hexRep]; + [hex_controller addRepresenter:asciiRep]; + [hex_controller addRepresenter:scrollRep]; + [hex_controller addRepresenter:lineRep]; + [hex_controller addRepresenter:statusRep]; + + /* Tell the layout rep which reps it should lay out. */ + [layoutRep addRepresenter:hexRep]; + [layoutRep addRepresenter:scrollRep]; + [layoutRep addRepresenter:asciiRep]; + [layoutRep addRepresenter:lineRep]; + [layoutRep addRepresenter:statusRep]; + + + [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + /* Grab the layout rep's view and stick it into our container. */ + NSView *layoutView = [layoutRep view]; + NSRect layoutViewFrame = self.memoryView.frame; + [layoutView setFrame:layoutViewFrame]; + [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin]; + [self.memoryView addSubview:layoutView]; +} + + (BOOL)autosavesInPlace { return YES; } @@ -333,6 +385,8 @@ static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) NSString *nsstring = @(string); // For ref-counting dispatch_async(dispatch_get_main_queue(), ^{ + [hex_controller reloadData]; + NSFont *font = [NSFont userFixedPitchFontOfSize:12]; NSUnderlineStyle underline = NSUnderlineStyleNone; if (attributes & GB_LOG_BOLD) { @@ -452,4 +506,44 @@ static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) [self log:log withAttributes:0]; } -@end +- (uint8_t) readMemory:(uint16_t)addr +{ + while (!is_inited); + return GB_read_memory(&gb, addr); +} + +- (void) writeMemory:(uint16_t)addr value:(uint8_t)value +{ + while (!is_inited); + GB_write_memory(&gb, addr, value); +} + +- (void) performAtomicBlock: (void (^)())block +{ + while (!is_inited); + bool was_running = running; + if (was_running) { + [self stop]; + } + block(); + if (was_running) { + [self start]; + } +} + +- (void) reloadMemoryView +{ + if (self.memoryWindow.isVisible) { + [hex_controller reloadData]; + } +} + +- (IBAction) showMemory:(id)sender +{ + if (!hex_controller) { + [self initMemoryView]; + } + [self.memoryWindow makeKeyAndOrderFront:sender]; +} + +@end \ No newline at end of file diff --git a/Cocoa/Document.xib b/Cocoa/Document.xib index 472f71f..33357b6 100644 --- a/Cocoa/Document.xib +++ b/Cocoa/Document.xib @@ -10,6 +10,8 @@ + + @@ -113,5 +115,17 @@ + + + + + + + + + + + + diff --git a/Cocoa/GBCompleteByteSlice.h b/Cocoa/GBCompleteByteSlice.h new file mode 100644 index 0000000..24f3ba0 --- /dev/null +++ b/Cocoa/GBCompleteByteSlice.h @@ -0,0 +1,7 @@ +#import "Document.h" +#import "HexFiend/HexFiend.h" +#import "HexFiend/HFByteSlice.h" + +@interface GBCompleteByteSlice : HFByteSlice +- (instancetype) initWithByteArray:(HFByteArray *)array; +@end diff --git a/Cocoa/GBCompleteByteSlice.m b/Cocoa/GBCompleteByteSlice.m new file mode 100644 index 0000000..44e7ee6 --- /dev/null +++ b/Cocoa/GBCompleteByteSlice.m @@ -0,0 +1,26 @@ +#import "GBCompleteByteSlice.h" + +@implementation GBCompleteByteSlice +{ + HFByteArray *_array; +} + +- (instancetype) initWithByteArray:(HFByteArray *)array +{ + if ((self = [super init])) { + _array = array; + } + return self; +} + +- (unsigned long long)length +{ + return [_array length]; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range +{ + [_array copyBytes:dst range:range]; +} + +@end diff --git a/Cocoa/GBMemoryByteArray.h b/Cocoa/GBMemoryByteArray.h new file mode 100644 index 0000000..9599175 --- /dev/null +++ b/Cocoa/GBMemoryByteArray.h @@ -0,0 +1,7 @@ +#import "Document.h" +#import "HexFiend/HexFiend.h" +#import "HexFiend/HFByteArray.h" + +@interface GBMemoryByteArray : HFByteArray +- (instancetype) initWithDocument:(Document *)document; +@end diff --git a/Cocoa/GBMemoryByteArray.m b/Cocoa/GBMemoryByteArray.m new file mode 100644 index 0000000..8a8c1e8 --- /dev/null +++ b/Cocoa/GBMemoryByteArray.m @@ -0,0 +1,64 @@ +#import "GBMemoryByteArray.h" +#import "GBCompleteByteSlice.h" + + +@implementation GBMemoryByteArray +{ + Document *_document; +} + +- (instancetype) initWithDocument:(Document *)document +{ + if ((self = [super init])) { + _document = document; + } + return self; +} + +- (unsigned long long)length +{ + return 0x10000; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range +{ + uint16_t addr = (uint16_t) range.location; + unsigned long long length = range.length; + while (length) { + *(dst++) = [_document readMemory:addr++]; + length--; + } +} + +- (NSArray *)byteSlices +{ + return @[[[GBCompleteByteSlice alloc] initWithByteArray:self]]; +} + +- (HFByteArray *)subarrayWithRange:(HFRange)range +{ + unsigned char arr[range.length]; + [self copyBytes:arr range:range]; + HFByteArray *ret = [[HFBTreeByteArray alloc] init]; + HFFullMemoryByteSlice *slice = [[HFFullMemoryByteSlice alloc] initWithData:[NSData dataWithBytes:arr length:range.length]]; + [ret insertByteSlice:slice inRange:HFRangeMake(0, 0)]; + return ret; +} + +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange +{ + if (slice.length != lrange.length) return; /* Insertion is not allowed, only overwriting. */ + [_document performAtomicBlock:^{ + uint8_t values[lrange.length]; + [slice copyBytes:values range:HFRangeMake(0, lrange.length)]; + uint16_t addr = (uint16_t) lrange.location; + uint8_t *src = values; + unsigned long long length = lrange.length; + while (length) { + [_document writeMemory:addr++ value:*(src++)]; + length--; + } + }]; +} + +@end diff --git a/Cocoa/License.html b/Cocoa/License.html index 158c0a5..d064d29 100644 --- a/Cocoa/License.html +++ b/Cocoa/License.html @@ -18,12 +18,19 @@ font-size: 11px; font-weight: normal; } + + h3 { + text-align:center; + font-size: 11px; + font-weight: bold; + } -

MIT License

-

Copyright © 2015-2016 Lior Halphon

+

SameBoy

+

MIT License

+

Copyright © 2015-2016 Lior Halphon

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -42,5 +49,32 @@ 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.

+ +

Third-party Libraries

+

HexFiend

+

Copyright © 2005-2009, Peter Ammon +All rights reserved.

+ +

Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:

+ +
    +
  • Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer.
  • +
  • Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution.
  • +
+ +

THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE

\ No newline at end of file diff --git a/Cocoa/MainMenu.xib b/Cocoa/MainMenu.xib index d3bc791..ae3a500 100644 --- a/Cocoa/MainMenu.xib +++ b/Cocoa/MainMenu.xib @@ -1,5 +1,5 @@ - + @@ -368,6 +368,13 @@ + + + + + + + diff --git a/HexFiend/HFAnnotatedTree.h b/HexFiend/HFAnnotatedTree.h new file mode 100644 index 0000000..32122a1 --- /dev/null +++ b/HexFiend/HFAnnotatedTree.h @@ -0,0 +1,57 @@ +// +// HFAnnotatedTree.h +// HexFiend_2 +// +// Copyright 2010 ridiculous_fish. All rights reserved. +// + +#import + +typedef unsigned long long (*HFAnnotatedTreeAnnotaterFunction_t)(id left, id right); + + +@interface HFAnnotatedTreeNode : NSObject { + HFAnnotatedTreeNode *left; + HFAnnotatedTreeNode *right; + HFAnnotatedTreeNode *parent; + uint32_t level; +@public + unsigned long long annotation; +} + +/* Pure virtual method, which must be overridden. */ +- (NSComparisonResult)compare:(HFAnnotatedTreeNode *)node; + +/* Returns the next in-order node. */ +- (id)nextNode; + +- (id)leftNode; +- (id)rightNode; +- (id)parentNode; + +#if ! NDEBUG +- (void)verifyIntegrity; +- (void)verifyAnnotation:(HFAnnotatedTreeAnnotaterFunction_t)annotater; +#endif + + +@end + + +@interface HFAnnotatedTree : NSObject { + HFAnnotatedTreeAnnotaterFunction_t annotater; + HFAnnotatedTreeNode *root; +} + +- (instancetype)initWithAnnotater:(HFAnnotatedTreeAnnotaterFunction_t)annotater; +- (void)insertNode:(HFAnnotatedTreeNode *)node; +- (void)removeNode:(HFAnnotatedTreeNode *)node; +- (id)rootNode; +- (id)firstNode; +- (BOOL)isEmpty; + +#if ! NDEBUG +- (void)verifyIntegrity; +#endif + +@end diff --git a/HexFiend/HFAnnotatedTree.m b/HexFiend/HFAnnotatedTree.m new file mode 100644 index 0000000..9e64b9a --- /dev/null +++ b/HexFiend/HFAnnotatedTree.m @@ -0,0 +1,432 @@ +// +// HFAnnotatedTree.m +// HexFiend_2 +// +// Copyright 2010 ridiculous_fish. All rights reserved. +// + +#import "HFAnnotatedTree.h" + +#if NDEBUG +#define VERIFY_INTEGRITY() do { } while (0) +#else +#define VERIFY_INTEGRITY() [self verifyIntegrity] +#endif + +/* HFAnnotatedTree is an AA tree. */ + +static unsigned long long null_annotater(id left, id right) { USE(left); USE(right); return 0; } +static void skew(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree); +static BOOL split(HFAnnotatedTreeNode *oldparent, HFAnnotatedTree *tree); +static void rebalanceAfterLeafAdd(HFAnnotatedTreeNode *n, HFAnnotatedTree *tree); +static void delete(HFAnnotatedTreeNode *n, HFAnnotatedTree *tree); +static void verify_integrity(HFAnnotatedTreeNode *n); + +static HFAnnotatedTreeNode *next_node(HFAnnotatedTreeNode *node); + +static void insert(HFAnnotatedTreeNode *root, HFAnnotatedTreeNode *node, HFAnnotatedTree *tree); + +static inline HFAnnotatedTreeNode *get_parent(HFAnnotatedTreeNode *node); +static inline HFAnnotatedTreeNode *get_root(HFAnnotatedTree *tree); +static inline HFAnnotatedTreeNode *create_root(void); +static inline HFAnnotatedTreeAnnotaterFunction_t get_annotater(HFAnnotatedTree *tree); + +static void reannotate(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree); + +static HFAnnotatedTreeNode *first_node(HFAnnotatedTreeNode *node); + +static HFAnnotatedTreeNode *left_child(HFAnnotatedTreeNode *node); +static HFAnnotatedTreeNode *right_child(HFAnnotatedTreeNode *node); + +@implementation HFAnnotatedTree + +- (instancetype)initWithAnnotater:(HFAnnotatedTreeAnnotaterFunction_t)annot { + self = [super init]; + annotater = annot ? annot : null_annotater; + /* root is always an HFAnnotatedTreeNode with a left child but no right child */ + root = create_root(); + return self; +} + +- (void)dealloc { + [root release]; + [super dealloc]; +} + +- (id)rootNode { + return root; +} + +- (id)firstNode { + return first_node(root); +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + HFAnnotatedTree *copied = [[[self class] alloc] init]; + copied->annotater = annotater; + [copied->root release]; + copied->root = [root mutableCopyWithZone:zone]; + return copied; +} + +- (BOOL)isEmpty { + /* We're empty if our root has no children. */ + return left_child(root) == nil && right_child(root) == nil; +} + +- (void)insertNode:(HFAnnotatedTreeNode *)node { + HFASSERT(node != nil); + HFASSERT(get_parent(node) == nil); + /* Insert into the root */ + insert(root, [node retain], self); + VERIFY_INTEGRITY(); +} + +- (void)removeNode:(HFAnnotatedTreeNode *)node { + HFASSERT(node != nil); + HFASSERT(get_parent(node) != nil); + delete(node, self); + [node release]; + VERIFY_INTEGRITY(); +} + +#if ! NDEBUG +- (void)verifyIntegrity { + [root verifyIntegrity]; + [root verifyAnnotation:annotater]; +} +#endif + +static HFAnnotatedTreeNode *get_root(HFAnnotatedTree *tree) { + return tree->root; +} + +static HFAnnotatedTreeAnnotaterFunction_t get_annotater(HFAnnotatedTree *tree) { + return tree->annotater; +} + +@end + +@implementation HFAnnotatedTreeNode + +- (void)dealloc { + [left release]; + [right release]; + [super dealloc]; +} + +- (NSComparisonResult)compare:(HFAnnotatedTreeNode *)node { + USE(node); + UNIMPLEMENTED(); +} + +- (id)nextNode { + return next_node(self); +} + +- (id)leftNode { return left; } +- (id)rightNode { return right; } +- (id)parentNode { return parent; } + +- (id)mutableCopyWithZone:(NSZone *)zone { + HFAnnotatedTreeNode *copied = [[[self class] alloc] init]; + if (left) { + copied->left = [left mutableCopyWithZone:zone]; + copied->left->parent = copied; + } + if (right) { + copied->right = [right mutableCopyWithZone:zone]; + copied->right->parent = copied; + } + copied->level = level; + copied->annotation = annotation; + return copied; +} + +static HFAnnotatedTreeNode *left_child(HFAnnotatedTreeNode *node) { + return node->left; +} + +static HFAnnotatedTreeNode *right_child(HFAnnotatedTreeNode *node) { + return node->right; +} + + +static HFAnnotatedTreeNode *create_root(void) { + HFAnnotatedTreeNode *result = [[HFAnnotatedTreeNode alloc] init]; + result->level = UINT_MAX; //the root has a huge level + return result; +} + +static void reannotate(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree) { + HFASSERT(node != nil); + HFASSERT(tree != nil); + const HFAnnotatedTreeAnnotaterFunction_t annotater = get_annotater(tree); + node->annotation = annotater(node->left, node->right); +} + +static void insert(HFAnnotatedTreeNode *root, HFAnnotatedTreeNode *node, HFAnnotatedTree *tree) { + /* Insert node at the proper place in the tree. root is the root node, and we always insert to the left of root */ + BOOL left = YES; + HFAnnotatedTreeNode *parentNode = root, *currentChild; + /* Descend the tree until we find where to insert */ + while ((currentChild = (left ? parentNode->left : parentNode->right)) != nil) { + parentNode = currentChild; + left = ([parentNode compare:node] >= 0); //if parentNode is larger than the child, then the child goes to the left of node + } + + /* Now insert, potentially unbalancing the tree */ + if (left) { + parentNode->left = node; + } + else { + parentNode->right = node; + } + + /* Tell our node about its new parent */ + node->parent = parentNode; + + /* Rebalance and update annotations */ + rebalanceAfterLeafAdd(node, tree); +} + +static void skew(HFAnnotatedTreeNode *oldparent, HFAnnotatedTree *tree) { + HFAnnotatedTreeNode *newp = oldparent->left; + + if (oldparent->parent->left == oldparent) { + /* oldparent is the left child of its parent. Substitute in our left child. */ + oldparent->parent->left = newp; + } + else { + /* oldparent is the right child of its parent. Substitute in our left child. */ + oldparent->parent->right = newp; + } + + /* Tell the child about its new parent */ + newp->parent = oldparent->parent; + + /* Adopt its right child as our left child, and tell it about its new parent */ + oldparent->left = newp->right; + if (oldparent->left) oldparent->left->parent = oldparent; + + /* We are now the right child of the new parent */ + newp->right = oldparent; + oldparent->parent = newp; + + /* If we're now a leaf, our level is 1. Otherwise, it's one more than the level of our child. */ + oldparent->level = oldparent->left ? oldparent->left->level + 1 : 1; + + /* oldparent and newp both had their children changed, so need to be reannotated */ + reannotate(oldparent, tree); + reannotate(newp, tree); +} + +static BOOL split(HFAnnotatedTreeNode *oldparent, HFAnnotatedTree *tree) { + HFAnnotatedTreeNode *newp = oldparent->right; + if (newp && newp->right && newp->right->level == oldparent->level) { + if (oldparent->parent->left == oldparent) oldparent->parent->left = newp; + else oldparent->parent->right = newp; + newp->parent = oldparent->parent; + oldparent->parent = newp; + + oldparent->right = newp->left; + if (oldparent->right) oldparent->right->parent = oldparent; + newp->left = oldparent; + newp->level = oldparent->level + 1; + + /* oldparent and newp both had their children changed, so need to be reannotated */ + reannotate(oldparent, tree); + reannotate(newp, tree); + + return YES; + } + return NO; +} + +static void rebalanceAfterLeafAdd(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree) { // n is a node that has just been inserted and is now a leaf node. + node->level = 1; + node->left = nil; + node->right = nil; + reannotate(node, tree); + HFAnnotatedTreeNode * const root = get_root(tree); + HFAnnotatedTreeNode *probe; + for (probe = node->parent; probe != root; probe = probe->parent) { + reannotate(probe, tree); + // At this point probe->parent->level == probe->level + if (probe->level != (probe->left ? probe->left->level + 1 : 1)) { + // At this point the tree is correct, except (AA2) for n->parent + skew(probe, tree); + // We handle it (a left add) by changing it into a right add using Skew + // If the original add was to the left side of a node that is on the + // right side of a horisontal link, probe now points to the rights side + // of the second horisontal link, which is correct. + + // However if the original add was to the left of node with a horizontal + // link, we must get to the right side of the second link. + if (!probe->right || probe->level != probe->right->level) probe = probe->parent; + } + if (! split(probe->parent, tree)) break; + } + while (probe) { + reannotate(probe, tree); + probe = probe->parent; + } +} + +static void delete(HFAnnotatedTreeNode *n, HFAnnotatedTree *tree) { // If n is not a leaf, we first swap it out with the leaf node that just + // precedes it. + HFAnnotatedTreeNode *leaf = n, *tmp; + + if (n->left) { + /* Descend the right subtree of our left child, to get the closest predecessor */ + for (leaf = n->left; leaf->right; leaf = leaf->right) {} + // When we stop, leaf has no 'right' child so it cannot have a left one + } + else if (n->right) { + /* We have no children that precede us, but we have a child after us, so use our closest successor */ + leaf = n->right; + } + + /* tmp is either the parent who loses the child, or tmp is our right subtree. Either way, we will have to reduce its level. */ + tmp = leaf->parent == n ? leaf : leaf->parent; + + /* Tell leaf's parent to forget about leaf */ + if (leaf->parent->left == leaf) { + leaf->parent->left = NULL; + } + else { + leaf->parent->right = NULL; + } + reannotate(leaf->parent, tree); + + if (n != leaf) { + /* Replace ourself as our parent's child with leaf */ + if (n->parent->left == n) n->parent->left = leaf; + else n->parent->right = leaf; + + /* Leaf's parent is our parent */ + leaf->parent = n->parent; + + /* Our left and right children are now leaf's left and right children */ + if (n->left) n->left->parent = leaf; + leaf->left = n->left; + if (n->right) n->right->parent = leaf; + leaf->right = n->right; + + /* Leaf's level is our level */ + leaf->level = n->level; + } + /* Since we adopted n's children, transferring the retain, tell n to forget about them so it doesn't release them */ + n->left = nil; + n->right = nil; + + // free (n); + + HFAnnotatedTreeNode * const root = get_root(tree); + while (tmp != root) { + reannotate(tmp, tree); + // One of tmp's childern had its level reduced + if (tmp->level > (tmp->left ? tmp->left->level + 1 : 1)) { // AA2 failed + tmp->level--; + if (split(tmp, tree)) { + if (split(tmp, tree)) skew(tmp->parent->parent, tree); + break; + } + tmp = tmp->parent; + } + else if (tmp->level <= (tmp->right ? tmp->right->level + 1 : 1)){ + break; + } + else { // AA3 failed + skew(tmp, tree); + //if (tmp->right) tmp->right->level = tmp->right->left ? tmp->right->left->level + 1 : 1; + if (tmp->level > tmp->parent->level) { + skew(tmp, tree); + split(tmp->parent->parent, tree); + break; + } + tmp = tmp->parent->parent; + } + } + while (tmp) { + reannotate(tmp, tree); + tmp = tmp->parent; + } +} + +static HFAnnotatedTreeNode *next_node(HFAnnotatedTreeNode *node) { + /* Return the next in-order node */ + HFAnnotatedTreeNode *result; + if (node->right) { + /* We have a right child, which is after us. Descend its left subtree. */ + result = node->right; + while (result->left) { + result = result->left; + } + } + else { + /* We have no right child. If we are our parent's left child, then our parent is after us. Otherwise, we're our parent's right child and it was before us, so ascend while we're the parent's right child. */ + result = node; + while (result->parent && result->parent->right == result) { + result = result->parent; + } + /* Now result is the left child of the parent (or has NULL parents), so its parent is the next node */ + result = result->parent; + } + /* Don't return the root */ + if (result != nil && result->parent == nil) { + result = next_node(result); + } + return result; +} + +static HFAnnotatedTreeNode *first_node(HFAnnotatedTreeNode *node) { + /* Return the first node */ + HFAnnotatedTreeNode *result = nil, *cursor = node->left; + while (cursor) { + /* Descend the left subtree */ + result = cursor; + cursor = cursor->left; + } + return result; +} + +static HFAnnotatedTreeNode *get_parent(HFAnnotatedTreeNode *node) { + HFASSERT(node != nil); + return node->parent; +} + +static void __attribute__((unused))verify_integrity(HFAnnotatedTreeNode *n) { + HFASSERT(!n->left || n->left->parent == n); + HFASSERT(!n->right || n->right->parent == n); + HFASSERT(!next_node(n) || [n compare:next_node(n)] <= 0); + HFASSERT(!n->parent || n->parent->level >= n->level); + if (n->parent == nil) { + /* root node */ + HFASSERT(n->level == UINT_MAX); + } + else { + /* non-root node */ + HFASSERT(n->level == (n->left == NULL ? 1 : n->left->level + 1)); + HFASSERT((n->level <= 1) || (n->right && n->level - n->right->level <= 1)); + } + HFASSERT(!n->parent || !n->parent->parent || + n->parent->parent->level > n->level); +} + +#if ! NDEBUG +- (void)verifyIntegrity { + [left verifyIntegrity]; + [right verifyIntegrity]; + verify_integrity(self); +} + +- (void)verifyAnnotation:(HFAnnotatedTreeAnnotaterFunction_t)annotater { + [left verifyAnnotation:annotater]; + [right verifyAnnotation:annotater]; + unsigned long long expectedAnnotation = annotater(left, right); + HFASSERT(annotation == expectedAnnotation); +} +#endif + +@end diff --git a/HexFiend/HFBTree.h b/HexFiend/HFBTree.h new file mode 100644 index 0000000..3bb8bd9 --- /dev/null +++ b/HexFiend/HFBTree.h @@ -0,0 +1,40 @@ +// +// HFBTree.h +// HexFiend +// +// + +#import + +typedef unsigned long long HFBTreeIndex; + +@class HFBTreeNode; + +@protocol HFBTreeEntry +- (unsigned long long)length; +@end + +@interface HFBTree : NSObject { + unsigned int depth; + HFBTreeNode *root; +} + +- (void)insertEntry:(id)entry atOffset:(HFBTreeIndex)offset; +- (id)entryContainingOffset:(HFBTreeIndex)offset beginningOffset:(HFBTreeIndex *)outBeginningOffset; +- (void)removeEntryAtOffset:(HFBTreeIndex)offset; +- (void)removeAllEntries; + +#if HFUNIT_TESTS +- (void)checkIntegrityOfCachedLengths; +- (void)checkIntegrityOfBTreeStructure; +#endif + +- (NSEnumerator *)entryEnumerator; +- (NSArray *)allEntries; + +- (HFBTreeIndex)length; + +/* Applies the given function to the entry at the given offset, continuing with subsequent entries until the function returns NO. Do not modify the tree from within this function. */ +- (void)applyFunction:(BOOL (*)(id entry, HFBTreeIndex offset, void *userInfo))func toEntriesStartingAtOffset:(HFBTreeIndex)offset withUserInfo:(void *)userInfo; + +@end diff --git a/HexFiend/HFBTree.m b/HexFiend/HFBTree.m new file mode 100644 index 0000000..5bb7806 --- /dev/null +++ b/HexFiend/HFBTree.m @@ -0,0 +1,1099 @@ +// +// HFBTree.m +// BTree +// +// Created by peter on 2/6/09. +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import "HFBTree.h" +#include + +#define FIXUP_LENGTHS 0 + +#define BTREE_BRANCH_ORDER 10 +#define BTREE_LEAF_ORDER 10 + +#define BTREE_ORDER 10 +#define BTREE_NODE_MINIMUM_VALUE_COUNT (BTREE_ORDER / 2) + +#define BTREE_LEAF_MINIMUM_VALUE_COUNT (BTREE_LEAF_ORDER / 2) + +#define BAD_INDEX ((ChildIndex_t)(-1)) +typedef unsigned int ChildIndex_t; + +/* How deep can our tree get? 128 is huge. */ +#define MAX_DEPTH 128 +#define BAD_DEPTH ((TreeDepth_t)(-1)) +typedef unsigned int TreeDepth_t; + +#define TreeEntry NSObject +#define HFBTreeLength(x) [(TreeEntry *)(x) length] + + +@class HFBTreeNode, HFBTreeBranch, HFBTreeLeaf; + +static TreeEntry *btree_search(HFBTree *tree, HFBTreeIndex offset, HFBTreeIndex *outBeginningOffset); +static id btree_insert_returning_retained_value_for_parent(HFBTree *tree, TreeEntry *entry, HFBTreeIndex offset); +static BOOL btree_remove(HFBTree *tree, HFBTreeIndex offset); +static void __attribute__((unused)) btree_recursive_check_integrity(HFBTree *tree, HFBTreeNode *branchOrLeaf, TreeDepth_t depth, HFBTreeNode **linkHelper); +#if FIXUP_LENGTHS +static HFBTreeIndex btree_recursive_fixup_cached_lengths(HFBTree *tree, HFBTreeNode *branchOrLeaf); +#endif +static HFBTreeIndex __attribute__((unused)) btree_recursive_check_integrity_of_cached_lengths(HFBTreeNode *branchOrLeaf); +static BOOL btree_are_cached_lengths_correct(HFBTreeNode *branchOrLeaf, HFBTreeIndex *outLength); +#if FIXUP_LENGTHS +static NSUInteger btree_entry_count(HFBTreeNode *branchOrLeaf); +#endif +static ChildIndex_t count_node_values(HFBTreeNode *node); +static HFBTreeIndex sum_child_lengths(const id *children, const BOOL isLeaf); +static HFBTreeNode *mutable_copy_node(HFBTreeNode *node, TreeDepth_t depth, HFBTreeNode **linkingHelper); + +#if NDEBUG +#define VERIFY_LENGTH(a) +#else +#define VERIFY_LENGTH(a) btree_recursive_check_integrity_of_cached_lengths((a)) +#endif + +#define IS_BRANCH(a) [(a) isKindOfClass:[HFBTreeBranch class]] +#define IS_LEAF(a) [(a) isKindOfClass:[HFBTreeLeaf class]] + +#define ASSERT_IS_BRANCH(a) HFASSERT(IS_BRANCH(a)) +#define ASSERT_IS_LEAF(a) HFASSERT(IS_LEAF(a)) + +#define GET_LENGTH(node, parentIsLeaf) ((parentIsLeaf) ? HFBTreeLength(node) : CHECK_CAST((node), HFBTreeNode)->subtreeLength) + +#define CHECK_CAST(a, b) ({HFASSERT([(a) isKindOfClass:[b class]]); (b *)(a);}) +#define CHECK_CAST_OR_NULL(a, b) ({HFASSERT((a == nil) || [(a) isKindOfClass:[b class]]); (b *)(a);}) + +#define DEFEAT_INLINE 1 + +#if DEFEAT_INLINE +#define FORCE_STATIC_INLINE static +#else +#define FORCE_STATIC_INLINE static __inline__ __attribute__((always_inline)) +#endif + +@interface HFBTreeEnumerator : NSEnumerator { + HFBTreeLeaf *currentLeaf; + ChildIndex_t childIndex; +} + +- (instancetype)initWithLeaf:(HFBTreeLeaf *)leaf; + +@end + +@interface HFBTreeNode : NSObject { + @public + NSUInteger rc; + HFBTreeIndex subtreeLength; + HFBTreeNode *left, *right; + id children[BTREE_ORDER]; +} + +@end + +@implementation HFBTreeNode + +- (id)retain { + HFAtomicIncrement(&rc, NO); + return self; +} + +- (oneway void)release { + NSUInteger result = HFAtomicDecrement(&rc, NO); + if (result == (NSUInteger)(-1)) { + [self dealloc]; + } +} + +- (NSUInteger)retainCount { + return 1 + rc; +} + +- (void)dealloc { + for (ChildIndex_t i=0; i < BTREE_BRANCH_ORDER; i++) { + if (! children[i]) break; + [children[i] release]; + } + [super dealloc]; +} + +- (NSString *)shortDescription { + return [NSString stringWithFormat:@"<%@: %p (%llu)>", [self class], self, subtreeLength]; +} + +@end + +@interface HFBTreeBranch : HFBTreeNode +@end + +@implementation HFBTreeBranch + +- (NSString *)description { + const char *lengthsMatchString = (subtreeLength == sum_child_lengths(children, NO) ? "" : " INCONSISTENT "); + NSMutableString *s = [NSMutableString stringWithFormat:@"<%@: %p (length: %llu%s) (children: %u) (", [self class], self, subtreeLength, lengthsMatchString, count_node_values(self)]; + NSUInteger i; + for (i=0; i < BTREE_ORDER; i++) { + if (children[i] == nil) break; + [s appendFormat:@"%s%@", (i == 0 ? "" : ", "), [children[i] shortDescription]]; + } + [s appendString:@")>"]; + return s; +} + +@end + +@interface HFBTreeLeaf : HFBTreeNode +@end + +@implementation HFBTreeLeaf + +- (NSString *)description { + NSMutableString *s = [NSMutableString stringWithFormat:@"<%@: %p (%u) (", [self class], self, count_node_values(self)]; + NSUInteger i; + for (i=0; i < BTREE_ORDER; i++) { + if (children[i] == nil) break; + [s appendFormat:@"%s%@", (i == 0 ? "" : ", "), children[i]]; + } + [s appendString:@")>"]; + return s; +} + +@end + +@implementation HFBTree + +- (instancetype)init { + self = [super init]; + depth = BAD_DEPTH; + root = nil; + return self; +} + +- (void)dealloc { + [root release]; + [super dealloc]; +} + +#if HFUNIT_TESTS +- (void)checkIntegrityOfCachedLengths { + if (root == nil) { + /* nothing */ + } + else { + btree_recursive_check_integrity_of_cached_lengths(root); + } +} + +- (void)checkIntegrityOfBTreeStructure { + if (depth == BAD_DEPTH) { + HFASSERT(root == nil); + } + else { + HFBTreeNode *linkHelper[MAX_DEPTH + 1] = {}; + btree_recursive_check_integrity(self, root, depth, linkHelper); + } +} +#endif + +- (HFBTreeIndex)length { + if (root == nil) return 0; + return ((HFBTreeNode *)root)->subtreeLength; +} + +- (void)insertEntry:(id)entryObj atOffset:(HFBTreeIndex)offset { + TreeEntry *entry = (TreeEntry *)entryObj; //avoid a conflicting types warning + HFASSERT(entry); + HFASSERT(offset <= [self length]); + if (! root) { + HFASSERT([self length] == 0); + HFASSERT(depth == BAD_DEPTH); + HFBTreeLeaf *leaf = [[HFBTreeLeaf alloc] init]; + leaf->children[0] = [entry retain]; + leaf->subtreeLength = HFBTreeLength(entry); + root = leaf; + depth = 0; + } + else { + HFBTreeNode *newParentValue = btree_insert_returning_retained_value_for_parent(self, entry, offset); + if (newParentValue) { + HFBTreeBranch *newRoot = [[HFBTreeBranch alloc] init]; + newRoot->children[0] = root; //transfer our retain + newRoot->children[1] = newParentValue; //transfer the retain we got from the function + newRoot->subtreeLength = HFSum(root->subtreeLength, newParentValue->subtreeLength); + root = newRoot; + depth++; + HFASSERT(depth <= MAX_DEPTH); + } +#if FIXUP_LENGTHS + HFBTreeIndex outLength = -1; + if (! btree_are_cached_lengths_correct(root, &outLength)) { + puts("Fixed up length after insertion"); + btree_recursive_fixup_cached_lengths(self, root); + } +#endif + } +} + +- (TreeEntry *)entryContainingOffset:(HFBTreeIndex)offset beginningOffset:(HFBTreeIndex *)outBeginningOffset { + HFASSERT(root != nil); + return btree_search(self, offset, outBeginningOffset); +} + +- (void)removeAllEntries { + [root release]; + root = nil; + depth = BAD_DEPTH; +} + +- (void)removeEntryAtOffset:(HFBTreeIndex)offset { + HFASSERT(root != nil); +#if FIXUP_LENGTHS + const NSUInteger beforeCount = btree_entry_count(root); +#endif + BOOL deleteRoot = btree_remove(self, offset); + if (deleteRoot) { + HFASSERT(count_node_values(root) <= 1); + id newRoot = [root->children[0] retain]; //may be nil! + [root release]; + root = newRoot; + depth--; + } +#if FIXUP_LENGTHS + const NSUInteger afterCount = btree_entry_count(root); + if (beforeCount != afterCount + 1) { + NSLog(@"Bad counts: before %lu, after %lu", beforeCount, afterCount); + } + HFBTreeIndex outLength = -1; + static NSUInteger fixupCount; + if (! btree_are_cached_lengths_correct(root, &outLength)) { + fixupCount++; + printf("Fixed up length after deletion (%lu)\n", (unsigned long)fixupCount); + btree_recursive_fixup_cached_lengths(self, root); + } + else { + //printf("Length post-deletion was OK! (%lu)\n", fixupCount); + } +#endif +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + USE(zone); + HFBTree *result = [[[self class] alloc] init]; + result->depth = depth; + HFBTreeNode *linkingHelper[MAX_DEPTH + 1]; + bzero(linkingHelper, (1 + depth) * sizeof *linkingHelper); + result->root = mutable_copy_node(root, depth, linkingHelper); + return result; +} + +FORCE_STATIC_INLINE ChildIndex_t count_node_values(HFBTreeNode *node) { + ChildIndex_t count; + for (count=0; count < BTREE_LEAF_ORDER; count++) { + if (node->children[count] == nil) break; + } + return count; +} + +FORCE_STATIC_INLINE HFBTreeIndex sum_child_lengths(const id *children, const BOOL isLeaf) { + HFBTreeIndex result = 0; + for (ChildIndex_t childIndex = 0; childIndex < BTREE_ORDER; childIndex++) { + id child = children[childIndex]; + if (! child) break; + HFBTreeIndex childLength = GET_LENGTH(child, isLeaf); + result = HFSum(result, childLength); + } + return result; +} + +FORCE_STATIC_INLINE HFBTreeIndex sum_N_child_lengths(const id *children, ChildIndex_t numChildren, const BOOL isLeaf) { + HFBTreeIndex result = 0; + for (ChildIndex_t childIndex = 0; childIndex < numChildren; childIndex++) { + id child = children[childIndex]; + HFASSERT(child != NULL); + HFBTreeIndex childLength = GET_LENGTH(child, isLeaf); + result = HFSum(result, childLength); + } + return result; +} + +FORCE_STATIC_INLINE ChildIndex_t index_containing_offset(HFBTreeNode *node, HFBTreeIndex offset, HFBTreeIndex * restrict outOffset, const BOOL isLeaf) { + ChildIndex_t childIndex; + HFBTreeIndex previousSum = 0; + const id *children = node->children; + for (childIndex = 0; childIndex < BTREE_ORDER; childIndex++) { + HFASSERT(children[childIndex] != nil); + HFBTreeIndex childLength = GET_LENGTH(children[childIndex], isLeaf); + HFBTreeIndex newSum = HFSum(childLength, previousSum); + if (newSum > offset) { + break; + } + previousSum = newSum; + } + *outOffset = previousSum; + return childIndex; +} + +FORCE_STATIC_INLINE id child_containing_offset(HFBTreeNode *node, HFBTreeIndex offset, HFBTreeIndex * restrict outOffset, const BOOL isLeaf) { + return node->children[index_containing_offset(node, offset, outOffset, isLeaf)]; +} + +FORCE_STATIC_INLINE ChildIndex_t index_for_child_at_offset(HFBTreeNode *node, HFBTreeIndex offset, const BOOL isLeaf) { + ChildIndex_t childIndex; + HFBTreeIndex previousSum = 0; + id *const children = node->children; + for (childIndex = 0; childIndex < BTREE_ORDER; childIndex++) { + if (previousSum == offset) break; + HFASSERT(children[childIndex] != nil); + HFBTreeIndex childLength = GET_LENGTH(children[childIndex], isLeaf); + previousSum = HFSum(childLength, previousSum); + HFASSERT(previousSum <= offset); + } + HFASSERT(childIndex <= BTREE_ORDER); //note we allow the child index to be one past the end (in which case we are sure to split the node) + HFASSERT(previousSum == offset); //but we still require the offset to be the sum of all the lengths of this node + return childIndex; +} + +FORCE_STATIC_INLINE ChildIndex_t child_index_for_insertion_at_offset(HFBTreeBranch *branch, HFBTreeIndex insertionOffset, HFBTreeIndex *outPriorCombinedOffset) { + ChildIndex_t indexForInsertion; + HFBTreeIndex priorCombinedOffset = 0; + id *const children = branch->children; + for (indexForInsertion = 0; indexForInsertion < BTREE_BRANCH_ORDER; indexForInsertion++) { + if (! children[indexForInsertion]) break; + HFBTreeNode *childNode = CHECK_CAST(children[indexForInsertion], HFBTreeNode); + HFBTreeIndex subtreeLength = childNode->subtreeLength; + HFASSERT(subtreeLength > 0); + HFBTreeIndex newOffset = HFSum(priorCombinedOffset, subtreeLength); + if (newOffset >= insertionOffset) { + break; + } + priorCombinedOffset = newOffset; + } + *outPriorCombinedOffset = priorCombinedOffset; + return indexForInsertion; +} + +FORCE_STATIC_INLINE ChildIndex_t child_index_for_deletion_at_offset(HFBTreeBranch *branch, HFBTreeIndex deletionOffset, HFBTreeIndex *outPriorCombinedOffset) { + ChildIndex_t indexForDeletion; + HFBTreeIndex priorCombinedOffset = 0; + for (indexForDeletion = 0; indexForDeletion < BTREE_BRANCH_ORDER; indexForDeletion++) { + HFASSERT(branch->children[indexForDeletion] != nil); + HFBTreeNode *childNode = CHECK_CAST(branch->children[indexForDeletion], HFBTreeNode); + HFBTreeIndex subtreeLength = childNode->subtreeLength; + HFASSERT(subtreeLength > 0); + HFBTreeIndex newOffset = HFSum(priorCombinedOffset, subtreeLength); + if (newOffset > deletionOffset) { + /* Key difference between insertion and deletion: insertion uses >=, while deletion uses > */ + break; + } + priorCombinedOffset = newOffset; + } + *outPriorCombinedOffset = priorCombinedOffset; + return indexForDeletion; +} + +FORCE_STATIC_INLINE void insert_value_into_array(id value, NSUInteger insertionIndex, id *array, NSUInteger arrayCount) { + HFASSERT(insertionIndex <= arrayCount); + HFASSERT(arrayCount > 0); + NSUInteger pushingIndex = arrayCount - 1; + while (pushingIndex > insertionIndex) { + array[pushingIndex] = array[pushingIndex - 1]; + pushingIndex--; + } + array[insertionIndex] = [value retain]; +} + + +FORCE_STATIC_INLINE void remove_value_from_array(NSUInteger removalIndex, id *array, NSUInteger arrayCount) { + HFASSERT(removalIndex < arrayCount); + HFASSERT(arrayCount > 0); + HFASSERT(array[removalIndex] != nil); + [array[removalIndex] release]; + for (NSUInteger pullingIndex = removalIndex + 1; pullingIndex < arrayCount; pullingIndex++) { + array[pullingIndex - 1] = array[pullingIndex]; + } + array[arrayCount - 1] = nil; +} + +static void split_array(const restrict id *values, ChildIndex_t valueCount, restrict id *left, restrict id *right, ChildIndex_t leftArraySizeForClearing) { + const ChildIndex_t midPoint = valueCount/2; + ChildIndex_t inputIndex = 0, outputIndex = 0; + while (inputIndex < midPoint) { + left[outputIndex++] = values[inputIndex++]; + } + + /* Clear the remainder of our left array. Right array does not have to be cleared. */ + HFASSERT(outputIndex <= leftArraySizeForClearing); + while (outputIndex < leftArraySizeForClearing) { + left[outputIndex++] = nil; + } + + /* Move the second half of our values into the right array */ + outputIndex = 0; + while (inputIndex < valueCount) { + right[outputIndex++] = values[inputIndex++]; + } +} + +FORCE_STATIC_INLINE HFBTreeNode *add_child_to_node_possibly_creating_split(HFBTreeNode *node, id value, ChildIndex_t insertionLocation, BOOL isLeaf) { + ChildIndex_t childCount = count_node_values(node); + HFASSERT(insertionLocation <= childCount); + if (childCount < BTREE_ORDER) { + /* No need to make a split */ + insert_value_into_array(value, insertionLocation, node->children, childCount + 1); + node->subtreeLength = HFSum(node->subtreeLength, GET_LENGTH(value, isLeaf)); + return nil; + } + + HFASSERT(node->children[BTREE_ORDER - 1] != nil); /* we require that it be full */ + id allEntries[BTREE_ORDER + 1]; + memcpy(allEntries, node->children, BTREE_ORDER * sizeof *node->children); + allEntries[BTREE_ORDER] = nil; + + /* insert_value_into_array applies a retain, so allEntries owns a retain on its values */ + insert_value_into_array(value, insertionLocation, allEntries, BTREE_ORDER + 1); + HFBTreeNode *newNode = [[[node class] alloc] init]; + + /* figure out our total length */ + HFBTreeIndex totalLength = HFSum(node->subtreeLength, GET_LENGTH(value, isLeaf)); + + /* Distribute half our values to the new leaf */ + split_array(allEntries, sizeof allEntries / sizeof *allEntries, node->children, newNode->children, BTREE_ORDER); + + /* figure out how much is in the new array */ + HFBTreeIndex newNodeLength = sum_child_lengths(newNode->children, isLeaf); + + /* update our lengths */ + HFASSERT(newNodeLength < totalLength); + newNode->subtreeLength = newNodeLength; + node->subtreeLength = totalLength - newNodeLength; + + /* Link it in */ + HFBTreeNode *rightNode = node->right; + newNode->right = rightNode; + if (rightNode) rightNode->left = newNode; + newNode->left = node; + node->right = newNode; + return newNode; +} + +FORCE_STATIC_INLINE void add_values_to_array(const id * restrict srcValues, NSUInteger amountToCopy, id * restrict targetValues, NSUInteger amountToPush) { + // a pushed value at index X goes to index X + amountToCopy + NSUInteger pushIndex = amountToPush; + while (pushIndex--) { + targetValues[amountToCopy + pushIndex] = targetValues[pushIndex]; + } + for (NSUInteger i = 0; i < amountToCopy; i++) { + targetValues[i] = [srcValues[i] retain]; + } +} + +FORCE_STATIC_INLINE void remove_values_from_array(id * restrict array, NSUInteger amountToRemove, NSUInteger totalArrayLength) { + HFASSERT(totalArrayLength >= amountToRemove); + /* Release existing values */ + NSUInteger i; + for (i=0; i < amountToRemove; i++) { + [array[i] release]; + } + /* Move remaining values */ + for (i=amountToRemove; i < totalArrayLength; i++) { + array[i - amountToRemove] = array[i]; + } + /* Clear the end */ + for (i=totalArrayLength - amountToRemove; i < totalArrayLength; i++) { + array[i] = nil; + } +} + +FORCE_STATIC_INLINE BOOL rebalance_node_by_distributing_to_neighbors(HFBTreeNode *node, ChildIndex_t childCount, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childCount < BTREE_NODE_MINIMUM_VALUE_COUNT); + BOOL result = NO; + HFBTreeNode *leftNeighbor = node->left, *rightNeighbor = node->right; + const ChildIndex_t leftSpaceAvailable = (leftNeighbor ? BTREE_ORDER - count_node_values(leftNeighbor) : 0); + const ChildIndex_t rightSpaceAvailable = (rightNeighbor ? BTREE_ORDER - count_node_values(rightNeighbor) : 0); + if (leftSpaceAvailable + rightSpaceAvailable >= childCount) { + /* We have enough space to redistribute. Try to do it in such a way that both neighbors end up with the same number of items. */ + ChildIndex_t itemCountForLeft = 0, itemCountForRight = 0, itemCountRemaining = childCount; + if (leftSpaceAvailable > rightSpaceAvailable) { + ChildIndex_t amountForLeft = MIN(leftSpaceAvailable - rightSpaceAvailable, itemCountRemaining); + itemCountForLeft += amountForLeft; + itemCountRemaining -= amountForLeft; + } + else if (rightSpaceAvailable > leftSpaceAvailable) { + ChildIndex_t amountForRight = MIN(rightSpaceAvailable - leftSpaceAvailable, itemCountRemaining); + itemCountForRight += amountForRight; + itemCountRemaining -= amountForRight; + } + /* Now distribute the remainder (if any) evenly, preferring the remainder to go left, because it is slightly cheaper to append to the left than prepend to the right */ + itemCountForRight += itemCountRemaining / 2; + itemCountForLeft += itemCountRemaining - (itemCountRemaining / 2); + HFASSERT(itemCountForLeft <= leftSpaceAvailable); + HFASSERT(itemCountForRight <= rightSpaceAvailable); + HFASSERT(itemCountForLeft + itemCountForRight == childCount); + + if (itemCountForLeft > 0) { + /* append to the end */ + HFBTreeIndex additionalLengthForLeft = sum_N_child_lengths(node->children, itemCountForLeft, isLeaf); + leftNeighbor->subtreeLength = HFSum(leftNeighbor->subtreeLength, additionalLengthForLeft); + add_values_to_array(node->children, itemCountForLeft, leftNeighbor->children + BTREE_ORDER - leftSpaceAvailable, 0); + HFASSERT(leftNeighbor->subtreeLength == sum_child_lengths(leftNeighbor->children, isLeaf)); + *modifiedLeftNeighbor = YES; + } + if (itemCountForRight > 0) { + /* append to the beginning */ + HFBTreeIndex additionalLengthForRight = sum_N_child_lengths(node->children + itemCountForLeft, itemCountForRight, isLeaf); + rightNeighbor->subtreeLength = HFSum(rightNeighbor->subtreeLength, additionalLengthForRight); + add_values_to_array(node->children + itemCountForLeft, itemCountForRight, rightNeighbor->children, BTREE_ORDER - rightSpaceAvailable); + HFASSERT(rightNeighbor->subtreeLength == sum_child_lengths(rightNeighbor->children, isLeaf)); + *modifiedRightNeighbor = YES; + } + /* Remove ourself from the linked list */ + if (leftNeighbor) { + leftNeighbor->right = rightNeighbor; + } + if (rightNeighbor) { + rightNeighbor->left = leftNeighbor; + } + /* Even though we've essentially orphaned ourself, we need to force ourselves consistent (by making ourselves empty) because our parent still references us, and we don't want to make our parent inconsistent. */ + for (ChildIndex_t childIndex = 0; node->children[childIndex] != nil; childIndex++) { + [node->children[childIndex] release]; + node->children[childIndex] = nil; + } + node->subtreeLength = 0; + + result = YES; + } + return result; +} + + +FORCE_STATIC_INLINE BOOL share_children(HFBTreeNode *node, ChildIndex_t childCount, HFBTreeNode *neighbor, BOOL isRightNeighbor, BOOL isLeaf) { + ChildIndex_t neighborCount = count_node_values(neighbor); + ChildIndex_t totalChildren = (childCount + neighborCount); + BOOL result = NO; + if (totalChildren <= 2 * BTREE_LEAF_ORDER && totalChildren >= 2 * BTREE_LEAF_MINIMUM_VALUE_COUNT) { + ChildIndex_t finalMyCount = totalChildren / 2; + ChildIndex_t finalNeighborCount = totalChildren - finalMyCount; + HFASSERT(finalNeighborCount < neighborCount); + HFASSERT(finalMyCount > childCount); + ChildIndex_t amountToTransfer = finalMyCount - childCount; + HFBTreeIndex lengthChange; + if (isRightNeighbor) { + /* Transfer from left end of right neighbor to this right end of this leaf. This retains the values. */ + add_values_to_array(neighbor->children, amountToTransfer, node->children + childCount, 0); + /* Remove from beginning of right neighbor. This releases them. */ + remove_values_from_array(neighbor->children, amountToTransfer, neighborCount); + lengthChange = sum_N_child_lengths(node->children + childCount, amountToTransfer, isLeaf); + } + else { + /* Transfer from right end of left neighbor to left end of this leaf */ + add_values_to_array(neighbor->children + neighborCount - amountToTransfer, amountToTransfer, node->children, childCount); + /* Remove from end of left neighbor */ + remove_values_from_array(neighbor->children + neighborCount - amountToTransfer, amountToTransfer, amountToTransfer); + lengthChange = sum_N_child_lengths(node->children, amountToTransfer, isLeaf); + } + HFASSERT(lengthChange <= neighbor->subtreeLength); + neighbor->subtreeLength -= lengthChange; + node->subtreeLength = HFSum(node->subtreeLength, lengthChange); + HFASSERT(count_node_values(node) == finalMyCount); + HFASSERT(count_node_values(neighbor) == finalNeighborCount); + result = YES; + } + return result; +} + +static BOOL rebalance_node_by_sharing_with_neighbors(HFBTreeNode *node, ChildIndex_t childCount, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childCount < BTREE_LEAF_MINIMUM_VALUE_COUNT); + BOOL result = NO; + HFBTreeNode *leftNeighbor = node->left, *rightNeighbor = node->right; + if (leftNeighbor) { + result = share_children(node, childCount, leftNeighbor, NO, isLeaf); + if (result) *modifiedLeftNeighbor = YES; + } + if (! result && rightNeighbor) { + result = share_children(node, childCount, rightNeighbor, YES, isLeaf); + if (result) *modifiedRightNeighbor = YES; + } + return result; +} + +/* Return YES if this leaf should be removed after rebalancing. Other nodes are never removed. */ +FORCE_STATIC_INLINE BOOL rebalance_node_after_deletion(HFBTreeNode *node, ChildIndex_t childCount, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childCount < BTREE_LEAF_MINIMUM_VALUE_COUNT); + /* We may only delete this leaf, and not adjacent leaves. Thus our rebalancing strategy is: + If the items to the left or right have sufficient space to hold us, then push our values left or right, and delete this node. + Otherwise, steal items from the left until we have the same number of items. */ + BOOL deleteNode = NO; + if (rebalance_node_by_distributing_to_neighbors(node, childCount, isLeaf, modifiedLeftNeighbor, modifiedRightNeighbor)) { + deleteNode = YES; + //puts("rebalance_node_by_distributing_to_neighbors"); + } + else if (rebalance_node_by_sharing_with_neighbors(node, childCount, isLeaf, modifiedLeftNeighbor, modifiedRightNeighbor)) { + deleteNode = NO; + //puts("rebalance_node_by_sharing_with_neighbors"); + } + else { + [NSException raise:NSInternalInconsistencyException format:@"Unable to rebalance after deleting node %@", node]; + } + return deleteNode; +} + + +FORCE_STATIC_INLINE BOOL remove_value_from_node_with_possible_rebalance(HFBTreeNode *node, ChildIndex_t childIndex, BOOL isRootNode, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childIndex < BTREE_ORDER); + HFASSERT(node != nil); + HFASSERT(node->children[childIndex] != nil); + HFBTreeIndex entryLength = GET_LENGTH(node->children[childIndex], isLeaf); + HFASSERT(entryLength <= node->subtreeLength); + node->subtreeLength -= entryLength; + BOOL deleteInputNode = NO; + +#if ! NDEBUG + const id savedChild = node->children[childIndex]; + NSUInteger childMultiplicity = 0; + NSUInteger v; + for (v = 0; v < BTREE_ORDER; v++) { + if (node->children[v] == savedChild) childMultiplicity++; + if (node->children[v] == nil) break; + } + +#endif + + /* Figure out how many children we have; start at one more than childIndex since we know that childIndex is a valid index */ + ChildIndex_t childCount; + for (childCount = childIndex + 1; childCount < BTREE_ORDER; childCount++) { + if (! node->children[childCount]) break; + } + + /* Remove our value at childIndex; this sends it a release message */ + remove_value_from_array(childIndex, node->children, childCount); + HFASSERT(childCount > 0); + childCount--; + +#if ! NDEBUG + for (v = 0; v < childCount; v++) { + if (node->children[v] == savedChild) childMultiplicity--; + } + HFASSERT(childMultiplicity == 1); +#endif + + if (childCount < BTREE_LEAF_MINIMUM_VALUE_COUNT && ! isRootNode) { + /* We have too few items; try to rebalance (this will always be possible except from the root node) */ + deleteInputNode = rebalance_node_after_deletion(node, childCount, isLeaf, modifiedLeftNeighbor, modifiedRightNeighbor); + } + else { + //NSLog(@"Deletion from %@ with %u remaining, %s root node, so no need to rebalance\n", node, childCount, isRootNode ? "is" : "is not"); + } + + return deleteInputNode; +} + +FORCE_STATIC_INLINE void update_node_having_changed_size_of_child(HFBTreeNode *node, BOOL isLeaf) { + HFBTreeIndex newLength = sum_child_lengths(node->children, isLeaf); + /* This should only be called if the length actually changes - so assert as such */ + /* I no longer think the above line is true. It's possible that we can delete a node, and then after a rebalance, we can become the same size we were before. */ + //HFASSERT(node->subtreeLength != newLength); + node->subtreeLength = newLength; +} + +struct SubtreeInfo_t { + HFBTreeBranch *branch; + ChildIndex_t childIndex; //childIndex is the index of the child of branch, not branch's index in its parent +}; + +static HFBTreeLeaf *btree_descend(HFBTree *tree, struct SubtreeInfo_t *outDescentInfo, HFBTreeIndex *insertionOffset, BOOL isForDelete) { + TreeDepth_t maxDepth = tree->depth; + HFASSERT(maxDepth != BAD_DEPTH && maxDepth <= MAX_DEPTH); + id currentBranchOrLeaf = tree->root; + HFBTreeIndex offsetForSubtree = *insertionOffset; + for (TreeDepth_t currentDepth = 0; currentDepth < maxDepth; currentDepth++) { + ASSERT_IS_BRANCH(currentBranchOrLeaf); + HFBTreeBranch *currentBranch = currentBranchOrLeaf; + HFBTreeIndex priorCombinedOffset = (HFBTreeIndex)-1; + ChildIndex_t nextChildIndex = (isForDelete ? child_index_for_deletion_at_offset : child_index_for_insertion_at_offset)(currentBranch, offsetForSubtree, &priorCombinedOffset); + outDescentInfo[currentDepth].branch = currentBranch; + outDescentInfo[currentDepth].childIndex = nextChildIndex; + offsetForSubtree -= priorCombinedOffset; + currentBranchOrLeaf = currentBranch->children[nextChildIndex]; + if (isForDelete) { + HFBTreeNode *node = currentBranchOrLeaf; + HFASSERT(node->subtreeLength > offsetForSubtree); + } + } + ASSERT_IS_LEAF(currentBranchOrLeaf); + *insertionOffset = offsetForSubtree; + return currentBranchOrLeaf; +} + +struct LeafInfo_t { + HFBTreeLeaf *leaf; + ChildIndex_t entryIndex; + HFBTreeIndex offsetOfEntryInTree; +}; + +static struct LeafInfo_t btree_find_leaf(HFBTree *tree, HFBTreeIndex offset) { + TreeDepth_t depth = tree->depth; + HFBTreeNode *currentNode = tree->root; + HFBTreeIndex remainingOffset = offset; + while (depth--) { + HFBTreeIndex beginningOffsetOfNode; + currentNode = child_containing_offset(currentNode, remainingOffset, &beginningOffsetOfNode, NO); + HFASSERT(beginningOffsetOfNode <= remainingOffset); + remainingOffset = remainingOffset - beginningOffsetOfNode; + } + ASSERT_IS_LEAF(currentNode); + HFBTreeIndex startOffsetOfEntry; + ChildIndex_t entryIndex = index_containing_offset(currentNode, remainingOffset, &startOffsetOfEntry, YES); + /* The offset of this entry is the requested offset minus the difference between its starting offset within the leaf and the requested offset within the leaf */ + HFASSERT(remainingOffset >= startOffsetOfEntry); + HFBTreeIndex offsetIntoEntry = remainingOffset - startOffsetOfEntry; + HFASSERT(offset >= offsetIntoEntry); + HFBTreeIndex beginningOffset = offset - offsetIntoEntry; + return (struct LeafInfo_t){.leaf = CHECK_CAST(currentNode, HFBTreeLeaf), .entryIndex = entryIndex, .offsetOfEntryInTree = beginningOffset}; +} + +static TreeEntry *btree_search(HFBTree *tree, HFBTreeIndex offset, HFBTreeIndex *outBeginningOffset) { + struct LeafInfo_t leafInfo = btree_find_leaf(tree, offset); + *outBeginningOffset = leafInfo.offsetOfEntryInTree; + return leafInfo.leaf->children[leafInfo.entryIndex]; +} + +static id btree_insert_returning_retained_value_for_parent(HFBTree *tree, TreeEntry *entry, HFBTreeIndex insertionOffset) { + struct SubtreeInfo_t descentInfo[MAX_DEPTH]; +#if ! NDEBUG + memset(descentInfo, -1, sizeof descentInfo); +#endif + HFBTreeIndex subtreeOffset = insertionOffset; + HFBTreeLeaf *leaf = btree_descend(tree, descentInfo, &subtreeOffset, NO); + ASSERT_IS_LEAF(leaf); + + ChildIndex_t insertionLocation = index_for_child_at_offset(leaf, subtreeOffset, YES); + HFBTreeNode *retainedValueToInsertIntoParentBranch = add_child_to_node_possibly_creating_split(leaf, entry, insertionLocation, YES); + + /* Walk up */ + TreeDepth_t depth = tree->depth; + HFASSERT(depth != BAD_DEPTH); + HFBTreeIndex entryLength = HFBTreeLength(entry); + while (depth--) { + HFBTreeBranch *branch = descentInfo[depth].branch; + branch->subtreeLength = HFSum(branch->subtreeLength, entryLength); + ChildIndex_t childIndex = descentInfo[depth].childIndex; + if (retainedValueToInsertIntoParentBranch) { + HFASSERT(branch->subtreeLength > retainedValueToInsertIntoParentBranch->subtreeLength); + /* Since we copied some stuff out from under ourselves, subtract its length */ + branch->subtreeLength -= retainedValueToInsertIntoParentBranch->subtreeLength; + HFBTreeNode *newRetainedValueToInsertIntoParentBranch = add_child_to_node_possibly_creating_split(branch, retainedValueToInsertIntoParentBranch, childIndex + 1, NO); + [retainedValueToInsertIntoParentBranch release]; + retainedValueToInsertIntoParentBranch = newRetainedValueToInsertIntoParentBranch; + } + } + return retainedValueToInsertIntoParentBranch; +} + +static BOOL btree_remove(HFBTree *tree, HFBTreeIndex deletionOffset) { + struct SubtreeInfo_t descentInfo[MAX_DEPTH]; +#if ! NDEBUG + memset(descentInfo, -1, sizeof descentInfo); +#endif + HFBTreeIndex subtreeOffset = deletionOffset; + HFBTreeLeaf *leaf = btree_descend(tree, descentInfo, &subtreeOffset, YES); + ASSERT_IS_LEAF(leaf); + + HFBTreeIndex previousOffsetSum = 0; + ChildIndex_t childIndex; + for (childIndex = 0; childIndex < BTREE_LEAF_ORDER; childIndex++) { + if (previousOffsetSum == subtreeOffset) break; + TreeEntry *entry = leaf->children[childIndex]; + HFASSERT(entry != nil); //if it were nil, then the offset is too large + HFBTreeIndex childLength = HFBTreeLength(entry); + previousOffsetSum = HFSum(childLength, previousOffsetSum); + } + HFASSERT(childIndex < BTREE_LEAF_ORDER); + HFASSERT(previousOffsetSum == subtreeOffset); + + TreeDepth_t depth = tree->depth; + HFASSERT(depth != BAD_DEPTH); + BOOL modifiedLeft = NO, modifiedRight = NO; + BOOL deleteNode = remove_value_from_node_with_possible_rebalance(leaf, childIndex, depth==0/*isRootNode*/, YES, &modifiedLeft, &modifiedRight); + HFASSERT(btree_are_cached_lengths_correct(leaf, NULL)); + while (depth--) { + HFBTreeBranch *branch = descentInfo[depth].branch; + ChildIndex_t branchChildIndex = descentInfo[depth].childIndex; + BOOL leftNeighborNeedsUpdating = modifiedLeft && branchChildIndex == 0; //if our child tweaked its left neighbor, and its left neighbor is not also a child of us, we need to inform its parent (which is our left neighbor) + BOOL rightNeighborNeedsUpdating = modifiedRight && (branchChildIndex + 1 == BTREE_BRANCH_ORDER || branch->children[branchChildIndex + 1] == NULL); //same goes for right + if (leftNeighborNeedsUpdating) { + HFASSERT(branch->left != NULL); +// NSLog(@"Updating lefty %p", branch->left); + update_node_having_changed_size_of_child(branch->left, NO); + } +#if ! NDEBUG + if (branch->left) HFASSERT(btree_are_cached_lengths_correct(branch->left, NULL)); +#endif + if (rightNeighborNeedsUpdating) { + HFASSERT(branch->right != NULL); +// NSLog(@"Updating righty %p", branch->right); + update_node_having_changed_size_of_child(branch->right, NO); + } +#if ! NDEBUG + if (branch->right) HFASSERT(btree_are_cached_lengths_correct(branch->right, NULL)); +#endif + update_node_having_changed_size_of_child(branch, NO); + modifiedLeft = NO; + modifiedRight = NO; + if (deleteNode) { + deleteNode = remove_value_from_node_with_possible_rebalance(branch, branchChildIndex, depth==0/*isRootNode*/, NO, &modifiedLeft, &modifiedRight); + } + else { + // update_node_having_changed_size_of_child(branch, NO); + // no need to delete parent nodes, so leave deleteNode as NO + } + /* Our parent may have to modify its left or right neighbor if we had to modify our left or right neighbor or if one of our children modified a neighbor that is not also a child of us. */ + modifiedLeft = modifiedLeft || leftNeighborNeedsUpdating; + modifiedRight = modifiedRight || rightNeighborNeedsUpdating; + } + + if (! deleteNode) { + /* Delete the root if it has one node and a depth of at least 1, or zero nodes and a depth of 0 */ + deleteNode = (tree->depth >= 1 && tree->root->children[1] == nil) || (tree->depth == 0 && tree->root->children[0] == nil); + } + return deleteNode; +} + +/* linkingHelper stores the last seen node for each depth. */ +static HFBTreeNode *mutable_copy_node(HFBTreeNode *node, TreeDepth_t depth, HFBTreeNode **linkingHelper) { + if (node == nil) return nil; + HFASSERT(depth != BAD_DEPTH); + Class class = (depth == 0 ? [HFBTreeLeaf class] : [HFBTreeBranch class]); + HFBTreeNode *result = [[class alloc] init]; + result->subtreeLength = node->subtreeLength; + + /* Link us in */ + HFBTreeNode *leftNeighbor = linkingHelper[0]; + if (leftNeighbor != nil) { + leftNeighbor->right = result; + result->left = leftNeighbor; + } + + /* Leave us for our future right neighbor to find */ + linkingHelper[0] = (void *)result; + + HFBTreeIndex index; + for (index = 0; index < BTREE_ORDER; index++) { + id child = node->children[index]; + if (! node->children[index]) break; + if (depth > 0) { + result->children[index] = mutable_copy_node(child, depth - 1, linkingHelper + 1); + } + else { + result->children[index] = [(TreeEntry *)child retain]; + } + } + return result; +} + +__attribute__((unused)) +static BOOL non_nulls_are_grouped_at_start(const id *ptr, NSUInteger count) { + BOOL hasSeenNull = NO; + for (NSUInteger i=0; i < count; i++) { + BOOL ptrIsNull = (ptr[i] == nil); + hasSeenNull = hasSeenNull || ptrIsNull; + if (hasSeenNull && ! ptrIsNull) { + return NO; + } + } + return YES; +} + + +static void btree_recursive_check_integrity(HFBTree *tree, HFBTreeNode *branchOrLeaf, TreeDepth_t depth, HFBTreeNode **linkHelper) { + HFASSERT(linkHelper[0] == branchOrLeaf->left); + if (linkHelper[0]) HFASSERT(linkHelper[0]->right == branchOrLeaf); + linkHelper[0] = branchOrLeaf; + + if (depth == 0) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + HFASSERT(non_nulls_are_grouped_at_start(leaf->children, BTREE_LEAF_ORDER)); + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + HFASSERT(non_nulls_are_grouped_at_start(branch->children, BTREE_BRANCH_ORDER)); + for (ChildIndex_t i = 0; i < BTREE_BRANCH_ORDER; i++) { + if (! branch->children[i]) break; + btree_recursive_check_integrity(tree, branch->children[i], depth - 1, linkHelper + 1); + } + } + ChildIndex_t childCount = count_node_values(branchOrLeaf); + if (depth < tree->depth) { // only the root may have fewer than BTREE_NODE_MINIMUM_VALUE_COUNT + HFASSERT(childCount >= BTREE_NODE_MINIMUM_VALUE_COUNT); + } + HFASSERT(childCount <= BTREE_ORDER); +} + +static HFBTreeIndex btree_recursive_check_integrity_of_cached_lengths(HFBTreeNode *branchOrLeaf) { + HFBTreeIndex result = 0; + if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i = 0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + result = HFSum(result, HFBTreeLength(leaf->children[i])); + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i = 0; i < BTREE_BRANCH_ORDER; i++) { + if (branch->children[i]) { + HFBTreeIndex subtreeLength = btree_recursive_check_integrity_of_cached_lengths(branch->children[i]); + result = HFSum(result, subtreeLength); + } + } + } + HFASSERT(result == branchOrLeaf->subtreeLength); + return result; +} + +static BOOL btree_are_cached_lengths_correct(HFBTreeNode *branchOrLeaf, HFBTreeIndex *outLength) { + if (! branchOrLeaf) { + if (outLength) *outLength = 0; + return YES; + } + HFBTreeIndex length = 0; + if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i=0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + length = HFSum(length, HFBTreeLength(leaf->children[i])); + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i=0; i < BTREE_BRANCH_ORDER; i++) { + if (! branch->children[i]) break; + HFBTreeIndex subLength = (HFBTreeIndex)-1; + if (! btree_are_cached_lengths_correct(branch->children[i], &subLength)) { + return NO; + } + length = HFSum(length, subLength); + } + } + if (outLength) *outLength = length; + return length == branchOrLeaf->subtreeLength; +} + +#if FIXUP_LENGTHS +static NSUInteger btree_entry_count(HFBTreeNode *branchOrLeaf) { + NSUInteger result = 0; + if (branchOrLeaf == nil) { + // do nothing + } + else if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i=0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + result++; + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i=0; i < BTREE_LEAF_ORDER; i++) { + if (! branch->children[i]) break; + result += btree_entry_count(branch->children[i]); + } + } + return result; +} + +static HFBTreeIndex btree_recursive_fixup_cached_lengths(HFBTree *tree, HFBTreeNode *branchOrLeaf) { + HFBTreeIndex result = 0; + if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i = 0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + result = HFSum(result, HFBTreeLength(leaf->children[i])); + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i = 0; i < BTREE_BRANCH_ORDER; i++) { + if (! branch->children[i]) break; + btree_recursive_fixup_cached_lengths(tree, branch->children[i]); + result = HFSum(result, CHECK_CAST(branch->children[i], HFBTreeNode)->subtreeLength); + } + } + branchOrLeaf->subtreeLength = result; + return result; +} +#endif + +FORCE_STATIC_INLINE void btree_apply_function_to_entries(HFBTree *tree, HFBTreeIndex offset, BOOL (*func)(id, HFBTreeIndex, void *), void *userInfo) { + struct LeafInfo_t leafInfo = btree_find_leaf(tree, offset); + HFBTreeLeaf *leaf = leafInfo.leaf; + ChildIndex_t entryIndex = leafInfo.entryIndex; + HFBTreeIndex leafOffset = leafInfo.offsetOfEntryInTree; + BOOL continueApplying = YES; + while (leaf != NULL) { + for (; entryIndex < BTREE_LEAF_ORDER; entryIndex++) { + TreeEntry *entry = leaf->children[entryIndex]; + if (! entry) break; + continueApplying = func(entry, leafOffset, userInfo); + if (! continueApplying) break; + leafOffset = HFSum(leafOffset, HFBTreeLength(entry)); + } + if (! continueApplying) break; + leaf = CHECK_CAST_OR_NULL(leaf->right, HFBTreeLeaf); + entryIndex = 0; + } +} + +- (NSEnumerator *)entryEnumerator { + if (! root) return [@[] objectEnumerator]; + HFBTreeLeaf *leaf = btree_find_leaf(self, 0).leaf; + return [[[HFBTreeEnumerator alloc] initWithLeaf:leaf] autorelease]; +} + + +static BOOL add_to_array(id entry, HFBTreeIndex offset __attribute__((unused)), void *array) { + [(id)array addObject:entry]; + return YES; +} + +- (NSArray *)allEntries { + if (! root) return @[]; + NSUInteger treeCapacity = 1; + unsigned int depthIndex = depth; + while (depthIndex--) treeCapacity *= BTREE_ORDER; + NSMutableArray *result = [NSMutableArray arrayWithCapacity: treeCapacity/2]; //assume we're half full + btree_apply_function_to_entries(self, 0, add_to_array, result); + return result; +} + +- (void)applyFunction:(BOOL (*)(id entry, HFBTreeIndex offset, void *userInfo))func toEntriesStartingAtOffset:(HFBTreeIndex)offset withUserInfo:(void *)userInfo { + NSParameterAssert(func != NULL); + if (! root) return; + btree_apply_function_to_entries(self, offset, func, userInfo); +} + +@end + + +@implementation HFBTreeEnumerator + +- (instancetype)initWithLeaf:(HFBTreeLeaf *)leaf { + NSParameterAssert(leaf != nil); + ASSERT_IS_LEAF(leaf); + currentLeaf = leaf; + return self; +} + +- (id)nextObject { + if (! currentLeaf) return nil; + if (childIndex >= BTREE_LEAF_ORDER || currentLeaf->children[childIndex] == nil) { + childIndex = 0; + currentLeaf = CHECK_CAST_OR_NULL(currentLeaf->right, HFBTreeLeaf); + } + if (currentLeaf == nil) return nil; + HFASSERT(currentLeaf->children[childIndex] != nil); + return currentLeaf->children[childIndex++]; +} + +@end diff --git a/HexFiend/HFBTreeByteArray.h b/HexFiend/HFBTreeByteArray.h new file mode 100644 index 0000000..3c1aac0 --- /dev/null +++ b/HexFiend/HFBTreeByteArray.h @@ -0,0 +1,30 @@ +// +// HFBTreeByteArray.h +// HexFiend_2 +// +// Created by peter on 4/28/09. +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import + +@class HFBTree; + +/*! @class HFBTreeByteArray +@brief The principal efficient implementation of HFByteArray. + +HFBTreeByteArray is an efficient subclass of HFByteArray that stores @link HFByteSlice HFByteSlices@endlink, using a 10-way B+ tree. This allows for insertion, deletion, and searching in approximately log-base-10 time. + +Create an HFBTreeByteArray via \c -init. It has no methods other than those on HFByteArray. +*/ + +@interface HFBTreeByteArray : HFByteArray { +@private + HFBTree *btree; +} + +/*! Designated initializer for HFBTreeByteArray. +*/ +- (instancetype)init; + +@end diff --git a/HexFiend/HFBTreeByteArray.m b/HexFiend/HFBTreeByteArray.m new file mode 100644 index 0000000..33ba967 --- /dev/null +++ b/HexFiend/HFBTreeByteArray.m @@ -0,0 +1,279 @@ +// +// HFBTreeByteArray.m +// HexFiend_2 +// +// Created by peter on 4/28/09. +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import + +@implementation HFBTreeByteArray + +- (instancetype)init { + if ((self = [super init])) { + btree = [[HFBTree alloc] init]; + } + return self; +} + +- (void)dealloc { + [btree release]; + [super dealloc]; +} + +- (unsigned long long)length { + return [btree length]; +} + +- (NSArray *)byteSlices { + return [btree allEntries]; +} + +- (NSEnumerator *)byteSliceEnumerator { + return [btree entryEnumerator]; +} + +- (NSString*)description { + NSMutableArray* result = [NSMutableArray array]; + NSEnumerator *enumer = [self byteSliceEnumerator]; + HFByteSlice *slice; + unsigned long long offset = 0; + while ((slice = [enumer nextObject])) { + unsigned long long length = [slice length]; + [result addObject:[NSString stringWithFormat:@"{%llu - %llu}", offset, length]]; + offset = HFSum(offset, length); + } + if (! [result count]) return @"(empty tree)"; + return [NSString stringWithFormat:@"<%@: %p>: %@", [self class], self, [result componentsJoinedByString:@" "]]; + +} + +struct HFBTreeByteArrayCopyInfo_t { + unsigned char *dst; + unsigned long long startingOffset; + NSUInteger remainingLength; +}; + +static BOOL copy_bytes(id entry, HFBTreeIndex offset, void *userInfo) { + struct HFBTreeByteArrayCopyInfo_t *info = userInfo; + HFByteSlice *slice = entry; + HFASSERT(slice != nil); + HFASSERT(info != NULL); + HFASSERT(offset <= info->startingOffset); + + unsigned long long sliceLength = [slice length]; + HFASSERT(sliceLength > 0); + unsigned long long offsetIntoSlice = info->startingOffset - offset; + HFASSERT(offsetIntoSlice < sliceLength); + NSUInteger amountToCopy = ll2l(MIN(info->remainingLength, sliceLength - offsetIntoSlice)); + HFRange srcRange = HFRangeMake(info->startingOffset - offset, amountToCopy); + [slice copyBytes:info->dst range:srcRange]; + info->dst += amountToCopy; + info->startingOffset = HFSum(info->startingOffset, amountToCopy); + info->remainingLength -= amountToCopy; + return info->remainingLength > 0; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); + HFASSERT(HFMaxRange(range) <= [self length]); + if (range.length > 0) { + struct HFBTreeByteArrayCopyInfo_t copyInfo = {.dst = dst, .remainingLength = ll2l(range.length), .startingOffset = range.location}; + [btree applyFunction:copy_bytes toEntriesStartingAtOffset:range.location withUserInfo:©Info]; + } +} + +- (HFByteSlice *)sliceContainingByteAtIndex:(unsigned long long)offset beginningOffset:(unsigned long long *)actualOffset { + return [btree entryContainingOffset:offset beginningOffset:actualOffset]; +} + +/* Given a HFByteArray and a range contained within it, return the first byte slice containing that range, and the range within that slice. Modifies the given range to reflect what you get when the returned slice is removed. */ +static inline HFByteSlice *findInitialSlice(HFBTree *btree, HFRange *inoutArrayRange, HFRange *outRangeWithinSlice) { + const HFRange arrayRange = *inoutArrayRange; + const unsigned long long arrayRangeEnd = HFMaxRange(arrayRange); + + unsigned long long offsetIntoSlice, lengthFromOffsetIntoSlice; + + unsigned long long beginningOffset; + HFByteSlice *slice = [btree entryContainingOffset:arrayRange.location beginningOffset:&beginningOffset]; + const unsigned long long sliceLength = [slice length]; + HFASSERT(beginningOffset <= arrayRange.location); + offsetIntoSlice = arrayRange.location - beginningOffset; + HFASSERT(offsetIntoSlice < sliceLength); + + unsigned long long sliceEndInArray = HFSum(sliceLength, beginningOffset); + if (sliceEndInArray <= arrayRangeEnd) { + /* Our slice ends before or at the requested range end */ + lengthFromOffsetIntoSlice = sliceLength - offsetIntoSlice; + } + else { + /* Our slice ends after the requested range end */ + unsigned long long overflow = sliceEndInArray - arrayRangeEnd; + HFASSERT(HFSum(overflow, offsetIntoSlice) < sliceLength); + lengthFromOffsetIntoSlice = sliceLength - HFSum(overflow, offsetIntoSlice); + } + + /* Set the out range to the input range minus the range consumed by the slice */ + inoutArrayRange->location = MIN(sliceEndInArray, arrayRangeEnd); + inoutArrayRange->length = arrayRangeEnd - inoutArrayRange->location; + + /* Set the out range within the slice to what we computed */ + *outRangeWithinSlice = HFRangeMake(offsetIntoSlice, lengthFromOffsetIntoSlice); + + return slice; +} + +- (BOOL)fastPathInsertByteSlice:(HFByteSlice *)slice atOffset:(unsigned long long)offset { + HFASSERT(offset > 0); + unsigned long long priorSliceOffset; + HFByteSlice *priorSlice = [btree entryContainingOffset:offset - 1 beginningOffset:&priorSliceOffset]; + HFByteSlice *appendedSlice = [priorSlice byteSliceByAppendingSlice:slice]; + if (appendedSlice) { + [btree removeEntryAtOffset:priorSliceOffset]; + [btree insertEntry:appendedSlice atOffset:priorSliceOffset]; + return YES; + } + else { + return NO; + } +} + +- (void)insertByteSlice:(HFByteSlice *)slice atOffset:(unsigned long long)offset { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + + if (offset == 0) { + [btree insertEntry:slice atOffset:0]; + } + else if (offset == [btree length]) { + if (! [self fastPathInsertByteSlice:slice atOffset:offset]) { + [btree insertEntry:slice atOffset:offset]; + } + } + else { + unsigned long long beginningOffset; + HFByteSlice *overlappingSlice = [btree entryContainingOffset:offset beginningOffset:&beginningOffset]; + if (beginningOffset == offset) { + if (! [self fastPathInsertByteSlice:slice atOffset:offset]) { + [btree insertEntry:slice atOffset:offset]; + } + } + else { + HFASSERT(offset > beginningOffset); + unsigned long long offsetIntoSlice = offset - beginningOffset; + unsigned long long sliceLength = [overlappingSlice length]; + HFASSERT(sliceLength > offsetIntoSlice); + HFByteSlice *left = [overlappingSlice subsliceWithRange:HFRangeMake(0, offsetIntoSlice)]; + HFByteSlice *right = [overlappingSlice subsliceWithRange:HFRangeMake(offsetIntoSlice, sliceLength - offsetIntoSlice)]; + [btree removeEntryAtOffset:beginningOffset]; + + [btree insertEntry:right atOffset:beginningOffset]; + + /* Try the fast appending path */ + HFByteSlice *joinedSlice = [left byteSliceByAppendingSlice:slice]; + if (joinedSlice) { + [btree insertEntry:joinedSlice atOffset:beginningOffset]; + } + else { + [btree insertEntry:slice atOffset:beginningOffset]; + [btree insertEntry:left atOffset:beginningOffset]; + } + } + } +} + +- (void)deleteBytesInRange:(HFRange)range { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + HFRange remainingRange = range; + + HFASSERT(HFMaxRange(range) <= [self length]); + if (range.length == 0) return; //nothing to delete + + //fast path for deleting everything + if (range.location == 0 && range.length == [self length]) { + [btree removeAllEntries]; + return; + } + + unsigned long long beforeLength = [self length]; + + unsigned long long rangeStartLocation = range.location; + HFByteSlice *beforeSlice = nil, *afterSlice = nil; + while (remainingRange.length > 0) { + HFRange rangeWithinSlice; + HFByteSlice *slice = findInitialSlice(btree, &remainingRange, &rangeWithinSlice); + const unsigned long long sliceLength = [slice length]; + const unsigned long long rangeWithinSliceEnd = HFMaxRange(rangeWithinSlice); + HFRange lefty = HFRangeMake(0, rangeWithinSlice.location); + HFRange righty = HFRangeMake(rangeWithinSliceEnd, sliceLength - rangeWithinSliceEnd); + HFASSERT(lefty.length == 0 || beforeSlice == nil); + HFASSERT(righty.length == 0 || afterSlice == nil); + + unsigned long long beginningOffset = remainingRange.location - HFMaxRange(rangeWithinSlice); + + if (lefty.length > 0){ + beforeSlice = [slice subsliceWithRange:lefty]; + rangeStartLocation = beginningOffset; + } + if (righty.length > 0) afterSlice = [slice subsliceWithRange:righty]; + + [btree removeEntryAtOffset:beginningOffset]; + remainingRange.location = beginningOffset; + } + if (afterSlice) { + [self insertByteSlice:afterSlice atOffset:rangeStartLocation]; + } + if (beforeSlice) { + [self insertByteSlice:beforeSlice atOffset:rangeStartLocation]; + } + + unsigned long long afterLength = [self length]; + HFASSERT(beforeLength - afterLength == range.length); +} + +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + + if (lrange.length > 0) { + [self deleteBytesInRange:lrange]; + } + if ([slice length] > 0) { + [self insertByteSlice:slice atOffset:lrange.location]; + } +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + USE(zone); + HFBTreeByteArray *result = [[[self class] alloc] init]; + [result->btree release]; + result->btree = [btree mutableCopy]; + return result; +} + +- (id)subarrayWithRange:(HFRange)range { + if (range.location == 0 && range.length == [self length]) { + return [[self mutableCopy] autorelease]; + } + HFBTreeByteArray *result = [[[[self class] alloc] init] autorelease]; + HFRange remainingRange = range; + unsigned long long offsetInResult = 0; + while (remainingRange.length > 0) { + HFRange rangeWithinSlice; + HFByteSlice *slice = findInitialSlice(btree, &remainingRange, &rangeWithinSlice); + HFByteSlice *subslice; + if (rangeWithinSlice.location == 0 && rangeWithinSlice.length == [slice length]) { + subslice = slice; + } + else { + subslice = [slice subsliceWithRange:rangeWithinSlice]; + } + [result insertByteSlice:subslice atOffset:offsetInResult]; + offsetInResult = HFSum(offsetInResult, rangeWithinSlice.length); + } + return result; +} + +@end diff --git a/HexFiend/HFByteArray.h b/HexFiend/HFByteArray.h new file mode 100644 index 0000000..dd452d5 --- /dev/null +++ b/HexFiend/HFByteArray.h @@ -0,0 +1,180 @@ +// +// HFByteArray.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +@class HFByteSlice, HFFileReference, HFByteRangeAttributeArray; + +typedef NS_ENUM(NSUInteger, HFByteArrayDataStringType) { + HFHexDataStringType, + HFASCIIDataStringType +}; + + +/*! @class HFByteArray +@brief The principal Model class for HexFiend's MVC architecture. + +HFByteArray implements the Model portion of HexFiend.framework. It is logically a mutable, resizable array of bytes, with a 64 bit length. It is somewhat analagous to a 64 bit version of NSMutableData, except that it is designed to enable efficient (faster than O(n)) implementations of insertion and deletion. + +HFByteArray, being an abstract class, will raise an exception if you attempt to instantiate it directly. For most uses, instantiate HFBTreeByteArray instead, with the usual [[class alloc] init]. + +HFByteArray also exposes itself as an array of @link HFByteSlice HFByteSlices@endlink, which are logically immutable arrays of bytes. which is useful for operations such as file saving that need to access the underlying byte slices. + +HFByteArray contains a generation count, which is incremented whenever the HFByteArray changes (to allow caches to be implemented on top of it). It also includes the notion of locking: a locked HFByteArray will raise an exception if written to, but it may still be read. + +ByteArrays have the usual threading restrictions for non-concurrent data structures. It is safe to read an HFByteArray concurrently from multiple threads. It is not safe to read an HFByteArray while it is being modified from another thread, nor is it safe to modify one simultaneously from two threads. + +HFByteArray is an abstract class. It will raise an exception if you attempt to instantiate it directly. The principal concrete subclass is HFBTreeByteArray. +*/ + +@class HFByteRangeAttributeArray; + +@interface HFByteArray : NSObject { +@private + NSUInteger changeLockCounter; + NSUInteger changeGenerationCount; +} + +/*! @name Initialization + */ +//@{ +/*! Initialize to a byte array containing only the given slice. */ +- (instancetype)initWithByteSlice:(HFByteSlice *)slice; + +/*! Initialize to a byte array containing the slices of the given array. */ +- (instancetype)initWithByteArray:(HFByteArray *)array; +//@} + + +/*! @name Accessing raw data +*/ +//@{ + +/*! Returns the length of the HFByteArray as a 64 bit unsigned long long. This is an abstract method that concrete subclasses must override. */ +- (unsigned long long)length; + +/*! Copies a range of bytes into a buffer. This is an abstract method that concrete subclasses must override. */ +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range; +//@} + +/*! @name Accessing byte slices + Methods to access the byte slices underlying the HFByteArray. +*/ +//@{ +/*! Returns the contents of the receiver as an array of byte slices. This is an abstract method that concrete subclasses must override. */ +- (NSArray *)byteSlices; + +/*! Returns an NSEnumerator representing the byte slices of the receiver. This is implemented as enumerating over the result of -byteSlices, but subclasses can override this to be more efficient. */ +- (NSEnumerator *)byteSliceEnumerator; + +/*! Returns the byte slice containing the byte at the given index, and the actual offset of this slice. */ +- (HFByteSlice *)sliceContainingByteAtIndex:(unsigned long long)offset beginningOffset:(unsigned long long *)actualOffset; +//@} + +/*! @name Modifying the byte array + Methods to modify the given byte array. +*/ +//@{ +/*! Insert an HFByteSlice in the given range. The maximum value of the range must not exceed the length of the subarray. The length of the given slice is not required to be equal to length of the range - in other words, this method may change the length of the receiver. This is an abstract method that concrete subclasses must override. */ +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange; + +/*! Insert an HFByteArray in the given range. This is implemented via calling insertByteSlice:inRange: with the byte slices from the given byte array. */ +- (void)insertByteArray:(HFByteArray *)array inRange:(HFRange)lrange; + +/*! Delete bytes in the given range. This is implemented on the base class by creating an empty byte array and inserting it in the range to be deleted, via insertByteSlice:inRange:. */ +- (void)deleteBytesInRange:(HFRange)range; + +/*! Returns a new HFByteArray containing the given range. This is an abstract method that concrete subclasses must override. */ +- (HFByteArray *)subarrayWithRange:(HFRange)range; +//@} + +/*! @name Write locking and generation count + Methods to lock and query the lock that prevents writes. +*/ +//@{ + +/*! Increment the change lock. Until the change lock reaches 0, all modifications to the receiver will raise an exception. */ +- (void)incrementChangeLockCounter; + +/*! Decrement the change lock. If the change lock reaches 0, modifications will be allowed again. */ +- (void)decrementChangeLockCounter; + +/*! Query if the changes are locked. This method is KVO compliant. */ +- (BOOL)changesAreLocked; +//@} + +/* @name Generation count + Manipulate the generation count */ +// @{ +/*! Increments the generation count, unless the receiver is locked, in which case it raises an exception. All subclasses of HFByteArray should call this method at the beginning of any overridden method that may modify the receiver. + @param sel The selector that would modify the receiver (e.g. deleteBytesInRange:). This is usually _cmd. */ +- (void)incrementGenerationOrRaiseIfLockedForSelector:(SEL)sel; + +/*! Return the change generation count. Every change to the ByteArray increments this by one or more. This can be used for caching layers on top of HFByteArray, to known when to expire their cache. */ +- (NSUInteger)changeGenerationCount; + +//@} + + + +/*! @name Searching +*/ +//@{ +/*! Searches the receiver for a byte array matching findBytes within the given range, and returns the index that it was found. This is a concrete method on HFByteArray. + @param findBytes The HFByteArray containing the data to be found (the needle to the receiver's haystack). + @param range The range of the receiver in which to search. The end of the range must not exceed the receiver's length. + @param forwards If this is YES, then the first match within the range is returned. Otherwise the last is returned. + @param progressTracker An HFProgressTracker to allow progress reporting and cancelleation for the search operation. + @return The index in the receiver of bytes equal to findBytes, or ULLONG_MAX if the byte array was not found (or the operation was cancelled) +*/ +- (unsigned long long)indexOfBytesEqualToBytes:(HFByteArray *)findBytes inRange:(HFRange)range searchingForwards:(BOOL)forwards trackingProgress:(id)progressTracker; +//@} + +@end + + +/*! @category HFByteArray(HFFileWriting) + @brief HFByteArray methods for writing to files, and preparing other HFByteArrays for potentially destructive file writes. +*/ +@interface HFByteArray (HFFileWriting) +/*! Attempts to write the receiver to a file. This is a concrete method on HFByteArray. + @param targetURL A URL to the file to be written to. It is OK for the receiver to contain one or more instances of HFByteSlice that are sourced from the file. + @param progressTracker An HFProgressTracker to allow progress reporting and cancelleation for the write operation. + @param error An out NSError parameter. + @return YES if the write succeeded, NO if it failed. +*/ +- (BOOL)writeToFile:(NSURL *)targetURL trackingProgress:(id)progressTracker error:(NSError **)error; + +/*! Returns the ranges of the file that would be modified, if the receiver were written to it. This is useful (for example) in determining if the clipboard can be preserved after a save operation. This is a concrete method on HFByteArray. + @param reference An HFFileReference to the file to be modified + @return An array of @link HFRangeWrapper HFRangeWrappers@endlink, representing the ranges of the file that would be affected. If no range would be affected, the result is an empty array. +*/ +- (NSArray *)rangesOfFileModifiedIfSavedToFile:(HFFileReference *)reference; + +/*! Attempts to modify the receiver so that it no longer depends on any of the HFRanges in the array within the given file. It is not necessary to perform this operation on the byte array that is being written to the file. + @param ranges An array of HFRangeWrappers, representing ranges in the given file that the receiver should no longer depend on. + @param reference The HFFileReference that the receiver should no longer depend on. + @param hint A dictionary that can be used to improve the efficiency of the operation, by allowing multiple byte arrays to share the same state. If you plan to call this method on multiple byte arrays, pass the first one an empty NSMutableDictionary, and pass the same dictionary to subsequent calls. + @return A YES return indicates the operation was successful, and the receiver no longer contains byte slices that source data from any of the ranges of the given file (or never did). A NO return indicates that breaking the dependencies would require too much memory, and so the receiver still depends on some of those ranges. +*/ +- (BOOL)clearDependenciesOnRanges:(NSArray *)ranges inFile:(HFFileReference *)reference hint:(NSMutableDictionary *)hint; + +@end + + +/*! @category HFByteArray(HFAttributes) + @brief HFByteArray methods for attributes of byte arrays. +*/ +@interface HFByteArray (HFAttributes) + +/*! Returns a byte range attribute array for the bytes in the given range. */ +- (HFByteRangeAttributeArray *)attributesForBytesInRange:(HFRange)range; + +/*! Returns the HFByteArray level byte range attribute array. Default is to return nil. */ +- (HFByteRangeAttributeArray *)byteRangeAttributeArray; + +@end diff --git a/HexFiend/HFByteArray.m b/HexFiend/HFByteArray.m new file mode 100644 index 0000000..55f25cc --- /dev/null +++ b/HexFiend/HFByteArray.m @@ -0,0 +1,218 @@ +// +// HFByteArray.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + + +@implementation HFByteArray + +- (instancetype)init { + if ([self class] == [HFByteArray class]) { + [NSException raise:NSInvalidArgumentException format:@"init sent to HFByteArray, but HFByteArray is an abstract class. Instantiate one of its subclasses instead, like HFBTreeByteArray."]; + } + return [super init]; +} + +- (instancetype)initWithByteSlice:(HFByteSlice *)slice { + if(!(self = [self init])) return nil; + self = [self init]; + [self insertByteSlice:slice inRange:HFRangeMake(0, 0)]; + return self; +} + +- (instancetype)initWithByteArray:(HFByteArray *)array { + if(!(self = [self init])) return nil; + NSEnumerator *e = [array byteSliceEnumerator]; + HFByteSlice *slice; + while((slice = [e nextObject])) { + [self insertByteSlice:slice inRange:HFRangeMake([self length], 0)]; + } + return self; +} + +- (NSArray *)byteSlices { UNIMPLEMENTED(); } +- (unsigned long long)length { UNIMPLEMENTED(); } +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range { USE(dst); USE(range); UNIMPLEMENTED_VOID(); } +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange { USE(slice); USE(lrange); UNIMPLEMENTED_VOID(); } + +- (NSEnumerator *)byteSliceEnumerator { + return [[self byteSlices] objectEnumerator]; +} + +- (HFByteSlice *)sliceContainingByteAtIndex:(unsigned long long)offset beginningOffset:(unsigned long long *)actualOffset { + HFByteSlice *slice; + unsigned long long current = 0; + NSEnumerator *enumer = [self byteSliceEnumerator]; + while ((slice = [enumer nextObject])) { + unsigned long long sum = HFSum([slice length], current); + if (sum > offset) break; + current = sum; + } + if (actualOffset) *actualOffset = current; + return slice; +} + +- (void)insertByteArray:(HFByteArray*)array inRange:(HFRange)lrange { + REQUIRE_NOT_NULL(array); + HFASSERT(HFRangeIsSubrangeOfRange(lrange, HFRangeMake(0, [self length]))); +#ifndef NDEBUG + unsigned long long expectedLength = [self length] - lrange.length + [array length]; +#endif + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + NSEnumerator *sliceEnumerator; + HFByteSlice *byteSlice; + if (array == self) { + /* Guard against self insertion */ + sliceEnumerator = [[array byteSlices] objectEnumerator]; + } + else { + sliceEnumerator = [array byteSliceEnumerator]; + } + while ((byteSlice = [sliceEnumerator nextObject])) { + [self insertByteSlice:byteSlice inRange:lrange]; + lrange.location += [byteSlice length]; + lrange.length = 0; + } + /* If there were no slices, delete the lrange */ + if (lrange.length > 0) { + [self deleteBytesInRange:lrange]; + } +#ifndef NDEBUG + HFASSERT(expectedLength == [self length]); +#endif +} + +- (HFByteArray *)subarrayWithRange:(HFRange)range { USE(range); UNIMPLEMENTED(); } + +- (id)mutableCopyWithZone:(NSZone *)zone { + USE(zone); + return [[self subarrayWithRange:HFRangeMake(0, [self length])] retain]; +} + +- (id)copyWithZone:(NSZone *)zone { + USE(zone); + return [[self subarrayWithRange:HFRangeMake(0, [self length])] retain]; +} + +- (void)deleteBytesInRange:(HFRange)lrange { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + HFByteSlice* slice = [[HFFullMemoryByteSlice alloc] initWithData:[NSData data]]; + [self insertByteSlice:slice inRange:lrange]; + [slice release]; +} + +- (BOOL)isEqual:v { + REQUIRE_NOT_NULL(v); + if (self == v) return YES; + else if (! [v isKindOfClass:[HFByteArray class]]) return NO; + else { + HFByteArray* obj = v; + unsigned long long length = [self length]; + if (length != [obj length]) return NO; + unsigned long long offset; + unsigned char buffer1[1024]; + unsigned char buffer2[sizeof buffer1 / sizeof *buffer1]; + for (offset = 0; offset < length; offset += sizeof buffer1) { + size_t amountToGrab = sizeof buffer1; + if (amountToGrab > length - offset) amountToGrab = ll2l(length - offset); + [self copyBytes:buffer1 range:HFRangeMake(offset, amountToGrab)]; + [obj copyBytes:buffer2 range:HFRangeMake(offset, amountToGrab)]; + if (memcmp(buffer1, buffer2, amountToGrab)) return NO; + } + } + return YES; +} + +- (unsigned long long)indexOfBytesEqualToBytes:(HFByteArray *)findBytes inRange:(HFRange)range searchingForwards:(BOOL)forwards trackingProgress:(id)progressTracker { + UNIMPLEMENTED(); +} + +- (BOOL)_debugIsEqual:(HFByteArray *)v { + REQUIRE_NOT_NULL(v); + if (! [v isKindOfClass:[HFByteArray class]]) return NO; + HFByteArray* obj = v; + unsigned long long length = [self length]; + if (length != [obj length]) { + printf("Lengths differ: %llu versus %llu\n", length, [obj length]); + abort(); + return NO; + } + + unsigned long long offset; + unsigned char buffer1[1024]; + unsigned char buffer2[sizeof buffer1 / sizeof *buffer1]; + for (offset = 0; offset < length; offset += sizeof buffer1) { + memset(buffer1, 0, sizeof buffer1); + memset(buffer2, 0, sizeof buffer2); + size_t amountToGrab = sizeof buffer1; + if (amountToGrab > length - offset) amountToGrab = ll2l(length - offset); + [self copyBytes:buffer1 range:HFRangeMake(offset, amountToGrab)]; + [obj copyBytes:buffer2 range:HFRangeMake(offset, amountToGrab)]; + size_t i; + for (i=0; i < amountToGrab; i++) { + if (buffer1[i] != buffer2[i]) { + printf("Inconsistency found at %llu (%02x versus %02x)\n", i + offset, buffer1[i], buffer2[i]); + abort(); + return NO; + } + } + } + return YES; +} + +- (NSData *)_debugData { + NSMutableData *data = [NSMutableData dataWithLength:(NSUInteger)[self length]]; + [self copyBytes:[data mutableBytes] range:HFRangeMake(0, [self length])]; + return data; +} + +- (BOOL)_debugIsEqualToData:(NSData *)val { + REQUIRE_NOT_NULL(val); + HFByteArray *byteArray = [[NSClassFromString(@"HFFullMemoryByteArray") alloc] init]; + HFByteSlice *byteSlice = [[HFFullMemoryByteSlice alloc] initWithData:val]; + [byteArray insertByteSlice:byteSlice inRange:HFRangeMake(0, 0)]; + [byteSlice release]; + BOOL result = [self _debugIsEqual:byteArray]; + [byteArray release]; + return result; +} + +- (void)incrementChangeLockCounter { + [self willChangeValueForKey:@"changesAreLocked"]; + if (HFAtomicIncrement(&changeLockCounter, NO) == 0) { + [NSException raise:NSInvalidArgumentException format:@"change lock counter overflow for %@", self]; + } + [self didChangeValueForKey:@"changesAreLocked"]; +} + +- (void)decrementChangeLockCounter { + [self willChangeValueForKey:@"changesAreLocked"]; + if (HFAtomicDecrement(&changeLockCounter, NO) == NSUIntegerMax) { + [NSException raise:NSInvalidArgumentException format:@"change lock counter underflow for %@", self]; + } + [self didChangeValueForKey:@"changesAreLocked"]; +} + +- (BOOL)changesAreLocked { + return !! changeLockCounter; +} + +- (NSUInteger)changeGenerationCount { + return changeGenerationCount; +} + +- (void)incrementGenerationOrRaiseIfLockedForSelector:(SEL)sel { + if (changeLockCounter) { + [NSException raise:NSInvalidArgumentException format:@"Selector %@ sent to a locked byte array %@", NSStringFromSelector(sel), self]; + } + else { + HFAtomicIncrement(&changeGenerationCount, YES); + } +} + +@end diff --git a/HexFiend/HFByteArray_Internal.h b/HexFiend/HFByteArray_Internal.h new file mode 100644 index 0000000..648dd92 --- /dev/null +++ b/HexFiend/HFByteArray_Internal.h @@ -0,0 +1,8 @@ +#import + +@interface HFByteArray (HFInternal) + +- (BOOL)_debugIsEqual:(HFByteArray *)val; +- (BOOL)_debugIsEqualToData:(NSData *)val; + +@end diff --git a/HexFiend/HFByteSlice.h b/HexFiend/HFByteSlice.h new file mode 100644 index 0000000..b17da08 --- /dev/null +++ b/HexFiend/HFByteSlice.h @@ -0,0 +1,53 @@ +// +// HFByteSlice.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +@class HFFileReference, HFByteRangeAttributeArray; + +/*! @class HFByteSlice +@brief A class representing a source of data for an HFByteArray. + +HFByteSlice is an abstract class encapsulating primitive data sources (files, memory buffers, etc.). Each source must support random access reads, and have a well defined length. All HFByteSlices are \b immutable. + +The two principal subclasses of HFByteSlice are HFSharedMemoryByteSlice and HFFileByteSlice, which respectively encapsulate data from memory and from a file. +*/ +@interface HFByteSlice : NSObject { + NSUInteger retainCount; +} + +/*! Return the length of the byte slice as a 64 bit value. This is an abstract method that concrete subclasses must override. */ +- (unsigned long long)length; + +/*! Copies a range of data from the byte slice into an in-memory buffer. This is an abstract method that concrete subclasses must override. */ +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range; + +/*! Returns a new slice containing a subrange of the given slice. This is an abstract method that concrete subclasses must override. */ +- (HFByteSlice *)subsliceWithRange:(HFRange)range; + +/*! Attempts to create a new byte slice by appending one byte slice to another. This does not modify the receiver or the slice argument (after all, both are immutable). This is provided as an optimization, and is allowed to return nil if the appending cannot be done efficiently. The default implementation returns nil. +*/ +- (HFByteSlice *)byteSliceByAppendingSlice:(HFByteSlice *)slice; + +/*! Returns YES if the receiver is sourced from a file. The default implementation returns NO. This is used to estimate cost when writing to a file. +*/ +- (BOOL)isSourcedFromFile; + +/*! For a given file reference, returns the range within the file that the receiver is sourced from. If the receiver is not sourced from this file, returns {ULLONG_MAX, ULLONG_MAX}. The default implementation returns {ULLONG_MAX, ULLONG_MAX}. This is used during file saving to to determine how to properly overwrite a given file. +*/ +- (HFRange)sourceRangeForFile:(HFFileReference *)reference; + +@end + +/*! @category HFByteSlice(HFAttributes) + @brief Methods for querying attributes of individual byte slices. */ +@interface HFByteSlice (HFAttributes) + +/*! Returns the attributes for the bytes in the given range. */ +- (HFByteRangeAttributeArray *)attributesForBytesInRange:(HFRange)range; + +@end diff --git a/HexFiend/HFByteSlice.m b/HexFiend/HFByteSlice.m new file mode 100644 index 0000000..fadb442 --- /dev/null +++ b/HexFiend/HFByteSlice.m @@ -0,0 +1,85 @@ +// +// HFByteSlice.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + + +@implementation HFByteSlice + +- (instancetype)init { + if ([self class] == [HFByteSlice class]) { + [NSException raise:NSInvalidArgumentException format:@"init sent to HFByteArray, but HFByteArray is an abstract class. Instantiate one of its subclasses instead."]; + } + return [super init]; +} + +- (unsigned long long)length { UNIMPLEMENTED(); } + +- (void)copyBytes:(unsigned char*)dst range:(HFRange)range { USE(dst); USE(range); UNIMPLEMENTED_VOID(); } + +- (HFByteSlice *)subsliceWithRange:(HFRange)range { USE(range); UNIMPLEMENTED(); } + +- (void)constructNewByteSlicesAboutRange:(HFRange)range first:(HFByteSlice **)first second:(HFByteSlice **)second { + const unsigned long long length = [self length]; + + //clip the range to our extent + range.location = llmin(range.location, length); + range.length = llmin(range.length, length - range.location); + + HFRange firstRange = {0, range.location}; + HFRange secondRange = {range.location + range.length, [self length] - (range.location + range.length)}; + + if (first) { + if (firstRange.length > 0) + *first = [self subsliceWithRange:firstRange]; + else + *first = nil; + } + + if (second) { + if (secondRange.length > 0) + *second = [self subsliceWithRange:secondRange]; + else + *second = nil; + } +} + +- (HFByteSlice *)byteSliceByAppendingSlice:(HFByteSlice *)slice { + USE(slice); + return nil; +} + +- (HFByteRangeAttributeArray *)attributesForBytesInRange:(HFRange)range { + USE(range); + return nil; +} + +- (BOOL)isSourcedFromFile { + return NO; +} + +- (HFRange)sourceRangeForFile:(HFFileReference *)reference { + USE(reference); + return HFRangeMake(ULLONG_MAX, ULLONG_MAX); +} + +- (id)retain { + HFAtomicIncrement(&retainCount, NO); + return self; +} + +- (oneway void)release { + if (HFAtomicDecrement(&retainCount, NO) == (NSUInteger)(-1)) { + [self dealloc]; + } +} + +- (NSUInteger)retainCount { + return 1 + retainCount; +} + +@end diff --git a/HexFiend/HFByteSlice_Private.h b/HexFiend/HFByteSlice_Private.h new file mode 100644 index 0000000..2827bfd --- /dev/null +++ b/HexFiend/HFByteSlice_Private.h @@ -0,0 +1,7 @@ +#import + +@interface HFByteSlice (HFByteSlice_Private) + +- (void)constructNewByteSlicesAboutRange:(HFRange)range first:(HFByteSlice **)first second:(HFByteSlice **)second; + +@end diff --git a/HexFiend/HFController.h b/HexFiend/HFController.h new file mode 100644 index 0000000..7bdc070 --- /dev/null +++ b/HexFiend/HFController.h @@ -0,0 +1,395 @@ +// +// HFController.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +#import + +/*! @header HFController + @abstract The HFController.h header contains the HFController class, which is a central class in Hex Fiend. +*/ + +@class HFRepresenter, HFByteArray, HFFileReference, HFControllerCoalescedUndo, HFByteRangeAttributeArray; + +/*! @enum HFControllerPropertyBits + The HFControllerPropertyBits bitmask is used to inform the HFRepresenters of a change in the current state that they may need to react to. A bitmask of the changed properties is passed to representerChangedProperties:. It is common for multiple properties to be included in such a bitmask. +*/ +typedef NS_OPTIONS(NSUInteger, HFControllerPropertyBits) { + HFControllerContentValue = 1 << 0, /*!< Indicates that the contents of the ByteArray has changed within the document. There is no indication as to what the change is. If redisplaying everything is expensive, Representers should cache their displayed data and compute any changes manually. */ + HFControllerContentLength = 1 << 1, /*!< Indicates that the length of the ByteArray has changed. */ + HFControllerDisplayedLineRange = 1 << 2, /*!< Indicates that the displayedLineRange property of the document has changed (e.g. the user scrolled). */ + HFControllerSelectedRanges = 1 << 3, /*!< Indicates that the selectedContentsRanges property of the document has changed (e.g. the user selected some other range). */ + HFControllerSelectionPulseAmount = 1 << 4, /*!< Indicates that the amount of "pulse" to show in the Find pulse indicator has changed. */ + HFControllerBytesPerLine = 1 << 5, /*!< Indicates that the number of bytes to show per line has changed. */ + HFControllerBytesPerColumn = 1 << 6, /*!< Indicates that the number of bytes per column (byte grouping) has changed. */ + HFControllerEditable = 1 << 7, /*!< Indicates that the document has become (or is no longer) editable. */ + HFControllerFont = 1 << 8, /*!< Indicates that the font property has changed. */ + HFControllerAntialias = 1 << 9, /*!< Indicates that the shouldAntialias property has changed. */ + HFControllerLineHeight = 1 << 10, /*!< Indicates that the lineHeight property has changed. */ + HFControllerViewSizeRatios = 1 << 11, /*!< Indicates that the optimum size for each view may have changed; used by HFLayoutController after font changes. */ + HFControllerByteRangeAttributes = 1 << 12, /*!< Indicates that some attributes of the ByteArray has changed within the document. There is no indication as to what the change is. */ + HFControllerByteGranularity = 1 << 13, /*!< Indicates that the byte granularity has changed. For example, when moving from ASCII to UTF-16, the byte granularity increases from 1 to 2. */ + HFControllerBookmarks = 1 << 14, /*!< Indicates that a bookmark has been added or removed. */ + HFControllerColorBytes = 1 << 15, /*!< Indicates that the shouldColorBytes property has changed. */ + HFControllerShowCallouts = 1 << 16, /*!< Indicates that the shouldShowCallouts property has changed. */ + HFControllerHideNullBytes = 1 << 17, /*!< Indicates that the shouldHideNullBytes property has changed. */ +}; + +/*! @enum HFControllerMovementDirection + +The HFControllerMovementDirection enum is used to specify a direction (either left or right) in various text editing APIs. HexFiend does not support left-to-right languages. +*/ +typedef NS_ENUM(NSInteger, HFControllerMovementDirection) { + HFControllerDirectionLeft, + HFControllerDirectionRight +}; + +/*! @enum HFControllerSelectionTransformation + +The HFControllerSelectionTransformation enum is used to specify what happens to the selection in various APIs. This is mainly interesting for text-editing style Representers. +*/ +typedef NS_ENUM(NSInteger, HFControllerSelectionTransformation) { + HFControllerDiscardSelection, /*!< The selection should be discarded. */ + HFControllerShiftSelection, /*!< The selection should be moved, without changing its length. */ + HFControllerExtendSelection /*!< The selection should be extended, changing its length. */ +}; + +/*! @enum HFControllerMovementGranularity + +The HFControllerMovementGranularity enum is used to specify the granularity of text movement in various APIs. This is mainly interesting for text-editing style Representers. +*/ +typedef NS_ENUM(NSInteger, HFControllerMovementGranularity) { + HFControllerMovementByte, /*!< Move by individual bytes */ + HFControllerMovementColumn, /*!< Move by a column */ + HFControllerMovementLine, /*!< Move by lines */ + HFControllerMovementPage, /*!< Move by pages */ + HFControllerMovementDocument /*!< Move by the whole document */ +}; + +/*! @enum HFEditMode + +HFEditMode enumerates the different edit modes that a document might be in. + */ +typedef NS_ENUM(NSInteger, HFEditMode) { + HFInsertMode, + HFOverwriteMode, + HFReadOnlyMode, +} ; + +/*! @class HFController +@brief A central class that acts as the controller layer for HexFiend.framework + +HFController acts as the controller layer in the MVC architecture of HexFiend. The HFController plays several significant central roles, including: + - Mediating between the data itself (in the HFByteArray) and the views of the data (the @link HFRepresenter HFRepresenters@endlink). + - Propagating changes to the views. + - Storing properties common to all Representers, such as the currently diplayed range, the currently selected range(s), the font, etc. + - Handling text editing actions, such as selection changes or insertions/deletions. + +An HFController is the top point of ownership for a HexFiend object graph. It retains both its ByteArray (model) and its array of Representers (views). + +You create an HFController via [[HFController alloc] init]. After that, give it an HFByteArray via setByteArray:, and some Representers via addRepresenter:. Then insert the Representers' views in a window, and you're done. + +*/ +@interface HFController : NSObject { +@private + NSMutableArray *representers; + HFByteArray *byteArray; + NSMutableArray *selectedContentsRanges; + HFRange displayedContentsRange; + HFFPRange displayedLineRange; + NSUInteger bytesPerLine; + NSUInteger bytesPerColumn; + CGFloat lineHeight; + + NSUInteger currentPropertyChangeToken; + NSMutableArray *additionalPendingTransactions; + HFControllerPropertyBits propertiesToUpdateInCurrentTransaction; + + NSUndoManager *undoManager; + NSMutableSet *undoOperations; + HFControllerCoalescedUndo *undoCoalescer; + + unsigned long long selectionAnchor; + HFRange selectionAnchorRange; + + CFAbsoluteTime pulseSelectionStartTime, pulseSelectionCurrentTime; + NSTimer *pulseSelectionTimer; + + /* Basic cache support */ + HFRange cachedRange; + NSData *cachedData; + NSUInteger cachedGenerationIndex; + + struct { + unsigned antialias:1; + unsigned colorbytes:1; + unsigned showcallouts:1; + unsigned hideNullBytes:1; + HFEditMode editMode:2; + unsigned editable:1; + unsigned selectable:1; + unsigned selectionInProgress:1; + unsigned shiftExtendSelection:1; + unsigned commandExtendSelection:1; + unsigned livereload:1; + } _hfflags; +} + +/*! @name Representer handling. + Methods for modifying the list of HFRepresenters attached to a controller. Attached representers receive the controllerDidChange: message when various properties of the controller change. A representer may only be attached to one controller at a time. Representers are retained by the controller. +*/ +//@{ +/// Gets the current array of representers attached to this controller. +@property (readonly, copy) NSArray *representers; + +/// Adds a new representer to this controller. +- (void)addRepresenter:(HFRepresenter *)representer; + +/// Removes an existing representer from this controller. The representer must be present in the array of representers. +- (void)removeRepresenter:(HFRepresenter *)representer; + +//@} + +/*! @name Property transactions + Methods for temporarily delaying notifying representers of property changes. There is a property transaction stack, and all property changes are collected until the last token is popped off the stack, at which point all representers are notified of all collected changes via representerChangedProperties:. To use this, call beginPropertyChangeTransaction, and record the token that is returned. Pass it to endPropertyChangeTransaction: to notify representers of all changed properties in bulk. + + Tokens cannot be popped out of order - they are used only as a correctness check. +*/ +//@{ +/*! Begins delaying property change transactions. Returns a token that should be passed to endPropertyChangeTransactions:. */ +- (NSUInteger)beginPropertyChangeTransaction; + +/*! Pass a token returned from beginPropertyChangeTransaction to this method to pop the transaction off the stack and, if the stack is empty, to notify Representers of all collected changes. Tokens cannot be popped out of order - they are used strictly as a correctness check. */ +- (void)endPropertyChangeTransaction:(NSUInteger)token; +//@} + +/*! @name Byte array + Set and get the byte array. */ +//@{ + +/*! The byte array must be non-nil. In general, HFRepresenters should not use this to determine what bytes to display. Instead they should use copyBytes:range: or dataForRange: below. */ +@property (nonatomic, strong) HFByteArray *byteArray; + +/*! Replaces the entire byte array with a new one, preserving as much of the selection as possible. Unlike setByteArray:, this method is undoable, and intended to be used from representers that make a global change (such as Replace All). */ +- (void)replaceByteArray:(HFByteArray *)newArray; +//@} + +/*! @name Properties shared between all representers + The following properties are considered global among all HFRepresenters attached to the receiver. +*/ +//@{ +/*! Returns the number of lines on which the cursor may be placed. This is always at least 1, and is equivalent to (unsigned long long)(HFRoundUpToNextMultiple(contentsLength, bytesPerLine) / bytesPerLine) */ +- (unsigned long long)totalLineCount; + +/*! Indicates the number of bytes per line, which is a global property among all the line-oriented representers. */ +- (NSUInteger)bytesPerLine; + +/*! Returns the height of a line, in points. This is generally determined by the font. Representers that wish to align things to lines should use this. */ +- (CGFloat)lineHeight; + +//@} + +/*! @name Selection pulsing + Used to show the current selection after a change, similar to Find in Safari +*/ +//{@ + +/*! Begins selection pulsing (e.g. following a successful Find operation). Representers will receive callbacks indicating that HFControllerSelectionPulseAmount has changed. */ +- (void)pulseSelection; + +/*! Return the amount that the "Find pulse indicator" should show. 0 means no pulse, 1 means maximum pulse. This is useful for Representers that support find and replace. */ +- (double)selectionPulseAmount; +//@} + +/*! @name Selection handling + Methods for manipulating the current selected ranges. Hex Fiend supports discontiguous selection. +*/ +//{@ + +/*! An array of HFRangeWrappers, representing the selected ranges. It satisfies the following: + The array is non-nil. + There always is at least one selected range. + If any range has length 0, that range is the only range. + No range extends beyond the contentsLength, with the exception of a single zero-length range at the end. + + When setting, the setter MUST obey the above criteria. A zero length range when setting or getting represents the cursor position. */ +@property (nonatomic, copy) NSArray *selectedContentsRanges; + +/*! Selects the entire contents. */ +- (IBAction)selectAll:(id)sender; + +/*! Returns the smallest value in the selected contents ranges, or the insertion location if the selection is empty. */ +- (unsigned long long)minimumSelectionLocation; + +/*! Returns the largest HFMaxRange of the selected contents ranges, or the insertion location if the selection is empty. */ +- (unsigned long long)maximumSelectionLocation; + +/*! Convenience method for creating a byte array containing all of the selected bytes. If the selection has length 0, this returns an empty byte array. */ +- (HFByteArray *)byteArrayForSelectedContentsRanges; +//@} + +/* Number of bytes used in each column for a text-style representer. */ +@property (nonatomic) NSUInteger bytesPerColumn; + +/*! @name Edit Mode + Determines what mode we're in, read-only, overwrite or insert. */ +@property (nonatomic) HFEditMode editMode; + +/*! @name Displayed line range + Methods for setting and getting the current range of displayed lines. +*/ +//{@ +/*! Get the current displayed line range. The displayed line range is an HFFPRange (range of long doubles) containing the lines that are currently displayed. + + The values may be fractional. That is, if only the bottom half of line 4 through the top two thirds of line 8 is shown, then the displayedLineRange.location will be 4.5 and the displayedLineRange.length will be 3.17 ( = 7.67 - 4.5). Representers are expected to be able to handle such fractional values. + + When setting the displayed line range, the given range must be nonnegative, and the maximum of the range must be no larger than the total line count. + +*/ +@property (nonatomic) HFFPRange displayedLineRange; + +/*! Modify the displayedLineRange so that as much of the given range as can fit is visible. If possible, moves by as little as possible so that the visible ranges before and afterward intersect with each other. */ +- (void)maximizeVisibilityOfContentsRange:(HFRange)range; + +/*! Modify the displayedLineRange as to center the given contents range. If the range is near the bottom or top, this will center as close as possible. If contents range is too large to fit, it centers the top of the range. contentsRange may be empty. */ +- (void)centerContentsRange:(HFRange)range; + +//@} + +/*! The current font. */ +@property (nonatomic, copy) NSFont *font; + +/*! The undo manager. If no undo manager is set, then undo is not supported. By default the undo manager is nil. +*/ +@property (nonatomic, strong) NSUndoManager *undoManager; + +/*! Whether the user can edit the document. */ +@property (nonatomic) BOOL editable; + +/*! Whether the text should be antialiased. Note that Mac OS X settings may prevent antialiasing text below a certain point size. */ +@property (nonatomic) BOOL shouldAntialias; + +/*! When enabled, characters have a background color that correlates to their byte values. */ +@property (nonatomic) BOOL shouldColorBytes; + +/*! When enabled, byte bookmarks display callout-style labels attached to them. */ +@property (nonatomic) BOOL shouldShowCallouts; + +/*! When enabled, null bytes are hidden in the hex view. */ +@property (nonatomic) BOOL shouldHideNullBytes; + +/*! When enabled, unmodified documents are auto refreshed to their latest on disk state. */ +@property (nonatomic) BOOL shouldLiveReload; + +/*! Representer initiated property changes + Called from a representer to indicate when some internal property of the representer has changed which requires that some properties be recalculated. +*/ +//@{ +/*! Callback for a representer-initiated change to some property. For example, if some property of a view changes that would cause the number of bytes per line to change, then the representer should call this method which will trigger the HFController to recompute the relevant properties. */ + +- (void)representer:(HFRepresenter *)rep changedProperties:(HFControllerPropertyBits)properties; +//@} + +/*! @name Mouse selection + Methods to handle mouse selection. Representers that allow text selection should call beginSelectionWithEvent:forByteIndex: upon receiving a mouseDown event, and then continueSelectionWithEvent:forByteIndex: for mouseDragged events, terminating with endSelectionWithEvent:forByteIndex: upon receiving the mouse up. HFController will compute the correct selected ranges and propagate any changes via the HFControllerPropertyBits mechanism. */ +//@{ +/*! Begin a selection session, with a mouse down at the given byte index. */ +- (void)beginSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex; + +/*! Continue a selection session, whe the user drags over the given byte index. */ +- (void)continueSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex; + +/*! End a selection session, with a mouse up at the given byte index. */ +- (void)endSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex; + +/*! @name Scrollling + Support for the mouse wheel and scroll bars. */ +//@{ +/*! Trigger scrolling appropriate for the given scroll event. */ +- (void)scrollWithScrollEvent:(NSEvent *)scrollEvent; + +/*! Trigger scrolling by the given number of lines. If lines is positive, then the document is scrolled down; otherwise it is scrolled up. */ +- (void)scrollByLines:(long double)lines; + +//@} + +/*! @name Keyboard navigation + Support for chaging the selection via the keyboard +*/ + +/*! General purpose navigation function. Modify the selection in the given direction by the given number of bytes. The selection is modifed according to the given transformation. If useAnchor is set, then anchored selection is used; otherwise any anchor is discarded. + + This has a few limitations: + - Only HFControllerDirectionLeft and HFControllerDirectionRight movement directions are supported. + - Anchored selection is not supported for HFControllerShiftSelection (useAnchor must be NO) +*/ +- (void)moveInDirection:(HFControllerMovementDirection)direction byByteCount:(unsigned long long)amountToMove withSelectionTransformation:(HFControllerSelectionTransformation)transformation usingAnchor:(BOOL)useAnchor; + +/*! Navigation designed for key events. */ +- (void)moveInDirection:(HFControllerMovementDirection)direction withGranularity:(HFControllerMovementGranularity)granularity andModifySelection:(BOOL)extendSelection; +- (void)moveToLineBoundaryInDirection:(HFControllerMovementDirection)direction andModifySelection:(BOOL)extendSelection; + +/*! @name Text editing + Methods to support common text editing operations */ +//@{ + +/*! Replaces the selection with the given data. For something like a hex view representer, it takes two keypresses to create a whole byte; the way this is implemented, the first keypress goes into the data as a complete byte, and the second one (if any) replaces it. If previousByteCount > 0, then that many prior bytes are replaced, without breaking undo coalescing. For previousByteCount to be > 0, the following must be true: There is only one selected range, and it is of length 0, and its location >= previousByteCount + + These functions return YES if they succeed, and NO if they fail. Currently they may fail only in overwrite mode, if you attempt to insert data that would require lengthening the byte array. + + These methods are undoable. + */ +- (BOOL)insertByteArray:(HFByteArray *)byteArray replacingPreviousBytes:(unsigned long long)previousByteCount allowUndoCoalescing:(BOOL)allowUndoCoalescing; +- (BOOL)insertData:(NSData *)data replacingPreviousBytes:(unsigned long long)previousByteCount allowUndoCoalescing:(BOOL)allowUndoCoalescing; + +/*! Deletes the selection. This operation is undoable. */ +- (void)deleteSelection; + +/*! If the selection is empty, deletes one byte in a given direction, which must be HFControllerDirectionLeft or HFControllerDirectionRight; if the selection is not empty, deletes the selection. Undoable. */ +- (void)deleteDirection:(HFControllerMovementDirection)direction; + +//@} + +/*! @name Reading data + Methods for reading data */ + +/*! Returns an NSData representing the given HFRange. The length of the HFRange must be of a size that can reasonably be fit in memory. This method may cache the result. */ +- (NSData *)dataForRange:(HFRange)range; + +/*! Copies data within the given HFRange into an in-memory buffer. This is equivalent to [[controller byteArray] copyBytes:bytes range:range]. */ +- (void)copyBytes:(unsigned char *)bytes range:(HFRange)range; + +/*! Returns total number of bytes. This is equivalent to [[controller byteArray] length]. */ +- (unsigned long long)contentsLength; + +- (void) reloadData; + +@end + +/*! A notification posted whenever any of the HFController's properties change. The object is the HFController. The userInfo contains one key, HFControllerChangedPropertiesKey, which contains an NSNumber with the changed properties as a HFControllerPropertyBits bitmask. This is useful for external objects to be notified of changes. HFRepresenters added to the HFController are notified via the controllerDidChange: message. +*/ +extern NSString * const HFControllerDidChangePropertiesNotification; + +/*! @name HFControllerDidChangePropertiesNotification keys +*/ +//@{ +extern NSString * const HFControllerChangedPropertiesKey; //!< A key in the HFControllerDidChangeProperties containing a bitmask of the changed properties, as a HFControllerPropertyBits +//@} + +/*! A notification posted from prepareForChangeInFile:fromWritingByteArray: because we are about to write a ByteArray to a file. The object is the FileReference. + Currently, HFControllers do not listen for this notification. This is because under GC there is no way of knowing whether the controller is live or not. However, pasteboard owners do listen for it, because as long as we own a pasteboard we are guaranteed to be live. +*/ +extern NSString * const HFPrepareForChangeInFileNotification; + +/*! @name HFPrepareForChangeInFileNotification keys +*/ +//@{ +extern NSString * const HFChangeInFileByteArrayKey; //!< A key in the HFPrepareForChangeInFileNotification specifying the byte array that will be written +extern NSString * const HFChangeInFileModifiedRangesKey; //!< A key in the HFPrepareForChangeInFileNotification specifying the array of HFRangeWrappers indicating which parts of the file will be modified +extern NSString * const HFChangeInFileShouldCancelKey; //!< A key in the HFPrepareForChangeInFileNotification specifying an NSValue containing a pointer to a BOOL. If set to YES, then someone was unable to prepare and the file should not be saved. It's a good idea to check if this value points to YES; if so your notification handler does not have to do anything. +extern NSString * const HFChangeInFileHintKey; //!< The hint parameter that you may pass to clearDependenciesOnRanges:inFile:hint: +//@} diff --git a/HexFiend/HFController.m b/HexFiend/HFController.m new file mode 100644 index 0000000..aaae78e --- /dev/null +++ b/HexFiend/HFController.m @@ -0,0 +1,1903 @@ +// +// HFController.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +/* Used for the anchor range and location */ +#define NO_SELECTION ULLONG_MAX + +#if ! NDEBUG +#define VALIDATE_SELECTION() [self _ensureSelectionIsValid] +#else +#define VALIDATE_SELECTION() do { } while (0) +#endif + +#define BENCHMARK_BYTEARRAYS 0 + +#define BEGIN_TRANSACTION() NSUInteger token = [self beginPropertyChangeTransaction] +#define END_TRANSACTION() [self endPropertyChangeTransaction:token] + +static const CGFloat kScrollMultiplier = (CGFloat)1.5; + +static const CFTimeInterval kPulseDuration = .2; + +static void *KVOContextChangesAreLocked = &KVOContextChangesAreLocked; + +NSString * const HFPrepareForChangeInFileNotification = @"HFPrepareForChangeInFileNotification"; +NSString * const HFChangeInFileByteArrayKey = @"HFChangeInFileByteArrayKey"; +NSString * const HFChangeInFileModifiedRangesKey = @"HFChangeInFileModifiedRangesKey"; +NSString * const HFChangeInFileShouldCancelKey = @"HFChangeInFileShouldCancelKey"; +NSString * const HFChangeInFileHintKey = @"HFChangeInFileHintKey"; + +NSString * const HFControllerDidChangePropertiesNotification = @"HFControllerDidChangePropertiesNotification"; +NSString * const HFControllerChangedPropertiesKey = @"HFControllerChangedPropertiesKey"; + + +typedef NS_ENUM(NSInteger, HFControllerSelectAction) { + eSelectResult, + eSelectAfterResult, + ePreserveSelection, + NUM_SELECTION_ACTIONS +}; + +@interface HFController (ForwardDeclarations) +- (void)_commandInsertByteArrays:(NSArray *)byteArrays inRanges:(NSArray *)ranges withSelectionAction:(HFControllerSelectAction)selectionAction; +- (void)_removeUndoManagerNotifications; +- (void)_removeAllUndoOperations; +- (void)_registerUndoOperationForInsertingByteArrays:(NSArray *)byteArrays inRanges:(NSArray *)ranges withSelectionAction:(HFControllerSelectAction)selectionAction; + +- (void)_updateBytesPerLine; +- (void)_updateDisplayedRange; +@end + +@interface NSEvent (HFLionStuff) +- (CGFloat)scrollingDeltaY; +- (BOOL)hasPreciseScrollingDeltas; +- (CGFloat)deviceDeltaY; +@end + +static inline Class preferredByteArrayClass(void) { + return [HFBTreeByteArray class]; +} + +@implementation HFController + +- (void)_sharedInit { + selectedContentsRanges = [[NSMutableArray alloc] initWithObjects:[HFRangeWrapper withRange:HFRangeMake(0, 0)], nil]; + byteArray = [[preferredByteArrayClass() alloc] init]; + [byteArray addObserver:self forKeyPath:@"changesAreLocked" options:0 context:KVOContextChangesAreLocked]; + selectionAnchor = NO_SELECTION; + undoOperations = [[NSMutableSet alloc] init]; +} + +- (instancetype)init { + self = [super init]; + [self _sharedInit]; + bytesPerLine = 16; + bytesPerColumn = 1; + _hfflags.editable = YES; + _hfflags.antialias = YES; + _hfflags.showcallouts = YES; + _hfflags.hideNullBytes = NO; + _hfflags.selectable = YES; + representers = [[NSMutableArray alloc] init]; + [self setFont:[NSFont fontWithName:HFDEFAULT_FONT size:HFDEFAULT_FONTSIZE]]; + return self; +} + +- (void)dealloc { + [representers makeObjectsPerformSelector:@selector(_setController:) withObject:nil]; + [representers release]; + [selectedContentsRanges release]; + [self _removeUndoManagerNotifications]; + [self _removeAllUndoOperations]; + [undoOperations release]; + [undoManager release]; + [undoCoalescer release]; + [_font release]; + [byteArray removeObserver:self forKeyPath:@"changesAreLocked"]; + [byteArray release]; + [cachedData release]; + [additionalPendingTransactions release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [coder encodeObject:representers forKey:@"HFRepresenters"]; + [coder encodeInt64:bytesPerLine forKey:@"HFBytesPerLine"]; + [coder encodeInt64:bytesPerColumn forKey:@"HFBytesPerColumn"]; + [coder encodeObject:_font forKey:@"HFFont"]; + [coder encodeDouble:lineHeight forKey:@"HFLineHeight"]; + [coder encodeBool:_hfflags.antialias forKey:@"HFAntialias"]; + [coder encodeBool:_hfflags.colorbytes forKey:@"HFColorBytes"]; + [coder encodeBool:_hfflags.showcallouts forKey:@"HFShowCallouts"]; + [coder encodeBool:_hfflags.hideNullBytes forKey:@"HFHidesNullBytes"]; + [coder encodeBool:_hfflags.livereload forKey:@"HFLiveReload"]; + [coder encodeInt:_hfflags.editMode forKey:@"HFEditMode"]; + [coder encodeBool:_hfflags.editable forKey:@"HFEditable"]; + [coder encodeBool:_hfflags.selectable forKey:@"HFSelectable"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super init]; + [self _sharedInit]; + bytesPerLine = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesPerLine"]; + bytesPerColumn = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesPerColumn"]; + _font = [[coder decodeObjectForKey:@"HFFont"] retain]; + lineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFLineHeight"]; + _hfflags.antialias = [coder decodeBoolForKey:@"HFAntialias"]; + _hfflags.colorbytes = [coder decodeBoolForKey:@"HFColorBytes"]; + _hfflags.livereload = [coder decodeBoolForKey:@"HFLiveReload"]; + + if ([coder containsValueForKey:@"HFEditMode"]) + _hfflags.editMode = [coder decodeIntForKey:@"HFEditMode"]; + else { + _hfflags.editMode = ([coder decodeBoolForKey:@"HFOverwriteMode"] + ? HFOverwriteMode : HFInsertMode); + } + + _hfflags.editable = [coder decodeBoolForKey:@"HFEditable"]; + _hfflags.selectable = [coder decodeBoolForKey:@"HFSelectable"]; + _hfflags.hideNullBytes = [coder decodeBoolForKey:@"HFHidesNullBytes"]; + representers = [[coder decodeObjectForKey:@"HFRepresenters"] retain]; + return self; +} + +- (NSArray *)representers { + return [[representers copy] autorelease]; +} + +- (void)notifyRepresentersOfChanges:(HFControllerPropertyBits)bits { + FOREACH(HFRepresenter*, rep, representers) { + [rep controllerDidChange:bits]; + } + + /* Post the HFControllerDidChangePropertiesNotification */ + NSNumber *number = [[NSNumber alloc] initWithUnsignedInteger:bits]; + NSDictionary *userInfo = [[NSDictionary alloc] initWithObjects:&number forKeys:(id *)&HFControllerChangedPropertiesKey count:1]; + [number release]; + [[NSNotificationCenter defaultCenter] postNotificationName:HFControllerDidChangePropertiesNotification object:self userInfo:userInfo]; + [userInfo release]; +} + +- (void)_firePropertyChanges { + NSMutableArray *pendingTransactions = additionalPendingTransactions; + NSUInteger pendingTransactionCount = [pendingTransactions count]; + additionalPendingTransactions = nil; + HFControllerPropertyBits propertiesToUpdate = propertiesToUpdateInCurrentTransaction; + propertiesToUpdateInCurrentTransaction = 0; + if (pendingTransactionCount > 0 || propertiesToUpdate != 0) { + BEGIN_TRANSACTION(); + while (pendingTransactionCount--) { + HFControllerPropertyBits propertiesInThisTransaction = [pendingTransactions[0] unsignedIntegerValue]; + [pendingTransactions removeObjectAtIndex:0]; + HFASSERT(propertiesInThisTransaction != 0); + [self notifyRepresentersOfChanges:propertiesInThisTransaction]; + } + [pendingTransactions release]; + if (propertiesToUpdate) { + [self notifyRepresentersOfChanges:propertiesToUpdate]; + } + END_TRANSACTION(); + } +} + +/* Inserts a "fence" so that all prior property change bits will be complete before any new ones */ +- (void)_insertPropertyChangeFence { + if (currentPropertyChangeToken == 0) { + HFASSERT(additionalPendingTransactions == nil); + /* There can be no prior property changes */ + HFASSERT(propertiesToUpdateInCurrentTransaction == 0); + return; + } + if (propertiesToUpdateInCurrentTransaction == 0) { + /* Nothing to fence */ + return; + } + if (additionalPendingTransactions == nil) additionalPendingTransactions = [[NSMutableArray alloc] init]; + [additionalPendingTransactions addObject:@(propertiesToUpdateInCurrentTransaction)]; + propertiesToUpdateInCurrentTransaction = 0; +} + +- (void)_addPropertyChangeBits:(HFControllerPropertyBits)bits { + propertiesToUpdateInCurrentTransaction |= bits; + if (currentPropertyChangeToken == 0) { + [self _firePropertyChanges]; + } +} + +- (NSUInteger)beginPropertyChangeTransaction { + HFASSERT(currentPropertyChangeToken < NSUIntegerMax); + return ++currentPropertyChangeToken; +} + +- (void)endPropertyChangeTransaction:(NSUInteger)token { + if (currentPropertyChangeToken != token) { + [NSException raise:NSInvalidArgumentException format:@"endPropertyChangeTransaction passed token %lu, but expected token %lu", (unsigned long)token, (unsigned long)currentPropertyChangeToken]; + } + HFASSERT(currentPropertyChangeToken > 0); + if (--currentPropertyChangeToken == 0) [self _firePropertyChanges]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (context == KVOContextChangesAreLocked) { + HFASSERT([keyPath isEqual:@"changesAreLocked"]); + [self _addPropertyChangeBits:HFControllerEditable]; + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)addRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + HFASSERT([representers indexOfObjectIdenticalTo:representer] == NSNotFound); + HFASSERT([representer controller] == nil); + [representer _setController:self]; + [representers addObject:representer]; + [representer controllerDidChange: -1]; +} + +- (void)removeRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + HFASSERT([representers indexOfObjectIdenticalTo:representer] != NSNotFound); + [representers removeObjectIdenticalTo:representer]; + [representer _setController:nil]; +} + +- (HFRange)_maximumDisplayedRangeSet { + unsigned long long contentsLength = [self contentsLength]; + HFRange maximumDisplayedRangeSet = HFRangeMake(0, HFRoundUpToNextMultipleSaturate(contentsLength, bytesPerLine)); + return maximumDisplayedRangeSet; +} + +- (unsigned long long)totalLineCount { + return HFDivideULLRoundingUp(HFRoundUpToNextMultipleSaturate([self contentsLength] - 1, bytesPerLine), bytesPerLine); +} + +- (HFFPRange)displayedLineRange { +#if ! NDEBUG + HFASSERT(displayedLineRange.location >= 0); + HFASSERT(displayedLineRange.length >= 0); + HFASSERT(displayedLineRange.location + displayedLineRange.length <= HFULToFP([self totalLineCount])); +#endif + return displayedLineRange; +} + +- (void)setDisplayedLineRange:(HFFPRange)range { +#if ! NDEBUG + HFASSERT(range.location >= 0); + HFASSERT(range.length >= 0); + HFASSERT(range.location + range.length <= HFULToFP([self totalLineCount])); +#endif + if (! HFFPRangeEqualsRange(range, displayedLineRange)) { + displayedLineRange = range; + [self _addPropertyChangeBits:HFControllerDisplayedLineRange]; + } +} + +- (CGFloat)lineHeight { + return lineHeight; +} + +- (void)setFont:(NSFont *)val { + if (val != _font) { + CGFloat priorLineHeight = [self lineHeight]; + + [_font release]; + _font = [val copy]; + + NSLayoutManager *manager = [[NSLayoutManager alloc] init]; + lineHeight = [manager defaultLineHeightForFont:_font]; + [manager release]; + + HFControllerPropertyBits bits = HFControllerFont; + if (lineHeight != priorLineHeight) bits |= HFControllerLineHeight; + [self _addPropertyChangeBits:bits]; + [self _insertPropertyChangeFence]; + [self _addPropertyChangeBits:HFControllerViewSizeRatios]; + [self _updateDisplayedRange]; + } +} + +- (BOOL)shouldAntialias { + return _hfflags.antialias; +} + +- (void)setShouldAntialias:(BOOL)antialias { + antialias = !! antialias; + if (antialias != _hfflags.antialias) { + _hfflags.antialias = antialias; + [self _addPropertyChangeBits:HFControllerAntialias]; + } +} + +- (BOOL)shouldColorBytes { + return _hfflags.colorbytes; +} + +- (void)setShouldColorBytes:(BOOL)colorbytes { + colorbytes = !! colorbytes; + if (colorbytes != _hfflags.colorbytes) { + _hfflags.colorbytes = colorbytes; + [self _addPropertyChangeBits:HFControllerColorBytes]; + } +} + +- (BOOL)shouldShowCallouts { + return _hfflags.showcallouts; +} + +- (void)setShouldShowCallouts:(BOOL)showcallouts { + showcallouts = !! showcallouts; + if (showcallouts != _hfflags.showcallouts) { + _hfflags.showcallouts = showcallouts; + [self _addPropertyChangeBits:HFControllerShowCallouts]; + } +} + +- (BOOL)shouldHideNullBytes { + return _hfflags.hideNullBytes; +} + +- (void)setShouldHideNullBytes:(BOOL)hideNullBytes +{ + hideNullBytes = !! hideNullBytes; + if (hideNullBytes != _hfflags.hideNullBytes) { + _hfflags.hideNullBytes = hideNullBytes; + [self _addPropertyChangeBits:HFControllerHideNullBytes]; + } +} + +- (BOOL)shouldLiveReload { + return _hfflags.livereload; +} + +- (void)setShouldLiveReload:(BOOL)livereload { + _hfflags.livereload = !!livereload; + +} + +- (void)setBytesPerColumn:(NSUInteger)val { + if (val != bytesPerColumn) { + bytesPerColumn = val; + [self _addPropertyChangeBits:HFControllerBytesPerColumn]; + } +} + +- (NSUInteger)bytesPerColumn { + return bytesPerColumn; +} + +- (BOOL)_shouldInvertSelectedRangesByAnchorRange { + return _hfflags.selectionInProgress && _hfflags.commandExtendSelection; +} + +- (NSArray *)_invertedSelectedContentsRanges { + HFASSERT([selectedContentsRanges count] > 0); + HFASSERT(selectionAnchorRange.location != NO_SELECTION); + if (selectionAnchorRange.length == 0) return [NSArray arrayWithArray:selectedContentsRanges]; + + NSArray *cleanedRanges = [HFRangeWrapper organizeAndMergeRanges:selectedContentsRanges]; + NSMutableArray *result = [NSMutableArray array]; + + /* Our algorithm works as follows - add any ranges outside of the selectionAnchorRange, clipped by the selectionAnchorRange. Then extract every "index" in our cleaned selected arrays that are within the selectionAnchorArray. An index is the location where a range starts or stops. Then use those indexes to create the inverted arrays. A range parity of 1 means that we are adding the range. */ + + /* Add all the ranges that are outside of selectionAnchorRange, clipping them if necessary */ + HFASSERT(HFSumDoesNotOverflow(selectionAnchorRange.location, selectionAnchorRange.length)); + FOREACH(HFRangeWrapper*, outsideWrapper, cleanedRanges) { + HFRange range = [outsideWrapper HFRange]; + if (range.location < selectionAnchorRange.location) { + HFRange clippedRange; + clippedRange.location = range.location; + HFASSERT(MIN(HFMaxRange(range), selectionAnchorRange.location) >= clippedRange.location); + clippedRange.length = MIN(HFMaxRange(range), selectionAnchorRange.location) - clippedRange.location; + [result addObject:[HFRangeWrapper withRange:clippedRange]]; + } + if (HFMaxRange(range) > HFMaxRange(selectionAnchorRange)) { + HFRange clippedRange; + clippedRange.location = MAX(range.location, HFMaxRange(selectionAnchorRange)); + HFASSERT(HFMaxRange(range) >= clippedRange.location); + clippedRange.length = HFMaxRange(range) - clippedRange.location; + [result addObject:[HFRangeWrapper withRange:clippedRange]]; + } + } + + HFASSERT(HFSumDoesNotOverflow(selectionAnchorRange.location, selectionAnchorRange.length)); + + NEW_ARRAY(unsigned long long, partitions, 2*[cleanedRanges count] + 2); + NSUInteger partitionCount, partitionIndex = 0; + + partitions[partitionIndex++] = selectionAnchorRange.location; + FOREACH(HFRangeWrapper*, wrapper, cleanedRanges) { + HFRange range = [wrapper HFRange]; + if (! HFIntersectsRange(range, selectionAnchorRange)) continue; + + partitions[partitionIndex++] = MAX(selectionAnchorRange.location, range.location); + partitions[partitionIndex++] = MIN(HFMaxRange(selectionAnchorRange), HFMaxRange(range)); + } + + // For some reason, using HFMaxRange confuses the static analyzer + partitions[partitionIndex++] = HFSum(selectionAnchorRange.location, selectionAnchorRange.length); + + partitionCount = partitionIndex; + HFASSERT((partitionCount % 2) == 0); + + partitionIndex = 0; + while (partitionIndex < partitionCount) { + HFASSERT(partitionIndex + 1 < partitionCount); + HFASSERT(partitions[partitionIndex] <= partitions[partitionIndex + 1]); + if (partitions[partitionIndex] < partitions[partitionIndex + 1]) { + HFRange range = HFRangeMake(partitions[partitionIndex], partitions[partitionIndex + 1] - partitions[partitionIndex]); + [result addObject:[HFRangeWrapper withRange:range]]; + } + partitionIndex += 2; + } + + FREE_ARRAY(partitions); + + if ([result count] == 0) [result addObject:[HFRangeWrapper withRange:HFRangeMake(selectionAnchor, 0)]]; + + return [HFRangeWrapper organizeAndMergeRanges:result]; +} + +#if ! NDEBUG +- (void)_ensureSelectionIsValid { + HFASSERT(selectedContentsRanges != nil); + HFASSERT([selectedContentsRanges count] > 0); + BOOL onlyOneWrapper = ([selectedContentsRanges count] == 1); + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + EXPECT_CLASS(wrapper, HFRangeWrapper); + HFRange range = [wrapper HFRange]; + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + if (onlyOneWrapper == NO) HFASSERT(range.length > 0); /* If we have more than one wrapper, then none of them should be zero length */ + } +} +#endif + +- (void)_setSingleSelectedContentsRange:(HFRange)newSelection { + HFASSERT(HFRangeIsSubrangeOfRange(newSelection, HFRangeMake(0, [self contentsLength]))); + BOOL selectionChanged; + if ([selectedContentsRanges count] == 1) { + selectionChanged = ! HFRangeEqualsRange([selectedContentsRanges[0] HFRange], newSelection); + } + else { + selectionChanged = YES; + } + + if (selectionChanged) { + [selectedContentsRanges removeAllObjects]; + [selectedContentsRanges addObject:[HFRangeWrapper withRange:newSelection]]; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + VALIDATE_SELECTION(); +} + +- (NSArray *)selectedContentsRanges { + VALIDATE_SELECTION(); + if ([self _shouldInvertSelectedRangesByAnchorRange]) return [self _invertedSelectedContentsRanges]; + else return [NSArray arrayWithArray:selectedContentsRanges]; +} + +- (unsigned long long)contentsLength { + if (! byteArray) return 0; + else return [byteArray length]; +} + +- (NSData *)dataForRange:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); // it doesn't make sense to ask for a buffer larger than can be stored in memory + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + + if(range.length == 0) { + // Don't throw out cache for an empty request! Also makes the analyzer happier. + return [NSData data]; + } + + NSUInteger newGenerationIndex = [byteArray changeGenerationCount]; + if (cachedData == nil || newGenerationIndex != cachedGenerationIndex || ! HFRangeIsSubrangeOfRange(range, cachedRange)) { + [cachedData release]; + cachedGenerationIndex = newGenerationIndex; + cachedRange = range; + NSUInteger length = ll2l(range.length); + unsigned char *data = check_malloc(length); + [byteArray copyBytes:data range:range]; + cachedData = [[NSData alloc] initWithBytesNoCopy:data length:length freeWhenDone:YES]; + } + + if (HFRangeEqualsRange(range, cachedRange)) { + return cachedData; + } + else { + HFASSERT(cachedRange.location <= range.location); + NSRange cachedDataSubrange; + cachedDataSubrange.location = ll2l(range.location - cachedRange.location); + cachedDataSubrange.length = ll2l(range.length); + return [cachedData subdataWithRange:cachedDataSubrange]; + } +} + +- (void)copyBytes:(unsigned char *)bytes range:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); // it doesn't make sense to ask for a buffer larger than can be stored in memory + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + [byteArray copyBytes:bytes range:range]; +} + +- (void)_updateDisplayedRange { + HFRange proposedNewDisplayRange; + HFFPRange proposedNewLineRange; + HFRange maxRangeSet = [self _maximumDisplayedRangeSet]; + NSUInteger maxBytesForViewSize = NSUIntegerMax; + double maxLines = DBL_MAX; + FOREACH(HFRepresenter*, rep, representers) { + NSView *view = [rep view]; + double repMaxLines = [rep maximumAvailableLinesForViewHeight:NSHeight([view frame])]; + if (repMaxLines != DBL_MAX) { + /* bytesPerLine may be ULONG_MAX. We want to compute the smaller of maxBytesForViewSize and ceil(repMaxLines) * bytesPerLine. If the latter expression overflows, the smaller is the former. */ + NSUInteger repMaxLinesUInt = (NSUInteger)ceil(repMaxLines); + NSUInteger maxLinesTimesBytesPerLine = repMaxLinesUInt * bytesPerLine; + /* Check if we overflowed */ + BOOL overflowed = (repMaxLinesUInt != 0 && (maxLinesTimesBytesPerLine / repMaxLinesUInt != bytesPerLine)); + if (! overflowed) { + maxBytesForViewSize = MIN(maxLinesTimesBytesPerLine, maxBytesForViewSize); + } + } + maxLines = MIN(repMaxLines, maxLines); + } + if (maxLines == DBL_MAX) { + proposedNewDisplayRange = HFRangeMake(0, 0); + proposedNewLineRange = (HFFPRange){0, 0}; + } + else { + unsigned long long maximumDisplayedBytes = MIN(maxRangeSet.length, maxBytesForViewSize); + HFASSERT(HFMaxRange(maxRangeSet) >= maximumDisplayedBytes); + + proposedNewDisplayRange.location = MIN(HFMaxRange(maxRangeSet) - maximumDisplayedBytes, displayedContentsRange.location); + proposedNewDisplayRange.location -= proposedNewDisplayRange.location % bytesPerLine; + proposedNewDisplayRange.length = MIN(HFMaxRange(maxRangeSet) - proposedNewDisplayRange.location, maxBytesForViewSize); + if (maxBytesForViewSize % bytesPerLine != 0) { + NSLog(@"Bad max bytes: %lu (%lu)", (unsigned long)maxBytesForViewSize, (unsigned long)bytesPerLine); + } + if (HFMaxRange(maxRangeSet) != ULLONG_MAX && (HFMaxRange(maxRangeSet) - proposedNewDisplayRange.location) % bytesPerLine != 0) { + NSLog(@"Bad max range minus: %llu (%lu)", HFMaxRange(maxRangeSet) - proposedNewDisplayRange.location, (unsigned long)bytesPerLine); + } + + long double lastLine = HFULToFP([self totalLineCount]); + proposedNewLineRange.length = MIN(maxLines, lastLine); + proposedNewLineRange.location = MIN(displayedLineRange.location, lastLine - proposedNewLineRange.length); + } + HFASSERT(HFRangeIsSubrangeOfRange(proposedNewDisplayRange, maxRangeSet)); + HFASSERT(proposedNewDisplayRange.location % bytesPerLine == 0); + if (! HFRangeEqualsRange(proposedNewDisplayRange, displayedContentsRange) || ! HFFPRangeEqualsRange(proposedNewLineRange, displayedLineRange)) { + displayedContentsRange = proposedNewDisplayRange; + displayedLineRange = proposedNewLineRange; + [self _addPropertyChangeBits:HFControllerDisplayedLineRange]; + } +} + +- (void)_ensureVisibilityOfLocation:(unsigned long long)location { + HFASSERT(location <= [self contentsLength]); + unsigned long long lineInt = location / bytesPerLine; + long double line = HFULToFP(lineInt); + HFASSERT(line >= 0); + line = MIN(line, HFULToFP([self totalLineCount]) - 1); + HFFPRange lineRange = [self displayedLineRange]; + HFFPRange newLineRange = lineRange; + if (line < lineRange.location) { + newLineRange.location = line; + } + else if (line >= lineRange.location + lineRange.length) { + HFASSERT(lineRange.location + lineRange.length >= 1); + newLineRange.location = lineRange.location + (line - (lineRange.location + lineRange.length - 1)); + } + [self setDisplayedLineRange:newLineRange]; +} + +- (void)maximizeVisibilityOfContentsRange:(HFRange)range { + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + + // Find the minimum move necessary to make range visible + HFFPRange displayRange = [self displayedLineRange]; + HFFPRange newDisplayRange = displayRange; + unsigned long long startLine = range.location / bytesPerLine; + unsigned long long endLine = HFDivideULLRoundingUp(HFRoundUpToNextMultipleSaturate(HFMaxRange(range), bytesPerLine), bytesPerLine); + HFASSERT(endLine > startLine || endLine == ULLONG_MAX); + long double linesInRange = HFULToFP(endLine - startLine); + long double linesToDisplay = MIN(displayRange.length, linesInRange); + HFASSERT(linesToDisplay <= linesInRange); + long double linesToMoveDownToMakeLastLineVisible = HFULToFP(endLine) - (displayRange.location + displayRange.length); + long double linesToMoveUpToMakeFirstLineVisible = displayRange.location - HFULToFP(startLine); + //HFASSERT(linesToMoveUpToMakeFirstLineVisible <= 0 || linesToMoveDownToMakeLastLineVisible <= 0); + // in general, we expect either linesToMoveUpToMakeFirstLineVisible to be <= zero, or linesToMoveDownToMakeLastLineVisible to be <= zero. However, if the available space is smaller than one line, then that won't be true. + if (linesToMoveDownToMakeLastLineVisible > 0) { + newDisplayRange.location += linesToMoveDownToMakeLastLineVisible; + } + else if (linesToMoveUpToMakeFirstLineVisible > 0 && linesToDisplay >= 1) { + // the >= 1 check prevents some wacky behavior when we have less than one line's worth of space, that caused bouncing between the top and bottom of the line + newDisplayRange.location -= linesToMoveUpToMakeFirstLineVisible; + } + + // Use the minimum movement if it would be visually helpful; otherwise just center. + if (HFFPIntersectsRange(displayRange, newDisplayRange)) { + [self setDisplayedLineRange:newDisplayRange]; + } else { + [self centerContentsRange:range]; + } +} + +- (void)centerContentsRange:(HFRange)range { + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + HFFPRange displayRange = [self displayedLineRange]; + const long double numDisplayedLines = displayRange.length; + HFFPRange newDisplayRange; + unsigned long long startLine = range.location / bytesPerLine; + unsigned long long endLine = HFDivideULLRoundingUp(HFRoundUpToNextMultipleSaturate(HFMaxRange(range), bytesPerLine), bytesPerLine); + HFASSERT(endLine > startLine || endLine == ULLONG_MAX); + long double linesInRange = HFULToFP(endLine - startLine); + + /* Handle the case of a line range bigger than we can display by choosing the top lines. */ + if (numDisplayedLines <= linesInRange) { + newDisplayRange = (HFFPRange){startLine, numDisplayedLines}; + } + else { + /* Construct a newDisplayRange that centers {startLine, endLine} */ + long double center = startLine + (endLine - startLine) / 2.; + newDisplayRange = (HFFPRange){center - numDisplayedLines / 2., numDisplayedLines}; + } + + /* Move the newDisplayRange up or down as necessary */ + newDisplayRange.location = fmaxl(newDisplayRange.location, (long double)0.); + newDisplayRange.location = fminl(newDisplayRange.location, HFULToFP([self totalLineCount]) - numDisplayedLines); + [self setDisplayedLineRange:newDisplayRange]; +} + +/* Clips the selection to a given length. If this would clip the entire selection, returns a zero length selection at the end. Indicates HFControllerSelectedRanges if the selection changes. */ +- (void)_clipSelectedContentsRangesToLength:(unsigned long long)newLength { + NSMutableArray *newTempSelection = [selectedContentsRanges mutableCopy]; + NSUInteger i, max = [newTempSelection count]; + for (i=0; i < max; i++) { + HFRange range = [newTempSelection[i] HFRange]; + if (HFMaxRange(range) > newLength) { + if (range.location > newLength) { + /* The range starts past our new max. Just remove this range entirely */ + [newTempSelection removeObjectAtIndex:i]; + i--; + max--; + } + else { + /* Need to clip this range */ + range.length = newLength - range.location; + newTempSelection[i] = [HFRangeWrapper withRange:range]; + } + } + } + [newTempSelection setArray:[HFRangeWrapper organizeAndMergeRanges:newTempSelection]]; + + /* If there are multiple empty ranges, remove all but the first */ + BOOL foundEmptyRange = NO; + max = [newTempSelection count]; + for (i=0; i < max; i++) { + HFRange range = [newTempSelection[i] HFRange]; + HFASSERT(HFMaxRange(range) <= newLength); + if (range.length == 0) { + if (foundEmptyRange) { + [newTempSelection removeObjectAtIndex:i]; + i--; + max--; + } + foundEmptyRange = YES; + } + } + if (max == 0) { + /* Removed all ranges - insert one at the end */ + [newTempSelection addObject:[HFRangeWrapper withRange:HFRangeMake(newLength, 0)]]; + } + + /* If something changed, set the new selection and post the change bit */ + if (! [selectedContentsRanges isEqualToArray:newTempSelection]) { + [selectedContentsRanges setArray:newTempSelection]; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + + [newTempSelection release]; +} + +- (void)setByteArray:(HFByteArray *)val { + REQUIRE_NOT_NULL(val); + BEGIN_TRANSACTION(); + [byteArray removeObserver:self forKeyPath:@"changesAreLocked"]; + [val retain]; + [byteArray release]; + byteArray = val; + [cachedData release]; + cachedData = nil; + [byteArray addObserver:self forKeyPath:@"changesAreLocked" options:0 context:KVOContextChangesAreLocked]; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits: HFControllerContentValue | HFControllerContentLength]; + [self _clipSelectedContentsRangesToLength:[byteArray length]]; + END_TRANSACTION(); +} + +- (HFByteArray *)byteArray { + return byteArray; +} + +- (void)_undoNotification:note { + USE(note); +} + +- (void)_removeUndoManagerNotifications { + if (undoManager) { + NSNotificationCenter *noter = [NSNotificationCenter defaultCenter]; + [noter removeObserver:self name:NSUndoManagerWillUndoChangeNotification object:undoManager]; + } +} + +- (void)_addUndoManagerNotifications { + if (undoManager) { + NSNotificationCenter *noter = [NSNotificationCenter defaultCenter]; + [noter addObserver:self selector:@selector(_undoNotification:) name:NSUndoManagerWillUndoChangeNotification object:undoManager]; + } +} + +- (void)_removeAllUndoOperations { + /* Remove all the undo operations, because some undo operation is unsupported. Note that if we were smarter we would keep a stack of undo operations and only remove ones "up to" a certain point. */ + [undoManager removeAllActionsWithTarget:self]; + [undoOperations makeObjectsPerformSelector:@selector(invalidate)]; + [undoOperations removeAllObjects]; +} + +- (void)setUndoManager:(NSUndoManager *)manager { + [self _removeUndoManagerNotifications]; + [self _removeAllUndoOperations]; + [manager retain]; + [undoManager release]; + undoManager = manager; + [self _addUndoManagerNotifications]; +} + +- (NSUndoManager *)undoManager { + return undoManager; +} + +- (NSUInteger)bytesPerLine { + return bytesPerLine; +} + +- (BOOL)editable { + return _hfflags.editable && ! [byteArray changesAreLocked] && _hfflags.editMode != HFReadOnlyMode; +} + +- (void)setEditable:(BOOL)flag { + if (flag != _hfflags.editable) { + _hfflags.editable = flag; + [self _addPropertyChangeBits:HFControllerEditable]; + } +} + +- (void)_updateBytesPerLine { + NSUInteger newBytesPerLine = NSUIntegerMax; + FOREACH(HFRepresenter*, rep, representers) { + NSView *view = [rep view]; + CGFloat width = [view frame].size.width; + NSUInteger repMaxBytesPerLine = [rep maximumBytesPerLineForViewWidth:width]; + HFASSERT(repMaxBytesPerLine > 0); + newBytesPerLine = MIN(repMaxBytesPerLine, newBytesPerLine); + } + if (newBytesPerLine != bytesPerLine) { + HFASSERT(newBytesPerLine > 0); + bytesPerLine = newBytesPerLine; + BEGIN_TRANSACTION(); + [self _addPropertyChangeBits:HFControllerBytesPerLine]; + END_TRANSACTION(); + } +} + +- (void)representer:(HFRepresenter *)rep changedProperties:(HFControllerPropertyBits)properties { + USE(rep); + HFControllerPropertyBits remainingProperties = properties; + BEGIN_TRANSACTION(); + if (remainingProperties & HFControllerBytesPerLine) { + [self _updateBytesPerLine]; + remainingProperties &= ~HFControllerBytesPerLine; + } + if (remainingProperties & HFControllerDisplayedLineRange) { + [self _updateDisplayedRange]; + remainingProperties &= ~HFControllerDisplayedLineRange; + } + if (remainingProperties & HFControllerByteRangeAttributes) { + [self _addPropertyChangeBits:HFControllerByteRangeAttributes]; + remainingProperties &= ~HFControllerByteRangeAttributes; + } + if (remainingProperties & HFControllerViewSizeRatios) { + [self _addPropertyChangeBits:HFControllerViewSizeRatios]; + remainingProperties &= ~HFControllerViewSizeRatios; + } + if (remainingProperties) { + NSLog(@"Unknown properties: %lx", (long)remainingProperties); + } + END_TRANSACTION(); +} + +- (HFByteArray *)byteArrayForSelectedContentsRanges { + HFByteArray *result = nil; + HFByteArray *bytes = [self byteArray]; + VALIDATE_SELECTION(); + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + HFByteArray *additionalBytes = [bytes subarrayWithRange:range]; + if (! result) { + result = additionalBytes; + } + else { + [result insertByteArray:additionalBytes inRange:HFRangeMake([result length], 0)]; + } + } + return result; +} + +/* Flattens the selected range to a single range (the selected range becomes any character within or between the selected ranges). Modifies the selectedContentsRanges and returns the new single HFRange. Does not call notifyRepresentersOfChanges: */ +- (HFRange)_flattenSelectionRange { + HFASSERT([selectedContentsRanges count] >= 1); + + HFRange resultRange = [selectedContentsRanges[0] HFRange]; + if ([selectedContentsRanges count] == 1) return resultRange; //already flat + + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange selectedRange = [wrapper HFRange]; + if (selectedRange.location < resultRange.location) { + /* Extend our result range backwards */ + resultRange.length += resultRange.location - selectedRange.location; + resultRange.location = selectedRange.location; + } + if (HFRangeExtendsPastRange(selectedRange, resultRange)) { + HFASSERT(selectedRange.location >= resultRange.location); //must be true by if statement above + resultRange.length = HFSum(selectedRange.location - resultRange.location, selectedRange.length); + } + } + [self _setSingleSelectedContentsRange:resultRange]; + return resultRange; +} + +- (unsigned long long)_minimumSelectionLocation { + HFASSERT([selectedContentsRanges count] >= 1); + unsigned long long minSelection = ULLONG_MAX; + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + minSelection = MIN(minSelection, range.location); + } + return minSelection; +} + +- (unsigned long long)_maximumSelectionLocation { + HFASSERT([selectedContentsRanges count] >= 1); + unsigned long long maxSelection = 0; + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + maxSelection = MAX(maxSelection, HFMaxRange(range)); + } + return maxSelection; +} + +- (unsigned long long)minimumSelectionLocation { + return [self _minimumSelectionLocation]; +} + +- (unsigned long long)maximumSelectionLocation { + return [self _maximumSelectionLocation]; +} + +/* Put the selection at the left or right end of the current selection, with zero length. Modifies the selectedContentsRanges and returns the new single HFRange. Does not call notifyRepresentersOfChanges: */ +- (HFRange)_telescopeSelectionRangeInDirection:(HFControllerMovementDirection)direction { + HFRange resultRange; + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + resultRange.location = (direction == HFControllerDirectionLeft ? [self _minimumSelectionLocation] : [self _maximumSelectionLocation]); + resultRange.length = 0; + [self _setSingleSelectedContentsRange:resultRange]; + return resultRange; +} + +- (void)beginSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)characterIndex { + USE(event); + HFASSERT(characterIndex <= [self contentsLength]); + + /* Determine how to perform the selection - normally, with command key, or with shift key. Command + shift is the same as command. The shift key closes the selection - the selected range becomes the single range containing the first and last selected character. */ + _hfflags.shiftExtendSelection = NO; + _hfflags.commandExtendSelection = NO; + NSUInteger flags = [event modifierFlags]; + if (flags & NSCommandKeyMask) _hfflags.commandExtendSelection = YES; + else if (flags & NSShiftKeyMask) _hfflags.shiftExtendSelection = YES; + + selectionAnchor = NO_SELECTION; + selectionAnchorRange = HFRangeMake(NO_SELECTION, 0); + + _hfflags.selectionInProgress = YES; + if (_hfflags.commandExtendSelection) { + /* The selection anchor is used to track the "invert" range. All characters within this range have their selection inverted. This is tracked by the _shouldInvertSelectedRangesByAnchorRange method. */ + selectionAnchor = characterIndex; + selectionAnchorRange = HFRangeMake(characterIndex, 0); + } + else if (_hfflags.shiftExtendSelection) { + /* The selection anchor is used to track the single (flattened) selected range. */ + HFRange selectedRange = [self _flattenSelectionRange]; + unsigned long long distanceFromRangeStart = HFAbsoluteDifference(selectedRange.location, characterIndex); + unsigned long long distanceFromRangeEnd = HFAbsoluteDifference(HFMaxRange(selectedRange), characterIndex); + if (selectedRange.length == 0) { + HFASSERT(distanceFromRangeStart == distanceFromRangeEnd); + selectionAnchor = selectedRange.location; + selectedRange.location = MIN(characterIndex, selectedRange.location); + selectedRange.length = distanceFromRangeStart; + } + else if (distanceFromRangeStart >= distanceFromRangeEnd) { + /* Push the "end forwards" */ + selectedRange.length = distanceFromRangeStart; + selectionAnchor = selectedRange.location; + } + else { + /* Push the "start back" */ + selectedRange.location = selectedRange.location + selectedRange.length - distanceFromRangeEnd; + selectedRange.length = distanceFromRangeEnd; + selectionAnchor = HFSum(selectedRange.length, selectedRange.location); + } + HFASSERT(HFRangeIsSubrangeOfRange(selectedRange, HFRangeMake(0, [self contentsLength]))); + selectionAnchorRange = selectedRange; + [self _setSingleSelectedContentsRange:selectedRange]; + } + else { + /* No modifier key selection. The selection anchor is not used. */ + [self _setSingleSelectedContentsRange:HFRangeMake(characterIndex, 0)]; + selectionAnchor = characterIndex; + } +} + +- (void)continueSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex { + USE(event); + HFASSERT(_hfflags.selectionInProgress); + HFASSERT(byteIndex <= [self contentsLength]); + BEGIN_TRANSACTION(); + if (_hfflags.commandExtendSelection) { + /* Clear any zero-length ranges, unless there's only one */ + NSUInteger rangeCount = [selectedContentsRanges count]; + NSUInteger rangeIndex = rangeCount; + while (rangeIndex-- > 0) { + if (rangeCount > 1 && [selectedContentsRanges[rangeIndex] HFRange].length == 0) { + [selectedContentsRanges removeObjectAtIndex:rangeIndex]; + rangeCount--; + } + } + selectionAnchorRange.location = MIN(byteIndex, selectionAnchor); + selectionAnchorRange.length = MAX(byteIndex, selectionAnchor) - selectionAnchorRange.location; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + else if (_hfflags.shiftExtendSelection) { + HFASSERT(selectionAnchorRange.location != NO_SELECTION); + HFASSERT(selectionAnchor != NO_SELECTION); + HFRange range; + if (! HFLocationInRange(byteIndex, selectionAnchorRange)) { + /* The character index is outside of the selection anchor range. The new range is just the selected anchor range combined with the character index. */ + range.location = MIN(byteIndex, selectionAnchorRange.location); + unsigned long long rangeEnd = MAX(byteIndex, HFSum(selectionAnchorRange.location, selectionAnchorRange.length)); + HFASSERT(rangeEnd >= range.location); + range.length = rangeEnd - range.location; + } + else { + /* The character is within the selection anchor range. We use the selection anchor index to determine which "side" of the range is selected. */ + range.location = MIN(selectionAnchor, byteIndex); + range.length = HFAbsoluteDifference(selectionAnchor, byteIndex); + } + [self _setSingleSelectedContentsRange:range]; + } + else { + /* No modifier key selection */ + HFRange range; + range.location = MIN(byteIndex, selectionAnchor); + range.length = MAX(byteIndex, selectionAnchor) - range.location; + [self _setSingleSelectedContentsRange:range]; + } + END_TRANSACTION(); + VALIDATE_SELECTION(); +} + +- (void)endSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)characterIndex { + USE(event); + HFASSERT(_hfflags.selectionInProgress); + HFASSERT(characterIndex <= [self contentsLength]); + if (_hfflags.commandExtendSelection) { + selectionAnchorRange.location = MIN(characterIndex, selectionAnchor); + selectionAnchorRange.length = MAX(characterIndex, selectionAnchor) - selectionAnchorRange.location; + + /* "Commit" our selectionAnchorRange */ + NSArray *newSelection = [self _invertedSelectedContentsRanges]; + [selectedContentsRanges setArray:newSelection]; + } + else if (_hfflags.shiftExtendSelection) { + HFASSERT(selectionAnchorRange.location != NO_SELECTION); + HFASSERT(selectionAnchor != NO_SELECTION); + HFRange range; + if (! HFLocationInRange(characterIndex, selectionAnchorRange)) { + /* The character index is outside of the selection anchor range. The new range is just the selected anchor range combined with the character index. */ + range.location = MIN(characterIndex, selectionAnchorRange.location); + unsigned long long rangeEnd = MAX(characterIndex, HFSum(selectionAnchorRange.location, selectionAnchorRange.length)); + HFASSERT(rangeEnd >= range.location); + range.length = rangeEnd - range.location; + } + else { + /* The character is within the selection anchor range. We use the selection anchor index to determine which "side" of the range is selected. */ + range.location = MIN(selectionAnchor, characterIndex); + range.length = HFAbsoluteDifference(selectionAnchor, characterIndex); + } + [self _setSingleSelectedContentsRange:range]; + } + else { + /* No modifier key selection */ + HFRange range; + range.location = MIN(characterIndex, selectionAnchor); + range.length = MAX(characterIndex, selectionAnchor) - range.location; + [self _setSingleSelectedContentsRange:range]; + } + + _hfflags.selectionInProgress = NO; + _hfflags.shiftExtendSelection = NO; + _hfflags.commandExtendSelection = NO; + selectionAnchor = NO_SELECTION; +} + +- (double)selectionPulseAmount { + double result = 0; + if (pulseSelectionStartTime > 0) { + CFTimeInterval diff = pulseSelectionCurrentTime - pulseSelectionStartTime; + if (diff > 0 && diff < kPulseDuration) { + result = 1. - fabs(diff * 2 - kPulseDuration) / kPulseDuration; + } + } + return result; +} + +- (void)firePulseTimer:(NSTimer *)timer { + USE(timer); + HFASSERT(pulseSelectionStartTime != 0); + pulseSelectionCurrentTime = CFAbsoluteTimeGetCurrent(); + [self _addPropertyChangeBits:HFControllerSelectionPulseAmount]; + if (pulseSelectionCurrentTime - pulseSelectionStartTime > kPulseDuration) { + [pulseSelectionTimer invalidate]; + [pulseSelectionTimer release]; + pulseSelectionTimer = nil; + } +} + +- (void)pulseSelection { + pulseSelectionStartTime = CFAbsoluteTimeGetCurrent(); + if (pulseSelectionTimer == nil) { + pulseSelectionTimer = [[NSTimer scheduledTimerWithTimeInterval:(1. / 30.) target:self selector:@selector(firePulseTimer:) userInfo:nil repeats:YES] retain]; + } +} + +- (void)scrollByLines:(long double)lines { + HFFPRange lineRange = [self displayedLineRange]; + HFASSERT(HFULToFP([self totalLineCount]) >= lineRange.length); + long double maxScroll = HFULToFP([self totalLineCount]) - lineRange.length; + if (lines < 0) { + lineRange.location -= MIN(lineRange.location, -lines); + } + else { + lineRange.location = MIN(maxScroll, lineRange.location + lines); + } + [self setDisplayedLineRange:lineRange]; +} + +- (void)scrollWithScrollEvent:(NSEvent *)scrollEvent { + HFASSERT(scrollEvent != NULL); + HFASSERT([scrollEvent type] == NSScrollWheel); + CGFloat preciseScroll = 0; + BOOL hasPreciseScroll; + + /* Prefer precise deltas */ + if ([scrollEvent respondsToSelector:@selector(hasPreciseScrollingDeltas)]) { + hasPreciseScroll = [scrollEvent hasPreciseScrollingDeltas]; + if (hasPreciseScroll) { + /* In this case, we're going to scroll by a certain number of points */ + preciseScroll = [scrollEvent scrollingDeltaY]; + } + } else if ([scrollEvent respondsToSelector:@selector(deviceDeltaY)]) { + /* Legacy (SnowLeopard) support */ + hasPreciseScroll = ([scrollEvent subtype] == 1); + if (hasPreciseScroll) { + preciseScroll = [scrollEvent deviceDeltaY]; + } + } else { + hasPreciseScroll = NO; + } + + long double scrollY = 0; + if (! hasPreciseScroll) { + scrollY = -kScrollMultiplier * [scrollEvent deltaY]; + } else { + scrollY = -preciseScroll / [self lineHeight]; + } + [self scrollByLines:scrollY]; +} + +- (void)setSelectedContentsRanges:(NSArray *)selectedRanges { + REQUIRE_NOT_NULL(selectedRanges); + [selectedContentsRanges setArray:selectedRanges]; + VALIDATE_SELECTION(); + selectionAnchor = NO_SELECTION; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; +} + +- (IBAction)selectAll:sender { + USE(sender); + if (_hfflags.selectable) { + [self _setSingleSelectedContentsRange:HFRangeMake(0, [self contentsLength])]; + } +} + +- (void)_addRangeToSelection:(HFRange)range { + [selectedContentsRanges addObject:[HFRangeWrapper withRange:range]]; + [selectedContentsRanges setArray:[HFRangeWrapper organizeAndMergeRanges:selectedContentsRanges]]; + VALIDATE_SELECTION(); +} + +- (void)_removeRangeFromSelection:(HFRange)inputRange withCursorLocationIfAllSelectionRemoved:(unsigned long long)cursorLocation { + NSUInteger selectionCount = [selectedContentsRanges count]; + HFASSERT(selectionCount > 0 && selectionCount <= NSUIntegerMax / 2); + NSUInteger rangeIndex = 0; + NSArray *wrappers; + NEW_ARRAY(HFRange, tempRanges, selectionCount * 2); + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + if (! HFIntersectsRange(range, inputRange)) { + tempRanges[rangeIndex++] = range; + } + else { + if (range.location < inputRange.location) { + tempRanges[rangeIndex++] = HFRangeMake(range.location, inputRange.location - range.location); + } + if (HFMaxRange(range) > HFMaxRange(inputRange)) { + tempRanges[rangeIndex++] = HFRangeMake(HFMaxRange(inputRange), HFMaxRange(range) - HFMaxRange(inputRange)); + } + } + } + if (rangeIndex == 0 || (rangeIndex == 1 && tempRanges[0].length == 0)) { + /* We removed all of our ranges. Telescope us. */ + HFASSERT(cursorLocation <= [self contentsLength]); + [self _setSingleSelectedContentsRange:HFRangeMake(cursorLocation, 0)]; + } + else { + wrappers = [HFRangeWrapper withRanges:tempRanges count:rangeIndex]; + [selectedContentsRanges setArray:[HFRangeWrapper organizeAndMergeRanges:wrappers]]; + } + FREE_ARRAY(tempRanges); + VALIDATE_SELECTION(); +} + +- (void)_moveDirectionDiscardingSelection:(HFControllerMovementDirection)direction byAmount:(unsigned long long)amountToMove { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + BEGIN_TRANSACTION(); + BOOL selectionWasEmpty = ([selectedContentsRanges count] == 1 && [selectedContentsRanges[0] HFRange].length == 0); + BOOL directionIsForward = (direction == HFControllerDirectionRight); + HFRange selectedRange = [self _telescopeSelectionRangeInDirection: (directionIsForward ? HFControllerDirectionRight : HFControllerDirectionLeft)]; + HFASSERT(selectedRange.length == 0); + HFASSERT([self contentsLength] >= selectedRange.location); + /* A movement of just 1 with a selection only clears the selection; it does not move the cursor */ + if (selectionWasEmpty || amountToMove > 1) { + if (direction == HFControllerDirectionLeft) { + selectedRange.location -= MIN(amountToMove, selectedRange.location); + } + else { + selectedRange.location += MIN(amountToMove, [self contentsLength] - selectedRange.location); + } + } + selectionAnchor = NO_SELECTION; + [self _setSingleSelectedContentsRange:selectedRange]; + [self _ensureVisibilityOfLocation:selectedRange.location]; + END_TRANSACTION(); +} + +/* In _extendSelectionInDirection:byAmount:, we only allow left/right movement. up/down is not allowed. */ +- (void)_extendSelectionInDirection:(HFControllerMovementDirection)direction byAmount:(unsigned long long)amountToMove { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + unsigned long long minSelection = [self _minimumSelectionLocation]; + unsigned long long maxSelection = [self _maximumSelectionLocation]; + BOOL selectionChanged = NO; + unsigned long long locationToMakeVisible = NO_SELECTION; + unsigned long long contentsLength = [self contentsLength]; + if (selectionAnchor == NO_SELECTION) { + /* Pick the anchor opposite the choice of direction */ + if (direction == HFControllerDirectionLeft) selectionAnchor = maxSelection; + else selectionAnchor = minSelection; + } + if (direction == HFControllerDirectionLeft) { + if (minSelection >= selectionAnchor && maxSelection > minSelection) { + unsigned long long amountToRemove = llmin(maxSelection - selectionAnchor, amountToMove); + unsigned long long amountToAdd = llmin(amountToMove - amountToRemove, selectionAnchor); + if (amountToRemove > 0) [self _removeRangeFromSelection:HFRangeMake(maxSelection - amountToRemove, amountToRemove) withCursorLocationIfAllSelectionRemoved:minSelection]; + if (amountToAdd > 0) [self _addRangeToSelection:HFRangeMake(selectionAnchor - amountToAdd, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = (amountToAdd > 0 ? selectionAnchor - amountToAdd : maxSelection - amountToRemove); + } + else { + if (minSelection > 0) { + NSUInteger amountToAdd = ll2l(llmin(minSelection, amountToMove)); + if (amountToAdd > 0) [self _addRangeToSelection:HFRangeMake(minSelection - amountToAdd, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = minSelection - amountToAdd; + } + } + } + else if (direction == HFControllerDirectionRight) { + if (maxSelection <= selectionAnchor && maxSelection > minSelection) { + HFASSERT(contentsLength >= maxSelection); + unsigned long long amountToRemove = ll2l(llmin(maxSelection - minSelection, amountToMove)); + unsigned long long amountToAdd = amountToMove - amountToRemove; + if (amountToRemove > 0) [self _removeRangeFromSelection:HFRangeMake(minSelection, amountToRemove) withCursorLocationIfAllSelectionRemoved:maxSelection]; + if (amountToAdd > 0) [self _addRangeToSelection:HFRangeMake(maxSelection, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = llmin(contentsLength, (amountToAdd > 0 ? maxSelection + amountToAdd : minSelection + amountToRemove)); + } + else { + if (maxSelection < contentsLength) { + NSUInteger amountToAdd = ll2l(llmin(contentsLength - maxSelection, amountToMove)); + [self _addRangeToSelection:HFRangeMake(maxSelection, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = maxSelection + amountToAdd; + } + } + } + if (selectionChanged) { + BEGIN_TRANSACTION(); + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + if (locationToMakeVisible != NO_SELECTION) [self _ensureVisibilityOfLocation:locationToMakeVisible]; + END_TRANSACTION(); + } +} + +/* Returns the distance to the next "word" (at least 1, unless we are empty). Here a word is identified as a column. If there are no columns, a word is a line. This is used for word movement (e.g. option + right arrow) */ +- (unsigned long long)_distanceToWordBoundaryForDirection:(HFControllerMovementDirection)direction { + unsigned long long result = 0, locationToConsider; + + /* Figure out how big a word is. By default, it's the column width, unless we have no columns, in which case it's the bytes per line. */ + NSUInteger wordGranularity = [self bytesPerColumn]; + if (wordGranularity == 0) wordGranularity = MAX(1u, [self bytesPerLine]); + if (selectionAnchor == NO_SELECTION) { + /* Pick the anchor inline with the choice of direction */ + if (direction == HFControllerDirectionLeft) locationToConsider = [self _minimumSelectionLocation]; + else locationToConsider = [self _maximumSelectionLocation]; + } else { + /* Just use the anchor */ + locationToConsider = selectionAnchor; + } + if (direction == HFControllerDirectionRight) { + result = HFRoundUpToNextMultipleSaturate(locationToConsider, wordGranularity) - locationToConsider; + } else { + result = locationToConsider % wordGranularity; + if (result == 0) result = wordGranularity; + } + return result; + +} + +/* Anchored selection is not allowed; neither is up/down movement */ +- (void)_shiftSelectionInDirection:(HFControllerMovementDirection)direction byAmount:(unsigned long long)amountToMove { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + HFASSERT(selectionAnchor == NO_SELECTION); + NSUInteger i, max = [selectedContentsRanges count]; + const unsigned long long maxLength = [self contentsLength]; + NSMutableArray *newRanges = [NSMutableArray arrayWithCapacity:max]; + BOOL hasAddedNonemptyRange = NO; + for (i=0; i < max; i++) { + HFRange range = [selectedContentsRanges[i] HFRange]; + HFASSERT(range.location <= maxLength && HFMaxRange(range) <= maxLength); + if (direction == HFControllerDirectionRight) { + unsigned long long offset = MIN(maxLength - range.location, amountToMove); + unsigned long long lengthToSubtract = MIN(range.length, amountToMove - offset); + range.location += offset; + range.length -= lengthToSubtract; + } + else { /* direction == HFControllerDirectionLeft */ + unsigned long long negOffset = MIN(amountToMove, range.location); + unsigned long long lengthToSubtract = MIN(range.length, amountToMove - negOffset); + range.location -= negOffset; + range.length -= lengthToSubtract; + } + [newRanges addObject:[HFRangeWrapper withRange:range]]; + hasAddedNonemptyRange = hasAddedNonemptyRange || (range.length > 0); + } + + newRanges = [[[HFRangeWrapper organizeAndMergeRanges:newRanges] mutableCopy] autorelease]; + + BOOL hasFoundEmptyRange = NO; + max = [newRanges count]; + for (i=0; i < max; i++) { + HFRange range = [newRanges[i] HFRange]; + if (range.length == 0) { + if (hasFoundEmptyRange || hasAddedNonemptyRange) { + [newRanges removeObjectAtIndex:i]; + i--; + max--; + } + hasFoundEmptyRange = YES; + } + } + [selectedContentsRanges setArray:newRanges]; + VALIDATE_SELECTION(); + [self _addPropertyChangeBits:HFControllerSelectedRanges]; +} + +__attribute__((unused)) +static BOOL rangesAreInAscendingOrder(NSEnumerator *rangeEnumerator) { + unsigned long long index = 0; + HFRangeWrapper *rangeWrapper; + while ((rangeWrapper = [rangeEnumerator nextObject])) { + HFRange range = [rangeWrapper HFRange]; + if (range.location < index) return NO; + index = HFSum(range.location, range.length); + } + return YES; +} + +- (BOOL)_registerCondemnedRangesForUndo:(NSArray *)ranges selectingRangesAfterUndo:(BOOL)selectAfterUndo { + HFASSERT(ranges != NULL); + HFASSERT(ranges != selectedContentsRanges); //selectedContentsRanges is mutable - we really don't want to stash it away with undo + BOOL result = NO; + NSUndoManager *manager = [self undoManager]; + NSUInteger rangeCount = [ranges count]; + if (! manager || ! rangeCount) return NO; + + HFASSERT(rangesAreInAscendingOrder([ranges objectEnumerator])); + + NSMutableArray *rangesToRestore = [NSMutableArray arrayWithCapacity:rangeCount]; + NSMutableArray *correspondingByteArrays = [NSMutableArray arrayWithCapacity:rangeCount]; + HFByteArray *bytes = [self byteArray]; + + /* Enumerate the ranges in forward order so when we insert them, we insert later ranges before earlier ones, so we don't have to worry about shifting indexes */ + FOREACH(HFRangeWrapper *, rangeWrapper, ranges) { + HFRange range = [rangeWrapper HFRange]; + if (range.length > 0) { + [rangesToRestore addObject:[HFRangeWrapper withRange:HFRangeMake(range.location, 0)]]; + [correspondingByteArrays addObject:[bytes subarrayWithRange:range]]; + result = YES; + } + } + + if (result) [self _registerUndoOperationForInsertingByteArrays:correspondingByteArrays inRanges:rangesToRestore withSelectionAction:(selectAfterUndo ? eSelectResult : eSelectAfterResult)]; + return result; +} + +- (void)_commandDeleteRanges:(NSArray *)rangesToDelete { + HFASSERT(rangesToDelete != selectedContentsRanges); //selectedContentsRanges is mutable - we really don't want to stash it away with undo + HFASSERT(rangesAreInAscendingOrder([rangesToDelete objectEnumerator])); + + /* Delete all the selection - in reverse order */ + unsigned long long minSelection = ULLONG_MAX; + BOOL somethingWasDeleted = NO; + [self _registerCondemnedRangesForUndo:rangesToDelete selectingRangesAfterUndo:YES]; + NSUInteger rangeIndex = [rangesToDelete count]; + HFASSERT(rangeIndex > 0); + while (rangeIndex--) { + HFRange range = [rangesToDelete[rangeIndex] HFRange]; + minSelection = llmin(range.location, minSelection); + if (range.length > 0) { + [byteArray deleteBytesInRange:range]; + somethingWasDeleted = YES; + } + } + + HFASSERT(minSelection != ULLONG_MAX); + if (somethingWasDeleted) { + BEGIN_TRANSACTION(); + [self _addPropertyChangeBits:HFControllerContentValue | HFControllerContentLength]; + [self _setSingleSelectedContentsRange:HFRangeMake(minSelection, 0)]; + [self _updateDisplayedRange]; + END_TRANSACTION(); + } + else { + NSBeep(); + } +} + +- (void)_commandInsertByteArrays:(NSArray *)byteArrays inRanges:(NSArray *)ranges withSelectionAction:(HFControllerSelectAction)selectionAction { + HFASSERT(selectionAction < NUM_SELECTION_ACTIONS); + REQUIRE_NOT_NULL(byteArrays); + REQUIRE_NOT_NULL(ranges); + HFASSERT([ranges count] == [byteArrays count]); + NSUInteger index, max = [ranges count]; + HFByteArray *bytes = [self byteArray]; + HFASSERT(rangesAreInAscendingOrder([ranges objectEnumerator])); + + NSMutableArray *byteArraysToInsertOnUndo = [NSMutableArray arrayWithCapacity:max]; + NSMutableArray *rangesToInsertOnUndo = [NSMutableArray arrayWithCapacity:max]; + + BEGIN_TRANSACTION(); + if (selectionAction == eSelectResult || selectionAction == eSelectAfterResult) { + [selectedContentsRanges removeAllObjects]; + } + unsigned long long endOfInsertedRanges = ULLONG_MAX; + for (index = 0; index < max; index++) { + HFRange range = [ranges[index] HFRange]; + HFByteArray *oldBytes = [bytes subarrayWithRange:range]; + [byteArraysToInsertOnUndo addObject:oldBytes]; + HFByteArray *newBytes = byteArrays[index]; + EXPECT_CLASS(newBytes, [HFByteArray class]); + [bytes insertByteArray:newBytes inRange:range]; + HFRange insertedRange = HFRangeMake(range.location, [newBytes length]); + HFRangeWrapper *insertedRangeWrapper = [HFRangeWrapper withRange:insertedRange]; + [rangesToInsertOnUndo addObject:insertedRangeWrapper]; + if (selectionAction == eSelectResult) { + [selectedContentsRanges addObject:insertedRangeWrapper]; + } + else { + endOfInsertedRanges = HFMaxRange(insertedRange); + } + } + if (selectionAction == eSelectAfterResult) { + HFASSERT([ranges count] > 0); + [selectedContentsRanges addObject:[HFRangeWrapper withRange:HFRangeMake(endOfInsertedRanges, 0)]]; + } + + if (selectionAction == ePreserveSelection) { + HFASSERT([selectedContentsRanges count] > 0); + [self _clipSelectedContentsRangesToLength:[self contentsLength]]; + } + + VALIDATE_SELECTION(); + HFASSERT([byteArraysToInsertOnUndo count] == [rangesToInsertOnUndo count]); + [self _registerUndoOperationForInsertingByteArrays:byteArraysToInsertOnUndo inRanges:rangesToInsertOnUndo withSelectionAction:(selectionAction == ePreserveSelection ? ePreserveSelection : eSelectAfterResult)]; + [self _updateDisplayedRange]; + [self maximizeVisibilityOfContentsRange:[selectedContentsRanges[0] HFRange]]; + [self _addPropertyChangeBits:HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges]; + END_TRANSACTION(); +} + +/* The user has hit undo after typing a string. */ +- (void)_commandReplaceBytesAfterBytesFromBeginning:(unsigned long long)leftOffset upToBytesFromEnd:(unsigned long long)rightOffset withByteArray:(HFByteArray *)bytesToReinsert { + HFASSERT(bytesToReinsert != NULL); + + BEGIN_TRANSACTION(); + HFByteArray *bytes = [self byteArray]; + unsigned long long contentsLength = [self contentsLength]; + HFASSERT(leftOffset <= contentsLength); + HFASSERT(rightOffset <= contentsLength); + HFASSERT(contentsLength - rightOffset >= leftOffset); + HFRange rangeToReplace = HFRangeMake(leftOffset, contentsLength - rightOffset - leftOffset); + [self _registerCondemnedRangesForUndo:[HFRangeWrapper withRanges:&rangeToReplace count:1] selectingRangesAfterUndo:NO]; + [bytes insertByteArray:bytesToReinsert inRange:rangeToReplace]; + [self _updateDisplayedRange]; + [self _setSingleSelectedContentsRange:HFRangeMake(rangeToReplace.location, [bytesToReinsert length])]; + [self _addPropertyChangeBits:HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges]; + END_TRANSACTION(); +} + +/* We use NSNumbers instead of long longs here because Tiger/PPC NSInvocation had trouble with long longs */ +- (void)_commandValueObjectsReplaceBytesAfterBytesFromBeginning:(NSNumber *)leftOffset upToBytesFromEnd:(NSNumber *)rightOffset withByteArray:(HFByteArray *)bytesToReinsert { + HFASSERT(leftOffset != NULL); + HFASSERT(rightOffset != NULL); + EXPECT_CLASS(leftOffset, NSNumber); + EXPECT_CLASS(rightOffset, NSNumber); + [self _commandReplaceBytesAfterBytesFromBeginning:[leftOffset unsignedLongLongValue] upToBytesFromEnd:[rightOffset unsignedLongLongValue] withByteArray:bytesToReinsert]; +} + +- (void)moveInDirection:(HFControllerMovementDirection)direction byByteCount:(unsigned long long)amountToMove withSelectionTransformation:(HFControllerSelectionTransformation)transformation usingAnchor:(BOOL)useAnchor { + if (! useAnchor) selectionAnchor = NO_SELECTION; + switch (transformation) { + case HFControllerDiscardSelection: + [self _moveDirectionDiscardingSelection:direction byAmount:amountToMove]; + break; + + case HFControllerShiftSelection: + [self _shiftSelectionInDirection:direction byAmount:amountToMove]; + break; + + case HFControllerExtendSelection: + [self _extendSelectionInDirection:direction byAmount:amountToMove]; + break; + + default: + [NSException raise:NSInvalidArgumentException format:@"Invalid transformation %ld", (long)transformation]; + break; + } + if (! useAnchor) selectionAnchor = NO_SELECTION; +} + +- (void)moveInDirection:(HFControllerMovementDirection)direction withGranularity:(HFControllerMovementGranularity)granularity andModifySelection:(BOOL)extendSelection { + HFASSERT(granularity == HFControllerMovementByte || granularity == HFControllerMovementColumn || granularity == HFControllerMovementLine || granularity == HFControllerMovementPage || granularity == HFControllerMovementDocument); + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + unsigned long long bytesToMove = 0; + switch (granularity) { + case HFControllerMovementByte: + bytesToMove = 1; + break; + case HFControllerMovementColumn: + /* This is a tricky case because the amount we have to move depends on our position in the column. */ + bytesToMove = [self _distanceToWordBoundaryForDirection:direction]; + break; + case HFControllerMovementLine: + bytesToMove = [self bytesPerLine]; + break; + case HFControllerMovementPage: + bytesToMove = HFProductULL([self bytesPerLine], HFFPToUL(MIN(floorl([self displayedLineRange].length), 1.))); + break; + case HFControllerMovementDocument: + bytesToMove = [self contentsLength]; + break; + } + HFControllerSelectionTransformation transformation = (extendSelection ? HFControllerExtendSelection : HFControllerDiscardSelection); + [self moveInDirection:direction byByteCount:bytesToMove withSelectionTransformation:transformation usingAnchor:YES]; +} + +- (void)moveToLineBoundaryInDirection:(HFControllerMovementDirection)direction andModifySelection:(BOOL)modifySelection { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + BEGIN_TRANSACTION(); + unsigned long long locationToMakeVisible; + HFRange additionalSelection; + + if (direction == HFControllerDirectionLeft) { + /* If we are at the beginning of a line, this should be a no-op */ + unsigned long long minLocation = [self _minimumSelectionLocation]; + unsigned long long newMinLocation = (minLocation / bytesPerLine) * bytesPerLine; + locationToMakeVisible = newMinLocation; + additionalSelection = HFRangeMake(newMinLocation, minLocation - newMinLocation); + } + else { + /* This always advances to the next line */ + unsigned long long maxLocation = [self _maximumSelectionLocation]; + unsigned long long proposedNewMaxLocation = HFRoundUpToNextMultipleSaturate(maxLocation, bytesPerLine); + unsigned long long newMaxLocation = MIN([self contentsLength], proposedNewMaxLocation); + HFASSERT(newMaxLocation >= maxLocation); + locationToMakeVisible = newMaxLocation; + additionalSelection = HFRangeMake(maxLocation, newMaxLocation - maxLocation); + } + + if (modifySelection) { + if (additionalSelection.length > 0) { + [self _addRangeToSelection:additionalSelection]; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + } + else { + [self _setSingleSelectedContentsRange:HFRangeMake(locationToMakeVisible, 0)]; + } + [self _ensureVisibilityOfLocation:locationToMakeVisible]; + END_TRANSACTION(); +} + +- (void)deleteSelection { + if ([self editMode] == HFOverwriteMode || ! [self editable]) { + NSBeep(); + } + else { + [self _commandDeleteRanges:[HFRangeWrapper organizeAndMergeRanges:selectedContentsRanges]]; + } +} + +// Called after Replace All is finished. +- (void)replaceByteArray:(HFByteArray *)newArray { + REQUIRE_NOT_NULL(newArray); + EXPECT_CLASS(newArray, HFByteArray); + HFRange entireRange = HFRangeMake(0, [self contentsLength]); + if ([self editMode] == HFOverwriteMode && [newArray length] != entireRange.length) { + NSBeep(); + } + else { + [self _commandInsertByteArrays:@[newArray] inRanges:[HFRangeWrapper withRanges:&entireRange count:1] withSelectionAction:ePreserveSelection]; + } +} + +- (BOOL)insertData:(NSData *)data replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing { + REQUIRE_NOT_NULL(data); + BOOL result; +#if ! NDEBUG + const unsigned long long startLength = [byteArray length]; + unsigned long long expectedNewLength; + if ([self editMode] == HFOverwriteMode) { + expectedNewLength = startLength; + } + else { + expectedNewLength = startLength + [data length] - previousBytes; + FOREACH(HFRangeWrapper*, wrapper, [self selectedContentsRanges]) expectedNewLength -= [wrapper HFRange].length; + } +#endif + HFByteSlice *slice = [[HFSharedMemoryByteSlice alloc] initWithUnsharedData:data]; + HFASSERT([slice length] == [data length]); + HFByteArray *array = [[preferredByteArrayClass() alloc] init]; + [array insertByteSlice:slice inRange:HFRangeMake(0, 0)]; + HFASSERT([array length] == [data length]); + result = [self insertByteArray:array replacingPreviousBytes:previousBytes allowUndoCoalescing:allowUndoCoalescing]; + [slice release]; + [array release]; +#if ! NDEBUG + HFASSERT((result && [byteArray length] == expectedNewLength) || (! result && [byteArray length] == startLength)); +#endif + return result; +} + +- (BOOL)_insertionModeCoreInsertByteArray:(HFByteArray *)bytesToInsert replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing outNewSingleSelectedRange:(HFRange *)outSelectedRange { + HFASSERT([self editMode] == HFInsertMode); + REQUIRE_NOT_NULL(bytesToInsert); + + /* Guard against overflow. If [bytesToInsert length] + [self contentsLength] - previousBytes overflows, then we can't do it */ + HFASSERT([self contentsLength] >= previousBytes); + if (! HFSumDoesNotOverflow([bytesToInsert length], [self contentsLength] - previousBytes)) { + return NO; //don't do anything + } + + + unsigned long long amountDeleted = 0, amountAdded = [bytesToInsert length]; + HFByteArray *bytes = [self byteArray]; + + /* Delete all the selection - in reverse order - except the last (really first) one, which we will overwrite. */ + NSArray *allRangesToRemove = [HFRangeWrapper organizeAndMergeRanges:[self selectedContentsRanges]]; + HFRange rangeToReplace = [allRangesToRemove[0] HFRange]; + HFASSERT(rangeToReplace.location == [self _minimumSelectionLocation]); + NSUInteger rangeIndex, rangeCount = [allRangesToRemove count]; + HFASSERT(rangeCount > 0); + NSMutableArray *rangesToDelete = [NSMutableArray arrayWithCapacity:rangeCount - 1]; + for (rangeIndex = rangeCount - 1; rangeIndex > 0; rangeIndex--) { + HFRangeWrapper *rangeWrapper = allRangesToRemove[rangeIndex]; + HFRange range = [rangeWrapper HFRange]; + if (range.length > 0) { + amountDeleted = HFSum(amountDeleted, range.length); + [rangesToDelete insertObject:rangeWrapper atIndex:0]; + } + } + + if ([rangesToDelete count] > 0) { + HFASSERT(rangesAreInAscendingOrder([rangesToDelete objectEnumerator])); + /* TODO: This is problematic because it overwrites the selection that gets set by _activateTypingUndoCoalescingForReplacingRange:, so we lose the first selection in a multiple selection scenario. */ + [self _registerCondemnedRangesForUndo:rangesToDelete selectingRangesAfterUndo:YES]; + NSEnumerator *enumer = [rangesToDelete reverseObjectEnumerator]; + HFRangeWrapper *rangeWrapper; + while ((rangeWrapper = [enumer nextObject])) { + [bytes deleteBytesInRange:[rangeWrapper HFRange]]; + } + } + + rangeToReplace.length = HFSum(rangeToReplace.length, previousBytes); + + /* Insert data */ +#if ! NDEBUG + unsigned long long expectedLength = [byteArray length] + [bytesToInsert length] - rangeToReplace.length; +#endif + [byteArray insertByteArray:bytesToInsert inRange:rangeToReplace]; +#if ! NDEBUG + HFASSERT(expectedLength == [byteArray length]); +#endif + + /* return the new selected range */ + *outSelectedRange = HFRangeMake(HFSum(rangeToReplace.location, amountAdded), 0); + return YES; +} + + +- (BOOL)_overwriteModeCoreInsertByteArray:(HFByteArray *)bytesToInsert replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing outRangeToRemoveFromSelection:(HFRange *)outRangeToRemove { + REQUIRE_NOT_NULL(bytesToInsert); + const unsigned long long byteArrayLength = [byteArray length]; + const unsigned long long bytesToInsertLength = [bytesToInsert length]; + HFRange firstSelectedRange = [selectedContentsRanges[0] HFRange]; + HFRange proposedRangeToOverwrite = HFRangeMake(firstSelectedRange.location, bytesToInsertLength); + HFASSERT(proposedRangeToOverwrite.location >= previousBytes); + proposedRangeToOverwrite.location -= previousBytes; + if (! HFRangeIsSubrangeOfRange(proposedRangeToOverwrite, HFRangeMake(0, byteArrayLength))) { + /* The user tried to overwrite past the end */ + NSBeep(); + return NO; + } + + [byteArray insertByteArray:bytesToInsert inRange:proposedRangeToOverwrite]; + + *outRangeToRemove = proposedRangeToOverwrite; + return YES; +} + +- (BOOL)insertByteArray:(HFByteArray *)bytesToInsert replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing { +#if ! NDEBUG + if (previousBytes > 0) { + NSArray *selectedRanges = [self selectedContentsRanges]; + HFASSERT([selectedRanges count] == 1); + HFRange selectedRange = [selectedRanges[0] HFRange]; + HFASSERT(selectedRange.location >= previousBytes); //don't try to delete more trailing bytes than we actually have! + } +#endif + REQUIRE_NOT_NULL(bytesToInsert); + + + BEGIN_TRANSACTION(); + unsigned long long beforeLength = [byteArray length]; + BOOL inOverwriteMode = [self editMode] == HFOverwriteMode; + HFRange modificationRange; //either range to remove from selection if in overwrite mode, or range to select if not + BOOL success; + if (inOverwriteMode) { + success = [self _overwriteModeCoreInsertByteArray:bytesToInsert replacingPreviousBytes:previousBytes allowUndoCoalescing:allowUndoCoalescing outRangeToRemoveFromSelection:&modificationRange]; + } + else { + success = [self _insertionModeCoreInsertByteArray:bytesToInsert replacingPreviousBytes:previousBytes allowUndoCoalescing:allowUndoCoalescing outNewSingleSelectedRange:&modificationRange]; + } + + if (success) { + /* Update our selection */ + [self _addPropertyChangeBits:HFControllerContentValue]; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits:HFControllerContentValue]; + if (inOverwriteMode) { + [self _removeRangeFromSelection:modificationRange withCursorLocationIfAllSelectionRemoved:HFMaxRange(modificationRange)]; + [self maximizeVisibilityOfContentsRange:[selectedContentsRanges[0] HFRange]]; + } + else { + [self _setSingleSelectedContentsRange:modificationRange]; + [self maximizeVisibilityOfContentsRange:modificationRange]; + } + if (beforeLength != [byteArray length]) [self _addPropertyChangeBits:HFControllerContentLength]; + } + END_TRANSACTION(); + return success; +} + +- (void)deleteDirection:(HFControllerMovementDirection)direction { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + if ([self editMode] != HFInsertMode || ! [self editable]) { + NSBeep(); + return; + } + unsigned long long minSelection = [self _minimumSelectionLocation]; + unsigned long long maxSelection = [self _maximumSelectionLocation]; + if (maxSelection != minSelection) { + [self deleteSelection]; + } + else { + HFRange rangeToDelete = HFRangeMake(minSelection, 1); + BOOL rangeIsValid; + if (direction == HFControllerDirectionLeft) { + rangeIsValid = (rangeToDelete.location > 0); + rangeToDelete.location--; + } + else { + rangeIsValid = (rangeToDelete.location < [self contentsLength]); + } + if (rangeIsValid) { + BEGIN_TRANSACTION(); + [byteArray deleteBytesInRange:rangeToDelete]; + [self _setSingleSelectedContentsRange:HFRangeMake(rangeToDelete.location, 0)]; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits:HFControllerSelectedRanges | HFControllerContentValue | HFControllerContentLength]; + END_TRANSACTION(); + } + } +} + +- (HFEditMode)editMode { + return _hfflags.editMode; +} + +- (void)setEditMode:(HFEditMode)val +{ + if (val != _hfflags.editMode) { + _hfflags.editMode = val; + // don't allow undo coalescing when switching modes + [self _addPropertyChangeBits:HFControllerEditable]; + } +} + +- (void)reloadData { + BEGIN_TRANSACTION(); + [cachedData release]; + cachedData = nil; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits: HFControllerContentValue]; + END_TRANSACTION(); +} + +#if BENCHMARK_BYTEARRAYS + ++ (void)_testByteArray { + HFByteArray* first = [[[HFFullMemoryByteArray alloc] init] autorelease]; + HFBTreeByteArray* second = [[[HFBTreeByteArray alloc] init] autorelease]; + first = nil; + // second = nil; + + //srandom(time(NULL)); + + unsigned opCount = 4096 * 512; + unsigned long long expectedLength = 0; + unsigned i; + for (i=1; i <= opCount; i++) { + @autoreleasepool { + NSUInteger op; + const unsigned long long length = [first length]; + unsigned long long offset; + unsigned long long number; + switch ((op = (random()%2))) { + case 0: { //insert + offset = random() % (1 + length); + HFByteSlice* slice = [[HFRandomDataByteSlice alloc] initWithRandomDataLength: 1 + random() % 1000]; + [first insertByteSlice:slice inRange:HFRangeMake(offset, 0)]; + [second insertByteSlice:slice inRange:HFRangeMake(offset, 0)]; + expectedLength += [slice length]; + [slice release]; + break; + } + case 1: { //delete + if (length > 0) { + offset = random() % length; + number = 1 + random() % (length - offset); + [first deleteBytesInRange:HFRangeMake(offset, number)]; + [second deleteBytesInRange:HFRangeMake(offset, number)]; + expectedLength -= number; + } + break; + } + } + } // @autoreleasepool + } +} + ++ (void)_testAttributeArrays { + HFByteRangeAttributeArray *naiveTree = [[HFNaiveByteRangeAttributeArray alloc] init]; + HFAnnotatedTreeByteRangeAttributeArray *smartTree = [[HFAnnotatedTreeByteRangeAttributeArray alloc] init]; + naiveTree = nil; + // smartTree = nil; + + NSString * const attributes[3] = {@"Alpha", @"Beta", @"Gamma"}; + + const NSUInteger supportedIndexEnd = NSNotFound; + NSUInteger round; + for (round = 0; round < 4096 * 256; round++) { + NSString *attribute = attributes[random() % (sizeof attributes / sizeof *attributes)]; + BOOL insert = ([smartTree isEmpty] || [naiveTree isEmpty] || (random() % 2)); + + unsigned long long end = random(); + unsigned long long start = random(); + if (end < start) { + unsigned long long temp = end; + end = start; + start = temp; + } + HFRange range = HFRangeMake(start, end - start); + + if (insert) { + [naiveTree addAttribute:attribute range:range]; + [smartTree addAttribute:attribute range:range]; + } + else { + [naiveTree removeAttribute:attribute range:range]; + [smartTree removeAttribute:attribute range:range]; + } + } + + [naiveTree release]; + [smartTree release]; +} + + ++ (void)initialize { + CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); + srandom(0); + [self _testByteArray]; + CFAbsoluteTime end = CFAbsoluteTimeGetCurrent(); + printf("Byte array time: %f\n", end - start); + + srandom(0); + start = CFAbsoluteTimeGetCurrent(); + [self _testAttributeArrays]; + end = CFAbsoluteTimeGetCurrent(); + printf("Attribute array time: %f\n", end - start); + + exit(0); +} + +#endif + +@end diff --git a/HexFiend/HFFullMemoryByteArray.h b/HexFiend/HFFullMemoryByteArray.h new file mode 100644 index 0000000..9debeb2 --- /dev/null +++ b/HexFiend/HFFullMemoryByteArray.h @@ -0,0 +1,21 @@ +// +// HFFullMemoryByteArray.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! + @class HFFullMemoryByteArray + @brief A naive subclass of HFByteArray suitable mainly for testing. Use HFBTreeByteArray instead. + + HFFullMemoryByteArray is a simple subclass of HFByteArray that does not store any byte slices. Because it stores all data in an NSMutableData, it is not efficient. It is mainly useful as a naive implementation for testing. Use HFBTreeByteArray instead. +*/ +@interface HFFullMemoryByteArray : HFByteArray { + NSMutableData *data; +} + + +@end diff --git a/HexFiend/HFFullMemoryByteArray.m b/HexFiend/HFFullMemoryByteArray.m new file mode 100644 index 0000000..f5a582b --- /dev/null +++ b/HexFiend/HFFullMemoryByteArray.m @@ -0,0 +1,70 @@ +// +// HFFullMemoryByteArray.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import + +@implementation HFFullMemoryByteArray + +- (instancetype)init { + self = [super init]; + data = [[NSMutableData alloc] init]; + return self; +} + +- (void)dealloc { + [data release]; + [super dealloc]; +} + +- (unsigned long long)length { + return [data length]; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range { + HFASSERT(range.length == 0 || dst != NULL); + HFASSERT(HFSumDoesNotOverflow(range.location, range.length)); + HFASSERT(range.location + range.length <= [self length]); + unsigned char* bytes = [data mutableBytes]; + memmove(dst, bytes + ll2l(range.location), ll2l(range.length)); +} + +- (HFByteArray *)subarrayWithRange:(HFRange)lrange { + HFRange entireRange = HFRangeMake(0, [self length]); + HFASSERT(HFRangeIsSubrangeOfRange(lrange, entireRange)); + NSRange range; + range.location = ll2l(lrange.location); + range.length = ll2l(lrange.length); + HFFullMemoryByteArray* result = [[[self class] alloc] init]; + [result->data setData:[data subdataWithRange:range]]; + return [result autorelease]; +} + +- (NSArray *)byteSlices { + return @[[[[HFFullMemoryByteSlice alloc] initWithData:data] autorelease]]; +} + +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + HFASSERT([slice length] <= NSUIntegerMax); + NSUInteger length = ll2l([slice length]); + NSRange range; + HFASSERT(lrange.location <= NSUIntegerMax); + HFASSERT(lrange.length <= NSUIntegerMax); + HFASSERT(HFSumDoesNotOverflow(lrange.location, lrange.length)); + range.location = ll2l(lrange.location); + range.length = ll2l(lrange.length); + + void* buff = check_malloc(length); + [slice copyBytes:buff range:HFRangeMake(0, length)]; + [data replaceBytesInRange:range withBytes:buff length:length]; + free(buff); +} + +@end diff --git a/HexFiend/HFFullMemoryByteSlice.h b/HexFiend/HFFullMemoryByteSlice.h new file mode 100644 index 0000000..ec195ca --- /dev/null +++ b/HexFiend/HFFullMemoryByteSlice.h @@ -0,0 +1,21 @@ +// +// HFFullMemoryByteSlice.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFFullMemoryByteSlice + + @brief A simple subclass of HFByteSlice that wraps an NSData. For most uses, prefer HFSharedMemoryByteSlice. +*/ +@interface HFFullMemoryByteSlice : HFByteSlice { + NSData *data; +} + +/*! Init with a given NSData, which is copied via the \c -copy message. */ +- (instancetype)initWithData:(NSData *)val; + +@end diff --git a/HexFiend/HFFullMemoryByteSlice.m b/HexFiend/HFFullMemoryByteSlice.m new file mode 100644 index 0000000..2a38ccd --- /dev/null +++ b/HexFiend/HFFullMemoryByteSlice.m @@ -0,0 +1,46 @@ +// +// HFFullMemoryByteSlice.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import "HFFullMemoryByteSlice.h" + + +@implementation HFFullMemoryByteSlice + +- (instancetype)initWithData:(NSData *)val { + REQUIRE_NOT_NULL(val); + self = [super init]; + data = [val copy]; + return self; +} + +- (void)dealloc { + [data release]; + [super dealloc]; +} + +- (unsigned long long)length { return [data length]; } + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)lrange { + NSRange range; + HFASSERT(lrange.location <= NSUIntegerMax); + HFASSERT(lrange.length <= NSUIntegerMax); + HFASSERT(lrange.location + lrange.length >= lrange.location); + range.location = ll2l(lrange.location); + range.length = ll2l(lrange.length); + [data getBytes:dst range:range]; +} + +- (HFByteSlice *)subsliceWithRange:(HFRange)range { + HFASSERT(range.length > 0); + HFASSERT(range.location < [self length]); + HFASSERT([self length] - range.location >= range.length); + HFASSERT(range.location <= NSUIntegerMax); + HFASSERT(range.length <= NSUIntegerMax); + return [[[[self class] alloc] initWithData:[data subdataWithRange:NSMakeRange(ll2l(range.location), ll2l(range.length))]] autorelease]; +} + +@end diff --git a/HexFiend/HFFunctions.h b/HexFiend/HFFunctions.h new file mode 100644 index 0000000..3d3586d --- /dev/null +++ b/HexFiend/HFFunctions.h @@ -0,0 +1,533 @@ +/* Functions and convenience methods for working with HFTypes */ + +#import +#import + +#define HFDEFAULT_FONT (@"Monaco") +#define HFDEFAULT_FONTSIZE ((CGFloat)10.) + +#define HFZeroRange (HFRange){0, 0} + +/*! + Makes an HFRange. An HFRange is like an NSRange except it uses unsigned long longs. +*/ +static inline HFRange HFRangeMake(unsigned long long loc, unsigned long long len) { + return (HFRange){loc, len}; +} + +/*! + Returns true if a given location is within a given HFRange. If the location is at the end of the range (range.location + range.length) this returns NO. +*/ +static inline BOOL HFLocationInRange(unsigned long long location, HFRange range) { + return location >= range.location && location - range.location < range.length; +} + +/*! + Like NSRangeToString but for HFRanges +*/ +static inline NSString* HFRangeToString(HFRange range) { + return [NSString stringWithFormat:@"{%llu, %llu}", range.location, range.length]; +} + +/*! + Converts a given HFFPRange to a string. +*/ +static inline NSString* HFFPRangeToString(HFFPRange range) { + return [NSString stringWithFormat:@"{%Lf, %Lf}", range.location, range.length]; +} + +/*! + Returns true if two HFRanges are equal. +*/ +static inline BOOL HFRangeEqualsRange(HFRange a, HFRange b) { + return a.location == b.location && a.length == b.length; +} + +/*! + Returns true if a + b does not overflow an unsigned long long. +*/ +static inline BOOL HFSumDoesNotOverflow(unsigned long long a, unsigned long long b) { + return a + b >= a; +} + +/*! + Returns true if a * b does not overflow an unsigned long long. +*/ +static inline BOOL HFProductDoesNotOverflow(unsigned long long a, unsigned long long b) { + if (b == 0) return YES; + unsigned long long result = a * b; + return result / b == a; +} + +/*! + Returns a * b as an NSUInteger. This asserts on overflow, unless NDEBUG is defined. +*/ +static inline NSUInteger HFProductInt(NSUInteger a, NSUInteger b) { + NSUInteger result = a * b; + assert(a == 0 || result / a == b); //detect overflow + return result; +} + +/*! + Returns a + b as an NSUInteger. This asserts on overflow unless NDEBUG is defined. +*/ +static inline NSUInteger HFSumInt(NSUInteger a, NSUInteger b) { + assert(a + b >= a); + return a + b; +} + +/*! + Returns a + b as an NSUInteger, saturating at NSUIntegerMax + */ +static inline NSUInteger HFSumIntSaturate(NSUInteger a, NSUInteger b) { + NSUInteger result = a + b; + return (result < a) ? NSUIntegerMax : result; +} + +/*! + Returns a + b as an unsigned long long, saturating at ULLONG_MAX + */ +static inline unsigned long long HFSumULLSaturate(unsigned long long a, unsigned long long b) { + unsigned long long result = a + b; + return (result < a) ? ULLONG_MAX : result; +} + +/*! + Returns a * b as an unsigned long long. This asserts on overflow, unless NDEBUG is defined. +*/ +static inline unsigned long long HFProductULL(unsigned long long a, unsigned long long b) { + unsigned long long result = a * b; + assert(HFProductDoesNotOverflow(a, b)); //detect overflow + return result; +} + +/*! + Returns a + b as an unsigned long long. This asserts on overflow, unless NDEBUG is defined. +*/ +static inline unsigned long long HFSum(unsigned long long a, unsigned long long b) { + assert(HFSumDoesNotOverflow(a, b)); + return a + b; +} + +/*! + Returns a + b as an unsigned long long. This asserts on overflow, unless NDEBUG is defined. + */ +static inline unsigned long long HFMaxULL(unsigned long long a, unsigned long long b) { + return a < b ? b : a; +} + +/*! + Returns a - b as an unsigned long long. This asserts on underflow (if b > a), unless NDEBUG is defined. +*/ +static inline unsigned long long HFSubtract(unsigned long long a, unsigned long long b) { + assert(a >= b); + return a - b; +} + +/*! + Returns the smallest multiple of B that is equal to or larger than A, and asserts on overflow. +*/ +static inline unsigned long long HFRoundUpToMultiple(unsigned long long a, unsigned long long b) { + // The usual approach of ((a + (b - 1)) / b) * b doesn't handle overflow correctly + unsigned long long remainder = a % b; + if (remainder == 0) return a; + else return HFSum(a, b - remainder); +} + +/*! + Returns the smallest multiple of B that is equal to or larger than A, and asserts on overflow. + */ +static inline NSUInteger HFRoundUpToMultipleInt(NSUInteger a, NSUInteger b) { + // The usual approach of ((a + (b - 1)) / b) * b doesn't handle overflow correctly + NSUInteger remainder = a % b; + if (remainder == 0) return a; + else return (NSUInteger)HFSum(a, b - remainder); +} + +/*! + Returns the least common multiple of A and B, and asserts on overflow or if A or B is zero. + */ +static inline NSUInteger HFLeastCommonMultiple(NSUInteger a, NSUInteger b) { + assert(a > 0); + assert(b > 0); + + /* Compute GCD. It ends up in U. */ + NSUInteger t, u = a, v = b; + while (v > 0) { + t = v; + v = u % v; + u = t; + } + + /* Return the product divided by the GCD, in an overflow safe manner */ + return HFProductInt(a/u, b); +} + + +/*! + Returns the smallest multiple of B strictly larger than A, or ULLONG_MAX if it would overflow +*/ +static inline unsigned long long HFRoundUpToNextMultipleSaturate(unsigned long long a, unsigned long long b) { + assert(b > 0); + unsigned long long result = a + (b - a % b); + if (result < a) result = ULLONG_MAX; //the saturation...on overflow go to the max + return result; +} + +/*! Like NSMaxRange, but for an HFRange. */ +static inline unsigned long long HFMaxRange(HFRange a) { + assert(HFSumDoesNotOverflow(a.location, a.length)); + return a.location + a.length; +} + +/*! Returns YES if needle is fully contained within haystack. Equal ranges are always considered to be subranges of each other (even if they are empty). Furthermore, a zero length needle at the end of haystack is considered a subrange - for example, {6, 0} is a subrange of {3, 3}. */ +static inline BOOL HFRangeIsSubrangeOfRange(HFRange needle, HFRange haystack) { + // If needle starts before haystack, or if needle is longer than haystack, it is not a subrange of haystack + if (needle.location < haystack.location || needle.length > haystack.length) return NO; + + // Their difference in lengths determines the maximum difference in their start locations. We know that these expressions cannot overflow because of the above checks. + return haystack.length - needle.length >= needle.location - haystack.location; +} + +/*! Splits a range about a subrange, returning by reference the prefix and suffix (which may have length zero). */ +static inline void HFRangeSplitAboutSubrange(HFRange range, HFRange subrange, HFRange *outPrefix, HFRange *outSuffix) { + // Requires it to be a subrange + assert(HFRangeIsSubrangeOfRange(subrange, range)); + outPrefix->location = range.location; + outPrefix->length = HFSubtract(subrange.location, range.location); + outSuffix->location = HFMaxRange(subrange); + outSuffix->length = HFMaxRange(range) - outSuffix->location; +} + +/*! Returns YES if the given ranges intersect. Two ranges are considered to intersect if they share at least one index in common. Thus, zero-length ranges do not intersect anything. */ +static inline BOOL HFIntersectsRange(HFRange a, HFRange b) { + // Ranges are said to intersect if they share at least one value. Therefore, zero length ranges never intersect anything. + if (a.length == 0 || b.length == 0) return NO; + + // rearrange (a.location < b.location + b.length && b.location < a.location + a.length) to not overflow + // = ! (a.location >= b.location + b.length || b.location >= a.location + a.length) + BOOL clause1 = (a.location >= b.location && a.location - b.location >= b.length); + BOOL clause2 = (b.location >= a.location && b.location - a.location >= a.length); + return ! (clause1 || clause2); +} + +/*! Returns YES if the given ranges intersect. Two ranges are considered to intersect if any fraction overlaps; zero-length ranges do not intersect anything. */ +static inline BOOL HFFPIntersectsRange(HFFPRange a, HFFPRange b) { + // Ranges are said to intersect if they share at least one value. Therefore, zero length ranges never intersect anything. + if (a.length == 0 || b.length == 0) return NO; + + if (a.location <= b.location && a.location + a.length >= b.location) return YES; + if (b.location <= a.location && b.location + b.length >= a.location) return YES; + return NO; +} + +/*! Returns a range containing the union of the given ranges. These ranges must either intersect or be adjacent: there cannot be any "holes" between them. */ +static inline HFRange HFUnionRange(HFRange a, HFRange b) { + assert(HFIntersectsRange(a, b) || HFMaxRange(a) == b.location || HFMaxRange(b) == a.location); + HFRange result; + result.location = MIN(a.location, b.location); + assert(HFSumDoesNotOverflow(a.location, a.length)); + assert(HFSumDoesNotOverflow(b.location, b.length)); + result.length = MAX(a.location + a.length, b.location + b.length) - result.location; + return result; +} + + +/*! Returns whether a+b > c+d, as if there were no overflow (so ULLONG_MAX + 1 > 10 + 20) */ +static inline BOOL HFSumIsLargerThanSum(unsigned long long a, unsigned long long b, unsigned long long c, unsigned long long d) { +#if 1 + // Theory: compare a/2 + b/2 to c/2 + d/2, and if they're equal, compare a%2 + b%2 to c%2 + d%2. We may get into trouble if a and b are both even and c and d are both odd: e.g. a = 2, b = 2, c = 1, d = 3. We would compare 1 + 1 vs 0 + 1, and therefore that 2 + 2 > 1 + 3. To address this, if both remainders are 1, we add this to the sum. We know this cannot overflow because ULLONG_MAX is odd, so (ULLONG_MAX/2) + (ULLONG_MAX/2) + 1 does not overflow. + unsigned int rem1 = (unsigned)(a%2 + b%2); + unsigned int rem2 = (unsigned)(c%2 + d%2); + unsigned long long sum1 = a/2 + b/2 + rem1/2; + unsigned long long sum2 = c/2 + d/2 + rem2/2; + if (sum1 > sum2) return YES; + else if (sum1 < sum2) return NO; + else { + // sum1 == sum2, so compare the remainders. But we have already added in the remainder / 2, so compare the remainders mod 2. + if (rem1%2 > rem2%2) return YES; + else return NO; + } +#else + /* Faster version, but not thoroughly tested yet. */ + unsigned long long xor1 = a^b; + unsigned long long xor2 = c^d; + unsigned long long avg1 = (a&b)+(xor1/2); + unsigned long long avg2 = (c&d)+(xor2/2); + unsigned s1l = avg1 > avg2; + unsigned eq = (avg1 == avg2); + return s1l | ((xor1 & ~xor2) & eq); +#endif +} + +/*! Returns the absolute value of a - b. */ +static inline unsigned long long HFAbsoluteDifference(unsigned long long a, unsigned long long b) { + if (a > b) return a - b; + else return b - a; +} + +/*! Returns true if the end of A is larger than the end of B. */ +static inline BOOL HFRangeExtendsPastRange(HFRange a, HFRange b) { + return HFSumIsLargerThanSum(a.location, a.length, b.location, b.length); +} + +/*! Returns a range containing all indexes in common betwen the two ranges. If there are no indexes in common, returns {0, 0}. */ +static inline HFRange HFIntersectionRange(HFRange range1, HFRange range2) { + unsigned long long minend = HFRangeExtendsPastRange(range2, range1) ? range1.location + range1.length : range2.location + range2.length; + if (range2.location <= range1.location && range1.location - range2.location < range2.length) { + return HFRangeMake(range1.location, minend - range1.location); + } + else if (range1.location <= range2.location && range2.location - range1.location < range1.length) { + return HFRangeMake(range2.location, minend - range2.location); + } + return HFRangeMake(0, 0); +} + +/*! ceil() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFCeil(CGFloat a) { + if (sizeof(a) == sizeof(float)) return (CGFloat)ceilf((float)a); + else return (CGFloat)ceil((double)a); +} + +/*! floor() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFFloor(CGFloat a) { + if (sizeof(a) == sizeof(float)) return (CGFloat)floorf((float)a); + else return (CGFloat)floor((double)a); +} + +/*! round() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFRound(CGFloat a) { + if (sizeof(a) == sizeof(float)) return (CGFloat)roundf((float)a); + else return (CGFloat)round((double)a); +} + +/*! fmin() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFMin(CGFloat a, CGFloat b) { + if (sizeof(a) == sizeof(float)) return (CGFloat)fminf((float)a, (float)b); + else return (CGFloat)fmin((double)a, (double)b); +} + +/*! fmax() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFMax(CGFloat a, CGFloat b) { + if (sizeof(a) == sizeof(float)) return (CGFloat)fmaxf((float)a, (float)b); + else return (CGFloat)fmax((double)a, (double)b); +} + +/*! Returns true if the given HFFPRanges are equal. */ +static inline BOOL HFFPRangeEqualsRange(HFFPRange a, HFFPRange b) { + return a.location == b.location && a.length == b.length; +} + +/*! copysign() for a CGFloat */ +static inline CGFloat HFCopysign(CGFloat a, CGFloat b) { +#if CGFLOAT_IS_DOUBLE + return copysign(a, b); +#else + return copysignf(a, b); +#endif +} + +/*! Atomically increments an NSUInteger, returning the new value. Optionally invokes a memory barrier. */ +static inline NSUInteger HFAtomicIncrement(volatile NSUInteger *ptr, BOOL barrier) { + return _Generic(ptr, + volatile unsigned *: (barrier ? OSAtomicIncrement32Barrier : OSAtomicIncrement32)((volatile int32_t *)ptr), +#if ULONG_MAX == UINT32_MAX + volatile unsigned long *: (barrier ? OSAtomicIncrement32Barrier : OSAtomicIncrement32)((volatile int32_t *)ptr), +#else + volatile unsigned long *: (barrier ? OSAtomicIncrement64Barrier : OSAtomicIncrement64)((volatile int64_t *)ptr), +#endif + volatile unsigned long long *: (barrier ? OSAtomicIncrement64Barrier : OSAtomicIncrement64)((volatile int64_t *)ptr)); +} + +/*! Atomically decrements an NSUInteger, returning the new value. Optionally invokes a memory barrier. */ +static inline NSUInteger HFAtomicDecrement(volatile NSUInteger *ptr, BOOL barrier) { + return _Generic(ptr, + volatile unsigned *: (barrier ? OSAtomicDecrement32Barrier : OSAtomicDecrement32)((volatile int32_t *)ptr), +#if ULONG_MAX == UINT32_MAX + volatile unsigned long *: (barrier ? OSAtomicDecrement32Barrier : OSAtomicDecrement32)((volatile int32_t *)ptr), +#else + volatile unsigned long *: (barrier ? OSAtomicDecrement64Barrier : OSAtomicDecrement64)((volatile int64_t *)ptr), +#endif + volatile unsigned long long *: (barrier ? OSAtomicDecrement64Barrier : OSAtomicDecrement64)((volatile int64_t *)ptr)); +} + +/*! Converts a long double to unsigned long long. Assumes that val is already an integer - use floorl or ceill */ +static inline unsigned long long HFFPToUL(long double val) { + assert(val >= 0); + assert(val <= ULLONG_MAX); + unsigned long long result = (unsigned long long)val; + assert((long double)result == val); + return result; +} + +/*! Converts an unsigned long long to a long double. */ +static inline long double HFULToFP(unsigned long long val) { + long double result = (long double)val; + assert(HFFPToUL(result) == val); + return result; +} + +/*! Convenience to return information about a CGAffineTransform for logging. */ +static inline NSString *HFDescribeAffineTransform(CGAffineTransform t) { + return [NSString stringWithFormat:@"%f %f 0\n%f %f 0\n%f %f 1", t.a, t.b, t.c, t.d, t.tx, t.ty]; +} + +/*! Returns 1 + floor(log base 10 of val). If val is 0, returns 1. */ +static inline NSUInteger HFCountDigitsBase10(unsigned long long val) { + const unsigned long long kValues[] = {0ULL, 9ULL, 99ULL, 999ULL, 9999ULL, 99999ULL, 999999ULL, 9999999ULL, 99999999ULL, 999999999ULL, 9999999999ULL, 99999999999ULL, 999999999999ULL, 9999999999999ULL, 99999999999999ULL, 999999999999999ULL, 9999999999999999ULL, 99999999999999999ULL, 999999999999999999ULL, 9999999999999999999ULL}; + NSUInteger low = 0, high = sizeof kValues / sizeof *kValues; + while (high > low) { + NSUInteger mid = (low + high)/2; //low + high cannot overflow + if (val > kValues[mid]) { + low = mid + 1; + } + else { + high = mid; + } + } + return MAX(1u, low); +} + +/*! Returns 1 + floor(log base 16 of val). If val is 0, returns 1. This works by computing the log base 2 based on the number of leading zeros, and then dividing by 4. */ +static inline NSUInteger HFCountDigitsBase16(unsigned long long val) { + /* __builtin_clzll doesn't like being passed 0 */ + if (val == 0) return 1; + + /* Compute the log base 2 */ + NSUInteger leadingZeros = (NSUInteger)__builtin_clzll(val); + NSUInteger logBase2 = (CHAR_BIT * sizeof val) - leadingZeros - 1; + return 1 + logBase2/4; +} + +/*! Returns YES if the given string encoding is a superset of ASCII. */ +BOOL HFStringEncodingIsSupersetOfASCII(NSStringEncoding encoding); + +/*! Returns the "granularity" of an encoding, in bytes. ASCII is 1, UTF-16 is 2, etc. Variable width encodings return the smallest (e.g. Shift-JIS returns 1). */ +uint8_t HFStringEncodingCharacterLength(NSStringEncoding encoding); + +/*! Converts an unsigned long long to NSUInteger. The unsigned long long should be no more than ULONG_MAX. */ +static inline NSUInteger ll2l(unsigned long long val) { assert(val <= ULONG_MAX); return (unsigned long)val; } + +/*! Converts an unsigned long long to uintptr_t. The unsigned long long should be no more than UINTPTR_MAX. */ +static inline uintptr_t ll2p(unsigned long long val) { assert(val <= UINTPTR_MAX); return (uintptr_t)val; } + +/*! Returns an unsigned long long, which must be no more than ULLONG_MAX, as an unsigned long. */ +static inline CGFloat ld2f(long double val) { +#if ! NDEBUG + if (isfinite(val)) { + assert(val <= CGFLOAT_MAX); + assert(val >= -CGFLOAT_MAX); + if ((val > 0 && val < CGFLOAT_MIN) || (val < 0 && val > -CGFLOAT_MIN)) { + NSLog(@"Warning - conversion of long double %Lf to CGFloat will result in the non-normal CGFloat %f", val, (CGFloat)val); + } + } +#endif + return (CGFloat)val; +} + +/*! Returns the quotient of a divided by b, rounding up, for unsigned long longs. Will not overflow. */ +static inline unsigned long long HFDivideULLRoundingUp(unsigned long long a, unsigned long long b) { + if (a == 0) return 0; + else return ((a - 1) / b) + 1; +} + +/*! Returns the quotient of a divided by b, rounding up, for NSUIntegers. Will not overflow. */ +static inline NSUInteger HFDivideULRoundingUp(NSUInteger a, NSUInteger b) { + if (a == 0) return 0; + else return ((a - 1) / b) + 1; +} + +/*! Draws a shadow. */ +void HFDrawShadow(CGContextRef context, NSRect rect, CGFloat size, NSRectEdge rectEdge, BOOL active, NSRect clip); + +/*! Registers a view to have the given notificationSEL invoked (taking the NSNotification object) when the window becomes or loses key. If appToo is YES, this also registers with NSApplication for Activate and Deactivate methods. */ +void HFRegisterViewForWindowAppearanceChanges(NSView *view, SEL notificationSEL, BOOL appToo); + +/*! Unregisters a view to have the given notificationSEL invoked when the window becomes or loses key. If appToo is YES, this also unregisters with NSApplication. */ +void HFUnregisterViewForWindowAppearanceChanges(NSView *view, BOOL appToo); + +/*! Returns a description of the given byte count (e.g. "24 kilobytes") */ +NSString *HFDescribeByteCount(unsigned long long count); + +/*! @brief An object wrapper for the HFRange type. + + A simple class responsible for holding an immutable HFRange as an object. Methods that logically work on multiple HFRanges usually take or return arrays of HFRangeWrappers. */ +@interface HFRangeWrapper : NSObject { + @public + HFRange range; +} + +/*! Returns the HFRange for this HFRangeWrapper. */ +- (HFRange)HFRange; + +/*! Creates an autoreleased HFRangeWrapper for this HFRange. */ ++ (HFRangeWrapper *)withRange:(HFRange)range; + +/*! Creates an NSArray of HFRangeWrappers for this HFRange. */ ++ (NSArray *)withRanges:(const HFRange *)ranges count:(NSUInteger)count; + +/*! Given an NSArray of HFRangeWrappers, get all of the HFRanges into a C array. */ ++ (void)getRanges:(HFRange *)ranges fromArray:(NSArray *)array; + +/*! Given an array of HFRangeWrappers, returns a "cleaned up" array of equivalent ranges. This new array represents the same indexes, but overlapping ranges will have been merged, and the ranges will be sorted in ascending order. */ ++ (NSArray *)organizeAndMergeRanges:(NSArray *)inputRanges; + +@end + +/*! @brief A set of HFRanges. HFRangeSet takes the interpetation that all zero-length ranges are identical. + + Essentially, a mutable array of ranges that is maintained to be sorted and minimized (i.e. merged with overlapping neighbors). + + TODO: The HexFiend codebase currently uses arrays of HFRangeWrappers that have been run through organizeAndMergeRanges:, and not HFRangeSet. The advantage of HFRangeSet is that the sorting & merging is implied by the type, instead of just tacitly assumed. This should lead to less confusion and fewer extra applications of organizeAndMergeRanges. + + TODO: HFRangeSet needs to be tested! I guarantee it has bugs! (Which doesn't matter right now because it's all dead code...) + */ +@interface HFRangeSet : NSObject { + @private + CFMutableArrayRef array; +} + +/*! Create a range set with just one range. */ ++ (HFRangeSet *)withRange:(HFRange)range; + +/*! Create a range set with a C array of ranges. No prior sorting is necessary. */ ++ (HFRangeSet *)withRanges:(const HFRange *)ranges count:(NSUInteger)count; + +/*! Create a range set with an array of HFRangeWrappers. No prior sorting is necessary. */ ++ (HFRangeSet *)withRangeWrappers:(NSArray *)ranges; + +/*! Create a range set as a copy of another. */ ++ (HFRangeSet *)withRangeSet:(HFRangeSet *)rangeSet; + +/*! Equivalent to HFRangeSet *x = [HFRangeSet withRange:range]; [x removeRange:rangeSet]; */ ++ (HFRangeSet *)complementOfRangeSet:(HFRangeSet *)rangeSet inRange:(HFRange)range; + +- (void)addRange:(HFRange)range; /*!< Union with range */ +- (void)removeRange:(HFRange)range; /*!< Subtract range */ +- (void)clipToRange:(HFRange)range; /*!< Intersect with range */ +- (void)toggleRange:(HFRange)range; /*!< Symmetric difference with range */ + +- (void)addRangeSet:(HFRangeSet *)rangeSet; /*!< Union with range set */ +- (void)removeRangeSet:(HFRangeSet *)rangeSet; /*!< Subtract range set */ +- (void)clipToRangeSet:(HFRangeSet *)rangeSet; /*!< Intersect with range set */ +- (void)toggleRangeSet:(HFRangeSet *)rangeSet; /*!< Symmetric difference with range set */ + + +- (BOOL)isEqualToRangeSet:(HFRangeSet *)rangeSet; /*!< Test if two range sets are equivalent. */ +- (BOOL)isEmpty; /*!< Test if range set is empty. */ + +- (BOOL)containsAllRange:(HFRange)range; /*!< Check if the range set covers all of a range. Always true if 'range' is zero length. */ +- (BOOL)overlapsAnyRange:(HFRange)range; /*!< Check if the range set covers any of a range. Never true if 'range' is zero length. */ +- (BOOL)containsAllRangeSet:(HFRangeSet *)rangeSet; /*!< Check if this range is a superset of another. */ +- (BOOL)overlapsAnyRangeSet:(HFRangeSet *)rangeSet; /*!< Check if this range has a nonempty intersection with another. */ + +- (HFRange)spanningRange; /*!< Return a single range that covers the entire range set */ + +- (void)assertIntegrity; + +@end + +#ifndef NDEBUG +void HFStartTiming(const char *name); +void HFStopTiming(void); +#endif diff --git a/HexFiend/HFFunctions.m b/HexFiend/HFFunctions.m new file mode 100644 index 0000000..b41a12f --- /dev/null +++ b/HexFiend/HFFunctions.m @@ -0,0 +1,1172 @@ +#import +#import + +#import "HFFunctions_Private.h" + +#ifndef NDEBUG +//#define USE_CHUD 1 +#endif + +#ifndef USE_CHUD +#define USE_CHUD 0 +#endif + +#if USE_CHUD +#import +#endif + +NSImage *HFImageNamed(NSString *name) { + HFASSERT(name != NULL); + NSImage *image = [NSImage imageNamed:name]; + if (image == NULL) { + NSString *imagePath = [[NSBundle bundleForClass:[HFController class]] pathForResource:name ofType:@"tiff"]; + if (! imagePath) { + NSLog(@"Unable to find image named %@.tiff", name); + } + else { + image = [[NSImage alloc] initByReferencingFile:imagePath]; + if (image == nil || ! [image isValid]) { + NSLog(@"Couldn't load image at path %@", imagePath); + [image release]; + image = nil; + } + else { + [image setName:name]; + } + } + } + return image; +} + +@implementation HFRangeWrapper + +- (HFRange)HFRange { return range; } + ++ (HFRangeWrapper *)withRange:(HFRange)range { + HFRangeWrapper *result = [[self alloc] init]; + result->range = range; + return [result autorelease]; +} + ++ (NSArray *)withRanges:(const HFRange *)ranges count:(NSUInteger)count { + HFASSERT(count == 0 || ranges != NULL); + NSUInteger i; + NSArray *result; + NEW_ARRAY(HFRangeWrapper *, wrappers, count); + for (i=0; i < count; i++) wrappers[i] = [self withRange:ranges[i]]; + result = [NSArray arrayWithObjects:wrappers count:count]; + FREE_ARRAY(wrappers); + return result; +} + +- (BOOL)isEqual:(id)obj { + if (! [obj isKindOfClass:[HFRangeWrapper class]]) return NO; + else return HFRangeEqualsRange(range, [obj HFRange]); +} + +- (NSUInteger)hash { + return (NSUInteger)(range.location + (range.length << 16)); +} + +- (id)copyWithZone:(NSZone *)zone { + USE(zone); + return [self retain]; +} + +- (NSString *)description { + return HFRangeToString(range); +} + +static int hfrange_compare(const void *ap, const void *bp) { + const HFRange *a = ap; + const HFRange *b = bp; + if (a->location < b->location) return -1; + else if (a->location > b->location) return 1; + else if (a->length < b->length) return -1; + else if (a->length > b->length) return 1; + else return 0; +} + ++ (NSArray *)organizeAndMergeRanges:(NSArray *)inputRanges { + HFASSERT(inputRanges != NULL); + NSUInteger leading = 0, trailing = 0, length = [inputRanges count]; + if (length == 0) return @[]; + else if (length == 1) return [NSArray arrayWithArray:inputRanges]; + + NEW_ARRAY(HFRange, ranges, length); + [self getRanges:ranges fromArray:inputRanges]; + qsort(ranges, length, sizeof ranges[0], hfrange_compare); + leading = 0; + while (leading < length) { + leading++; + if (leading < length) { + HFRange leadRange = ranges[leading], trailRange = ranges[trailing]; + if (HFIntersectsRange(leadRange, trailRange) || HFMaxRange(leadRange) == trailRange.location || HFMaxRange(trailRange) == leadRange.location) { + ranges[trailing] = HFUnionRange(leadRange, trailRange); + } + else { + trailing++; + ranges[trailing] = ranges[leading]; + } + } + } + NSArray *result = [HFRangeWrapper withRanges:ranges count:trailing + 1]; + FREE_ARRAY(ranges); + return result; +} + ++ (void)getRanges:(HFRange *)ranges fromArray:(NSArray *)array { + HFASSERT(ranges != NULL || [array count] == 0); + if (ranges) { + FOREACH(HFRangeWrapper*, wrapper, array) *ranges++ = [wrapper HFRange]; + } +} + +@end + +@implementation HFRangeSet +// HFRangeSet is implemented as a CFMutableArray of uintptr_t "fenceposts". The array +// is even in length, sorted, duplicate free, and considered to include the ranges +// [array[0], array[1]), [array[2], array[3]), ..., [array[2n], array[2n+1]) + +CFComparisonResult uintptrComparator(const void *val1, const void *val2, void *context) { + (void)context; + uintptr_t a = (uintptr_t)val1; + uintptr_t b = (uintptr_t)val2; + if(a < b) return kCFCompareLessThan; + if(a > b) return kCFCompareGreaterThan; + return kCFCompareEqualTo; +} + +static void HFRangeSetAddRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + + const void *x[2] = { (void*)a, (void*)b }; + if(idxa >= count) { + CFArrayReplaceValues(array, CFRangeMake(count, 0), x, 2); + return; + } + if(idxb == 0) { + CFArrayReplaceValues(array, CFRangeMake(0, 0), x, 2); + return; + } + + // Clear fenceposts strictly between 'a' and 'b', and then possibly + // add 'a' or 'b' as fenceposts. + CFIndex cutloc = (uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxa+1 : idxa; + CFIndex cutlen = idxb - cutloc; + + bool inca = cutloc % 2 == 0; // Include 'a' if it would begin an included range + bool incb = (count - cutlen + inca) % 2 == 1; // The set must be even, which tells us about 'b'. + + CFArrayReplaceValues(array, CFRangeMake(cutloc, cutlen), x+inca, inca+incb); + assert(CFArrayGetCount(array) % 2 == 0); +} + +static void HFRangeSetRemoveRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) return; + + // Remove fenceposts strictly between 'a' and 'b', and then possibly + // add 'a' or 'b' as fenceposts. + CFIndex cutloc = (uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxa+1 : idxa; + CFIndex cutlen = idxb - cutloc; + + bool inca = cutloc % 2 == 1; // Include 'a' if it would end an included range + bool incb = (count - cutlen + inca) % 2 == 1; // The set must be even, which tells us about 'b'. + + const void *x[2] = { (void*)a, (void*)b }; + CFArrayReplaceValues(array, CFRangeMake(cutloc, cutlen), x+inca, inca+incb); + assert(CFArrayGetCount(array) % 2 == 0); +} + +static void HFRangeSetToggleRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + + // In the fencepost representation, simply toggling the existence of + // fenceposts 'a' and 'b' achieves symmetric difference. + + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + if((uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a) { + CFArrayRemoveValueAtIndex(array, idxa); + } else { + CFArrayInsertValueAtIndex(array, idxa, (void*)a); + } + + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if((uintptr_t)CFArrayGetValueAtIndex(array, idxb) == b) { + CFArrayRemoveValueAtIndex(array, idxb); + } else { + CFArrayInsertValueAtIndex(array, idxb, (void*)b); + } + + assert(CFArrayGetCount(array) % 2 == 0); +} + +static BOOL HFRangeSetContainsAllRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) return NO; + + // Optimization: if the indexes are far enough apart, then obviouly there's a gap. + if(idxb - idxa >= 2) return NO; + + // The first fencepost >= 'b' must end an include range, a must be in the same range. + return idxb%2 == 1 && idxa == ((uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxb-1 : idxb); +} + +static BOOL HFRangeSetOverlapsAnyRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) return NO; + + // Optimization: if the indexes are far enough apart, then obviouly there's overlap. + if(idxb - idxa >= 2) return YES; + + if((uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a) { + // 'a' is an included fencepost, or instead 'b' makes it past an included fencepost. + return idxa % 2 == 0 || b > (uintptr_t)CFArrayGetValueAtIndex(array, idxa+1); + } else { + // 'a' lies in an included range, or instead 'b' makes it past an included fencepost. + return idxa % 2 == 1 || b > (uintptr_t)CFArrayGetValueAtIndex(array, idxa); + } +} + +- (instancetype)init { + if(!(self = [super init])) return nil; + array = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); + return self; +} + +- (void)dealloc { + CFRelease(array); + [super dealloc]; +} + ++ (HFRangeSet *)withRange:(HFRange)range { + HFRangeSet *newSet = [[[HFRangeSet alloc] init] autorelease]; + if(range.length > 0) { + CFArrayAppendValue(newSet->array, (void*)ll2p(range.location)); + CFArrayAppendValue(newSet->array, (void*)ll2p(HFMaxRange(range))); + } + return newSet; +} + ++ (HFRangeSet *)withRanges:(const HFRange *)ranges count:(NSUInteger)count { + // FIXME: Stub. Don't rely on the thing we're replacing! + return [HFRangeSet withRangeWrappers:[HFRangeWrapper withRanges:ranges count:count]]; +} + ++ (HFRangeSet *)withRangeWrappers:(NSArray *)ranges { + HFRangeSet *newSet = [[[HFRangeSet alloc] init] autorelease]; + FOREACH(HFRangeWrapper *, wrapper, [HFRangeWrapper organizeAndMergeRanges:ranges]) { + if(wrapper->range.length > 0) { + CFArrayAppendValue(newSet->array, (void*)ll2p(wrapper->range.location)); + CFArrayAppendValue(newSet->array, (void*)ll2p(HFMaxRange(wrapper->range))); + } + } + return newSet; +} + ++ (HFRangeSet *)withRangeSet:(HFRangeSet *)rangeSet { + return [[rangeSet copy] autorelease]; +} + ++ (HFRangeSet *)complementOfRangeSet:(HFRangeSet *)rangeSet inRange:(HFRange)range { + if(range.length <= 0) { + // Complement in empty is... empty! + return [HFRangeSet withRange:HFZeroRange]; + } + uintptr_t a = ll2p(range.location); + uintptr_t b = ll2p(HFMaxRange(range)); + CFIndex count = CFArrayGetCount(rangeSet->array); + CFIndex idxa = CFArrayBSearchValues(rangeSet->array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(rangeSet->array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) + return [HFRangeSet withRange:range]; + + // Alright, the trivial responses are past. We'll need to build a new set. + // Given the fencepost representation of sets, we can efficiently produce an + // inverted set by just copying the fenceposts between 'a' and 'b', and then + // maybe including 'a' and 'b'. + + HFRangeSet *newSet = [[[HFRangeSet alloc] init] autorelease]; + + // newSet must contain all the fenceposts strictly between 'a' and 'b' + CFIndex copyloc = (uintptr_t)CFArrayGetValueAtIndex(rangeSet->array, idxa) == a ? idxa+1 : idxa; + CFIndex copylen = idxb - copyloc; + + // Include 'a' if it's needed to invert the parity of the copy. + if(copyloc % 2 == 0) CFArrayAppendValue(newSet->array, &a); + + CFArrayAppendArray(newSet->array, rangeSet->array, CFRangeMake(copyloc, copylen)); + + // Include 'b' if it's needed to close off the set. + if(CFArrayGetCount(newSet->array) % 2 == 1) + CFArrayAppendValue(newSet->array, &b); + + assert(CFArrayGetCount(newSet->array) % 2 == 0); + return newSet; +} + + +- (void)addRange:(HFRange)range { + if(range.length == 0) return; + HFRangeSetAddRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} +- (void)removeRange:(HFRange)range { + if(range.length == 0) return; + HFRangeSetRemoveRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} +- (void)toggleRange:(HFRange)range { + if(range.length == 0) return; + HFRangeSetToggleRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} + +- (void)clipToRange:(HFRange)range { + if(range.length <= 0) { + CFArrayRemoveAllValues(array); + return; + } + uintptr_t a = ll2p(range.location); + uintptr_t b = ll2p(HFMaxRange(range)); + CFIndex count = CFArrayGetCount(array); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) { + CFArrayRemoveAllValues(array); + return; + } + + // Keep only fenceposts strictly between 'a' and 'b', and then possibly + // add 'a' or 'b' as fenceposts. + CFIndex keeploc = (uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxa+1 : idxa; + CFIndex keeplen = idxb - keeploc; + + // Include 'a' if it's needed to keep the parity straight. + if(keeploc % 2 == 1) { + keeploc--; keeplen++; + CFArraySetValueAtIndex(array, keeploc, (void*)a); + } + + if(keeploc > 0) + CFArrayReplaceValues(array, CFRangeMake(0, keeploc), NULL, 0); + if(keeploc+keeplen < count) + CFArrayReplaceValues(array, CFRangeMake(0, keeplen), NULL, 0); + + // Include 'b' if it's needed to keep the length even. + if(keeplen % 2 == 1) { + CFArrayAppendValue(array, (void*)b); + } + + assert(CFArrayGetCount(array) % 2 == 0); +} + + +- (void)addRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + HFRangeSetAddRange(array, (uintptr_t)CFArrayGetValueAtIndex(a, i2), (uintptr_t)CFArrayGetValueAtIndex(a, i2+1)); + } +} +- (void)removeRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + HFRangeSetRemoveRange(array, (uintptr_t)CFArrayGetValueAtIndex(a, i2), (uintptr_t)CFArrayGetValueAtIndex(a, i2+1)); + } +} +- (void)toggleRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + HFRangeSetToggleRange(array, (uintptr_t)CFArrayGetValueAtIndex(a, i2), (uintptr_t)CFArrayGetValueAtIndex(a, i2+1)); + } +} + +- (void)clipToRangeSet:(HFRangeSet *)rangeSet { + HFRange span = [rangeSet spanningRange]; + [self clipToRange:span]; + [self removeRangeSet:[HFRangeSet complementOfRangeSet:rangeSet inRange:span]]; +} + +- (BOOL)isEqualToRangeSet:(HFRangeSet *)rangeSet { + // Because our arrays are fully normalized, this just checks for array equality. + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + if(c != CFArrayGetCount(array)) + return NO; + + // Optimization: For long arrays, check the last few first, + // since appending to ranges is probably a common usage pattern. + const CFIndex opt_end = 10; + if(c > 2*opt_end) { + for(CFIndex i = c - 2*opt_end; i < c; i++) { + if(CFArrayGetValueAtIndex(a, i) != CFArrayGetValueAtIndex(array, i)) + return NO; + } + c -= 2*opt_end; + } + + for(CFIndex i = 0; i < c; i++) { + if(CFArrayGetValueAtIndex(a, i) != CFArrayGetValueAtIndex(array, i)) + return NO; + } + + return YES; +} + +- (BOOL)isEmpty { + return CFArrayGetCount(array) == 0; +} + +- (BOOL)containsAllRange:(HFRange)range { + if(range.length == 0) return YES; + return HFRangeSetContainsAllRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} + +- (BOOL)overlapsAnyRange:(HFRange)range { + if(range.length == 0) return NO; + return HFRangeSetOverlapsAnyRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} + +- (BOOL)containsAllRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + + // Optimization: check if containment is possible. + if(!HFRangeIsSubrangeOfRange([rangeSet spanningRange], [self spanningRange])) { + return NO; + } + + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + uintptr_t x = (uintptr_t)CFArrayGetValueAtIndex(a, i2); + uintptr_t y = (uintptr_t)CFArrayGetValueAtIndex(a, i2+1); + if(!HFRangeSetContainsAllRange(array, x, y)) return NO; + } + return YES; +} + +- (BOOL)overlapsAnyRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + + // Optimization: check if overlap is possible. + if(!HFIntersectsRange([rangeSet spanningRange], [self spanningRange])) { + return NO; + } + + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + uintptr_t x = (uintptr_t)CFArrayGetValueAtIndex(a, i2); + uintptr_t y = (uintptr_t)CFArrayGetValueAtIndex(a, i2+1); + if(!HFRangeSetOverlapsAnyRange(array, x, y)) return YES; + } + return NO; +} + + +- (HFRange)spanningRange { + CFIndex count = CFArrayGetCount(array); + if(count == 0) return HFZeroRange; + + uintptr_t a = (uintptr_t)CFArrayGetValueAtIndex(array, 0); + uintptr_t b = (uintptr_t)CFArrayGetValueAtIndex(array, count-2) + (uintptr_t)CFArrayGetValueAtIndex(array, count-1); + + return HFRangeMake(a, b-a); +} + +- (void)assertIntegrity { + CFIndex count = CFArrayGetCount(array); + HFASSERT(count % 2 == 0); + if(count == 0) return; + + uintptr_t prev = (uintptr_t)CFArrayGetValueAtIndex(array, 0); + for(CFIndex i = 1; i < count; i++) { + uintptr_t val = (uintptr_t)CFArrayGetValueAtIndex(array, i); + HFASSERT(val > prev); + prev = val; + } +} + +- (BOOL)isEqual:(id)object { + if(![object isKindOfClass:[HFRangeSet class]]) + return false; + return [self isEqualToRangeSet:object]; +} + +- (NSUInteger)hash { + CFIndex count = CFArrayGetCount(array); + NSUInteger x = 0; + for(CFIndex i2 = 0; i2 < count; i2 += 2) { + uintptr_t a = (uintptr_t)CFArrayGetValueAtIndex(array, i2); + uintptr_t b = (uintptr_t)CFArrayGetValueAtIndex(array, i2+1); +#if 6364136223846793005 < NSUIntegerMax + x = (6364136223846793005 * (uint64_t)x + a); +#else + x = (NSUInteger)(1103515245 * (uint64_t)x + a); +#endif + x ^= (NSUInteger)b; + } + return x; +} + +- (id)copyWithZone:(NSZone *)zone { + HFRangeSet *newSet = [[HFRangeSet allocWithZone:zone] init]; + CFRelease(newSet->array); + newSet->array = (CFMutableArrayRef)[[NSMutableArray allocWithZone:zone] initWithArray:(NSArray*)array copyItems:NO]; + return newSet; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + NSUInteger count = CFArrayGetCount(array); + NEW_ARRAY(uint64_t, values, count); + + // Fill array with 64-bit, little endian bytes. + if(sizeof(const void *) == sizeof(uint64_t)) { + // Hooray, we can just use CFArrayGetValues + CFArrayGetValues(array, CFRangeMake(0, count), (const void **)&values); +#if __LITTLE_ENDIAN__ +#else + // Boo, we have to swap everything. + for(NSUInteger i = 0; i < count; i++) { + values[i] = CFSwapInt64HostToLittle(values[i]); + } +#endif + } else { + // Boo, we have to iterate through the array. + NSUInteger i = 0; + FOREACH(id, val, (NSArray*)array) { + values[i++] = CFSwapInt64HostToLittle((uint64_t)(const void *)val); + } + } + [aCoder encodeBytes:values length:count * sizeof(*values)]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if(!(self = [super init])) return nil; + + NSUInteger count; + uint64_t *values = [aDecoder decodeBytesWithReturnedLength:&count]; + array = CFArrayCreateMutable(kCFAllocatorDefault, count+1, NULL); + + for(NSUInteger i = 0; i < count; i++) { + uint64_t x = CFSwapInt64LittleToHost(values[i]); + if(x > UINTPTR_MAX) + goto fail; + CFArrayAppendValue(array, (const void *)(uintptr_t)x); + } + if(CFArrayGetCount(array)%2 != 0) + goto fail; + return self; + +fail: + CFRelease(array); + [super release]; + return nil; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { + NSUInteger base = state->state; + NSUInteger length = CFArrayGetCount(array)/2; + NSUInteger i = 0; + + while(i < len && base + i < length) { + uintptr_t a = (uintptr_t)CFArrayGetValueAtIndex(array, 2*i); + uintptr_t b = (uintptr_t)CFArrayGetValueAtIndex(array, 2*i+1); + stackbuf[i] = [HFRangeWrapper withRange:HFRangeMake(a, b-a)]; + } + + state->state = base + i; + state->itemsPtr = stackbuf; + state->mutationsPtr = &state->extra[0]; // Use simple mutation checking. + state->extra[0] = length; + + return i; +} + +@end + + +BOOL HFStringEncodingIsSupersetOfASCII(NSStringEncoding encoding) { + switch (CFStringConvertNSStringEncodingToEncoding(encoding)) { + case kCFStringEncodingMacRoman: return YES; + case kCFStringEncodingWindowsLatin1: return YES; + case kCFStringEncodingISOLatin1: return YES; + case kCFStringEncodingNextStepLatin: return YES; + case kCFStringEncodingASCII: return YES; + case kCFStringEncodingUnicode: return NO; + case kCFStringEncodingUTF8: return YES; + case kCFStringEncodingNonLossyASCII: return NO; +// case kCFStringEncodingUTF16: return NO; + case kCFStringEncodingUTF16BE: return NO; + case kCFStringEncodingUTF16LE: return NO; + case kCFStringEncodingUTF32: return NO; + case kCFStringEncodingUTF32BE: return NO; + case kCFStringEncodingUTF32LE: return NO; + case kCFStringEncodingMacJapanese: return NO; + case kCFStringEncodingMacChineseTrad: return YES; + case kCFStringEncodingMacKorean: return YES; + case kCFStringEncodingMacArabic: return NO; + case kCFStringEncodingMacHebrew: return NO; + case kCFStringEncodingMacGreek: return YES; + case kCFStringEncodingMacCyrillic: return YES; + case kCFStringEncodingMacDevanagari: return YES; + case kCFStringEncodingMacGurmukhi: return YES; + case kCFStringEncodingMacGujarati: return YES; + case kCFStringEncodingMacOriya: return YES; + case kCFStringEncodingMacBengali: return YES; + case kCFStringEncodingMacTamil: return YES; + case kCFStringEncodingMacTelugu: return YES; + case kCFStringEncodingMacKannada: return YES; + case kCFStringEncodingMacMalayalam: return YES; + case kCFStringEncodingMacSinhalese: return YES; + case kCFStringEncodingMacBurmese: return YES; + case kCFStringEncodingMacKhmer: return YES; + case kCFStringEncodingMacThai: return YES; + case kCFStringEncodingMacLaotian: return YES; + case kCFStringEncodingMacGeorgian: return YES; + case kCFStringEncodingMacArmenian: return YES; + case kCFStringEncodingMacChineseSimp: return YES; + case kCFStringEncodingMacTibetan: return YES; + case kCFStringEncodingMacMongolian: return YES; + case kCFStringEncodingMacEthiopic: return YES; + case kCFStringEncodingMacCentralEurRoman: return YES; + case kCFStringEncodingMacVietnamese: return YES; + case kCFStringEncodingMacExtArabic: return YES; + case kCFStringEncodingMacSymbol: return NO; + case kCFStringEncodingMacDingbats: return NO; + case kCFStringEncodingMacTurkish: return YES; + case kCFStringEncodingMacCroatian: return YES; + case kCFStringEncodingMacIcelandic: return YES; + case kCFStringEncodingMacRomanian: return YES; + case kCFStringEncodingMacCeltic: return YES; + case kCFStringEncodingMacGaelic: return YES; + case kCFStringEncodingMacFarsi: return YES; + case kCFStringEncodingMacUkrainian: return NO; + case kCFStringEncodingMacInuit: return YES; + case kCFStringEncodingMacVT100: return YES; + case kCFStringEncodingMacHFS: return YES; + case kCFStringEncodingISOLatin2: return YES; + case kCFStringEncodingISOLatin3: return YES; + case kCFStringEncodingISOLatin4: return YES; + case kCFStringEncodingISOLatinCyrillic: return YES; + case kCFStringEncodingISOLatinArabic: return NO; + case kCFStringEncodingISOLatinGreek: return YES; + case kCFStringEncodingISOLatinHebrew: return YES; + case kCFStringEncodingISOLatin5: return YES; + case kCFStringEncodingISOLatin6: return YES; + case kCFStringEncodingISOLatinThai: return YES; + case kCFStringEncodingISOLatin7: return YES; + case kCFStringEncodingISOLatin8: return YES; + case kCFStringEncodingISOLatin9: return YES; + case kCFStringEncodingISOLatin10: return YES; + case kCFStringEncodingDOSLatinUS: return YES; + case kCFStringEncodingDOSGreek: return YES; + case kCFStringEncodingDOSBalticRim: return YES; + case kCFStringEncodingDOSLatin1: return YES; + case kCFStringEncodingDOSGreek1: return YES; + case kCFStringEncodingDOSLatin2: return YES; + case kCFStringEncodingDOSCyrillic: return YES; + case kCFStringEncodingDOSTurkish: return YES; + case kCFStringEncodingDOSPortuguese: return YES; + case kCFStringEncodingDOSIcelandic: return YES; + case kCFStringEncodingDOSHebrew: return YES; + case kCFStringEncodingDOSCanadianFrench: return YES; + case kCFStringEncodingDOSArabic: return YES; + case kCFStringEncodingDOSNordic: return YES; + case kCFStringEncodingDOSRussian: return YES; + case kCFStringEncodingDOSGreek2: return YES; + case kCFStringEncodingDOSThai: return YES; + case kCFStringEncodingDOSJapanese: return YES; + case kCFStringEncodingDOSChineseSimplif: return YES; + case kCFStringEncodingDOSKorean: return YES; + case kCFStringEncodingDOSChineseTrad: return YES; + case kCFStringEncodingWindowsLatin2: return YES; + case kCFStringEncodingWindowsCyrillic: return YES; + case kCFStringEncodingWindowsGreek: return YES; + case kCFStringEncodingWindowsLatin5: return YES; + case kCFStringEncodingWindowsHebrew: return YES; + case kCFStringEncodingWindowsArabic: return YES; + case kCFStringEncodingWindowsBalticRim: return YES; + case kCFStringEncodingWindowsVietnamese: return YES; + case kCFStringEncodingWindowsKoreanJohab: return YES; + case kCFStringEncodingANSEL: return NO; + case kCFStringEncodingJIS_X0201_76: return NO; + case kCFStringEncodingJIS_X0208_83: return NO; + case kCFStringEncodingJIS_X0208_90: return NO; + case kCFStringEncodingJIS_X0212_90: return NO; + case kCFStringEncodingJIS_C6226_78: return NO; + case 0x0628/*kCFStringEncodingShiftJIS_X0213*/: return NO; + case kCFStringEncodingShiftJIS_X0213_MenKuTen: return NO; + case kCFStringEncodingGB_2312_80: return NO; + case kCFStringEncodingGBK_95: return NO; + case kCFStringEncodingGB_18030_2000: return NO; + case kCFStringEncodingKSC_5601_87: return NO; + case kCFStringEncodingKSC_5601_92_Johab: return NO; + case kCFStringEncodingCNS_11643_92_P1: return NO; + case kCFStringEncodingCNS_11643_92_P2: return NO; + case kCFStringEncodingCNS_11643_92_P3: return NO; + case kCFStringEncodingISO_2022_JP: return NO; + case kCFStringEncodingISO_2022_JP_2: return NO; + case kCFStringEncodingISO_2022_JP_1: return NO; + case kCFStringEncodingISO_2022_JP_3: return NO; + case kCFStringEncodingISO_2022_CN: return NO; + case kCFStringEncodingISO_2022_CN_EXT: return NO; + case kCFStringEncodingISO_2022_KR: return NO; + case kCFStringEncodingEUC_JP: return YES; + case kCFStringEncodingEUC_CN: return YES; + case kCFStringEncodingEUC_TW: return YES; + case kCFStringEncodingEUC_KR: return YES; + case kCFStringEncodingShiftJIS: return NO; + case kCFStringEncodingKOI8_R: return YES; + case kCFStringEncodingBig5: return YES; + case kCFStringEncodingMacRomanLatin1: return YES; + case kCFStringEncodingHZ_GB_2312: return NO; + case kCFStringEncodingBig5_HKSCS_1999: return YES; + case kCFStringEncodingVISCII: return YES; // though not quite + case kCFStringEncodingKOI8_U: return YES; + case kCFStringEncodingBig5_E: return YES; + case kCFStringEncodingNextStepJapanese: return YES; + case kCFStringEncodingEBCDIC_US: return NO; + case kCFStringEncodingEBCDIC_CP037: return NO; + default: + NSLog(@"Unknown string encoding %lu in %s", (unsigned long)encoding, __FUNCTION__); + return NO; + } +} + +uint8_t HFStringEncodingCharacterLength(NSStringEncoding encoding) { + switch (CFStringConvertNSStringEncodingToEncoding(encoding)) { + case kCFStringEncodingMacRoman: return 1; + case kCFStringEncodingWindowsLatin1: return 1; + case kCFStringEncodingISOLatin1: return 1; + case kCFStringEncodingNextStepLatin: return 1; + case kCFStringEncodingASCII: return 1; + case kCFStringEncodingUnicode: return 2; + case kCFStringEncodingUTF8: return 1; + case kCFStringEncodingNonLossyASCII: return 1; + // case kCFStringEncodingUTF16: return 2; + case kCFStringEncodingUTF16BE: return 2; + case kCFStringEncodingUTF16LE: return 2; + case kCFStringEncodingUTF32: return 4; + case kCFStringEncodingUTF32BE: return 4; + case kCFStringEncodingUTF32LE: return 4; + case kCFStringEncodingMacJapanese: return 1; + case kCFStringEncodingMacChineseTrad: return 1; // ?? + case kCFStringEncodingMacKorean: return 1; + case kCFStringEncodingMacArabic: return 1; + case kCFStringEncodingMacHebrew: return 1; + case kCFStringEncodingMacGreek: return 1; + case kCFStringEncodingMacCyrillic: return 1; + case kCFStringEncodingMacDevanagari: return 1; + case kCFStringEncodingMacGurmukhi: return 1; + case kCFStringEncodingMacGujarati: return 1; + case kCFStringEncodingMacOriya: return 1; + case kCFStringEncodingMacBengali: return 1; + case kCFStringEncodingMacTamil: return 1; + case kCFStringEncodingMacTelugu: return 1; + case kCFStringEncodingMacKannada: return 1; + case kCFStringEncodingMacMalayalam: return 1; + case kCFStringEncodingMacSinhalese: return 1; + case kCFStringEncodingMacBurmese: return 1; + case kCFStringEncodingMacKhmer: return 1; + case kCFStringEncodingMacThai: return 1; + case kCFStringEncodingMacLaotian: return 1; + case kCFStringEncodingMacGeorgian: return 1; + case kCFStringEncodingMacArmenian: return 1; + case kCFStringEncodingMacChineseSimp: return 1; + case kCFStringEncodingMacTibetan: return 1; + case kCFStringEncodingMacMongolian: return 1; + case kCFStringEncodingMacEthiopic: return 1; + case kCFStringEncodingMacCentralEurRoman: return 1; + case kCFStringEncodingMacVietnamese: return 1; + case kCFStringEncodingMacExtArabic: return 1; + case kCFStringEncodingMacSymbol: return 1; + case kCFStringEncodingMacDingbats: return 1; + case kCFStringEncodingMacTurkish: return 1; + case kCFStringEncodingMacCroatian: return 1; + case kCFStringEncodingMacIcelandic: return 1; + case kCFStringEncodingMacRomanian: return 1; + case kCFStringEncodingMacCeltic: return 1; + case kCFStringEncodingMacGaelic: return 1; + case kCFStringEncodingMacFarsi: return 1; + case kCFStringEncodingMacUkrainian: return 1; + case kCFStringEncodingMacInuit: return 1; + case kCFStringEncodingMacVT100: return 1; + case kCFStringEncodingMacHFS: return 1; + case kCFStringEncodingISOLatin2: return 1; + case kCFStringEncodingISOLatin3: return 1; + case kCFStringEncodingISOLatin4: return 1; + case kCFStringEncodingISOLatinCyrillic: return 1; + case kCFStringEncodingISOLatinArabic: return 1; + case kCFStringEncodingISOLatinGreek: return 1; + case kCFStringEncodingISOLatinHebrew: return 1; + case kCFStringEncodingISOLatin5: return 1; + case kCFStringEncodingISOLatin6: return 1; + case kCFStringEncodingISOLatinThai: return 1; + case kCFStringEncodingISOLatin7: return 1; + case kCFStringEncodingISOLatin8: return 1; + case kCFStringEncodingISOLatin9: return 1; + case kCFStringEncodingISOLatin10: return 1; + case kCFStringEncodingDOSLatinUS: return 1; + case kCFStringEncodingDOSGreek: return 1; + case kCFStringEncodingDOSBalticRim: return 1; + case kCFStringEncodingDOSLatin1: return 1; + case kCFStringEncodingDOSGreek1: return 1; + case kCFStringEncodingDOSLatin2: return 1; + case kCFStringEncodingDOSCyrillic: return 1; + case kCFStringEncodingDOSTurkish: return 1; + case kCFStringEncodingDOSPortuguese: return 1; + case kCFStringEncodingDOSIcelandic: return 1; + case kCFStringEncodingDOSHebrew: return 1; + case kCFStringEncodingDOSCanadianFrench: return 1; + case kCFStringEncodingDOSArabic: return 1; + case kCFStringEncodingDOSNordic: return 1; + case kCFStringEncodingDOSRussian: return 1; + case kCFStringEncodingDOSGreek2: return 1; + case kCFStringEncodingDOSThai: return 1; + case kCFStringEncodingDOSJapanese: return 1; + case kCFStringEncodingDOSChineseSimplif: return 1; + case kCFStringEncodingDOSKorean: return 1; + case kCFStringEncodingDOSChineseTrad: return 1; + case kCFStringEncodingWindowsLatin2: return 1; + case kCFStringEncodingWindowsCyrillic: return 1; + case kCFStringEncodingWindowsGreek: return 1; + case kCFStringEncodingWindowsLatin5: return 1; + case kCFStringEncodingWindowsHebrew: return 1; + case kCFStringEncodingWindowsArabic: return 1; + case kCFStringEncodingWindowsBalticRim: return 1; + case kCFStringEncodingWindowsVietnamese: return 1; + case kCFStringEncodingWindowsKoreanJohab: return 1; + case kCFStringEncodingANSEL: return 1; + case kCFStringEncodingJIS_X0201_76: return 1; + case kCFStringEncodingJIS_X0208_83: return 1; + case kCFStringEncodingJIS_X0208_90: return 1; + case kCFStringEncodingJIS_X0212_90: return 1; + case kCFStringEncodingJIS_C6226_78: return 1; + case 0x0628/*kCFStringEncodingShiftJIS_X0213*/: return 1; + case kCFStringEncodingShiftJIS_X0213_MenKuTen: return 1; + case kCFStringEncodingGB_2312_80: return 1; + case kCFStringEncodingGBK_95: return 1; + case kCFStringEncodingGB_18030_2000: return 1; + case kCFStringEncodingKSC_5601_87: return 1; + case kCFStringEncodingKSC_5601_92_Johab: return 1; + case kCFStringEncodingCNS_11643_92_P1: return 1; + case kCFStringEncodingCNS_11643_92_P2: return 1; + case kCFStringEncodingCNS_11643_92_P3: return 1; + case kCFStringEncodingISO_2022_JP: return 1; + case kCFStringEncodingISO_2022_JP_2: return 1; + case kCFStringEncodingISO_2022_JP_1: return 1; + case kCFStringEncodingISO_2022_JP_3: return 1; + case kCFStringEncodingISO_2022_CN: return 1; + case kCFStringEncodingISO_2022_CN_EXT: return 1; + case kCFStringEncodingISO_2022_KR: return 1; + case kCFStringEncodingEUC_JP: return 1; + case kCFStringEncodingEUC_CN: return 1; + case kCFStringEncodingEUC_TW: return 1; + case kCFStringEncodingEUC_KR: return 1; + case kCFStringEncodingShiftJIS: return 1; + case kCFStringEncodingKOI8_R: return 1; + case kCFStringEncodingBig5: return 2; //yay, a 2 + case kCFStringEncodingMacRomanLatin1: return 1; + case kCFStringEncodingHZ_GB_2312: return 2; + case kCFStringEncodingBig5_HKSCS_1999: return 1; + case kCFStringEncodingVISCII: return 1; + case kCFStringEncodingKOI8_U: return 1; + case kCFStringEncodingBig5_E: return 2; + case kCFStringEncodingNextStepJapanese: return YES; // ?? + case kCFStringEncodingEBCDIC_US: return 1; //lol + case kCFStringEncodingEBCDIC_CP037: return 1; + case kCFStringEncodingUTF7: return 1; + case kCFStringEncodingUTF7_IMAP : return 1; + default: + NSLog(@"Unknown string encoding %lx in %s", (long)encoding, __FUNCTION__); + return 1; + } +} + +/* Converts a hexadecimal digit into a corresponding 4 bit unsigned int; returns -1 on failure. The ... is a gcc extension. */ +static NSInteger char2hex(unichar c) { + switch (c) { + case '0' ... '9': return c - '0'; + case 'a' ... 'f': return c - 'a' + 10; + case 'A' ... 'F': return c - 'A' + 10; + default: return -1; + } +} + +static unsigned char hex2char(NSUInteger c) { + HFASSERT(c < 16); + return "0123456789ABCDEF"[c]; +} + +NSData *HFDataFromHexString(NSString *string, BOOL* isMissingLastNybble) { + REQUIRE_NOT_NULL(string); + NSUInteger stringIndex=0, resultIndex=0, max=[string length]; + NSMutableData* result = [NSMutableData dataWithLength:(max + 1)/2]; + unsigned char* bytes = [result mutableBytes]; + + NSUInteger numNybbles = 0; + unsigned char byteValue = 0; + + for (stringIndex = 0; stringIndex < max; stringIndex++) { + NSInteger val = char2hex([string characterAtIndex:stringIndex]); + if (val < 0) continue; + numNybbles++; + byteValue = byteValue * 16 + (unsigned char)val; + if (! (numNybbles % 2)) { + bytes[resultIndex++] = byteValue; + byteValue = 0; + } + } + + if (isMissingLastNybble) *isMissingLastNybble = (numNybbles % 2); + + //final nibble + if (numNybbles % 2) { + bytes[resultIndex++] = byteValue; + } + + [result setLength:resultIndex]; + return result; +} + +NSString *HFHexStringFromData(NSData *data) { + REQUIRE_NOT_NULL(data); + NSUInteger dataLength = [data length]; + NSUInteger stringLength = HFProductInt(dataLength, 2); + const unsigned char *bytes = [data bytes]; + unsigned char *charBuffer = check_malloc(stringLength); + NSUInteger charIndex = 0, byteIndex; + for (byteIndex = 0; byteIndex < dataLength; byteIndex++) { + unsigned char byte = bytes[byteIndex]; + charBuffer[charIndex++] = hex2char(byte >> 4); + charBuffer[charIndex++] = hex2char(byte & 0xF); + } + return [[[NSString alloc] initWithBytesNoCopy:charBuffer length:stringLength encoding:NSASCIIStringEncoding freeWhenDone:YES] autorelease]; +} + +void HFSetFDShouldCache(int fd, BOOL shouldCache) { + int result = fcntl(fd, F_NOCACHE, !shouldCache); + if (result == -1) { + int err = errno; + NSLog(@"fcntl(%d, F_NOCACHE, %d) returned error %d: %s", fd, !shouldCache, err, strerror(err)); + } +} + +NSString *HFDescribeByteCount(unsigned long long count) { + return HFDescribeByteCountWithPrefixAndSuffix(NULL, count, NULL); +} + +/* A big_num represents a number in some base. Here it is value = big * base + little. */ +typedef struct big_num { + unsigned int big; + unsigned long long little; +} big_num; + +static inline big_num divide_bignum_by_2(big_num a, unsigned long long base) { + //value = a.big * base + a.little; + big_num result; + result.big = a.big / 2; + unsigned int shiftedRemainder = (unsigned int)(a.little & 1); + result.little = a.little / 2; + if (a.big & 1) { + //need to add base/2 to result.little. We know that won't overflow because result.little is already a.little / 2 + result.little += base / 2; + + // If we shift off a bit for base/2, and we also shifted off a bit for a.little/2, then we have a carry bit we need to add + if ((base & 1) && shiftedRemainder) { + /* Is there a chance that adding 1 will overflow? We know base is odd (base & 1), so consider an example of base = 9. Then the largest that result.little could be is (9 - 1)/2 + base/2 = 8. We could add 1 and get back to base, but we can never exceed base, so we cannot overflow an unsigned long long. */ + result.little += 1; + HFASSERT(result.little <= base); + if (result.little == base) { + result.big++; + result.little = 0; + } + } + } + HFASSERT(result.little < base); + return result; +} + +static inline big_num add_big_nums(big_num a, big_num b, unsigned long long base) { + /* Perform the addition result += left. The addition is: + result.big = a.big + b.big + (a.little + b.little) / base + result.little = (a.little + b.little) % base + + a.little + b.little may overflow, so we have to take some care in how we calculate them. + Since both a.little and b.little are less than base, we know that if we overflow, we can subtract base from it to underflow and still get the same remainder. + */ + unsigned long long remainder = a.little + b.little; + unsigned int dividend = 0; + // remainder < a.little detects overflow, and remainder >= base detects the case where we did not overflow but are larger than base + if (remainder < a.little || remainder >= base) { + remainder -= base; + dividend++; + } + HFASSERT(remainder < base); + + big_num result = {a.big + b.big + dividend, remainder}; + return result; +} + + +/* Returns the first digit after the decimal point for a / b, rounded off, without overflow. This may return 10, indicating that the digit is 0 and we should carry. */ +static unsigned int computeRemainderPrincipalDigit(unsigned long long a, unsigned long long base) { + struct big_num result = {0, 0}, left = {(unsigned)(a / base), a % base}, right = {(unsigned)(100 / base), 100 % base}; + while (right.big > 0 || right.little > 0) { + /* Determine the least significant bit of right, which is right.big * base + right.little */ + unsigned int bigTermParity = (base & 1) && (right.big & 1); + unsigned int littleTermParity = (unsigned)(right.little & 1); + if (bigTermParity != littleTermParity) result = add_big_nums(result, left, base); + + right = divide_bignum_by_2(right, base); + left = add_big_nums(left, left, base); + } + + //result.big now contains 100 * a / base + unsigned int principalTwoDigits = (unsigned int)(result.big % 100); + unsigned int principalDigit = (principalTwoDigits / 10) + ((principalTwoDigits % 10) >= 5); + return principalDigit; +} + +NSString *HFDescribeByteCountWithPrefixAndSuffix(const char *stringPrefix, unsigned long long count, const char *stringSuffix) { + if (! stringPrefix) stringPrefix = ""; + if (! stringSuffix) stringSuffix = ""; + + if (count == 0) return [NSString stringWithFormat:@"%s0 bytes%s", stringPrefix, stringSuffix]; + + const struct { + unsigned long long size; + const char *suffix; + } suffixes[] = { + {1ULL<<0, "byte"}, + {1ULL<<10, "byte"}, + {1ULL<<20, "kilobyte"}, + {1ULL<<30, "megabyte"}, + {1ULL<<40, "gigabyte"}, + {1ULL<<50, "terabyte"}, + {1ULL<<60, "petabyte"}, + {ULLONG_MAX, "exabyte"} + }; + const unsigned numSuffixes = sizeof suffixes / sizeof *suffixes; + //HFASSERT((sizeof sizes / sizeof *sizes) == (sizeof suffixes / sizeof *suffixes)); + unsigned i; + unsigned long long base; + for (i=0; i < numSuffixes; i++) { + if (count < suffixes[i].size || suffixes[i].size == ULLONG_MAX) break; + } + + if (i >= numSuffixes) return [NSString stringWithFormat:@"%san unbelievable number of bytes%s", stringPrefix, stringSuffix]; + base = suffixes[i-1].size; + + unsigned long long dividend = count / base; + unsigned int remainderPrincipalDigit = computeRemainderPrincipalDigit(count % base, base); + HFASSERT(remainderPrincipalDigit <= 10); + if (remainderPrincipalDigit == 10) { + /* Carry */ + dividend++; + remainderPrincipalDigit = 0; + } + + BOOL needsPlural = (dividend != 1 || remainderPrincipalDigit > 0); + + char remainderBuff[64]; + if (remainderPrincipalDigit > 0) snprintf(remainderBuff, sizeof remainderBuff, ".%u", remainderPrincipalDigit); + else remainderBuff[0] = 0; + + char* resultPointer = NULL; + int numChars = asprintf(&resultPointer, "%s%llu%s %s%s%s", stringPrefix, dividend, remainderBuff, suffixes[i].suffix, needsPlural ? "s" : "", stringSuffix); + if (numChars < 0) return NULL; + return [[[NSString alloc] initWithBytesNoCopy:resultPointer length:numChars encoding:NSASCIIStringEncoding freeWhenDone:YES] autorelease]; +} + +static CGFloat interpolateShadow(CGFloat val) { + //A value of 1 means we are at the rightmost, and should return our max value. By adjusting the scale, we control how quickly the shadow drops off. + CGFloat scale = 1.4; + return (CGFloat)(expm1(val * scale) / expm1(scale)); +} + +void HFDrawShadow(CGContextRef ctx, NSRect rect, CGFloat shadowSize, NSRectEdge rectEdge, BOOL drawActive, NSRect clip) { + NSRect remainingRect, unused; + NSDivideRect(rect, &remainingRect, &unused, shadowSize, rectEdge); + + CGFloat maxAlpha = (drawActive ? .25 : .10); + + for (CGFloat i=0; i < shadowSize; i++) { + NSRect shadowLine; + NSDivideRect(remainingRect, &shadowLine, &remainingRect, 1, rectEdge); + + NSRect clippedLine = NSIntersectionRect(shadowLine, clip); + if (! NSIsEmptyRect(clippedLine)) { + CGFloat gray = 0.; + CGFloat alpha = maxAlpha * interpolateShadow((shadowSize - i) / shadowSize); + CGContextSetGrayFillColor(ctx, gray, alpha); + CGContextFillRect(ctx, NSRectToCGRect(clippedLine)); + } + } + +} + +void HFRegisterViewForWindowAppearanceChanges(NSView *self, SEL notificationSEL, BOOL appToo) { + NSWindow *window = [self window]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + if (window) { + [center addObserver:self selector:notificationSEL name:NSWindowDidBecomeKeyNotification object:window]; + [center addObserver:self selector:notificationSEL name:NSWindowDidResignKeyNotification object:window]; + } + if (appToo) { + [center addObserver:self selector:notificationSEL name:NSApplicationDidBecomeActiveNotification object:nil]; + [center addObserver:self selector:notificationSEL name:NSApplicationDidResignActiveNotification object:nil]; + } +} + +void HFUnregisterViewForWindowAppearanceChanges(NSView *self, BOOL appToo) { + NSWindow *window = [self window]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + if (window) { + [center removeObserver:self name:NSWindowDidBecomeKeyNotification object:window]; + [center removeObserver:self name:NSWindowDidResignKeyNotification object:window]; + } + if (appToo) { + [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil]; + [center removeObserver:self name:NSApplicationDidResignActiveNotification object:nil]; + } +} + +#if USE_CHUD +void HFStartTiming(const char *name) { + static BOOL inited; + if (! inited) { + inited = YES; + chudInitialize(); + chudSetErrorLogFile(stderr); + chudAcquireRemoteAccess(); + } + chudStartRemotePerfMonitor(name); + +} + +void HFStopTiming(void) { + chudStopRemotePerfMonitor(); +} +#else +void HFStartTiming(const char *name) { USE(name); } +void HFStopTiming(void) { } +#endif diff --git a/HexFiend/HFFunctions_Private.h b/HexFiend/HFFunctions_Private.h new file mode 100644 index 0000000..3595d3d --- /dev/null +++ b/HexFiend/HFFunctions_Private.h @@ -0,0 +1,52 @@ +#import + +@class HFController; + +static inline BOOL HFIsRunningOnMountainLionOrLater(void) { + return NSAppKitVersionNumber >= NSAppKitVersionNumber10_8; +} + +/* Returns the first index where the strings differ. If the strings do not differ in any characters but are of different lengths, returns the smaller length; if they are the same length and do not differ, returns NSUIntegerMax */ +static inline NSUInteger HFIndexOfFirstByteThatDiffers(const unsigned char *a, NSUInteger len1, const unsigned char *b, NSUInteger len2) { + NSUInteger endIndex = MIN(len1, len2); + for (NSUInteger i = 0; i < endIndex; i++) { + if (a[i] != b[i]) return i; + } + if (len1 != len2) return endIndex; + return NSUIntegerMax; +} + +/* Returns the last index where the strings differ. If the strings do not differ in any characters but are of different lengths, returns the larger length; if they are the same length and do not differ, returns NSUIntegerMax */ +static inline NSUInteger HFIndexOfLastByteThatDiffers(const unsigned char *a, NSUInteger len1, const unsigned char *b, NSUInteger len2) { + if (len1 != len2) return MAX(len1, len2); + NSUInteger i = len1; + while (i--) { + if (a[i] != b[i]) return i; + } + return NSUIntegerMax; +} + +static inline unsigned long long llmin(unsigned long long a, unsigned long long b) { + return a < b ? a : b; +} + +__private_extern__ NSImage *HFImageNamed(NSString *name); + +/* Returns an NSData from an NSString containing hexadecimal characters. Characters that are not hexadecimal digits are silently skipped. Returns by reference whether the last byte contains only one nybble, in which case it will be returned in the low 4 bits of the last byte. */ +__private_extern__ NSData *HFDataFromHexString(NSString *string, BOOL* isMissingLastNybble); + +__private_extern__ NSString *HFHexStringFromData(NSData *data); + +/* Modifies F_NOCACHE for a given file descriptor */ +__private_extern__ void HFSetFDShouldCache(int fd, BOOL shouldCache); + +__private_extern__ NSString *HFDescribeByteCountWithPrefixAndSuffix(const char *stringPrefix, unsigned long long count, const char *stringSuffix); + +/* Function for OSAtomicAdd64 that just does a non-atomic add on PowerPC. This should not be used where atomicity is critical; an example where this is used is updating a progress bar. */ +static inline int64_t HFAtomicAdd64(int64_t a, volatile int64_t *b) { +#if __ppc__ + return *b += a; +#else + return OSAtomicAdd64(a, b); +#endif +} diff --git a/HexFiend/HFGlyphTrie.h b/HexFiend/HFGlyphTrie.h new file mode 100644 index 0000000..36d4b08 --- /dev/null +++ b/HexFiend/HFGlyphTrie.h @@ -0,0 +1,49 @@ +/* HFGlyphTrie is used to represent a trie of glyphs that allows multiple concurrent readers, along with one writer. */ + +#import + +/* BranchFactor is in bits */ +#define kHFGlyphTrieBranchFactor 4 +#define kHFGlyphTrieBranchCount (1 << kHFGlyphTrieBranchFactor) + +typedef uint16_t HFGlyphFontIndex; +#define kHFGlyphFontIndexInvalid ((HFGlyphFontIndex)(-1)) + +#define kHFGlyphInvalid kCGFontIndexInvalid + +struct HFGlyph_t { + HFGlyphFontIndex fontIndex; + CGGlyph glyph; +}; + +static inline BOOL HFGlyphEqualsGlyph(struct HFGlyph_t a, struct HFGlyph_t b) __attribute__((unused)); +static inline BOOL HFGlyphEqualsGlyph(struct HFGlyph_t a, struct HFGlyph_t b) { + return a.glyph == b.glyph && a.fontIndex == b.fontIndex; +} + +struct HFGlyphTrieBranch_t { + __strong void *children[kHFGlyphTrieBranchCount]; +}; + +struct HFGlyphTrieLeaf_t { + struct HFGlyph_t glyphs[kHFGlyphTrieBranchCount]; +}; + +struct HFGlyphTrie_t { + uint8_t branchingDepth; + struct HFGlyphTrieBranch_t root; +}; + +/* Initializes a trie witha given key size */ +__private_extern__ void HFGlyphTrieInitialize(struct HFGlyphTrie_t *trie, uint8_t keySize); + +/* Inserts a glyph into the trie */ +__private_extern__ void HFGlyphTrieInsert(struct HFGlyphTrie_t *trie, NSUInteger key, struct HFGlyph_t value); + +/* Attempts to fetch a glyph. If the glyph is not present, returns an HFGlyph_t set to all bits 0. */ +__private_extern__ struct HFGlyph_t HFGlyphTrieGet(const struct HFGlyphTrie_t *trie, NSUInteger key); + +/* Frees all storage associated with a glyph tree. This is not necessary to call under GC. */ +__private_extern__ void HFGlyphTreeFree(struct HFGlyphTrie_t * trie); + + diff --git a/HexFiend/HFGlyphTrie.m b/HexFiend/HFGlyphTrie.m new file mode 100644 index 0000000..db94782 --- /dev/null +++ b/HexFiend/HFGlyphTrie.m @@ -0,0 +1,93 @@ +#import "HFGlyphTrie.h" +#import + +/* If branchingDepth is 1, then this is a leaf and there's nothing to free (a parent frees its children). If branchingDepth is 2, then this is a branch whose children are leaves, so we have to free the leaves but we do not recurse. If branchingDepth is greater than 2, we do have to recurse. */ +static void freeTrie(struct HFGlyphTrieBranch_t *branch, uint8_t branchingDepth) { + HFASSERT(branchingDepth >= 1); + NSUInteger i; + if (branchingDepth > 2) { + /* Recurse */ + for (i=0; i < kHFGlyphTrieBranchCount; i++) { + if (branch->children[i]) { + freeTrie(branch->children[i], branchingDepth - 1); + } + } + } + if (branchingDepth > 1) { + /* Free our children */ + for (i=0; i < kHFGlyphTrieBranchCount; i++) { + free(branch->children[i]); + } + } +} + +static void insertTrie(void *node, uint8_t branchingDepth, NSUInteger key, struct HFGlyph_t value) { + HFASSERT(node != NULL); + HFASSERT(branchingDepth >= 1); + if (branchingDepth == 1) { + /* Leaf */ + HFASSERT(key < kHFGlyphTrieBranchCount); + ((struct HFGlyphTrieLeaf_t *)node)->glyphs[key] = value; + } else { + /* Branch */ + struct HFGlyphTrieBranch_t *branch = node; + NSUInteger keySlice = key & ((1 << kHFGlyphTrieBranchFactor) - 1), keyRemainder = key >> kHFGlyphTrieBranchFactor; + __strong void *child = branch->children[keySlice]; + if (child == NULL) { + if (branchingDepth == 2) { + child = calloc(1, sizeof(struct HFGlyphTrieLeaf_t)); + } else { + child = calloc(1, sizeof(struct HFGlyphTrieBranch_t)); + } + /* We just zeroed out a block of memory and we are about to write its address somewhere where another thread could read it, so we need a memory barrier. */ + OSMemoryBarrier(); + branch->children[keySlice] = child; + } + insertTrie(child, branchingDepth - 1, keyRemainder, value); + } +} + +static struct HFGlyph_t getTrie(const void *node, uint8_t branchingDepth, NSUInteger key) { + HFASSERT(node != NULL); + HFASSERT(branchingDepth >= 1); + if (branchingDepth == 1) { + /* Leaf */ + HFASSERT(key < kHFGlyphTrieBranchCount); + return ((const struct HFGlyphTrieLeaf_t *)node)->glyphs[key]; + } else { + /* Branch */ + const struct HFGlyphTrieBranch_t *branch = node; + NSUInteger keySlice = key & ((1 << kHFGlyphTrieBranchFactor) - 1), keyRemainder = key >> kHFGlyphTrieBranchFactor; + if (branch->children[keySlice] == NULL) { + /* Not found */ + return (struct HFGlyph_t){0, 0}; + } else { + /* This dereference requires a data dependency barrier */ + return getTrie(branch->children[keySlice], branchingDepth - 1, keyRemainder); + } + } +} + +void HFGlyphTrieInsert(struct HFGlyphTrie_t *trie, NSUInteger key, struct HFGlyph_t value) { + insertTrie(&trie->root, trie->branchingDepth, key, value); +} + +struct HFGlyph_t HFGlyphTrieGet(const struct HFGlyphTrie_t *trie, NSUInteger key) { + struct HFGlyph_t result = getTrie(&trie->root, trie->branchingDepth, key); + return result; +} + +void HFGlyphTrieInitialize(struct HFGlyphTrie_t *trie, uint8_t keySize) { + /* If the branch factor is 4 (bits) and the key size is 2 bytes = 16 bits, initialize branching depth to 16/4 = 4 */ + uint8_t keyBits = keySize * CHAR_BIT; + HFASSERT(keyBits % kHFGlyphTrieBranchFactor == 0); + trie->branchingDepth = keyBits / kHFGlyphTrieBranchFactor; + memset(&trie->root, 0, sizeof(trie->root)); +} + +void HFGlyphTreeFree(struct HFGlyphTrie_t * trie) { + /* Don't try to free under GC. And don't free if it's never been initialized. */ + if (trie->branchingDepth > 0) { + freeTrie(&trie->root, trie->branchingDepth); + } +} diff --git a/HexFiend/HFHexTextRepresenter.h b/HexFiend/HFHexTextRepresenter.h new file mode 100644 index 0000000..ecbb1a0 --- /dev/null +++ b/HexFiend/HFHexTextRepresenter.h @@ -0,0 +1,21 @@ +// +// HFHexTextRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFHexTextRepresenter + + @brief HFHexTextRepresenter is an HFRepresenter responsible for showing data in hexadecimal form. + + HFHexTextRepresenter is an HFRepresenter responsible for showing data in hexadecimal form. It has no methods except those inherited from HFTextRepresenter. +*/ +@interface HFHexTextRepresenter : HFTextRepresenter { + unsigned long long omittedNybbleLocation; + unsigned char unpartneredLastNybble; +} + +@end diff --git a/HexFiend/HFHexTextRepresenter.m b/HexFiend/HFHexTextRepresenter.m new file mode 100644 index 0000000..f98382b --- /dev/null +++ b/HexFiend/HFHexTextRepresenter.m @@ -0,0 +1,203 @@ +// +// HFHexTextRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +@interface HFHexPasteboardOwner : HFPasteboardOwner { + NSUInteger _bytesPerColumn; +} +@property (nonatomic) NSUInteger bytesPerColumn; +@end + +static inline unsigned char hex2char(NSUInteger c) { + HFASSERT(c < 16); + return "0123456789ABCDEF"[c]; +} + +@implementation HFHexPasteboardOwner + +@synthesize bytesPerColumn = _bytesPerColumn; + +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { + if(!dataLength) return 0; + // -1 because no trailing space for an exact multiple. + unsigned long long spaces = _bytesPerColumn ? (dataLength-1)/_bytesPerColumn : 0; + if ((ULLONG_MAX - spaces)/2 <= dataLength) return ULLONG_MAX; + else return dataLength*2 + spaces; +} + +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { + HFASSERT([type isEqual:NSStringPboardType]); + if(length == 0) { + [pboard setString:@"" forType:type]; + return; + } + HFByteArray *byteArray = [self byteArray]; + HFASSERT(length <= NSUIntegerMax); + NSUInteger dataLength = ll2l(length); + NSUInteger stringLength = ll2l([self stringLengthForDataLength:length]); + HFASSERT(stringLength < ULLONG_MAX); + NSUInteger offset = 0, stringOffset = 0, remaining = dataLength; + unsigned char * restrict const stringBuffer = check_malloc(stringLength); + while (remaining > 0) { + unsigned char dataBuffer[64 * 1024]; + NSUInteger amountToCopy = MIN(sizeof dataBuffer, remaining); + NSUInteger bound = offset + amountToCopy - 1; + [byteArray copyBytes:dataBuffer range:HFRangeMake(offset, amountToCopy)]; + + if(_bytesPerColumn > 0 && offset > 0) { // ensure offset > 0 to skip adding a leading space + NSUInteger left = _bytesPerColumn - (offset % _bytesPerColumn); + if(left != _bytesPerColumn) { + while(left-- > 0 && offset <= bound) { + unsigned char c = dataBuffer[offset++]; + stringBuffer[stringOffset] = hex2char(c >> 4); + stringBuffer[stringOffset + 1] = hex2char(c & 0xF); + stringOffset += 2; + } + } + if(offset <= bound) + stringBuffer[stringOffset++] = ' '; + } + + if(_bytesPerColumn > 0) while(offset+_bytesPerColumn <= bound) { + for(NSUInteger j = 0; j < _bytesPerColumn; j++) { + unsigned char c = dataBuffer[offset++]; + stringBuffer[stringOffset] = hex2char(c >> 4); + stringBuffer[stringOffset + 1] = hex2char(c & 0xF); + stringOffset += 2; + } + stringBuffer[stringOffset++] = ' '; + } + + while (offset <= bound) { + unsigned char c = dataBuffer[offset++]; + stringBuffer[stringOffset] = hex2char(c >> 4); + stringBuffer[stringOffset + 1] = hex2char(c & 0xF); + stringOffset += 2; + } + + remaining -= amountToCopy; + } + + NSString *string = [[NSString alloc] initWithBytesNoCopy:stringBuffer length:stringLength encoding:NSASCIIStringEncoding freeWhenDone:YES]; + [pboard setString:string forType:type]; + [string release]; +} + +@end + +@implementation HFHexTextRepresenter + +/* No extra NSCoder support needed */ + +- (Class)_textViewClass { + return [HFRepresenterHexTextView class]; +} + +- (void)initializeView { + [super initializeView]; + [[self view] setBytesBetweenVerticalGuides:4]; + unpartneredLastNybble = UCHAR_MAX; + omittedNybbleLocation = ULLONG_MAX; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(0, 0); +} + +- (void)_clearOmittedNybble { + unpartneredLastNybble = UCHAR_MAX; + omittedNybbleLocation = ULLONG_MAX; +} + +- (BOOL)_insertionShouldDeleteLastNybble { + /* Either both the omittedNybbleLocation and unpartneredLastNybble are invalid (set to their respective maxima), or neither are */ + HFASSERT((omittedNybbleLocation == ULLONG_MAX) == (unpartneredLastNybble == UCHAR_MAX)); + /* We should delete the last nybble if our omittedNybbleLocation is the point where we would insert */ + BOOL result = NO; + if (omittedNybbleLocation != ULLONG_MAX) { + HFController *controller = [self controller]; + NSArray *selectedRanges = [controller selectedContentsRanges]; + if ([selectedRanges count] == 1) { + HFRange selectedRange = [selectedRanges[0] HFRange]; + result = (selectedRange.length == 0 && selectedRange.location > 0 && selectedRange.location - 1 == omittedNybbleLocation); + } + } + return result; +} + +- (BOOL)_canInsertText:(NSString *)text { + REQUIRE_NOT_NULL(text); + NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEFabcdef"]; + return [text rangeOfCharacterFromSet:characterSet].location != NSNotFound; +} + +- (void)insertText:(NSString *)text { + REQUIRE_NOT_NULL(text); + if (! [self _canInsertText:text]) { + /* The user typed invalid data, and we can ignore it */ + return; + } + + BOOL shouldReplacePriorByte = [self _insertionShouldDeleteLastNybble]; + if (shouldReplacePriorByte) { + HFASSERT(unpartneredLastNybble < 16); + /* Prepend unpartneredLastNybble as a nybble */ + text = [NSString stringWithFormat:@"%1X%@", unpartneredLastNybble, text]; + } + BOOL isMissingLastNybble; + NSData *data = HFDataFromHexString(text, &isMissingLastNybble); + HFASSERT([data length] > 0); + HFASSERT(shouldReplacePriorByte != isMissingLastNybble); + HFController *controller = [self controller]; + BOOL success = [controller insertData:data replacingPreviousBytes: (shouldReplacePriorByte ? 1 : 0) allowUndoCoalescing:YES]; + if (isMissingLastNybble && success) { + HFASSERT([data length] > 0); + HFASSERT(unpartneredLastNybble == UCHAR_MAX); + [data getBytes:&unpartneredLastNybble range:NSMakeRange([data length] - 1, 1)]; + NSArray *selectedRanges = [controller selectedContentsRanges]; + HFASSERT([selectedRanges count] >= 1); + HFRange selectedRange = [selectedRanges[0] HFRange]; + HFASSERT(selectedRange.location > 0); + omittedNybbleLocation = HFSubtract(selectedRange.location, 1); + } + else { + [self _clearOmittedNybble]; + } +} + +- (NSData *)dataFromPasteboardString:(NSString *)string { + REQUIRE_NOT_NULL(string); + return HFDataFromHexString(string, NULL); +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & HFControllerHideNullBytes) { + [[self view] setHidesNullBytes:[[self controller] shouldHideNullBytes]]; + } + [super controllerDidChange:bits]; + if (bits & (HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges)) { + [self _clearOmittedNybble]; + } +} + +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + HFByteArray *selection = [[self controller] byteArrayForSelectedContentsRanges]; + HFASSERT(selection != NULL); + if ([selection length] == 0) { + NSBeep(); + } else { + HFHexPasteboardOwner *owner = [HFHexPasteboardOwner ownPasteboard:pb forByteArray:selection withTypes:@[HFPrivateByteArrayPboardType, NSStringPboardType]]; + [owner setBytesPerLine:[self bytesPerLine]]; + owner.bytesPerColumn = self.bytesPerColumn; + } +} + +@end diff --git a/HexFiend/HFHexTextView.h b/HexFiend/HFHexTextView.h new file mode 100644 index 0000000..3222b65 --- /dev/null +++ b/HexFiend/HFHexTextView.h @@ -0,0 +1,15 @@ +// +// HFHexTextView.h +// HexFiend_2 +// +// Copyright 2007 __MyCompanyName__. All rights reserved. +// + +#import + + +@interface HFHexTextView : NSTextView { + +} + +@end diff --git a/HexFiend/HFHexTextView.m b/HexFiend/HFHexTextView.m new file mode 100644 index 0000000..9e6ae47 --- /dev/null +++ b/HexFiend/HFHexTextView.m @@ -0,0 +1,13 @@ +// +// HFHexTextView.m +// HexFiend_2 +// +// Copyright 2007 __MyCompanyName__. All rights reserved. +// + +#import "HFHexTextView.h" + + +@implementation HFHexTextView + +@end diff --git a/HexFiend/HFLayoutRepresenter.h b/HexFiend/HFLayoutRepresenter.h new file mode 100644 index 0000000..a493304 --- /dev/null +++ b/HexFiend/HFLayoutRepresenter.h @@ -0,0 +1,80 @@ +// +// HFLayoutRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFLayoutRepresenter + @brief An HFRepresenter responsible for arranging the views of other HFRepresenters attached to the same HFController. + + HFLayoutRepresenter is an HFRepresenter that manages the views of other HFRepresenters. It arranges their views in its own view, mediating between them to determine their position and size, as well as global properties such as bytes per line. + + HFLayoutRepresenter has an array of representers attached to it. When you add an HFRepresenter to this array, HFLayoutRepresenter will add the view of the representer as a subview of its own view. + + \b Layout + + HFLayoutRepresenter is capable of arranging the views of other HFRepresenters to fit within the bounds of its view. The layout process depends on three things: + + -# The \c frame and \c autoresizingMask of the representers' views. + -# The \c minimumViewWidthForBytesPerLine: method, which determines the largest number of bytes per line that the representer can display for a given view width. + -# The representer's \c layoutPosition. This is an NSPoint, but it is not used geometrically. Instead, the relative values of the X and Y coordinates of the \c layoutPosition determine the relative positioning of the views, as described below. + + Thus, to have your subclass of HFRepresenter participate in the HFLayoutRepresenter system, override \c defaultLayoutPosition: to control its positioning, and possibly \\c minimumViewWidthForBytesPerLine: if your representer requires a certain width to display some bytes per line. Then ensure your view has its autoresizing mask set properly, and if its frame is fixed size, ensure that its frame is correct as well. + + The layout process, in detail, is: + + -# The views are sorted vertically by the Y component of their representers' \c layoutPosition into "slices." Smaller values appear towards the bottom of the layout view. There is no space between slices. + -# Views with equal Y components are sorted horizontally by the X component of their representers' \c layoutPosition, with smaller values appearing on the left. + -# The height of each slice is determined by the tallest view within it, excluding views that have \c NSViewHeightSizable set. If there is any leftover vertical space, it is distributed equally among all slices with at least one view with \c NSViewHeightSizable set. + -# If the layout representer is not set to maximize the bytes per line (BPL), then the BPL from the HFController is used. Otherwise: + -# Each representer is queried for its \c minimumViewWidthForBytesPerLine: + -# The largest BPL allowing each row to fit within the layout width is determined via a binary search. + -# The BPL is rounded down to a multiple of the bytes per column (if non-zero). + -# The BPL is then set on the controller. + -# For each row, each view is assigned its minimum view width for the BPL. + -# If there is any horizontal space left over, it is divided evenly between all views in that slice that have \c NSViewWidthSizable set in their autoresizing mask. + +*/ +@interface HFLayoutRepresenter : HFRepresenter { + NSMutableArray *representers; + BOOL maximizesBytesPerLine; +} + +/*! @name Managed representers + Managing the list of representers laid out by the receiver +*/ +//@{ +/// Return the array of representers managed by the receiver. */ +@property (readonly, copy) NSArray *representers; + +/*! Adds a new representer to the receiver, triggering relayout. */ +- (void)addRepresenter:(HFRepresenter *)representer; + +/*! Removes a representer to the receiver (which must be present in the receiver's array of representers), triggering relayout. */ +- (void)removeRepresenter:(HFRepresenter *)representer; +//@} + +/*! When enabled, the receiver will attempt to maximize the bytes per line so as to consume as much as possible of the bounds rect. If this is YES, then upon relayout, the receiver will recalculate the maximum number of bytes per line that can fit in its boundsRectForLayout. If this is NO, then the receiver will not change the bytes per line. */ +@property (nonatomic) BOOL maximizesBytesPerLine; + +/*! @name Layout + Methods to get information about layout, and to explicitly trigger it. +*/ +//@{ +/*! Returns the smallest width that produces the same layout (and, if maximizes bytesPerLine, the same bytes per line) as the proposed width. */ +- (CGFloat)minimumViewWidthForLayoutInProposedWidth:(CGFloat)proposedWidth; + +/*! Returns the maximum bytes per line that can fit in the proposed width (ignoring maximizesBytesPerLine). This is always a multiple of the bytesPerColumn, and always at least bytesPerColumn. */ +- (NSUInteger)maximumBytesPerLineForLayoutInProposedWidth:(CGFloat)proposedWidth; + +/*! Returns the smallest width that can support the given bytes per line. */ +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine; + +/*! Relayouts are triggered when representers are added and removed, or when the view is resized. You may call this explicitly to trigger a relayout. */ +- (void)performLayout; +//@} + +@end diff --git a/HexFiend/HFLayoutRepresenter.m b/HexFiend/HFLayoutRepresenter.m new file mode 100644 index 0000000..c36c0ea --- /dev/null +++ b/HexFiend/HFLayoutRepresenter.m @@ -0,0 +1,361 @@ +// +// HFRepresenterLayoutView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +@interface HFRepresenterLayoutViewInfo : NSObject { +@public + HFRepresenter *rep; + NSView *view; + NSPoint layoutPosition; + NSRect frame; + NSUInteger autoresizingMask; +} + +@end + +@implementation HFRepresenterLayoutViewInfo + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ : %@>", view, NSStringFromRect(frame)]; +} + +@end + +@implementation HFLayoutRepresenter + +static NSInteger sortByLayoutPosition(id a, id b, void *self) { + USE(self); + NSPoint pointA = [a layoutPosition]; + NSPoint pointB = [b layoutPosition]; + if (pointA.y < pointB.y) return -1; + else if (pointA.y > pointB.y) return 1; + else if (pointA.x < pointB.x) return -1; + else if (pointA.x > pointB.x) return 1; + else return 0; +} + +- (NSArray *)arraysOfLayoutInfos { + if (! representers) return nil; + + NSMutableArray *result = [NSMutableArray array]; + NSArray *reps = [representers sortedArrayUsingFunction:sortByLayoutPosition context:self]; + NSMutableArray *currentReps = [NSMutableArray array]; + CGFloat currentRepY = - CGFLOAT_MAX; + FOREACH(HFRepresenter*, rep, reps) { + HFRepresenterLayoutViewInfo *info = [[HFRepresenterLayoutViewInfo alloc] init]; + info->rep = rep; + info->view = [rep view]; + info->frame = [info->view frame]; + info->layoutPosition = [rep layoutPosition]; + info->autoresizingMask = [info->view autoresizingMask]; + if (info->layoutPosition.y != currentRepY && [currentReps count] > 0) { + [result addObject:[[currentReps copy] autorelease]]; + [currentReps removeAllObjects]; + } + currentRepY = info->layoutPosition.y; + [currentReps addObject:info]; + [info release]; + } + if ([currentReps count]) [result addObject:[[currentReps copy] autorelease]]; + return result; +} + +- (NSRect)boundsRectForLayout { + NSRect result = [[self view] bounds]; + /* Sometimes when we are not yet in a window, we get wonky bounds, so be paranoid. */ + if (result.size.width < 0 || result.size.height < 0) result = NSZeroRect; + return result; +} + +- (CGFloat)_computeMinHeightForLayoutInfos:(NSArray *)infos { + CGFloat result = 0; + HFASSERT(infos != NULL); + HFASSERT([infos count] > 0); + FOREACH(HFRepresenterLayoutViewInfo *, info, infos) { + if (! (info->autoresizingMask & NSViewHeightSizable)) result = MAX(result, NSHeight([info->view frame])); + } + return result; +} + +- (void)_applyYLocation:(CGFloat)yLocation andMinHeight:(CGFloat)height toInfos:(NSArray *)layoutInfos { + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + info->frame.origin.y = yLocation; + if (info->autoresizingMask & NSViewHeightSizable) info->frame.size.height = height; + } +} + +- (void)_layoutInfosHorizontally:(NSArray *)infos inRect:(NSRect)layoutRect withBytesPerLine:(NSUInteger)bytesPerLine { + CGFloat nextX = NSMinX(layoutRect); + NSUInteger numHorizontallyResizable = 0; + FOREACH(HFRepresenterLayoutViewInfo *, info, infos) { + CGFloat minWidth = [info->rep minimumViewWidthForBytesPerLine:bytesPerLine]; + info->frame.origin.x = nextX; + info->frame.size.width = minWidth; + nextX += minWidth; + numHorizontallyResizable += !! (info->autoresizingMask & NSViewWidthSizable); + } + + CGFloat remainingWidth = NSMaxX(layoutRect) - nextX; + if (numHorizontallyResizable > 0 && remainingWidth > 0) { + NSView *view = [self view]; + CGFloat remainingPixels = [view convertSize:NSMakeSize(remainingWidth, 0) toView:nil].width; + HFASSERT(remainingPixels > 0); + CGFloat pixelsPerView = HFFloor(HFFloor(remainingPixels) / (CGFloat)numHorizontallyResizable); + if (pixelsPerView > 0) { + CGFloat pointsPerView = [view convertSize:NSMakeSize(pixelsPerView, 0) fromView:nil].width; + CGFloat pointsAdded = 0; + FOREACH(HFRepresenterLayoutViewInfo *, info, infos) { + info->frame.origin.x += pointsAdded; + if (info->autoresizingMask & NSViewWidthSizable) { + info->frame.size.width += pointsPerView; + pointsAdded += pointsPerView; + } + } + } + } +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + CGFloat result = 0; + NSArray *arraysOfLayoutInfos = [self arraysOfLayoutInfos]; + + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + CGFloat minWidthForRow = 0; + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + minWidthForRow += [info->rep minimumViewWidthForBytesPerLine:bytesPerLine]; + } + result = MAX(result, minWidthForRow); + } + return result; +} + +- (NSUInteger)_computeBytesPerLineForArraysOfLayoutInfos:(NSArray *)arraysOfLayoutInfos forLayoutInRect:(NSRect)layoutRect { + /* The granularity is our own granularity (probably 1), LCMed with the granularities of all other representers */ + NSUInteger granularity = [self byteGranularity]; + FOREACH(HFRepresenter *, representer, representers) { + granularity = HFLeastCommonMultiple(granularity, [representer byteGranularity]); + } + HFASSERT(granularity >= 1); + + NSUInteger newNumGranules = (NSUIntegerMax - 1) / granularity; + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + NSUInteger maxKnownGood = 0, minKnownBad = newNumGranules + 1; + while (maxKnownGood + 1 < minKnownBad) { + CGFloat requiredSpace = 0; + NSUInteger proposedNumGranules = maxKnownGood + (minKnownBad - maxKnownGood)/2; + NSUInteger proposedBytesPerLine = proposedNumGranules * granularity; + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + requiredSpace += [info->rep minimumViewWidthForBytesPerLine:proposedBytesPerLine]; + if (requiredSpace > NSWidth(layoutRect)) break; + } + if (requiredSpace > NSWidth(layoutRect)) minKnownBad = proposedNumGranules; + else maxKnownGood = proposedNumGranules; + } + newNumGranules = maxKnownGood; + } + return MAX(1u, newNumGranules) * granularity; +} + +- (BOOL)_anyLayoutInfoIsVerticallyResizable:(NSArray *)vals { + HFASSERT(vals != NULL); + FOREACH(HFRepresenterLayoutViewInfo *, info, vals) { + if (info->autoresizingMask & NSViewHeightSizable) return YES; + } + return NO; +} + +- (BOOL)_addVerticalHeight:(CGFloat)heightPoints andOffset:(CGFloat)offsetPoints toLayoutInfos:(NSArray *)layoutInfos { + BOOL isVerticallyResizable = [self _anyLayoutInfoIsVerticallyResizable:layoutInfos]; + CGFloat totalHeight = [self _computeMinHeightForLayoutInfos:layoutInfos] + heightPoints; + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + info->frame.origin.y += offsetPoints; + if (isVerticallyResizable) { + if (info->autoresizingMask & NSViewHeightSizable) { + info->frame.size.height = totalHeight; + } + else { + CGFloat diff = totalHeight - info->frame.size.height; + HFASSERT(diff >= 0); + info->frame.origin.y += HFFloor(diff); + } + } + } + return isVerticallyResizable; +} + +- (void)_distributeVerticalSpace:(CGFloat)space toArraysOfLayoutInfos:(NSArray *)arraysOfLayoutInfos { + HFASSERT(space >= 0); + HFASSERT(arraysOfLayoutInfos != NULL); + + NSUInteger consumers = 0; + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + if ([self _anyLayoutInfoIsVerticallyResizable:layoutInfos]) consumers++; + } + if (consumers > 0) { + NSView *view = [self view]; + CGFloat availablePixels = [view convertSize:NSMakeSize(0, space) toView:nil].height; + HFASSERT(availablePixels > 0); + CGFloat pixelsPerView = HFFloor(HFFloor(availablePixels) / (CGFloat)consumers); + CGFloat pointsPerView = [view convertSize:NSMakeSize(0, pixelsPerView) fromView:nil].height; + CGFloat yOffset = 0; + if (pointsPerView > 0) { + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + if ([self _addVerticalHeight:pointsPerView andOffset:yOffset toLayoutInfos:layoutInfos]) { + yOffset += pointsPerView; + } + } + } + } +} + +- (void)performLayout { + HFController *controller = [self controller]; + if (! controller) return; + if (! representers) return; + + NSArray *arraysOfLayoutInfos = [self arraysOfLayoutInfos]; + if (! [arraysOfLayoutInfos count]) return; + + NSUInteger transaction = [controller beginPropertyChangeTransaction]; + + NSRect layoutRect = [self boundsRectForLayout]; + + NSUInteger bytesPerLine; + if (maximizesBytesPerLine) bytesPerLine = [self _computeBytesPerLineForArraysOfLayoutInfos:arraysOfLayoutInfos forLayoutInRect:layoutRect]; + else bytesPerLine = [controller bytesPerLine]; + + CGFloat yPosition = NSMinY(layoutRect); + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + HFASSERT([layoutInfos count] > 0); + CGFloat minHeight = [self _computeMinHeightForLayoutInfos:layoutInfos]; + [self _applyYLocation:yPosition andMinHeight:minHeight toInfos:layoutInfos]; + yPosition += minHeight; + [self _layoutInfosHorizontally:layoutInfos inRect:layoutRect withBytesPerLine:bytesPerLine]; + } + + CGFloat remainingVerticalSpace = NSMaxY(layoutRect) - yPosition; + if (remainingVerticalSpace > 0) { + [self _distributeVerticalSpace:remainingVerticalSpace toArraysOfLayoutInfos:arraysOfLayoutInfos]; + } + + FOREACH(NSArray *, layoutInfoArray, arraysOfLayoutInfos) { + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfoArray) { + [info->view setFrame:info->frame]; + } + } + + [controller endPropertyChangeTransaction:transaction]; +} + +- (NSArray *)representers { + return representers ? [[representers copy] autorelease] : @[]; +} + +- (instancetype)init { + self = [super init]; + maximizesBytesPerLine = YES; + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self view]]; + [representers release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeObject:representers forKey:@"HFRepresenters"]; + [coder encodeBool:maximizesBytesPerLine forKey:@"HFMaximizesBytesPerLine"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + representers = [[coder decodeObjectForKey:@"HFRepresenters"] retain]; + maximizesBytesPerLine = [coder decodeBoolForKey:@"HFMaximizesBytesPerLine"]; + NSView *view = [self view]; + [view setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameChanged:) name:NSViewFrameDidChangeNotification object:view]; + return self; +} + +- (void)addRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + if (! representers) representers = [[NSMutableArray alloc] init]; + HFASSERT([representers indexOfObjectIdenticalTo:representer] == NSNotFound); + [representers addObject:representer]; + HFASSERT([[representer view] superview] != [self view]); + [[self view] addSubview:[representer view]]; + [self performLayout]; +} + +- (void)removeRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + HFASSERT([representers indexOfObjectIdenticalTo:representer] != NSNotFound); + NSView *view = [representer view]; + HFASSERT([view superview] == [self view]); + [view removeFromSuperview]; + [representers removeObjectIdenticalTo:representer]; + [self performLayout]; +} + +- (void)frameChanged:(NSNotification *)note { + USE(note); + [self performLayout]; +} + +- (void)initializeView { + NSView *view = [self view]; + [view setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameChanged:) name:NSViewFrameDidChangeNotification object:view]; +} + +- (NSView *)createView { + return [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]; +} + +- (void)setMaximizesBytesPerLine:(BOOL)val { + maximizesBytesPerLine = val; +} + +- (BOOL)maximizesBytesPerLine { + return maximizesBytesPerLine; +} + +- (NSUInteger)maximumBytesPerLineForLayoutInProposedWidth:(CGFloat)proposedWidth { + NSArray *arraysOfLayoutInfos = [self arraysOfLayoutInfos]; + if (! [arraysOfLayoutInfos count]) return 0; + + NSRect layoutRect = [self boundsRectForLayout]; + layoutRect.size.width = proposedWidth; + + NSUInteger bytesPerLine = [self _computeBytesPerLineForArraysOfLayoutInfos:arraysOfLayoutInfos forLayoutInRect:layoutRect]; + return bytesPerLine; +} + +- (CGFloat)minimumViewWidthForLayoutInProposedWidth:(CGFloat)proposedWidth { + NSUInteger bytesPerLine; + if ([self maximizesBytesPerLine]) { + bytesPerLine = [self maximumBytesPerLineForLayoutInProposedWidth:proposedWidth]; + } else { + bytesPerLine = [[self controller] bytesPerLine]; + } + CGFloat newWidth = [self minimumViewWidthForBytesPerLine:bytesPerLine]; + return newWidth; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + [super controllerDidChange:bits]; + if (bits & (HFControllerViewSizeRatios | HFControllerBytesPerColumn | HFControllerByteGranularity)) { + [self performLayout]; + } +} + +@end diff --git a/HexFiend/HFLineCountingRepresenter.h b/HexFiend/HFLineCountingRepresenter.h new file mode 100644 index 0000000..b711e4e --- /dev/null +++ b/HexFiend/HFLineCountingRepresenter.h @@ -0,0 +1,67 @@ +// +// HFLineCountingRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @enum HFLineNumberFormat + HFLineNumberFormat is a simple enum used to determine whether line numbers are in decimal or hexadecimal format. +*/ +typedef NS_ENUM(NSUInteger, HFLineNumberFormat) { + HFLineNumberFormatDecimal, //!< Decimal line numbers + HFLineNumberFormatHexadecimal, //!< Hexadecimal line numbers + HFLineNumberFormatMAXIMUM //!< One more than the maximum valid line number format, so that line number formats can be cycled through easily +}; + +/*! @class HFLineCountingRepresenter + @brief The HFRepresenter used to show the "line number gutter." + + HFLineCountingRepresenter is the HFRepresenter used to show the "line number gutter." HFLineCountingRepresenter makes space for a certain number of digits. +*/ +@interface HFLineCountingRepresenter : HFRepresenter { + CGFloat lineHeight; + NSUInteger digitsToRepresentContentsLength; + NSUInteger minimumDigitCount; + HFLineNumberFormat lineNumberFormat; + NSInteger interiorShadowEdge; + CGFloat preferredWidth; + CGFloat digitAdvance; +} + +/// The minimum digit count. The receiver will always ensure it is big enough to display at least the minimum digit count. The default is 2. +@property (nonatomic) NSUInteger minimumDigitCount; + +/// The number of digits we are making space for. +@property (readonly) NSUInteger digitCount; + +/// The current width that the HFRepresenter prefers to be laid out with. +@property (readonly) CGFloat preferredWidth; + +/// The line number format. +@property (nonatomic) HFLineNumberFormat lineNumberFormat; + +/// Switches to the next line number format. This is called from the view. +- (void)cycleLineNumberFormat; + +/// The edge (as an NSRectEdge) on which the view draws an interior shadow. -1 means no edge. +@property (nonatomic) NSInteger interiorShadowEdge; + +/// The border color used at the edges specified by -borderedEdges. +@property (nonatomic, copy) NSColor *borderColor; + +/*! The edges on which borders are drawn. The edge returned by interiorShadowEdge always has a border drawn. The edges are specified by a bitwise or of 1 left shifted by the NSRectEdge values. For example, to draw a border on the min x and max y edges use: (1 << NSMinXEdge) | (1 << NSMaxYEdge). 0 (or -1) specfies no edges. */ +@property (nonatomic) NSInteger borderedEdges; + +/// The background color +@property (nonatomic, copy) NSColor *backgroundColor; + +@property NSUInteger valueOffset; + +@end + +/*! Notification posted when the HFLineCountingRepresenter's width has changed because the number of digits it wants to show has increased or decreased. The object is the HFLineCountingRepresenter; there is no user info. +*/ +extern NSString *const HFLineCountingRepresenterMinimumViewWidthChanged; diff --git a/HexFiend/HFLineCountingRepresenter.m b/HexFiend/HFLineCountingRepresenter.m new file mode 100644 index 0000000..d6b0560 --- /dev/null +++ b/HexFiend/HFLineCountingRepresenter.m @@ -0,0 +1,250 @@ +// +// HFLineCountingRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +NSString *const HFLineCountingRepresenterMinimumViewWidthChanged = @"HFLineCountingRepresenterMinimumViewWidthChanged"; + +/* Returns the maximum advance in points for a hexadecimal digit for the given font (interpreted as a screen font) */ +static CGFloat maximumDigitAdvanceForFont(NSFont *font) { + REQUIRE_NOT_NULL(font); + font = [font screenFont]; + CGFloat maxDigitAdvance = 0; + NSDictionary *attributesDictionary = [[NSDictionary alloc] initWithObjectsAndKeys:font, NSFontAttributeName, nil]; + NSTextStorage *storage = [[NSTextStorage alloc] init]; + NSLayoutManager *manager = [[NSLayoutManager alloc] init]; + [storage setFont:font]; + [storage addLayoutManager:manager]; + + NSSize advancements[16] = {}; + NSGlyph glyphs[16]; + + /* Generate a glyph for every hex digit */ + for (NSUInteger i=0; i < 16; i++) { + char c = "0123456789ABCDEF"[i]; + NSString *string = [[NSString alloc] initWithBytes:&c length:1 encoding:NSASCIIStringEncoding]; + [storage replaceCharactersInRange:NSMakeRange(0, (i ? 1 : 0)) withString:string]; + [string release]; + glyphs[i] = [manager glyphAtIndex:0 isValidIndex:NULL]; + HFASSERT(glyphs[i] != NSNullGlyph); + } + + /* Get the advancements of each of those glyphs */ + [font getAdvancements:advancements forGlyphs:glyphs count:sizeof glyphs / sizeof *glyphs]; + + [manager release]; + [attributesDictionary release]; + [storage release]; + + /* Find the widest digit */ + for (NSUInteger i=0; i < sizeof glyphs / sizeof *glyphs; i++) { + maxDigitAdvance = HFMax(maxDigitAdvance, advancements[i].width); + } + return maxDigitAdvance; +} + +@implementation HFLineCountingRepresenter + +- (instancetype)init { + if ((self = [super init])) { + minimumDigitCount = 2; + digitsToRepresentContentsLength = minimumDigitCount; + interiorShadowEdge = NSMaxXEdge; + + _borderedEdges = (1 << NSMaxXEdge); + _borderColor = [[NSColor darkGrayColor] retain]; + _backgroundColor = [[NSColor colorWithCalibratedWhite:(CGFloat).87 alpha:1] retain]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeDouble:lineHeight forKey:@"HFLineHeight"]; + [coder encodeInt64:minimumDigitCount forKey:@"HFMinimumDigitCount"]; + [coder encodeInt64:lineNumberFormat forKey:@"HFLineNumberFormat"]; + [coder encodeObject:self.backgroundColor forKey:@"HFBackgroundColor"]; + [coder encodeObject:self.borderColor forKey:@"HFBorderColor"]; + [coder encodeInt64:self.borderedEdges forKey:@"HFBorderedEdges"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + lineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFLineHeight"]; + minimumDigitCount = (NSUInteger)[coder decodeInt64ForKey:@"HFMinimumDigitCount"]; + lineNumberFormat = (HFLineNumberFormat)[coder decodeInt64ForKey:@"HFLineNumberFormat"]; + + _borderedEdges = [coder decodeObjectForKey:@"HFBorderedEdges"] ? (NSInteger)[coder decodeInt64ForKey:@"HFBorderedEdges"] : 0; + _borderColor = [[coder decodeObjectForKey:@"HFBorderColor"] ?: [NSColor darkGrayColor] retain]; + _backgroundColor = [[coder decodeObjectForKey:@"HFBackgroundColor"] ?: [NSColor colorWithCalibratedWhite:(CGFloat).87 alpha:1] retain]; + + return self; +} + +- (void)dealloc { + [_borderColor release]; + [_backgroundColor release]; + [super dealloc]; +} + +- (NSView *)createView { + HFLineCountingView *result = [[HFLineCountingView alloc] initWithFrame:NSMakeRect(0, 0, 60, 10)]; + [result setRepresenter:self]; + [result setAutoresizingMask:NSViewHeightSizable]; + return result; +} + +- (void)postMinimumViewWidthChangedNotification { + [[NSNotificationCenter defaultCenter] postNotificationName:HFLineCountingRepresenterMinimumViewWidthChanged object:self]; +} + +- (void)updateDigitAdvanceWithFont:(NSFont *)font { + CGFloat newDigitAdvance = maximumDigitAdvanceForFont(font); + if (digitAdvance != newDigitAdvance) { + digitAdvance = newDigitAdvance; + [self postMinimumViewWidthChangedNotification]; + } +} + +- (void)updateFontAndLineHeight { + HFLineCountingView *view = [self view]; + HFController *controller = [self controller]; + NSFont *font = controller ? [controller font] : [NSFont fontWithName:HFDEFAULT_FONT size:HFDEFAULT_FONTSIZE]; + [view setFont:font]; + [view setLineHeight: controller ? [controller lineHeight] : HFDEFAULT_FONTSIZE]; + [self updateDigitAdvanceWithFont:font]; +} + +- (void)updateLineNumberFormat { + [[self view] setLineNumberFormat:lineNumberFormat]; +} + +- (void)updateBytesPerLine { + [[self view] setBytesPerLine:[[self controller] bytesPerLine]]; +} + +- (void)updateLineRangeToDraw { + HFFPRange lineRange = {0, 0}; + HFController *controller = [self controller]; + if (controller) { + lineRange = [controller displayedLineRange]; + } + [[self view] setLineRangeToDraw:lineRange]; +} + +- (CGFloat)preferredWidth { + if (digitAdvance == 0) { + /* This may happen if we were loaded from a nib. We are lazy about fetching the controller's font to avoid ordering issues with nib unarchival. */ + [self updateFontAndLineHeight]; + } + return (CGFloat)10. + digitsToRepresentContentsLength * digitAdvance; +} + +- (void)updateMinimumViewWidth { + HFController *controller = [self controller]; + if (controller) { + unsigned long long contentsLength = [controller contentsLength]; + NSUInteger bytesPerLine = [controller bytesPerLine]; + /* We want to know how many lines are displayed. That's equal to the contentsLength divided by bytesPerLine rounded down, except in the case that we're at the end of a line, in which case we need to show one more. Hence adding 1 and dividing gets us the right result. */ + unsigned long long lineCount = contentsLength / bytesPerLine; + unsigned long long contentsLengthRoundedToLine = HFProductULL(lineCount, bytesPerLine) - 1; + NSUInteger digitCount = [HFLineCountingView digitsRequiredToDisplayLineNumber:contentsLengthRoundedToLine inFormat:lineNumberFormat]; + NSUInteger digitWidth = MAX(minimumDigitCount, digitCount); + if (digitWidth != digitsToRepresentContentsLength) { + digitsToRepresentContentsLength = digitWidth; + [self postMinimumViewWidthChangedNotification]; + } + } +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + USE(bytesPerLine); + return [self preferredWidth]; +} + +- (HFLineNumberFormat)lineNumberFormat { + return lineNumberFormat; +} + +- (void)setLineNumberFormat:(HFLineNumberFormat)format { + HFASSERT(format < HFLineNumberFormatMAXIMUM); + lineNumberFormat = format; + [self updateLineNumberFormat]; + [self updateMinimumViewWidth]; +} + + +- (void)cycleLineNumberFormat { + lineNumberFormat = (lineNumberFormat + 1) % HFLineNumberFormatMAXIMUM; + [self updateLineNumberFormat]; + [self updateMinimumViewWidth]; +} + +- (void)initializeView { + [self updateFontAndLineHeight]; + [self updateLineNumberFormat]; + [self updateBytesPerLine]; + [self updateLineRangeToDraw]; + [self updateMinimumViewWidth]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & HFControllerDisplayedLineRange) [self updateLineRangeToDraw]; + if (bits & HFControllerBytesPerLine) [self updateBytesPerLine]; + if (bits & (HFControllerFont | HFControllerLineHeight)) [self updateFontAndLineHeight]; + if (bits & (HFControllerContentLength)) [self updateMinimumViewWidth]; +} + +- (void)setMinimumDigitCount:(NSUInteger)width { + minimumDigitCount = width; + [self updateMinimumViewWidth]; +} + +- (NSUInteger)minimumDigitCount { + return minimumDigitCount; +} + +- (NSUInteger)digitCount { + return digitsToRepresentContentsLength; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(-1, 0); +} + +- (void)setInteriorShadowEdge:(NSInteger)edge { + self->interiorShadowEdge = edge; + if ([self isViewLoaded]) { + [[self view] setNeedsDisplay:YES]; + } +} + +- (NSInteger)interiorShadowEdge { + return interiorShadowEdge; +} + + +- (void)setBorderColor:(NSColor *)color { + [_borderColor autorelease]; + _borderColor = [color copy]; + if ([self isViewLoaded]) { + [[self view] setNeedsDisplay:YES]; + } +} + +- (void)setBackgroundColor:(NSColor *)color { + [_backgroundColor autorelease]; + _backgroundColor = [color copy]; + if ([self isViewLoaded]) { + [[self view] setNeedsDisplay:YES]; + } +} + +@end diff --git a/HexFiend/HFLineCountingView.h b/HexFiend/HFLineCountingView.h new file mode 100644 index 0000000..2eb90af --- /dev/null +++ b/HexFiend/HFLineCountingView.h @@ -0,0 +1,32 @@ +// +// HFLineCountingView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +@interface HFLineCountingView : NSView { + NSLayoutManager *layoutManager; + NSTextStorage *textStorage; + NSTextContainer *textContainer; + NSDictionary *textAttributes; + + unsigned long long storedLineIndex; + NSUInteger storedLineCount; + BOOL useStringDrawingPath; + BOOL registeredForAppNotifications; +} + +@property (nonatomic, copy) NSFont *font; +@property (nonatomic) CGFloat lineHeight; +@property (nonatomic) HFFPRange lineRangeToDraw; +@property (nonatomic) NSUInteger bytesPerLine; +@property (nonatomic) HFLineNumberFormat lineNumberFormat; +@property (nonatomic, assign) HFLineCountingRepresenter *representer; + ++ (NSUInteger)digitsRequiredToDisplayLineNumber:(unsigned long long)lineNumber inFormat:(HFLineNumberFormat)format; + +@end diff --git a/HexFiend/HFLineCountingView.m b/HexFiend/HFLineCountingView.m new file mode 100644 index 0000000..6c9578d --- /dev/null +++ b/HexFiend/HFLineCountingView.m @@ -0,0 +1,689 @@ +// +// HFLineCountingView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +#define TIME_LINE_NUMBERS 0 + +#define HEX_LINE_NUMBERS_HAVE_0X_PREFIX 0 + +#define INVALID_LINE_COUNT NSUIntegerMax + +#if TIME_LINE_NUMBERS +@interface HFTimingTextView : NSTextView +@end +@implementation HFTimingTextView +- (void)drawRect:(NSRect)rect { + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + [super drawRect:rect]; + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"TextView line number time: %f", endTime - startTime); +} +@end +#endif + +@implementation HFLineCountingView + +- (void)_sharedInitLineCountingView { + layoutManager = [[NSLayoutManager alloc] init]; + textStorage = [[NSTextStorage alloc] init]; + [textStorage addLayoutManager:layoutManager]; + textContainer = [[NSTextContainer alloc] init]; + [textContainer setLineFragmentPadding:(CGFloat)5]; + [textContainer setContainerSize:NSMakeSize(self.bounds.size.width, [textContainer containerSize].height)]; + [layoutManager addTextContainer:textContainer]; +} + +- (instancetype)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self _sharedInitLineCountingView]; + } + return self; +} + +- (void)dealloc { + HFUnregisterViewForWindowAppearanceChanges(self, registeredForAppNotifications); + [_font release]; + [layoutManager release]; + [textContainer release]; + [textStorage release]; + [textAttributes release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeObject:_font forKey:@"HFFont"]; + [coder encodeDouble:_lineHeight forKey:@"HFLineHeight"]; + [coder encodeObject:_representer forKey:@"HFRepresenter"]; + [coder encodeInt64:_bytesPerLine forKey:@"HFBytesPerLine"]; + [coder encodeInt64:_lineNumberFormat forKey:@"HFLineNumberFormat"]; + [coder encodeBool:useStringDrawingPath forKey:@"HFUseStringDrawingPath"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + [self _sharedInitLineCountingView]; + _font = [[coder decodeObjectForKey:@"HFFont"] retain]; + _lineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFLineHeight"]; + _representer = [coder decodeObjectForKey:@"HFRepresenter"]; + _bytesPerLine = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesPerLine"]; + _lineNumberFormat = (NSUInteger)[coder decodeInt64ForKey:@"HFLineNumberFormat"]; + useStringDrawingPath = [coder decodeBoolForKey:@"HFUseStringDrawingPath"]; + return self; +} + +- (BOOL)isFlipped { return YES; } + +- (void)getLineNumberFormatString:(char *)outString length:(NSUInteger)length { + HFLineNumberFormat format = self.lineNumberFormat; + if (format == HFLineNumberFormatDecimal) { + strlcpy(outString, "%llu", length); + } + else if (format == HFLineNumberFormatHexadecimal) { +#if HEX_LINE_NUMBERS_HAVE_0X_PREFIX + // we want a format string like 0x%08llX + snprintf(outString, length, "0x%%0%lullX", (unsigned long)self.representer.digitCount - 2); +#else + // we want a format string like %08llX + snprintf(outString, length, "%%0%lullX", (unsigned long)self.representer.digitCount); +#endif + } + else { + strlcpy(outString, "", length); + } +} + +- (void)windowDidChangeKeyStatus:(NSNotification *)note { + USE(note); + [self setNeedsDisplay:YES]; +} + +- (void)viewDidMoveToWindow { + HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !registeredForAppNotifications); + registeredForAppNotifications = YES; + [super viewDidMoveToWindow]; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + HFUnregisterViewForWindowAppearanceChanges(self, NO); + [super viewWillMoveToWindow:newWindow]; +} + +- (void)drawGradientWithClip:(NSRect)clip { + [_representer.backgroundColor set]; + NSRectFill(clip); + + NSInteger shadowEdge = _representer.interiorShadowEdge; + + if (shadowEdge >= 0) { + const CGFloat shadowWidth = 6; + NSWindow *window = self.window; + BOOL drawActive = (window == nil || [window isKeyWindow] || [window isMainWindow]); + HFDrawShadow([[NSGraphicsContext currentContext] graphicsPort], self.bounds, shadowWidth, shadowEdge, drawActive, clip); + } +} + +- (void)drawDividerWithClip:(NSRect)clipRect { + USE(clipRect); + + +#if 1 + NSInteger edges = _representer.borderedEdges; + NSRect bounds = self.bounds; + + + // -1 means to draw no edges + if (edges == -1) { + edges = 0; + } + + [_representer.borderColor set]; + + if ((edges & (1 << NSMinXEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.width = 1; + lineRect.origin.x = 0; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + if ((edges & (1 << NSMaxXEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.width = 1; + lineRect.origin.x = NSMaxX(bounds) - lineRect.size.width; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + if ((edges & (1 << NSMinYEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.height = 1; + lineRect.origin.y = 0; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + if ((edges & (1 << NSMaxYEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.height = 1; + lineRect.origin.y = NSMaxY(bounds) - lineRect.size.height; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + + // Backwards compatibility to always draw a border on the edge with the interior shadow + + NSRect lineRect = bounds; + lineRect.size.width = 1; + NSInteger shadowEdge = _representer.interiorShadowEdge; + if (shadowEdge == NSMaxXEdge) { + lineRect.origin.x = NSMaxX(bounds) - lineRect.size.width; + } else if (shadowEdge == NSMinXEdge) { + lineRect.origin.x = NSMinX(bounds); + } else { + lineRect = NSZeroRect; + } + + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + +#else + + + if (NSIntersectsRect(lineRect, clipRect)) { + // this looks better when we have no shadow + [[NSColor lightGrayColor] set]; + NSRect bounds = self.bounds; + NSRect lineRect = bounds; + lineRect.origin.x += lineRect.size.width - 2; + lineRect.size.width = 1; + NSRectFill(NSIntersectionRect(lineRect, clipRect)); + [[NSColor whiteColor] set]; + lineRect.origin.x += 1; + NSRectFill(NSIntersectionRect(lineRect, clipRect)); + } +#endif +} + +static inline int common_prefix_length(const char *a, const char *b) { + int i; + for (i=0; ; i++) { + char ac = a[i]; + char bc = b[i]; + if (ac != bc || ac == 0 || bc == 0) break; + } + return i; +} + +/* Drawing with NSLayoutManager is necessary because the 10_2 typesetting behavior used by the old string drawing does the wrong thing for fonts like Bitstream Vera Sans Mono. Also it's an optimization for drawing the shadow. */ +- (void)drawLineNumbersWithClipLayoutManagerPerLine:(NSRect)clipRect { +#if TIME_LINE_NUMBERS + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); +#endif + NSUInteger previousTextStorageCharacterCount = [textStorage length]; + + CGFloat verticalOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + NSRect textRect = self.bounds; + textRect.size.height = _lineHeight; + textRect.origin.y -= verticalOffset * _lineHeight; + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + unsigned long long lineValue = lineIndex * _bytesPerLine; + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + char previousBuff[256]; + int previousStringLength = (int)previousTextStorageCharacterCount; + BOOL conversionResult = [[textStorage string] getCString:previousBuff maxLength:sizeof previousBuff encoding:NSASCIIStringEncoding]; + HFASSERT(conversionResult); + while (linesRemaining--) { + char formatString[64]; + [self getLineNumberFormatString:formatString length:sizeof formatString]; + + if (NSIntersectsRect(textRect, clipRect)) { + NSString *replacementCharacters = nil; + NSRange replacementRange; + char buff[256]; + int newStringLength = snprintf(buff, sizeof buff, formatString, lineValue); + HFASSERT(newStringLength > 0); + int prefixLength = common_prefix_length(previousBuff, buff); + HFASSERT(prefixLength <= newStringLength); + HFASSERT(prefixLength <= previousStringLength); + replacementRange = NSMakeRange(prefixLength, previousStringLength - prefixLength); + replacementCharacters = [[NSString alloc] initWithBytesNoCopy:buff + prefixLength length:newStringLength - prefixLength encoding:NSASCIIStringEncoding freeWhenDone:NO]; + NSUInteger glyphCount; + [textStorage replaceCharactersInRange:replacementRange withString:replacementCharacters]; + if (previousTextStorageCharacterCount == 0) { + NSDictionary *atts = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, nil]; + [textStorage setAttributes:atts range:NSMakeRange(0, newStringLength)]; + [atts release]; + } + glyphCount = [layoutManager numberOfGlyphs]; + if (glyphCount > 0) { + CGFloat maxX = NSMaxX([layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphCount - 1 effectiveRange:NULL]); + [layoutManager drawGlyphsForGlyphRange:NSMakeRange(0, glyphCount) atPoint:NSMakePoint(textRect.origin.x + textRect.size.width - maxX, textRect.origin.y)]; + } + previousTextStorageCharacterCount = newStringLength; + [replacementCharacters release]; + memcpy(previousBuff, buff, newStringLength + 1); + previousStringLength = newStringLength; + } + textRect.origin.y += _lineHeight; + lineIndex++; + lineValue = HFSum(lineValue, _bytesPerLine); + } +#if TIME_LINE_NUMBERS + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"Line number time: %f", endTime - startTime); +#endif +} + +- (void)drawLineNumbersWithClipStringDrawing:(NSRect)clipRect { + CGFloat verticalOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + NSRect textRect = self.bounds; + textRect.size.height = _lineHeight; + textRect.size.width -= 5; + textRect.origin.y -= verticalOffset * _lineHeight + 1; + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + unsigned long long lineValue = lineIndex * _bytesPerLine; + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + if (! textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + } + + char formatString[64]; + [self getLineNumberFormatString:formatString length:sizeof formatString]; + + while (linesRemaining--) { + if (NSIntersectsRect(textRect, clipRect)) { + char buff[256]; + int newStringLength = snprintf(buff, sizeof buff, formatString, lineValue); + HFASSERT(newStringLength > 0); + NSString *string = [[NSString alloc] initWithBytesNoCopy:buff length:newStringLength encoding:NSASCIIStringEncoding freeWhenDone:NO]; + [string drawInRect:textRect withAttributes:textAttributes]; + [string release]; + } + textRect.origin.y += _lineHeight; + lineIndex++; + if (linesRemaining > 0) lineValue = HFSum(lineValue, _bytesPerLine); //we could do this unconditionally, but then we risk overflow + } +} + +- (NSUInteger)characterCountForLineRange:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); + NSUInteger characterCount; + + NSUInteger lineCount = ll2l(range.length); + const NSUInteger stride = _bytesPerLine; + HFLineCountingRepresenter *rep = self.representer; + HFLineNumberFormat format = self.lineNumberFormat; + if (format == HFLineNumberFormatDecimal) { + unsigned long long lineValue = HFProductULL(range.location, _bytesPerLine); + characterCount = lineCount /* newlines */; + while (lineCount--) { + characterCount += HFCountDigitsBase10(lineValue); + lineValue += stride; + } + } + else if (format == HFLineNumberFormatHexadecimal) { + characterCount = ([rep digitCount] + 1) * lineCount; // +1 for newlines + } + else { + characterCount = -1; + } + return characterCount; +} + +- (NSString *)newLineStringForRange:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); + if(range.length == 0) + return [[NSString alloc] init]; // Placate the analyzer. + + NSUInteger lineCount = ll2l(range.length); + const NSUInteger stride = _bytesPerLine; + unsigned long long lineValue = HFProductULL(range.location, _bytesPerLine); + NSUInteger characterCount = [self characterCountForLineRange:range]; + char *buffer = check_malloc(characterCount); + NSUInteger bufferIndex = 0; + + char formatString[64]; + [self getLineNumberFormatString:formatString length:sizeof formatString]; + + while (lineCount--) { + int charCount = sprintf(buffer + bufferIndex, formatString, lineValue); + HFASSERT(charCount > 0); + bufferIndex += charCount; + buffer[bufferIndex++] = '\n'; + lineValue += stride; + } + HFASSERT(bufferIndex == characterCount); + + NSString *string = [[NSString alloc] initWithBytesNoCopy:(void *)buffer length:bufferIndex encoding:NSASCIIStringEncoding freeWhenDone:YES]; + return string; +} + +- (void)updateLayoutManagerWithLineIndex:(unsigned long long)startingLineIndex lineCount:(NSUInteger)linesRemaining { + const BOOL debug = NO; + [textStorage beginEditing]; + + if (storedLineCount == INVALID_LINE_COUNT) { + /* This usually indicates that our bytes per line or line number format changed, and we need to just recalculate everything */ + NSString *string = [self newLineStringForRange:HFRangeMake(startingLineIndex, linesRemaining)]; + [textStorage replaceCharactersInRange:NSMakeRange(0, [textStorage length]) withString:string]; + [string release]; + + } + else { + HFRange leftRangeToReplace, rightRangeToReplace; + HFRange leftRangeToStore, rightRangeToStore; + + HFRange oldRange = HFRangeMake(storedLineIndex, storedLineCount); + HFRange newRange = HFRangeMake(startingLineIndex, linesRemaining); + HFRange rangeToPreserve = HFIntersectionRange(oldRange, newRange); + + if (rangeToPreserve.length == 0) { + leftRangeToReplace = oldRange; + leftRangeToStore = newRange; + rightRangeToReplace = HFZeroRange; + rightRangeToStore = HFZeroRange; + } + else { + if (debug) NSLog(@"Preserving %llu", rangeToPreserve.length); + HFASSERT(HFRangeIsSubrangeOfRange(rangeToPreserve, newRange)); + HFASSERT(HFRangeIsSubrangeOfRange(rangeToPreserve, oldRange)); + const unsigned long long maxPreserve = HFMaxRange(rangeToPreserve); + leftRangeToReplace = HFRangeMake(oldRange.location, rangeToPreserve.location - oldRange.location); + leftRangeToStore = HFRangeMake(newRange.location, rangeToPreserve.location - newRange.location); + rightRangeToReplace = HFRangeMake(maxPreserve, HFMaxRange(oldRange) - maxPreserve); + rightRangeToStore = HFRangeMake(maxPreserve, HFMaxRange(newRange) - maxPreserve); + } + + if (debug) NSLog(@"Changing %@ -> %@", HFRangeToString(oldRange), HFRangeToString(newRange)); + if (debug) NSLog(@"LEFT: %@ -> %@", HFRangeToString(leftRangeToReplace), HFRangeToString(leftRangeToStore)); + if (debug) NSLog(@"RIGHT: %@ -> %@", HFRangeToString(rightRangeToReplace), HFRangeToString(rightRangeToStore)); + + HFASSERT(leftRangeToReplace.length == 0 || HFRangeIsSubrangeOfRange(leftRangeToReplace, oldRange)); + HFASSERT(rightRangeToReplace.length == 0 || HFRangeIsSubrangeOfRange(rightRangeToReplace, oldRange)); + + if (leftRangeToReplace.length > 0 || leftRangeToStore.length > 0) { + NSUInteger charactersToDelete = [self characterCountForLineRange:leftRangeToReplace]; + NSRange rangeToDelete = NSMakeRange(0, charactersToDelete); + if (leftRangeToStore.length == 0) { + [textStorage deleteCharactersInRange:rangeToDelete]; + if (debug) NSLog(@"Left deleting text range %@", NSStringFromRange(rangeToDelete)); + } + else { + NSString *leftRangeString = [self newLineStringForRange:leftRangeToStore]; + [textStorage replaceCharactersInRange:rangeToDelete withString:leftRangeString]; + if (debug) NSLog(@"Replacing text range %@ with %@", NSStringFromRange(rangeToDelete), leftRangeString); + [leftRangeString release]; + } + } + + if (rightRangeToReplace.length > 0 || rightRangeToStore.length > 0) { + NSUInteger charactersToDelete = [self characterCountForLineRange:rightRangeToReplace]; + NSUInteger stringLength = [textStorage length]; + HFASSERT(charactersToDelete <= stringLength); + NSRange rangeToDelete = NSMakeRange(stringLength - charactersToDelete, charactersToDelete); + if (rightRangeToStore.length == 0) { + [textStorage deleteCharactersInRange:rangeToDelete]; + if (debug) NSLog(@"Right deleting text range %@", NSStringFromRange(rangeToDelete)); + } + else { + NSString *rightRangeString = [self newLineStringForRange:rightRangeToStore]; + [textStorage replaceCharactersInRange:rangeToDelete withString:rightRangeString]; + if (debug) NSLog(@"Replacing text range %@ with %@ (for range %@)", NSStringFromRange(rangeToDelete), rightRangeString, HFRangeToString(rightRangeToStore)); + [rightRangeString release]; + } + } + } + + if (! textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + [textStorage setAttributes:textAttributes range:NSMakeRange(0, [textStorage length])]; + } + + [textStorage endEditing]; + +#if ! NDEBUG + NSString *comparisonString = [self newLineStringForRange:HFRangeMake(startingLineIndex, linesRemaining)]; + if (! [comparisonString isEqualToString:[textStorage string]]) { + NSLog(@"Not equal!"); + NSLog(@"Expected:\n%@", comparisonString); + NSLog(@"Actual:\n%@", [textStorage string]); + } + HFASSERT([comparisonString isEqualToString:[textStorage string]]); + [comparisonString release]; +#endif + + storedLineIndex = startingLineIndex; + storedLineCount = linesRemaining; +} + +- (void)drawLineNumbersWithClipSingleStringDrawing:(NSRect)clipRect { + USE(clipRect); + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + + CGFloat linesToVerticallyOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + CGFloat verticalOffset = linesToVerticallyOffset * _lineHeight + 1; + NSRect textRect = self.bounds; + textRect.size.width -= 5; + textRect.origin.y -= verticalOffset; + textRect.size.height += verticalOffset; + + if (! textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + [mutableStyle setMinimumLineHeight:_lineHeight]; + [mutableStyle setMaximumLineHeight:_lineHeight]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + } + + + NSString *string = [self newLineStringForRange:HFRangeMake(lineIndex, linesRemaining)]; + [string drawInRect:textRect withAttributes:textAttributes]; + [string release]; +} + +- (void)drawLineNumbersWithClipSingleStringCellDrawing:(NSRect)clipRect { + USE(clipRect); + const CGFloat cellTextContainerPadding = 2.f; + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + + CGFloat linesToVerticallyOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + CGFloat verticalOffset = linesToVerticallyOffset * _lineHeight + 1; + NSRect textRect = self.bounds; + textRect.size.width -= 5; + textRect.origin.y -= verticalOffset; + textRect.origin.x += cellTextContainerPadding; + textRect.size.height += verticalOffset; + + if (! textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + [mutableStyle setMinimumLineHeight:_lineHeight]; + [mutableStyle setMaximumLineHeight:_lineHeight]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + } + + NSString *string = [self newLineStringForRange:HFRangeMake(lineIndex, linesRemaining)]; + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:textAttributes]; + [string release]; + NSCell *cell = [[NSCell alloc] initTextCell:@""]; + [cell setAttributedStringValue:attributedString]; + [cell drawWithFrame:textRect inView:self]; + [[NSColor purpleColor] set]; + NSFrameRect(textRect); + [cell release]; + [attributedString release]; +} + +- (void)drawLineNumbersWithClipFullLayoutManager:(NSRect)clipRect { + USE(clipRect); + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + if (lineIndex != storedLineIndex || linesRemaining != storedLineCount) { + [self updateLayoutManagerWithLineIndex:lineIndex lineCount:linesRemaining]; + } + + CGFloat verticalOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + + NSPoint textPoint = self.bounds.origin; + textPoint.y -= verticalOffset * _lineHeight; + [layoutManager drawGlyphsForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs]) atPoint:textPoint]; +} + +- (void)drawLineNumbersWithClip:(NSRect)clipRect { +#if TIME_LINE_NUMBERS + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); +#endif + NSInteger drawingMode = (useStringDrawingPath ? 1 : 3); + switch (drawingMode) { + // Drawing can't be done right if fonts are wider than expected, but all + // of these have rather nasty behavior in that case. I've commented what + // that behavior is; the comment is hypothetical 'could' if it shouldn't + // actually be a problem in practice. + // TODO: Make a drawing mode that is "Fonts could get clipped if too wide" + // because that seems like better behavior than any of these. + case 0: + // Most fonts are too wide and every character gets piled on right (unreadable). + [self drawLineNumbersWithClipLayoutManagerPerLine:clipRect]; + break; + case 1: + // Last characters could get omitted (*not* clipped) if too wide. + // Also, most fonts have bottoms clipped (very unsigntly). + [self drawLineNumbersWithClipStringDrawing:clipRect]; + break; + case 2: + // Most fonts are too wide and wrap (breaks numbering). + [self drawLineNumbersWithClipFullLayoutManager:clipRect]; + break; + case 3: + // Fonts could wrap if too wide (breaks numbering). + // *Note that that this is the only mode that generally works.* + [self drawLineNumbersWithClipSingleStringDrawing:clipRect]; + break; + case 4: + // Most fonts are too wide and wrap (breaks numbering). + [self drawLineNumbersWithClipSingleStringCellDrawing:clipRect]; + break; + } +#if TIME_LINE_NUMBERS + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"Line number time: %f", endTime - startTime); +#endif +} + +- (void)drawRect:(NSRect)clipRect { + [self drawGradientWithClip:clipRect]; + [self drawDividerWithClip:clipRect]; + [self drawLineNumbersWithClip:clipRect]; +} + +- (void)setLineRangeToDraw:(HFFPRange)range { + if (! HFFPRangeEqualsRange(range, _lineRangeToDraw)) { + _lineRangeToDraw = range; + [self setNeedsDisplay:YES]; + } +} + +- (void)setBytesPerLine:(NSUInteger)val { + if (_bytesPerLine != val) { + _bytesPerLine = val; + storedLineCount = INVALID_LINE_COUNT; + [self setNeedsDisplay:YES]; + } +} + +- (void)setLineNumberFormat:(HFLineNumberFormat)format { + if (format != _lineNumberFormat) { + _lineNumberFormat = format; + storedLineCount = INVALID_LINE_COUNT; + [self setNeedsDisplay:YES]; + } +} + +- (BOOL)canUseStringDrawingPathForFont:(NSFont *)testFont { + NSString *name = [testFont fontName]; + // No, Menlo does not work here. + return [name isEqualToString:@"Monaco"] || [name isEqualToString:@"Courier"] || [name isEqualToString:@"Consolas"]; +} + +- (void)setFont:(NSFont *)val { + if (val != _font) { + [_font release]; + _font = [val copy]; + [textStorage deleteCharactersInRange:NSMakeRange(0, [textStorage length])]; //delete the characters so we know to set the font next time we render + [textAttributes release]; + textAttributes = nil; + storedLineCount = INVALID_LINE_COUNT; + useStringDrawingPath = [self canUseStringDrawingPathForFont:_font]; + [self setNeedsDisplay:YES]; + } +} + +- (void)setLineHeight:(CGFloat)height { + if (_lineHeight != height) { + _lineHeight = height; + [self setNeedsDisplay:YES]; + } +} + +- (void)setFrameSize:(NSSize)size { + [super setFrameSize:size]; + [textContainer setContainerSize:NSMakeSize(self.bounds.size.width, [textContainer containerSize].height)]; +} + +- (void)mouseDown:(NSEvent *)event { + USE(event); + // [_representer cycleLineNumberFormat]; +} + +- (void)scrollWheel:(NSEvent *)scrollEvent { + [_representer.controller scrollWithScrollEvent:scrollEvent]; +} + ++ (NSUInteger)digitsRequiredToDisplayLineNumber:(unsigned long long)lineNumber inFormat:(HFLineNumberFormat)format { + switch (format) { + case HFLineNumberFormatDecimal: return HFCountDigitsBase10(lineNumber); +#if HEX_LINE_NUMBERS_HAVE_0X_PREFIX + case HFLineNumberFormatHexadecimal: return 2 + HFCountDigitsBase16(lineNumber); +#else + case HFLineNumberFormatHexadecimal: return HFCountDigitsBase16(lineNumber); +#endif + default: return 0; + } +} + +@end diff --git a/HexFiend/HFPasteboardOwner.h b/HexFiend/HFPasteboardOwner.h new file mode 100644 index 0000000..d09c579 --- /dev/null +++ b/HexFiend/HFPasteboardOwner.h @@ -0,0 +1,51 @@ +// +// HFPasteboardOwner.h +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import + +@class HFByteArray; + +extern NSString *const HFPrivateByteArrayPboardType; + +@interface HFPasteboardOwner : NSObject { + @private + HFByteArray *byteArray; + NSPasteboard *pasteboard; //not retained + unsigned long long dataAmountToCopy; + NSUInteger bytesPerLine; + BOOL retainedSelfOnBehalfOfPboard; + BOOL backgroundCopyOperationFinished; + BOOL didStartModalSessionForBackgroundCopyOperation; +} + +/* Creates an HFPasteboardOwner to own the given pasteboard with the given types. Note that the NSPasteboard retains its owner. */ ++ (id)ownPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types; +- (HFByteArray *)byteArray; + +/* Performs a copy to pasteboard with progress reporting. This must be overridden if you support types other than the private pboard type. */ +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker; + +/* NSPasteboard delegate methods, declared here to indicate that subclasses should call super */ +- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type; +- (void)pasteboardChangedOwner:(NSPasteboard *)pboard; + +/* Useful property that several pasteboard types want to know */ +@property (nonatomic) NSUInteger bytesPerLine; + +/* For efficiency, Hex Fiend writes pointers to HFByteArrays into pasteboards. In the case that the user quits and relaunches Hex Fiend, we don't want to read a pointer from the old process, so each process we generate a UUID. This is constant for the lifetime of the process. */ ++ (NSString *)uuid; + +/* Unpacks a byte array from a pasteboard, preferring HFPrivateByteArrayPboardType */ ++ (HFByteArray *)unpackByteArrayFromPasteboard:(NSPasteboard *)pasteboard; + +/* Used to handle the case where copying data will require a lot of memory and give the user a chance to confirm. */ +- (unsigned long long)amountToCopyForDataLength:(unsigned long long)numBytes stringLength:(unsigned long long)stringLength; + +/* Must be overridden to return the length of a string containing this number of bytes. */ +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength; + +@end diff --git a/HexFiend/HFPasteboardOwner.m b/HexFiend/HFPasteboardOwner.m new file mode 100755 index 0000000..0ca341d --- /dev/null +++ b/HexFiend/HFPasteboardOwner.m @@ -0,0 +1,287 @@ +// +// HFPasteboardOwner.m +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import + +NSString *const HFPrivateByteArrayPboardType = @"HFPrivateByteArrayPboardType"; + +@implementation HFPasteboardOwner + ++ (void)initialize { + if (self == [HFPasteboardOwner class]) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareCommonPasteboardsForChangeInFileNotification:) name:HFPrepareForChangeInFileNotification object:nil]; + } +} + +- (instancetype)initWithPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types { + REQUIRE_NOT_NULL(pboard); + REQUIRE_NOT_NULL(array); + REQUIRE_NOT_NULL(types); + self = [super init]; + byteArray = [array retain]; + pasteboard = pboard; + [pasteboard declareTypes:types owner:self]; + + // get notified when we're about to write a file, so that if they're overwriting a file backing part of our byte array, we can properly clear or preserve our pasteboard + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeInFileNotification:) name:HFPrepareForChangeInFileNotification object:nil]; + + return self; +} ++ (id)ownPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types { + return [[[self alloc] initWithPasteboard:pboard forByteArray:array withTypes:types] autorelease]; +} + +- (void)tearDownPasteboardReferenceIfExists { + if (pasteboard) { + pasteboard = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:HFPrepareForChangeInFileNotification object:nil]; + } + if (retainedSelfOnBehalfOfPboard) { + CFRelease(self); + retainedSelfOnBehalfOfPboard = NO; + } +} + + ++ (HFByteArray *)_unpackByteArrayFromDictionary:(NSDictionary *)byteArrayDictionary { + HFByteArray *result = nil; + if (byteArrayDictionary) { + NSString *uuid = byteArrayDictionary[@"HFUUID"]; + if ([uuid isEqual:[self uuid]]) { + result = (HFByteArray *)[byteArrayDictionary[@"HFByteArray"] unsignedLongValue]; + } + } + return result; +} + ++ (HFByteArray *)unpackByteArrayFromPasteboard:(NSPasteboard *)pasteboard { + REQUIRE_NOT_NULL(pasteboard); + HFByteArray *result = [self _unpackByteArrayFromDictionary:[pasteboard propertyListForType:HFPrivateByteArrayPboardType]]; + return result; +} + +/* Try to fix up commonly named pasteboards when a file is about to be saved */ ++ (void)prepareCommonPasteboardsForChangeInFileNotification:(NSNotification *)notification { + const BOOL *cancellationPointer = [[notification userInfo][HFChangeInFileShouldCancelKey] pointerValue]; + if (*cancellationPointer) return; //don't do anything if someone requested cancellation + + NSDictionary *userInfo = [notification userInfo]; + NSArray *changedRanges = userInfo[HFChangeInFileModifiedRangesKey]; + HFFileReference *fileReference = [notification object]; + NSMutableDictionary *hint = userInfo[HFChangeInFileHintKey]; + + NSString * const names[] = {NSGeneralPboard, NSFindPboard, NSDragPboard}; + NSUInteger i; + for (i=0; i < sizeof names / sizeof *names; i++) { + NSPasteboard *pboard = [NSPasteboard pasteboardWithName:names[i]]; + HFByteArray *byteArray = [self unpackByteArrayFromPasteboard:pboard]; + if (byteArray && ! [byteArray clearDependenciesOnRanges:changedRanges inFile:fileReference hint:hint]) { + /* This pasteboard no longer works */ + [pboard declareTypes:@[] owner:nil]; + } + } +} + +- (void)changeInFileNotification:(NSNotification *)notification { + HFASSERT(pasteboard != nil); + HFASSERT(byteArray != nil); + NSDictionary *userInfo = [notification userInfo]; + const BOOL *cancellationPointer = [userInfo[HFChangeInFileShouldCancelKey] pointerValue]; + if (*cancellationPointer) return; //don't do anything if someone requested cancellation + NSMutableDictionary *hint = userInfo[HFChangeInFileHintKey]; + + NSArray *changedRanges = [notification userInfo][HFChangeInFileModifiedRangesKey]; + HFFileReference *fileReference = [notification object]; + if (! [byteArray clearDependenciesOnRanges:changedRanges inFile:fileReference hint:hint]) { + /* We can't do it */ + [self tearDownPasteboardReferenceIfExists]; + } +} + +- (void)dealloc { + [self tearDownPasteboardReferenceIfExists]; + [byteArray release]; + [super dealloc]; +} + +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { + USE(length); + USE(pboard); + USE(type); + USE(tracker); + UNIMPLEMENTED_VOID(); +} + +- (void)backgroundMoveDataToPasteboard:(NSString *)type { + @autoreleasepool { + [self writeDataInBackgroundToPasteboard:pasteboard ofLength:dataAmountToCopy forType:type trackingProgress:nil]; + [self performSelectorOnMainThread:@selector(backgroundMoveDataFinished:) withObject:nil waitUntilDone:NO]; + } +} + +- (void)backgroundMoveDataFinished:unused { + USE(unused); + HFASSERT(backgroundCopyOperationFinished == NO); + backgroundCopyOperationFinished = YES; + if (! didStartModalSessionForBackgroundCopyOperation) { + /* We haven't started the modal session, so make sure it never happens */ + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(beginModalSessionForBackgroundCopyOperation:) object:nil]; + CFRunLoopWakeUp(CFRunLoopGetCurrent()); + } + else { + /* We have started the modal session, so end it. */ + [NSApp stopModalWithCode:0]; + //stopModal: won't trigger unless we post a do-nothing event + NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0]; + [NSApp postEvent:event atStart:NO]; + } +} + +- (void)beginModalSessionForBackgroundCopyOperation:(id)unused { + USE(unused); + HFASSERT(backgroundCopyOperationFinished == NO); + HFASSERT(didStartModalSessionForBackgroundCopyOperation == NO); + didStartModalSessionForBackgroundCopyOperation = YES; +} + +- (BOOL)moveDataWithProgressReportingToPasteboard:(NSPasteboard *)pboard forType:(NSString *)type { + // The -[NSRunLoop runMode:beforeDate:] call in the middle of this function can cause it to be + // called reentrantly, which was previously causing leaks and use-after-free crashes. For + // some reason this happens basically always when copying lots of data into VMware Fusion. + // I'm not even sure what the ideal behavior would be here, but am fairly certain that this + // is the best that can be done without rewriting a portion of the background copying code. + // TODO: Figure out what the ideal behavior should be here. + + HFASSERT(pboard == pasteboard); + [self retain]; //resolving the pasteboard may release us, which deallocates us, which deallocates our tracker...make sure we survive through this function + /* Give the user a chance to request a smaller amount if it's really big */ + unsigned long long availableAmount = [byteArray length]; + unsigned long long amountToCopy = [self amountToCopyForDataLength:availableAmount stringLength:[self stringLengthForDataLength:availableAmount]]; + if (amountToCopy > 0) { + + backgroundCopyOperationFinished = NO; + didStartModalSessionForBackgroundCopyOperation = NO; + dataAmountToCopy = amountToCopy; + [NSThread detachNewThreadSelector:@selector(backgroundMoveDataToPasteboard:) toTarget:self withObject:type]; + [self performSelector:@selector(beginModalSessionForBackgroundCopyOperation:) withObject:nil afterDelay:1.0 inModes:@[NSModalPanelRunLoopMode]]; + while (! backgroundCopyOperationFinished) { + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate distantFuture]]; + } + } + [self release]; + return YES; +} + +- (void)pasteboardChangedOwner:(NSPasteboard *)pboard { + HFASSERT(pasteboard == pboard); + [self tearDownPasteboardReferenceIfExists]; +} + +- (HFByteArray *)byteArray { + return byteArray; +} + +- (void)pasteboard:(NSPasteboard *)pboard provideDataForType:(NSString *)type { + if (! pasteboard) { + /* Don't do anything, because we've torn down our pasteboard */ + return; + } + if ([type isEqualToString:HFPrivateByteArrayPboardType]) { + if (! retainedSelfOnBehalfOfPboard) { + retainedSelfOnBehalfOfPboard = YES; + CFRetain(self); + } + NSDictionary *dict = @{@"HFByteArray": @((unsigned long)byteArray), + @"HFUUID": [[self class] uuid]}; + [pboard setPropertyList:dict forType:type]; + } + else { + if (! [self moveDataWithProgressReportingToPasteboard:pboard forType:type]) { + [pboard setData:[NSData data] forType:type]; + } + } +} + +- (void)setBytesPerLine:(NSUInteger)val { bytesPerLine = val; } +- (NSUInteger)bytesPerLine { return bytesPerLine; } + ++ (NSString *)uuid { + static NSString *uuid; + if (! uuid) { + CFUUIDRef uuidRef = CFUUIDCreate(NULL); + uuid = (NSString *)CFUUIDCreateString(NULL, uuidRef); + CFRelease(uuidRef); + } + return uuid; +} + +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { USE(dataLength); UNIMPLEMENTED(); } + +- (unsigned long long)amountToCopyForDataLength:(unsigned long long)numBytes stringLength:(unsigned long long)stringLength { + unsigned long long dataLengthResult, stringLengthResult; + NSInteger alertReturn = NSIntegerMax; + const unsigned long long copyOption1 = MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT; + const unsigned long long copyOption2 = MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT; + NSString *option1String = HFDescribeByteCount(copyOption1); + NSString *option2String = HFDescribeByteCount(copyOption2); + NSString* dataSizeDescription = HFDescribeByteCount(stringLength); + if (stringLength >= MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT) { + NSString *option1 = [@"Copy " stringByAppendingString:option1String]; + NSString *option2 = [@"Copy " stringByAppendingString:option2String]; + alertReturn = NSRunAlertPanel(@"Large Clipboard", @"The copied data would occupy %@ if written to the clipboard. This is larger than the system clipboard supports. Do you want to copy only part of the data?", @"Cancel", option1, option2, dataSizeDescription); + switch (alertReturn) { + case NSAlertDefaultReturn: + default: + stringLengthResult = 0; + break; + case NSAlertAlternateReturn: + stringLengthResult = copyOption1; + break; + case NSAlertOtherReturn: + stringLengthResult = copyOption2; + break; + } + + } + else if (stringLength >= MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT) { + NSString *option1 = [@"Copy " stringByAppendingString:HFDescribeByteCount(stringLength)]; + NSString *option2 = [@"Copy " stringByAppendingString:HFDescribeByteCount(copyOption2)]; + alertReturn = NSRunAlertPanel(@"Large Clipboard", @"The copied data would occupy %@ if written to the clipboard. Performing this copy may take a long time. Do you want to copy only part of the data?", @"Cancel", option1, option2, dataSizeDescription); + switch (alertReturn) { + case NSAlertDefaultReturn: + default: + stringLengthResult = 0; + break; + case NSAlertAlternateReturn: + stringLengthResult = stringLength; + break; + case NSAlertOtherReturn: + stringLengthResult = copyOption2; + break; + } + } + else { + /* Small enough to copy it all */ + stringLengthResult = stringLength; + } + + /* Convert from string length to data length */ + if (stringLengthResult == stringLength) { + dataLengthResult = numBytes; + } + else { + unsigned long long divisor = stringLength / numBytes; + dataLengthResult = stringLengthResult / divisor; + } + + return dataLengthResult; +} + +@end diff --git a/HexFiend/HFPrivilegedHelperConnection.h b/HexFiend/HFPrivilegedHelperConnection.h new file mode 100644 index 0000000..bbee7a9 --- /dev/null +++ b/HexFiend/HFPrivilegedHelperConnection.h @@ -0,0 +1,3 @@ +#ifndef HF_NO_PRIVILEGED_FILE_OPERATIONS +#define HF_NO_PRIVILEGED_FILE_OPERATIONS +#endif \ No newline at end of file diff --git a/HexFiend/HFRepresenter.h b/HexFiend/HFRepresenter.h new file mode 100644 index 0000000..1a15f3e --- /dev/null +++ b/HexFiend/HFRepresenter.h @@ -0,0 +1,121 @@ +// +// HFRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +/*! @class HFRepresenter + @brief The principal view class of Hex Fiend's MVC architecture. + + HFRepresenter is a class that visually represents some property of the HFController, such as the data (in various formats), the scroll position, the line number, etc. An HFRepresenter is added to an HFController and then gets notified of changes to various properties, through the controllerDidChange: methods. + + HFRepresenters also have a view, accessible through the -view method. The HFRepresenter is expected to update its view to reflect the relevant properties of its HFController. If the user can interact with the view, then the HFRepresenter should pass any changes down to the HFController, which will subsequently notify all HFRepresenters of the change. + + HFRepresenter is an abstract class, with a different subclass for each possible view type. Because HFController interacts with HFRepresenters, rather than views directly, an HFRepresenter can use standard Cocoa views and controls. + + To add a new view type: + + -# Create a subclass of HFRepresenter + -# Override \c -createView to return a view (note that this method should transfer ownership) + -# Override \c -controllerDidChange:, checking the bitmask to see what properties have changed and updating your view as appropriate + -# If you plan on using this view together with other views, override \c +defaultLayoutPosition to control how your view gets positioned in an HFLayoutRepresenter + -# If your view's width depends on the properties of the controller, override some of the measurement methods, such as \c +maximumBytesPerLineForViewWidth:, so that your view gets sized correctly + +*/ +@interface HFRepresenter : NSObject { + @private + id view; + HFController *controller; + NSPoint layoutPosition; +} + +/*! @name View management + Methods related to accessing and initializing the representer's view. +*/ +//@{ +/*! Returns the view for the receiver, creating it if necessary. The view for the HFRepresenter is initially nil. When the \c -view method is called, if the view is nil, \c -createView is called and then the result is stored. This method should not be overridden; however you may want to call it to access the view. +*/ +- (id)view; + +/*! Returns YES if the view has been created, NO if it has not. To create the view, call the view method. + */ +- (BOOL)isViewLoaded; + +/*! Override point for creating the view displaying this representation. This is called on your behalf the first time the \c -view method is called, so you would not want to call this explicitly; however this method must be overridden. This follows the "create" rule, and so it should return a retained view. +*/ +- (NSView *)createView NS_RETURNS_RETAINED; + +/*! Override point for initialization of view, after the HFRepresenter has the view set as its -view property. The default implementation does nothing. +*/ +- (void)initializeView; + +//@} + +/*! @name Accessing the HFController +*/ +//@{ +/*! Returns the HFController for the receiver. This is set by the controller from the call to \c addRepresenter:. A representer can only be in one controller at a time. */ +- (HFController *)controller; +//@} + +/*! @name Property change notifications +*/ +//@{ +/*! Indicates that the properties indicated by the given bits did change, and the view should be updated as to reflect the appropriate properties. This is the main mechanism by which representers are notified of changes to the controller. +*/ +- (void)controllerDidChange:(HFControllerPropertyBits)bits; +//@} + +/*! @name HFController convenience methods + Convenience covers for certain HFController methods +*/ +//@{ +/*! Equivalent to [[self controller] bytesPerLine] */ +- (NSUInteger)bytesPerLine; + +/*! Equivalent to [[self controller] bytesPerColumn] */ +- (NSUInteger)bytesPerColumn; + +/*! Equivalent to [[self controller] representer:self changedProperties:properties] . You may call this when some internal aspect of the receiver's view (such as its frame) has changed in a way that may globally change some property of the controller, and the controller should recalculate those properties. For example, the text representers call this with HFControllerDisplayedLineRange when the view grows vertically, because more data may be displayed. +*/ +- (void)representerChangedProperties:(HFControllerPropertyBits)properties; +//@} + +/*! @name Measurement + Methods related to measuring the HFRepresenter, so that it can be laid out properly by an HFLayoutController. All of these methods are candidates for overriding. +*/ +//@{ +/*! Returns the maximum number of bytes per line for the given view size. The default value is NSUIntegerMax, which means that the representer can display any number of lines for the given view size. */ +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth; + +/*! Returns the minimum view frame size for the given bytes per line. Default is to return 0, which means that the representer can display the given bytes per line in any view size. Fixed width views should return their fixed width. */ +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine; + +/*! Returns the maximum number of lines that could be displayed at once for a given view height. Default is to return DBL_MAX. */ +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight; +//@} + +/*! Returns the required byte granularity. HFLayoutRepresenter will constrain the bytes per line to a multiple of the granularity, e.g. so that UTF-16 characters are not split across lines. If different representers have different granularities, then it will constrain it to a multiple of all granularities, which may be very large. The default implementation returns 1. */ +- (NSUInteger)byteGranularity; + +/*! @name Auto-layout methods + Methods for simple auto-layout by HFLayoutRepresenter. See the HFLayoutRepresenter class for discussion of how it lays out representer views. +*/ +//@{ + + +/// The layout position for the receiver. +@property (nonatomic) NSPoint layoutPosition; + +/*! Returns the default layout position for representers of this class. Within the -init method, the view's layout position is set to the default for this class. You may override this to control the default layout position. See HFLayoutRepresenter for a discussion of the significance of the layout postition. +*/ ++ (NSPoint)defaultLayoutPosition; + +//@} + + +@end diff --git a/HexFiend/HFRepresenter.m b/HexFiend/HFRepresenter.m new file mode 100644 index 0000000..510e3a0 --- /dev/null +++ b/HexFiend/HFRepresenter.m @@ -0,0 +1,120 @@ +// +// HFRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import "HFRepresenter.h" + +@implementation HFRepresenter + +- (id)view { + if (! view) { + view = [self createView]; + [self initializeView]; + } + return view; +} + +- (BOOL)isViewLoaded { + return !! view; +} + +- (void)initializeView { + +} + +- (instancetype)init { + self = [super init]; + [self setLayoutPosition:[[self class] defaultLayoutPosition]]; + return self; +} + +- (void)dealloc { + [view release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [coder encodeObject:controller forKey:@"HFController"]; + [coder encodePoint:layoutPosition forKey:@"HFLayoutPosition"]; + [coder encodeObject:view forKey:@"HFRepresenterView"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super init]; + layoutPosition = [coder decodePointForKey:@"HFLayoutPosition"]; + controller = [coder decodeObjectForKey:@"HFController"]; // not retained + view = [[coder decodeObjectForKey:@"HFRepresenterView"] retain]; + return self; +} + +- (NSView *)createView { + UNIMPLEMENTED(); +} + +- (HFController *)controller { + return controller; +} + +- (void)_setController:(HFController *)val { + controller = val; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + USE(bits); +} + +- (NSUInteger)bytesPerLine { + HFASSERT([self controller] != nil); + return [[self controller] bytesPerLine]; +} + +- (NSUInteger)bytesPerColumn { + HFASSERT([self controller] != nil); + return [[self controller] bytesPerColumn]; +} + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth { + USE(viewWidth); + return NSUIntegerMax; +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + USE(bytesPerLine); + return 0; +} + +- (NSUInteger)byteGranularity { + return 1; +} + +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight { + USE(viewHeight); + return DBL_MAX; +} + +- (void)selectAll:sender { + [[self controller] selectAll:sender]; +} + +- (void)representerChangedProperties:(HFControllerPropertyBits)properties { + [[self controller] representer:self changedProperties:properties]; +} + +- (void)setLayoutPosition:(NSPoint)position { + layoutPosition = position; +} + +- (NSPoint)layoutPosition { + return layoutPosition; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(0, 0); +} + +@end diff --git a/HexFiend/HFRepresenterHexTextView.h b/HexFiend/HFRepresenterHexTextView.h new file mode 100644 index 0000000..8098846 --- /dev/null +++ b/HexFiend/HFRepresenterHexTextView.h @@ -0,0 +1,21 @@ +// +// HFRepresenterHexTextView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + + +@interface HFRepresenterHexTextView : HFRepresenterTextView { + CGGlyph glyphTable[17]; + CGFloat glyphAdvancement; + CGFloat spaceAdvancement; + + BOOL hidesNullBytes; +} + +@property(nonatomic) BOOL hidesNullBytes; + +@end diff --git a/HexFiend/HFRepresenterHexTextView.m b/HexFiend/HFRepresenterHexTextView.m new file mode 100644 index 0000000..6df41d8 --- /dev/null +++ b/HexFiend/HFRepresenterHexTextView.m @@ -0,0 +1,95 @@ +// +// HFRepresenterHexTextView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +@implementation HFRepresenterHexTextView + +- (void)generateGlyphTable { + const UniChar hexchars[17] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F',' '/* Plus a space char at the end for null bytes. */}; + _Static_assert(sizeof(CGGlyph[17]) == sizeof(glyphTable), "glyphTable is the wrong type"); + NSFont *font = [[self font] screenFont]; + + bool t = CTFontGetGlyphsForCharacters((CTFontRef)font, hexchars, glyphTable, 17); + HFASSERT(t); // We don't take kindly to strange fonts around here. + + CGFloat maxAdv = 0.0; + for(int i = 0; i < 17; i++) maxAdv = HFMax(maxAdv, [font advancementForGlyph:glyphTable[i]].width); + glyphAdvancement = maxAdv; + spaceAdvancement = maxAdv; +} + +- (void)setFont:(NSFont *)font { + [super setFont:font]; + [self generateGlyphTable]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + [self generateGlyphTable]; + return self; +} + +//no need for encodeWithCoder + +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount { + HFASSERT(bytes != NULL); + HFASSERT(glyphs != NULL); + HFASSERT(numBytes <= NSUIntegerMax); + HFASSERT(resultGlyphCount != NULL); + const NSUInteger bytesPerColumn = [self bytesPerColumn]; + NSUInteger glyphIndex = 0, byteIndex = 0; + NSUInteger remainingBytesInThisColumn = (bytesPerColumn ? bytesPerColumn - offsetIntoLine % bytesPerColumn : NSUIntegerMax); + CGFloat advanceBetweenColumns = [self advanceBetweenColumns]; + while (byteIndex < numBytes) { + unsigned char byte = bytes[byteIndex++]; + + CGFloat glyphAdvancementPlusAnySpace = glyphAdvancement; + if (--remainingBytesInThisColumn == 0) { + remainingBytesInThisColumn = bytesPerColumn; + glyphAdvancementPlusAnySpace += advanceBetweenColumns; + } + + BOOL useBlank = (hidesNullBytes && byte == 0); + advances[glyphIndex] = CGSizeMake(glyphAdvancement, 0); + glyphs[glyphIndex++] = (struct HFGlyph_t){.fontIndex = 0, .glyph = glyphTable[(useBlank? 16: byte >> 4)]}; + advances[glyphIndex] = CGSizeMake(glyphAdvancementPlusAnySpace, 0); + glyphs[glyphIndex++] = (struct HFGlyph_t){.fontIndex = 0, .glyph = glyphTable[(useBlank? 16: byte & 0xF)]}; + } + + *resultGlyphCount = glyphIndex; +} + +- (CGFloat)advancePerCharacter { + return 2 * glyphAdvancement; +} + +- (CGFloat)advanceBetweenColumns { + return glyphAdvancement; +} + +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount { + return 2 * byteCount; +} + +- (BOOL)hidesNullBytes { + return hidesNullBytes; +} + +- (void)setHidesNullBytes:(BOOL)flag +{ + flag = !! flag; + if (hidesNullBytes != flag) { + hidesNullBytes = flag; + [self setNeedsDisplay:YES]; + } +} + +@end diff --git a/HexFiend/HFRepresenterStringEncodingTextView.h b/HexFiend/HFRepresenterStringEncodingTextView.h new file mode 100644 index 0000000..2a87ada --- /dev/null +++ b/HexFiend/HFRepresenterStringEncodingTextView.h @@ -0,0 +1,37 @@ +// +// HFRepresenterStringEncodingTextView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +@interface HFRepresenterStringEncodingTextView : HFRepresenterTextView { + /* Tier 0 data (always up to date) */ + NSStringEncoding encoding; + uint8_t bytesPerChar; + + /* Tier 1 data (computed synchronously on-demand) */ + BOOL tier1DataIsStale; + struct HFGlyph_t replacementGlyph; + CGFloat glyphAdvancement; + + /* Tier 2 data (computed asynchronously on-demand) */ + struct HFGlyphTrie_t glyphTable; + + NSArray *fontCache; + + /* Background thread */ + OSSpinLock glyphLoadLock; + BOOL requestedCancel; + NSMutableArray *fonts; + NSMutableIndexSet *requestedCharacters; + NSOperationQueue *glyphLoader; +} + +/// Set and get the NSStringEncoding that is used +@property (nonatomic) NSStringEncoding encoding; + +@end diff --git a/HexFiend/HFRepresenterStringEncodingTextView.m b/HexFiend/HFRepresenterStringEncodingTextView.m new file mode 100644 index 0000000..fa8bcb1 --- /dev/null +++ b/HexFiend/HFRepresenterStringEncodingTextView.m @@ -0,0 +1,540 @@ +// +// HFRepresenterStringEncodingTextView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#include + +@implementation HFRepresenterStringEncodingTextView + +static NSString *copy1CharStringForByteValue(unsigned long long byteValue, NSUInteger bytesPerChar, NSStringEncoding encoding) { + NSString *result = nil; + unsigned char bytes[sizeof byteValue]; + /* If we are little endian, then the bytesPerChar doesn't matter, because it will all come out the same. If we are big endian, then it does matter. */ +#if ! __BIG_ENDIAN__ + *(unsigned long long *)bytes = byteValue; +#else + if (bytesPerChar == sizeof(uint8_t)) { + *(uint8_t *)bytes = (uint8_t)byteValue; + } else if (bytesPerChar == sizeof(uint16_t)) { + *(uint16_t *)bytes = (uint16_t)byteValue; + } else if (bytesPerChar == sizeof(uint32_t)) { + *(uint32_t *)bytes = (uint32_t)byteValue; + } else if (bytesPerChar == sizeof(uint64_t)) { + *(uint64_t *)bytes = (uint64_t)byteValue; + } else { + [NSException raise:NSInvalidArgumentException format:@"Unsupported bytesPerChar of %u", bytesPerChar]; + } +#endif + + /* ASCII is mishandled :( */ + BOOL encodingOK = YES; + if (encoding == NSASCIIStringEncoding && bytesPerChar == 1 && bytes[0] > 0x7F) { + encodingOK = NO; + } + + + + /* Now create a string from these bytes */ + if (encodingOK) { + result = [[NSString alloc] initWithBytes:bytes length:bytesPerChar encoding:encoding]; + + if ([result length] > 1) { + /* Try precomposing it */ + NSString *temp = [[result precomposedStringWithCompatibilityMapping] copy]; + [result release]; + result = temp; + } + + /* Ensure it has exactly one character */ + if ([result length] != 1) { + [result release]; + result = nil; + } + } + + /* All done */ + return result; +} + +static BOOL getGlyphs(CGGlyph *glyphs, NSString *string, NSFont *inputFont) { + NSUInteger length = [string length]; + HFASSERT(inputFont != nil); + NEW_ARRAY(UniChar, chars, length); + [string getCharacters:chars range:NSMakeRange(0, length)]; + bool result = CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars, glyphs, length); + /* A NO return means some or all characters were not mapped. This is OK. We'll use the replacement glyph. Unless we're calculating the replacement glyph! Hmm...maybe we should have a series of replacement glyphs that we try? */ + + //////////////////////// + // Workaround for a Mavericks bug. Still present as of 10.9.5 + // TODO: Hmm, still? Should look into this again, either it's not a bug or Apple needs a poke. + if(!result) for(NSUInteger i = 0; i < length; i+=15) { + CFIndex x = length-i; + if(x > 15) x = 15; + result = CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars+i, glyphs+i, x); + if(!result) break; + } + //////////////////////// + + FREE_ARRAY(chars); + return result; +} + +static void generateGlyphs(NSFont *baseFont, NSMutableArray *fonts, struct HFGlyph_t *outGlyphs, NSInteger bytesPerChar, NSStringEncoding encoding, const NSUInteger *charactersToLoad, NSUInteger charactersToLoadCount, CGFloat *outMaxAdvance) { + /* If the caller wants the advance, initialize it to 0 */ + if (outMaxAdvance) *outMaxAdvance = 0; + + /* Invalid glyph marker */ + const struct HFGlyph_t invalidGlyph = {.fontIndex = kHFGlyphFontIndexInvalid, .glyph = -1}; + + NSCharacterSet *coveredSet = [baseFont coveredCharacterSet]; + NSMutableString *coveredGlyphFetchingString = [[NSMutableString alloc] init]; + NSMutableIndexSet *coveredGlyphIndexes = [[NSMutableIndexSet alloc] init]; + NSMutableString *substitutionFontsGlyphFetchingString = [[NSMutableString alloc] init]; + NSMutableIndexSet *substitutionGlyphIndexes = [[NSMutableIndexSet alloc] init]; + + /* Loop over all the characters, appending them to our glyph fetching string */ + NSUInteger idx; + for (idx = 0; idx < charactersToLoadCount; idx++) { + NSString *string = copy1CharStringForByteValue(charactersToLoad[idx], bytesPerChar, encoding); + if (string == nil) { + /* This byte value is not represented in this char set (e.g. upper 128 in ASCII) */ + outGlyphs[idx] = invalidGlyph; + } else { + if ([coveredSet characterIsMember:[string characterAtIndex:0]]) { + /* It's covered by our base font */ + [coveredGlyphFetchingString appendString:string]; + [coveredGlyphIndexes addIndex:idx]; + } else { + /* Maybe there's a substitution font */ + [substitutionFontsGlyphFetchingString appendString:string]; + [substitutionGlyphIndexes addIndex:idx]; + } + } + [string release]; + } + + + /* Fetch the non-substitute glyphs */ + { + NEW_ARRAY(CGGlyph, cgglyphs, [coveredGlyphFetchingString length]); + BOOL success = getGlyphs(cgglyphs, coveredGlyphFetchingString, baseFont); + HFASSERT(success == YES); + NSUInteger numGlyphs = [coveredGlyphFetchingString length]; + + /* Fill in our glyphs array */ + NSUInteger coveredGlyphIdx = [coveredGlyphIndexes firstIndex]; + for (NSUInteger i=0; i < numGlyphs; i++) { + outGlyphs[coveredGlyphIdx] = (struct HFGlyph_t){.fontIndex = 0, .glyph = cgglyphs[i]}; + coveredGlyphIdx = [coveredGlyphIndexes indexGreaterThanIndex:coveredGlyphIdx]; + + /* Record the advancement. Note that this may be more efficient to do in bulk. */ + if (outMaxAdvance) *outMaxAdvance = HFMax(*outMaxAdvance, [baseFont advancementForGlyph:cgglyphs[i]].width); + + } + HFASSERT(coveredGlyphIdx == NSNotFound); //we must have exhausted the table + FREE_ARRAY(cgglyphs); + } + + /* Now do substitution glyphs. */ + { + NSUInteger substitutionGlyphIndex = [substitutionGlyphIndexes firstIndex], numSubstitutionChars = [substitutionFontsGlyphFetchingString length]; + for (NSUInteger i=0; i < numSubstitutionChars; i++) { + CTFontRef substitutionFont = CTFontCreateForString((CTFontRef)baseFont, (CFStringRef)substitutionFontsGlyphFetchingString, CFRangeMake(i, 1)); + if (substitutionFont) { + /* We have a font for this string */ + CGGlyph glyph; + unichar c = [substitutionFontsGlyphFetchingString characterAtIndex:i]; + NSString *substring = [[NSString alloc] initWithCharacters:&c length:1]; + BOOL success = getGlyphs(&glyph, substring, (NSFont *)substitutionFont); + [substring release]; + + if (! success) { + /* Turns out there wasn't a glyph like we thought there would be, so set an invalid glyph marker */ + outGlyphs[substitutionGlyphIndex] = invalidGlyph; + } else { + /* Find the index in fonts. If none, add to it. */ + HFASSERT(fonts != nil); + NSUInteger fontIndex = [fonts indexOfObject:(id)substitutionFont]; + if (fontIndex == NSNotFound) { + [fonts addObject:(id)substitutionFont]; + fontIndex = [fonts count] - 1; + } + + /* Now make the glyph */ + HFASSERT(fontIndex < UINT16_MAX); + outGlyphs[substitutionGlyphIndex] = (struct HFGlyph_t){.fontIndex = (uint16_t)fontIndex, .glyph = glyph}; + } + + /* We're done with this */ + CFRelease(substitutionFont); + + } + substitutionGlyphIndex = [substitutionGlyphIndexes indexGreaterThanIndex:substitutionGlyphIndex]; + } + } + + [coveredGlyphFetchingString release]; + [coveredGlyphIndexes release]; + [substitutionFontsGlyphFetchingString release]; + [substitutionGlyphIndexes release]; +} + +static int compareGlyphFontIndexes(const void *p1, const void *p2) { + const struct HFGlyph_t *g1 = p1, *g2 = p2; + if (g1->fontIndex != g2->fontIndex) { + /* Prefer to sort by font index */ + return (g1->fontIndex > g2->fontIndex) - (g2->fontIndex > g1->fontIndex); + } else { + /* If they have equal font indexes, sort by glyph value */ + return (g1->glyph > g2->glyph) - (g2->glyph > g1->glyph); + } +} + +- (void)threadedPrecacheGlyphs:(const struct HFGlyph_t *)glyphs withFonts:(NSArray *)localFonts count:(NSUInteger)count { + /* This method draws glyphs anywhere, so that they get cached by CG and drawing them a second time can be fast. */ + NSUInteger i, validGlyphCount; + + /* We can use 0 advances */ + NEW_ARRAY(CGSize, advances, count); + bzero(advances, count * sizeof *advances); + + /* Make a local copy of the glyphs, and sort them according to their font index so that we can draw them with the fewest runs. */ + NEW_ARRAY(struct HFGlyph_t, validGlyphs, count); + + validGlyphCount = 0; + for (i=0; i < count; i++) { + if (glyphs[i].glyph <= kCGGlyphMax && glyphs[i].fontIndex != kHFGlyphFontIndexInvalid) { + validGlyphs[validGlyphCount++] = glyphs[i]; + } + } + qsort(validGlyphs, validGlyphCount, sizeof *validGlyphs, compareGlyphFontIndexes); + + /* Remove duplicate glyphs */ + NSUInteger trailing = 0; + struct HFGlyph_t lastGlyph = {.glyph = kCGFontIndexInvalid, .fontIndex = kHFGlyphFontIndexInvalid}; + for (i=0; i < validGlyphCount; i++) { + if (! HFGlyphEqualsGlyph(lastGlyph, validGlyphs[i])) { + lastGlyph = validGlyphs[i]; + validGlyphs[trailing++] = lastGlyph; + } + } + validGlyphCount = trailing; + + /* Draw the glyphs in runs */ + NEW_ARRAY(CGGlyph, cgglyphs, count); + NSImage *glyphDrawingImage = [[NSImage alloc] initWithSize:NSMakeSize(100, 100)]; + [glyphDrawingImage lockFocus]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + HFGlyphFontIndex runFontIndex = -1; + NSUInteger runLength = 0; + for (i=0; i <= validGlyphCount; i++) { + if (i == validGlyphCount || validGlyphs[i].fontIndex != runFontIndex) { + /* End the current run */ + if (runLength > 0) { + NSLog(@"Drawing with %@", [localFonts[runFontIndex] screenFont]); + [[localFonts[runFontIndex] screenFont] set]; + CGContextSetTextPosition(ctx, 0, 50); + CGContextShowGlyphsWithAdvances(ctx, cgglyphs, advances, runLength); + } + NSLog(@"Drew a run of length %lu", (unsigned long)runLength); + runLength = 0; + if (i < validGlyphCount) runFontIndex = validGlyphs[i].fontIndex; + } + if (i < validGlyphCount) { + /* Append to the current run */ + cgglyphs[runLength++] = validGlyphs[i].glyph; + } + } + + /* All done */ + [glyphDrawingImage unlockFocus]; + [glyphDrawingImage release]; + FREE_ARRAY(advances); + FREE_ARRAY(validGlyphs); + FREE_ARRAY(cgglyphs); +} + +- (void)threadedLoadGlyphs:(id)unused { + /* Note that this is running on a background thread */ + USE(unused); + + /* Do some things under the lock. Someone else may wish to read fonts, and we're going to write to it, so make a local copy. Also figure out what characters to load. */ + NSMutableArray *localFonts; + NSIndexSet *charactersToLoad; + OSSpinLockLock(&glyphLoadLock); + localFonts = [fonts mutableCopy]; + charactersToLoad = requestedCharacters; + /* Set requestedCharacters to nil so that the caller knows we aren't going to check again, and will have to re-invoke us. */ + requestedCharacters = nil; + OSSpinLockUnlock(&glyphLoadLock); + + /* The base font is the first font */ + NSFont *font = localFonts[0]; + + NSUInteger charVal, glyphIdx, charCount = [charactersToLoad count]; + NEW_ARRAY(struct HFGlyph_t, glyphs, charCount); + + /* Now generate our glyphs */ + NEW_ARRAY(NSUInteger, characters, charCount); + [charactersToLoad getIndexes:characters maxCount:charCount inIndexRange:NULL]; + generateGlyphs(font, localFonts, glyphs, bytesPerChar, encoding, characters, charCount, NULL); + FREE_ARRAY(characters); + + /* The first time we draw glyphs, it's slow, so pre-cache them by drawing them now. */ + // This was disabled because it blows up the CG glyph cache + // [self threadedPrecacheGlyphs:glyphs withFonts:localFonts count:charCount]; + + /* Replace fonts. Do this before we insert into the glyph trie, because the glyph trie references fonts that we're just now putting in the fonts array. */ + id oldFonts; + OSSpinLockLock(&glyphLoadLock); + oldFonts = fonts; + fonts = localFonts; + OSSpinLockUnlock(&glyphLoadLock); + [oldFonts release]; + + /* Now insert all of the glyphs into the glyph trie */ + glyphIdx = 0; + for (charVal = [charactersToLoad firstIndex]; charVal != NSNotFound; charVal = [charactersToLoad indexGreaterThanIndex:charVal]) { + HFGlyphTrieInsert(&glyphTable, charVal, glyphs[glyphIdx++]); + } + FREE_ARRAY(glyphs); + + /* Trigger a redisplay */ + [self performSelectorOnMainThread:@selector(triggerRedisplay:) withObject:nil waitUntilDone:NO]; + + /* All done. We inherited the retain on requestedCharacters, so release it. */ + [charactersToLoad release]; +} + +- (void)triggerRedisplay:unused { + USE(unused); + [self setNeedsDisplay:YES]; +} + +- (void)beginLoadGlyphsForCharacters:(NSIndexSet *)charactersToLoad { + /* Create the operation (and maybe the operation queue itself) */ + if (! glyphLoader) { + glyphLoader = [[NSOperationQueue alloc] init]; + [glyphLoader setMaxConcurrentOperationCount:1]; + } + if (! fonts) { + NSFont *font = [self font]; + fonts = [[NSMutableArray alloc] initWithObjects:&font count:1]; + } + + BOOL needToStartOperation; + OSSpinLockLock(&glyphLoadLock); + if (requestedCharacters) { + /* There's a pending request, so just add to it */ + [requestedCharacters addIndexes:charactersToLoad]; + needToStartOperation = NO; + } else { + /* There's no pending request, so we will create one */ + requestedCharacters = [charactersToLoad mutableCopy]; + needToStartOperation = YES; + } + OSSpinLockUnlock(&glyphLoadLock); + + if (needToStartOperation) { + NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadedLoadGlyphs:) object:charactersToLoad]; + [glyphLoader addOperation:op]; + [op release]; + } +} + +- (void)dealloc { + HFGlyphTreeFree(&glyphTable); + [fonts release]; + [super dealloc]; +} + +- (void)staleTieredProperties { + tier1DataIsStale = YES; + /* We have to free the glyph table */ + requestedCancel = YES; + [glyphLoader waitUntilAllOperationsAreFinished]; + requestedCancel = NO; + HFGlyphTreeFree(&glyphTable); + HFGlyphTrieInitialize(&glyphTable, bytesPerChar); + [fonts release]; + fonts = nil; + [fontCache release]; + fontCache = nil; +} + +- (void)setFont:(NSFont *)font { + [self staleTieredProperties]; + /* fonts is preloaded with our one font */ + if (! fonts) fonts = [[NSMutableArray alloc] init]; + [fonts addObject:font]; + [super setFont:font]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + encoding = (NSStringEncoding)[coder decodeInt64ForKey:@"HFStringEncoding"]; + bytesPerChar = HFStringEncodingCharacterLength(encoding); + [self staleTieredProperties]; + return self; +} + +- (instancetype)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + encoding = NSMacOSRomanStringEncoding; + bytesPerChar = HFStringEncodingCharacterLength(encoding); + [self staleTieredProperties]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeInt64:encoding forKey:@"HFStringEncoding"]; +} + +- (NSStringEncoding)encoding { + return encoding; +} + +- (void)setEncoding:(NSStringEncoding)val { + if (encoding != val) { + /* Our glyph table is now stale. Call this first to ensure our background operation is complete. */ + [self staleTieredProperties]; + + /* Store the new encoding. */ + encoding = val; + + /* Compute bytes per character */ + bytesPerChar = HFStringEncodingCharacterLength(encoding); + HFASSERT(bytesPerChar > 0); + + /* Ensure the tree knows about the new bytes per character */ + HFGlyphTrieInitialize(&glyphTable, bytesPerChar); + + /* Redraw ourselves with our new glyphs */ + [self setNeedsDisplay:YES]; + } +} + +- (void)loadTier1Data { + NSFont *font = [self font]; + + /* Use the max advance as the glyph advance */ + glyphAdvancement = HFCeil([font maximumAdvancement].width); + + /* Generate replacementGlyph */ + CGGlyph glyph[1]; + BOOL foundReplacement = NO; + if (! foundReplacement) foundReplacement = getGlyphs(glyph, @".", font); + if (! foundReplacement) foundReplacement = getGlyphs(glyph, @"*", font); + if (! foundReplacement) foundReplacement = getGlyphs(glyph, @"!", font); + if (! foundReplacement) { + /* Really we should just fall back to another font in this case */ + [NSException raise:NSInternalInconsistencyException format:@"Unable to find replacement glyph for font %@", font]; + } + replacementGlyph.fontIndex = 0; + replacementGlyph.glyph = glyph[0]; + + /* We're no longer stale */ + tier1DataIsStale = NO; +} + +/* Override of base class method for font substitution */ +- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx { + HFASSERT(idx != kHFGlyphFontIndexInvalid); + if (idx >= [fontCache count]) { + /* Our font cache is out of date. Take the lock and update the cache. */ + NSArray *newFonts = nil; + OSSpinLockLock(&glyphLoadLock); + HFASSERT(idx < [fonts count]); + newFonts = [fonts copy]; + OSSpinLockUnlock(&glyphLoadLock); + + /* Store the new cache */ + [fontCache release]; + fontCache = newFonts; + + /* Now our cache should be up to date */ + HFASSERT(idx < [fontCache count]); + } + return fontCache[idx]; +} + +/* Override of base class method in case we are 16 bit */ +- (NSUInteger)bytesPerCharacter { + return bytesPerChar; +} + +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount { + HFASSERT(bytes != NULL); + HFASSERT(glyphs != NULL); + HFASSERT(resultGlyphCount != NULL); + HFASSERT(advances != NULL); + USE(offsetIntoLine); + + /* Ensure we have advance, etc. before trying to use it */ + if (tier1DataIsStale) [self loadTier1Data]; + + CGSize advance = CGSizeMake(glyphAdvancement, 0); + NSMutableIndexSet *charactersToLoad = nil; //note: in UTF-32 this may have to move to an NSSet + + const uint8_t localBytesPerChar = bytesPerChar; + NSUInteger charIndex, numChars = numBytes / localBytesPerChar, byteIndex = 0; + for (charIndex = 0; charIndex < numChars; charIndex++) { + NSUInteger character = -1; + if (localBytesPerChar == 1) { + character = *(const uint8_t *)(bytes + byteIndex); + } else if (localBytesPerChar == 2) { + character = *(const uint16_t *)(bytes + byteIndex); + } else if (localBytesPerChar == 4) { + character = *(const uint32_t *)(bytes + byteIndex); + } + + struct HFGlyph_t glyph = HFGlyphTrieGet(&glyphTable, character); + if (glyph.glyph == 0 && glyph.fontIndex == 0) { + /* Unloaded glyph, so load it */ + if (! charactersToLoad) charactersToLoad = [[NSMutableIndexSet alloc] init]; + [charactersToLoad addIndex:character]; + glyph = replacementGlyph; + } else if (glyph.glyph == (uint16_t)-1 && glyph.fontIndex == kHFGlyphFontIndexInvalid) { + /* Missing glyph, so ignore it */ + glyph = replacementGlyph; + } else { + /* Valid glyph */ + } + + HFASSERT(glyph.fontIndex != kHFGlyphFontIndexInvalid); + + advances[charIndex] = advance; + glyphs[charIndex] = glyph; + byteIndex += localBytesPerChar; + } + *resultGlyphCount = numChars; + + if (charactersToLoad) { + [self beginLoadGlyphsForCharacters:charactersToLoad]; + [charactersToLoad release]; + } +} + +- (CGFloat)advancePerCharacter { + /* The glyph advancement is determined by our glyph table */ + if (tier1DataIsStale) [self loadTier1Data]; + return glyphAdvancement; +} + +- (CGFloat)advanceBetweenColumns { + return 0; //don't have any space between columns +} + +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount { + return byteCount / [self bytesPerCharacter]; +} + +@end diff --git a/HexFiend/HFRepresenterTextView.h b/HexFiend/HFRepresenterTextView.h new file mode 100644 index 0000000..7e9edbb --- /dev/null +++ b/HexFiend/HFRepresenterTextView.h @@ -0,0 +1,146 @@ +// +// HFRepresenterTextView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +/* Bytes per column philosophy + + _hftvflags.bytesPerColumn is the number of bytes that should be displayed consecutively, as one column. A space separates one column from the next. HexFiend 1.0 displayed 1 byte per column, and setting bytesPerColumn to 1 in this version reproduces that behavior. The vertical guidelines displayed by HexFiend 1.0 are only drawn when bytesPerColumn is set to 1. + + We use some number of bits to hold the number of bytes per column, so the highest value we can store is ((2 ^ numBits) - 1). We can't tell the user that the max is not a power of 2, so we pin the value to the highest representable power of 2, or (2 ^ (numBits - 1)). We allow integral values from 0 to the pinned maximum, inclusive; powers of 2 are not required. The setter method uses HFTV_BYTES_PER_COLUMN_MAX_VALUE to stay within the representable range. + + Since a value of zero is nonsensical, we can use it to specify no spaces at all. +*/ + +#define HFTV_BYTES_PER_COLUMN_MAX_VALUE (1 << (HFTV_BYTES_PER_COLUMN_BITFIELD_SIZE - 1)) + +@class HFTextRepresenter; + + +/* The base class for HFTextRepresenter views - such as the hex or ASCII text view */ +@interface HFRepresenterTextView : NSView { +@private; + HFTextRepresenter *representer; + NSArray *cachedSelectedRanges; + CGFloat verticalOffset; + CGFloat horizontalContainerInset; + CGFloat defaultLineHeight; + NSTimer *caretTimer; + NSWindow *pulseWindow; + NSRect pulseWindowBaseFrameInScreenCoordinates; + NSRect lastDrawnCaretRect; + NSRect caretRectToDraw; + NSUInteger bytesBetweenVerticalGuides; + NSUInteger startingLineBackgroundColorIndex; + NSArray *rowBackgroundColors; + NSMutableDictionary *callouts; + + void (^byteColoring)(uint8_t byte, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a); + + struct { + unsigned antialias:1; + unsigned drawCallouts:1; + unsigned editable:1; + unsigned caretVisible:1; + unsigned registeredForAppNotifications:1; + unsigned withinMouseDown:1; + unsigned receivedMouseUp:1; + } _hftvflags; +} + +- (instancetype)initWithRepresenter:(HFTextRepresenter *)rep; +- (void)clearRepresenter; + +- (HFTextRepresenter *)representer; + +@property (nonatomic, copy) NSFont *font; + +/* Set and get data. setData: will invalidate the correct regions (perhaps none) */ +@property (nonatomic, copy) NSData *data; +@property (nonatomic) CGFloat verticalOffset; +@property (nonatomic) NSUInteger startingLineBackgroundColorIndex; +@property (nonatomic, getter=isEditable) BOOL editable; +@property (nonatomic, copy) NSArray *styles; +@property (nonatomic) BOOL shouldAntialias; + +- (BOOL)behavesAsTextField; +- (BOOL)showsFocusRing; +- (BOOL)isWithinMouseDown; + +- (NSRect)caretRect; + +@property (nonatomic) BOOL shouldDrawCallouts; + +- (void)setByteColoring:(void (^)(uint8_t byte, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a))coloring; + +- (NSPoint)originForCharacterAtByteIndex:(NSInteger)index; +- (NSUInteger)indexOfCharacterAtPoint:(NSPoint)point; + +/* The amount of padding space to inset from the left and right side. */ +@property (nonatomic) CGFloat horizontalContainerInset; + +/* The number of bytes between vertical guides. 0 means no drawing of guides. */ +@property (nonatomic) NSUInteger bytesBetweenVerticalGuides; + +/* To be invoked from drawRect:. */ +- (void)drawCaretIfNecessaryWithClip:(NSRect)clipRect; +- (void)drawSelectionIfNecessaryWithClip:(NSRect)clipRect; + +/* For font substitution. An index of 0 means the default (base) font. */ +- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx; + +/* Uniformly "rounds" the byte range so that it contains an integer number of characters. The algorithm is to "floor:" any character intersecting the min of the range are included, and any character extending beyond the end of the range is excluded. If both the min and the max are within a single character, then an empty range is returned. */ +- (NSRange)roundPartialByteRange:(NSRange)byteRange; + +- (void)drawTextWithClip:(NSRect)clipRect restrictingToTextInRanges:(NSArray *)restrictingToRanges; + +/* Must be overridden */ +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount; + +- (void)extractGlyphsForBytes:(const unsigned char *)bytePtr range:(NSRange)byteRange intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances withInclusionRanges:(NSArray *)restrictingToRanges initialTextOffset:(CGFloat *)initialTextOffset resultingGlyphCount:(NSUInteger *)resultingGlyphCount; + +/* Must be overridden - returns the max number of glyphs for a given number of bytes */ +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount; + +- (void)updateSelectedRanges; +- (void)terminateSelectionPulse; // Start fading the pulse. + +/* Given a rect edge, return an NSRect representing the maximum edge in that direction. The dimension in the direction of the edge is 0 (so if edge is NSMaxXEdge, the resulting width is 0). The returned rect is in the coordinate space of the receiver's view. If the byte range is not displayed, returns NSZeroRect. + */ +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forRange:(NSRange)range; + +/* The background color for the line at the given index. You may override this to return different colors. You may return nil to draw no color in this line (and then the empty space color will appear) */ +- (NSColor *)backgroundColorForLine:(NSUInteger)line; +- (NSColor *)backgroundColorForEmptySpace; + +/* Defaults to 1, may override */ +- (NSUInteger)bytesPerCharacter; + +/* Cover method for [[self representer] bytesPerLine] and [[self representer] bytesPerColumn] */ +- (NSUInteger)bytesPerLine; +- (NSUInteger)bytesPerColumn; + +- (CGFloat)lineHeight; + +/* Following two must be overridden */ +- (CGFloat)advanceBetweenColumns; +- (CGFloat)advancePerCharacter; + +- (CGFloat)advancePerColumn; +- (CGFloat)totalAdvanceForBytesInRange:(NSRange)range; + +/* Returns the number of lines that could be shown in this view at its given height (expressed in its local coordinate space) */ +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight; + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth; +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine; + +- (IBAction)selectAll:sender; + + +@end diff --git a/HexFiend/HFRepresenterTextView.m b/HexFiend/HFRepresenterTextView.m new file mode 100644 index 0000000..95a763d --- /dev/null +++ b/HexFiend/HFRepresenterTextView.m @@ -0,0 +1,1760 @@ +// +// HFRepresenterTextView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import +#import +#import + +static const NSTimeInterval HFCaretBlinkFrequency = 0.56; + +@implementation HFRepresenterTextView + +- (NSUInteger)_getGlyphs:(CGGlyph *)glyphs forString:(NSString *)string font:(NSFont *)inputFont { + NSUInteger length = [string length]; + UniChar chars[256]; + HFASSERT(length <= sizeof chars / sizeof *chars); + HFASSERT(inputFont != nil); + [string getCharacters:chars range:NSMakeRange(0, length)]; + if (! CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars, glyphs, length)) { + /* Some or all characters were not mapped. This is OK. We'll use the replacement glyph. */ + } + return length; +} + +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingLayoutManager:(NSLayoutManager *)layoutManager glyphs:(CGGlyph *)glyphs { + HFASSERT(layoutManager != NULL); + HFASSERT(string != NULL); + NSGlyph nsglyphs[GLYPH_BUFFER_SIZE]; + [[[layoutManager textStorage] mutableString] setString:string]; + NSUInteger glyphIndex, glyphCount = [layoutManager getGlyphs:nsglyphs range:NSMakeRange(0, MIN(GLYPH_BUFFER_SIZE, [layoutManager numberOfGlyphs]))]; + if (glyphs != NULL) { + /* Convert from unsigned int NSGlyphs to unsigned short CGGlyphs */ + for (glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) { + /* Get rid of NSControlGlyph */ + NSGlyph modifiedGlyph = nsglyphs[glyphIndex] == NSControlGlyph ? NSNullGlyph : nsglyphs[glyphIndex]; + HFASSERT(modifiedGlyph <= USHRT_MAX); + glyphs[glyphIndex] = (CGGlyph)modifiedGlyph; + } + } + return glyphCount; +} + +/* Returns the number of glyphs for the given string, using the given text view, and generating the glyphs if the glyphs parameter is not NULL */ +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingTextView:(NSTextView *)textView glyphs:(CGGlyph *)glyphs { + HFASSERT(string != NULL); + HFASSERT(textView != NULL); + [textView setString:string]; + [textView setNeedsDisplay:YES]; //ligature generation doesn't seem to happen without this, for some reason. This seems very fragile! We should find a better way to get this ligature information!! + return [self _glyphsForString:string withGeneratingLayoutManager:[textView layoutManager] glyphs:glyphs]; +} + +- (NSArray *)displayedSelectedContentsRanges { + if (! cachedSelectedRanges) { + cachedSelectedRanges = [[[self representer] displayedSelectedContentsRanges] copy]; + } + return cachedSelectedRanges; +} + +- (BOOL)_shouldHaveCaretTimer { + NSWindow *window = [self window]; + if (window == NULL) return NO; + if (! [window isKeyWindow]) return NO; + if (self != [window firstResponder]) return NO; + if (! _hftvflags.editable) return NO; + NSArray *ranges = [self displayedSelectedContentsRanges]; + if ([ranges count] != 1) return NO; + NSRange range = [ranges[0] rangeValue]; + if (range.length != 0) return NO; + return YES; +} + +- (NSUInteger)_effectiveBytesPerColumn { + /* returns the bytesPerColumn, unless it's larger than the bytes per character, in which case it returns 0 */ + NSUInteger bytesPerColumn = [self bytesPerColumn], bytesPerCharacter = [self bytesPerCharacter]; + return bytesPerColumn >= bytesPerCharacter ? bytesPerColumn : 0; +} + +// note: index may be negative +- (NSPoint)originForCharacterAtByteIndex:(NSInteger)index { + NSPoint result; + NSInteger bytesPerLine = (NSInteger)[self bytesPerLine]; + + // We want a nonnegative remainder + NSInteger lineIndex = index / bytesPerLine; + NSInteger byteIndexIntoLine = index % bytesPerLine; + while (byteIndexIntoLine < 0) { + byteIndexIntoLine += bytesPerLine; + lineIndex--; + } + + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + NSUInteger numConsumedColumns = (bytesPerColumn ? byteIndexIntoLine / bytesPerColumn : 0); + NSUInteger characterIndexIntoLine = byteIndexIntoLine / [self bytesPerCharacter]; + + result.x = [self horizontalContainerInset] + characterIndexIntoLine * [self advancePerCharacter] + numConsumedColumns * [self advanceBetweenColumns]; + result.y = (lineIndex - [self verticalOffset]) * [self lineHeight]; + + return result; +} + +- (NSUInteger)indexOfCharacterAtPoint:(NSPoint)point { + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + HFASSERT(bytesPerLine % bytesPerCharacter == 0); + CGFloat advancePerCharacter = [self advancePerCharacter]; + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + CGFloat floatRow = (CGFloat)floor([self verticalOffset] + point.y / [self lineHeight]); + NSUInteger byteIndexWithinRow; + + // to compute the column, we need to solve for byteIndexIntoLine in something like this: point.x = [self advancePerCharacter] * charIndexIntoLine + [self spaceBetweenColumns] * floor(byteIndexIntoLine / [self bytesPerColumn]). Start by computing the column (or if bytesPerColumn is 0, we don't have columns) + CGFloat insetX = point.x - [self horizontalContainerInset]; + if (insetX < 0) { + //handle the case of dragging within the container inset + byteIndexWithinRow = 0; + } + else if (bytesPerColumn == 0) { + /* We don't have columns */ + byteIndexWithinRow = bytesPerCharacter * (NSUInteger)(insetX / advancePerCharacter); + } + else { + CGFloat advancePerColumn = [self advancePerColumn]; + HFASSERT(advancePerColumn > 0); + CGFloat floatColumn = insetX / advancePerColumn; + HFASSERT(floatColumn >= 0 && floatColumn <= NSUIntegerMax); + CGFloat startOfColumn = advancePerColumn * HFFloor(floatColumn); + HFASSERT(startOfColumn <= insetX); + CGFloat xOffsetWithinColumn = insetX - startOfColumn; + CGFloat charIndexWithinColumn = xOffsetWithinColumn / advancePerCharacter; //charIndexWithinColumn may be larger than bytesPerColumn if the user clicked on the space between columns + HFASSERT(charIndexWithinColumn >= 0 && charIndexWithinColumn <= NSUIntegerMax / bytesPerCharacter); + NSUInteger byteIndexWithinColumn = bytesPerCharacter * (NSUInteger)charIndexWithinColumn; + byteIndexWithinRow = bytesPerColumn * (NSUInteger)floatColumn + byteIndexWithinColumn; //this may trigger overflow to the next column, but that's OK + byteIndexWithinRow = MIN(byteIndexWithinRow, bytesPerLine); //don't let clicking to the right of the line overflow to the next line + } + HFASSERT(floatRow >= 0 && floatRow <= NSUIntegerMax); + NSUInteger row = (NSUInteger)floatRow; + return (row * bytesPerLine + byteIndexWithinRow) / bytesPerCharacter; +} + +- (NSRect)caretRect { + NSArray *ranges = [self displayedSelectedContentsRanges]; + HFASSERT([ranges count] == 1); + NSRange range = [ranges[0] rangeValue]; + HFASSERT(range.length == 0); + + NSPoint caretBaseline = [self originForCharacterAtByteIndex:range.location]; + return NSMakeRect(caretBaseline.x - 1, caretBaseline.y, 1, [self lineHeight]); +} + +- (void)_blinkCaret:(NSTimer *)timer { + HFASSERT(timer == caretTimer); + if (_hftvflags.caretVisible) { + _hftvflags.caretVisible = NO; + [self setNeedsDisplayInRect:lastDrawnCaretRect]; + caretRectToDraw = NSZeroRect; + } + else { + _hftvflags.caretVisible = YES; + caretRectToDraw = [self caretRect]; + [self setNeedsDisplayInRect:caretRectToDraw]; + } +} + +- (void)_updateCaretTimerWithFirstResponderStatus:(BOOL)treatAsHavingFirstResponder { + BOOL hasCaretTimer = !! caretTimer; + BOOL shouldHaveCaretTimer = treatAsHavingFirstResponder && [self _shouldHaveCaretTimer]; + if (shouldHaveCaretTimer == YES && hasCaretTimer == NO) { + caretTimer = [[NSTimer timerWithTimeInterval:HFCaretBlinkFrequency target:self selector:@selector(_blinkCaret:) userInfo:nil repeats:YES] retain]; + NSRunLoop *loop = [NSRunLoop currentRunLoop]; + [loop addTimer:caretTimer forMode:NSDefaultRunLoopMode]; + [loop addTimer:caretTimer forMode:NSModalPanelRunLoopMode]; + if ([self enclosingMenuItem] != NULL) { + [loop addTimer:caretTimer forMode:NSEventTrackingRunLoopMode]; + } + } + else if (shouldHaveCaretTimer == NO && hasCaretTimer == YES) { + [caretTimer invalidate]; + [caretTimer release]; + caretTimer = nil; + caretRectToDraw = NSZeroRect; + if (! NSIsEmptyRect(lastDrawnCaretRect)) { + [self setNeedsDisplayInRect:lastDrawnCaretRect]; + } + } + HFASSERT(shouldHaveCaretTimer == !! caretTimer); +} + +- (void)_updateCaretTimer { + [self _updateCaretTimerWithFirstResponderStatus: self == [[self window] firstResponder]]; +} + +/* When you click or type, the caret appears immediately - do that here */ +- (void)_forceCaretOnIfHasCaretTimer { + if (caretTimer) { + [caretTimer invalidate]; + [caretTimer release]; + caretTimer = nil; + [self _updateCaretTimer]; + + _hftvflags.caretVisible = YES; + caretRectToDraw = [self caretRect]; + [self setNeedsDisplayInRect:caretRectToDraw]; + } +} + +/* Returns the range of lines containing the selected contents ranges (as NSValues containing NSRanges), or {NSNotFound, 0} if ranges is nil or empty */ +- (NSRange)_lineRangeForContentsRanges:(NSArray *)ranges { + NSUInteger minLine = NSUIntegerMax; + NSUInteger maxLine = 0; + NSUInteger bytesPerLine = [self bytesPerLine]; + FOREACH(NSValue *, rangeValue, ranges) { + NSRange range = [rangeValue rangeValue]; + if (range.length > 0) { + NSUInteger lineForRangeStart = range.location / bytesPerLine; + NSUInteger lineForRangeEnd = NSMaxRange(range) / bytesPerLine; + HFASSERT(lineForRangeStart <= lineForRangeEnd); + minLine = MIN(minLine, lineForRangeStart); + maxLine = MAX(maxLine, lineForRangeEnd); + } + } + if (minLine > maxLine) return NSMakeRange(NSNotFound, 0); + else return NSMakeRange(minLine, maxLine - minLine + 1); +} + +- (NSRect)_rectForLineRange:(NSRange)lineRange { + HFASSERT(lineRange.location != NSNotFound); + NSUInteger bytesPerLine = [self bytesPerLine]; + NSRect bounds = [self bounds]; + NSRect result; + result.origin.x = NSMinX(bounds); + result.size.width = NSWidth(bounds); + result.origin.y = [self originForCharacterAtByteIndex:lineRange.location * bytesPerLine].y; + result.size.height = [self lineHeight] * lineRange.length; + return result; +} + +static int range_compare(const void *ap, const void *bp) { + const NSRange *a = ap; + const NSRange *b = bp; + if (a->location < b->location) return -1; + if (a->location > b->location) return 1; + if (a->length < b->length) return -1; + if (a->length > b->length) return 1; + return 0; +} + +enum LineCoverage_t { + eCoverageNone, + eCoveragePartial, + eCoverageFull +}; + +- (void)_linesWithParityChangesFromRanges:(const NSRange *)oldRanges count:(NSUInteger)oldRangeCount toRanges:(const NSRange *)newRanges count:(NSUInteger)newRangeCount intoIndexSet:(NSMutableIndexSet *)result { + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger oldParity=0, newParity=0; + NSUInteger oldRangeIndex = 0, newRangeIndex = 0; + NSUInteger currentCharacterIndex = MIN(oldRanges[oldRangeIndex].location, newRanges[newRangeIndex].location); + oldParity = (currentCharacterIndex >= oldRanges[oldRangeIndex].location); + newParity = (currentCharacterIndex >= newRanges[newRangeIndex].location); + // NSLog(@"Old %s, new %s at %u (%u, %u)", oldParity ? "on" : "off", newParity ? "on" : "off", currentCharacterIndex, oldRanges[oldRangeIndex].location, newRanges[newRangeIndex].location); + for (;;) { + NSUInteger oldDivision = NSUIntegerMax, newDivision = NSUIntegerMax; + /* Move up to the next parity change */ + if (oldRangeIndex < oldRangeCount) { + const NSRange oldRange = oldRanges[oldRangeIndex]; + oldDivision = oldRange.location + (oldParity ? oldRange.length : 0); + } + if (newRangeIndex < newRangeCount) { + const NSRange newRange = newRanges[newRangeIndex]; + newDivision = newRange.location + (newParity ? newRange.length : 0); + } + + NSUInteger division = MIN(oldDivision, newDivision); + HFASSERT(division > currentCharacterIndex); + + // NSLog(@"Division %u", division); + + if (division == NSUIntegerMax) break; + + if (oldParity != newParity) { + /* The parities did not match through this entire range, so add all intersected lines to the result index set */ + NSUInteger startLine = currentCharacterIndex / bytesPerLine; + NSUInteger endLine = HFDivideULRoundingUp(division, bytesPerLine); + HFASSERT(endLine >= startLine); + // NSLog(@"Adding lines %u -> %u", startLine, endLine); + [result addIndexesInRange:NSMakeRange(startLine, endLine - startLine)]; + } + if (division == oldDivision) { + oldRangeIndex += oldParity; + oldParity = ! oldParity; + // NSLog(@"Old range switching %s at %u", oldParity ? "on" : "off", division); + } + if (division == newDivision) { + newRangeIndex += newParity; + newParity = ! newParity; + // NSLog(@"New range switching %s at %u", newParity ? "on" : "off", division); + } + currentCharacterIndex = division; + } +} + +- (void)_addLinesFromRanges:(const NSRange *)ranges count:(NSUInteger)count toIndexSet:(NSMutableIndexSet *)set { + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger i; + for (i=0; i < count; i++) { + NSUInteger firstLine = ranges[i].location / bytesPerLine; + NSUInteger lastLine = HFDivideULRoundingUp(NSMaxRange(ranges[i]), bytesPerLine); + [set addIndexesInRange:NSMakeRange(firstLine, lastLine - firstLine)]; + } +} + +- (NSIndexSet *)_indexSetOfLinesNeedingRedrawWhenChangingSelectionFromRanges:(NSArray *)oldSelectedRangeArray toRanges:(NSArray *)newSelectedRangeArray { + NSUInteger oldRangeCount = 0, newRangeCount = 0; + + NEW_ARRAY(NSRange, oldRanges, [oldSelectedRangeArray count]); + NEW_ARRAY(NSRange, newRanges, [newSelectedRangeArray count]); + + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + + /* Extract all the ranges into a local array */ + FOREACH(NSValue *, rangeValue1, oldSelectedRangeArray) { + NSRange range = [rangeValue1 rangeValue]; + if (range.length > 0) { + oldRanges[oldRangeCount++] = range; + } + } + FOREACH(NSValue *, rangeValue2, newSelectedRangeArray) { + NSRange range = [rangeValue2 rangeValue]; + if (range.length > 0) { + newRanges[newRangeCount++] = range; + } + } + +#if ! NDEBUG + /* Assert that ranges of arrays do not have any self-intersection; this is supposed to be enforced by our HFController. Also assert that they aren't "just touching"; if they are they should be merged into a single range. */ + for (NSUInteger i=0; i < oldRangeCount; i++) { + for (NSUInteger j=i+1; j < oldRangeCount; j++) { + HFASSERT(NSIntersectionRange(oldRanges[i], oldRanges[j]).length == 0); + HFASSERT(NSMaxRange(oldRanges[i]) != oldRanges[j].location && NSMaxRange(oldRanges[j]) != oldRanges[i].location); + } + } + for (NSUInteger i=0; i < newRangeCount; i++) { + for (NSUInteger j=i+1; j < newRangeCount; j++) { + HFASSERT(NSIntersectionRange(newRanges[i], newRanges[j]).length == 0); + HFASSERT(NSMaxRange(newRanges[i]) != newRanges[j].location && NSMaxRange(newRanges[j]) != newRanges[i].location); + } + } +#endif + + if (newRangeCount == 0) { + [self _addLinesFromRanges:oldRanges count:oldRangeCount toIndexSet:result]; + } + else if (oldRangeCount == 0) { + [self _addLinesFromRanges:newRanges count:newRangeCount toIndexSet:result]; + } + else { + /* Sort the arrays, since _linesWithParityChangesFromRanges needs it */ + qsort(oldRanges, oldRangeCount, sizeof *oldRanges, range_compare); + qsort(newRanges, newRangeCount, sizeof *newRanges, range_compare); + + [self _linesWithParityChangesFromRanges:oldRanges count:oldRangeCount toRanges:newRanges count:newRangeCount intoIndexSet:result]; + } + + FREE_ARRAY(oldRanges); + FREE_ARRAY(newRanges); + + return result; +} + +- (void)updateSelectedRanges { + NSArray *oldSelectedRanges = cachedSelectedRanges; + cachedSelectedRanges = [[[self representer] displayedSelectedContentsRanges] copy]; + NSIndexSet *indexSet = [self _indexSetOfLinesNeedingRedrawWhenChangingSelectionFromRanges:oldSelectedRanges toRanges:cachedSelectedRanges]; + BOOL lastCaretRectNeedsRedraw = ! NSIsEmptyRect(lastDrawnCaretRect); + NSRange lineRangeToInvalidate = NSMakeRange(NSUIntegerMax, 0); + for (NSUInteger lineIndex = [indexSet firstIndex]; ; lineIndex = [indexSet indexGreaterThanIndex:lineIndex]) { + if (lineIndex != NSNotFound && NSMaxRange(lineRangeToInvalidate) == lineIndex) { + lineRangeToInvalidate.length++; + } + else { + if (lineRangeToInvalidate.length > 0) { + NSRect rectToInvalidate = [self _rectForLineRange:lineRangeToInvalidate]; + [self setNeedsDisplayInRect:rectToInvalidate]; + lastCaretRectNeedsRedraw = lastCaretRectNeedsRedraw && ! NSContainsRect(rectToInvalidate, lastDrawnCaretRect); + } + lineRangeToInvalidate = NSMakeRange(lineIndex, 1); + } + if (lineIndex == NSNotFound) break; + } + + if (lastCaretRectNeedsRedraw) [self setNeedsDisplayInRect:lastDrawnCaretRect]; + [oldSelectedRanges release]; //balance the retain we borrowed from the ivar + [self _updateCaretTimer]; + [self _forceCaretOnIfHasCaretTimer]; + + // A new pulse window will be created at the new selected range if necessary. + [self terminateSelectionPulse]; +} + +- (void)drawPulseBackgroundInRect:(NSRect)pulseRect { + [[NSColor yellowColor] set]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextSaveGState(ctx); + [[NSBezierPath bezierPathWithRoundedRect:pulseRect xRadius:25 yRadius:25] addClip]; + NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[NSColor yellowColor] endingColor:[NSColor colorWithCalibratedRed:(CGFloat)1. green:(CGFloat).75 blue:0 alpha:1]]; + [gradient drawInRect:pulseRect angle:90]; + [gradient release]; + CGContextRestoreGState(ctx); +} + +- (void)fadePulseWindowTimer:(NSTimer *)timer { + // TODO: close & invalidate immediatley if view scrolls. + NSWindow *window = [timer userInfo]; + CGFloat alpha = [window alphaValue]; + alpha -= (CGFloat)(3. / 30.); + if (alpha < 0) { + [window close]; + [timer invalidate]; + } + else { + [window setAlphaValue:alpha]; + } +} + +- (void)terminateSelectionPulse { + if (pulseWindow) { + [[self window] removeChildWindow:pulseWindow]; + [pulseWindow setFrame:pulseWindowBaseFrameInScreenCoordinates display:YES animate:NO]; + [NSTimer scheduledTimerWithTimeInterval:1. / 30. target:self selector:@selector(fadePulseWindowTimer:) userInfo:pulseWindow repeats:YES]; + //release is not necessary, since it relases when closed by default + pulseWindow = nil; + pulseWindowBaseFrameInScreenCoordinates = NSZeroRect; + } +} + +- (void)drawCaretIfNecessaryWithClip:(NSRect)clipRect { + NSRect caretRect = NSIntersectionRect(caretRectToDraw, clipRect); + if (! NSIsEmptyRect(caretRect)) { + [[NSColor blackColor] set]; + NSRectFill(caretRect); + lastDrawnCaretRect = caretRect; + } + if (NSIsEmptyRect(caretRectToDraw)) lastDrawnCaretRect = NSZeroRect; +} + + +/* This is the color when we are the first responder in the key window */ +- (NSColor *)primaryTextSelectionColor { + return [NSColor selectedTextBackgroundColor]; +} + +/* This is the color when we are not in the key window */ +- (NSColor *)inactiveTextSelectionColor { + return [NSColor colorWithCalibratedWhite: (CGFloat)(212./255.) alpha:1]; +} + +/* This is the color when we are not the first responder, but we are in the key window */ +- (NSColor *)secondaryTextSelectionColor { + return [[self primaryTextSelectionColor] blendedColorWithFraction:.66 ofColor:[NSColor colorWithCalibratedWhite:.8f alpha:1]]; +} + +- (NSColor *)textSelectionColor { + NSWindow *window = [self window]; + if (window == nil) return [self primaryTextSelectionColor]; + else if (! [window isKeyWindow]) return [self inactiveTextSelectionColor]; + else if (self != [window firstResponder]) return [self secondaryTextSelectionColor]; + else return [self primaryTextSelectionColor]; +} + +- (void)drawSelectionIfNecessaryWithClip:(NSRect)clipRect { + NSArray *ranges = [self displayedSelectedContentsRanges]; + NSUInteger bytesPerLine = [self bytesPerLine]; + [[self textSelectionColor] set]; + CGFloat lineHeight = [self lineHeight]; + FOREACH(NSValue *, rangeValue, ranges) { + NSRange range = [rangeValue rangeValue]; + if (range.length > 0) { + NSUInteger startByteIndex = range.location; + NSUInteger endByteIndexForThisRange = range.location + range.length - 1; + NSUInteger byteIndex = startByteIndex; + while (byteIndex <= endByteIndexForThisRange) { + NSUInteger endByteIndexForLine = ((byteIndex / bytesPerLine) + 1) * bytesPerLine - 1; + NSUInteger endByteForThisLineOfRange = MIN(endByteIndexForThisRange, endByteIndexForLine); + NSPoint startPoint = [self originForCharacterAtByteIndex:byteIndex]; + NSPoint endPoint = [self originForCharacterAtByteIndex:endByteForThisLineOfRange]; + NSRect selectionRect = NSMakeRect(startPoint.x, startPoint.y, endPoint.x + [self advancePerCharacter] - startPoint.x, lineHeight); + NSRect clippedSelectionRect = NSIntersectionRect(selectionRect, clipRect); + if (! NSIsEmptyRect(clippedSelectionRect)) { + NSRectFill(clippedSelectionRect); + } + byteIndex = endByteForThisLineOfRange + 1; + } + } + } +} + +- (BOOL)acceptsFirstResponder { + return YES; +} + +- (BOOL)hasVisibleDisplayedSelectedContentsRange { + FOREACH(NSValue *, rangeValue, [self displayedSelectedContentsRanges]) { + NSRange range = [rangeValue rangeValue]; + if (range.length > 0) { + return YES; + } + } + return NO; +} + +- (BOOL)becomeFirstResponder { + BOOL result = [super becomeFirstResponder]; + [self _updateCaretTimerWithFirstResponderStatus:YES]; + if ([self showsFocusRing] || [self hasVisibleDisplayedSelectedContentsRange]) { + [self setNeedsDisplay:YES]; + } + return result; +} + +- (BOOL)resignFirstResponder { + BOOL result = [super resignFirstResponder]; + [self _updateCaretTimerWithFirstResponderStatus:NO]; + BOOL needsRedisplay = NO; + if ([self showsFocusRing]) needsRedisplay = YES; + else if (! NSIsEmptyRect(lastDrawnCaretRect)) needsRedisplay = YES; + else if ([self hasVisibleDisplayedSelectedContentsRange]) needsRedisplay = YES; + if (needsRedisplay) [self setNeedsDisplay:YES]; + return result; +} + +- (instancetype)initWithRepresenter:(HFTextRepresenter *)rep { + self = [super initWithFrame:NSMakeRect(0, 0, 1, 1)]; + horizontalContainerInset = 4; + representer = rep; + _hftvflags.editable = YES; + + return self; +} + +- (void)clearRepresenter { + representer = nil; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeObject:representer forKey:@"HFRepresenter"]; + [coder encodeObject:_font forKey:@"HFFont"]; + [coder encodeObject:_data forKey:@"HFData"]; + [coder encodeDouble:verticalOffset forKey:@"HFVerticalOffset"]; + [coder encodeDouble:horizontalContainerInset forKey:@"HFHorizontalContainerOffset"]; + [coder encodeDouble:defaultLineHeight forKey:@"HFDefaultLineHeight"]; + [coder encodeInt64:bytesBetweenVerticalGuides forKey:@"HFBytesBetweenVerticalGuides"]; + [coder encodeInt64:startingLineBackgroundColorIndex forKey:@"HFStartingLineBackgroundColorIndex"]; + [coder encodeObject:rowBackgroundColors forKey:@"HFRowBackgroundColors"]; + [coder encodeBool:_hftvflags.antialias forKey:@"HFAntialias"]; + [coder encodeBool:_hftvflags.drawCallouts forKey:@"HFDrawCallouts"]; + [coder encodeBool:_hftvflags.editable forKey:@"HFEditable"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + representer = [coder decodeObjectForKey:@"HFRepresenter"]; + _font = [[coder decodeObjectForKey:@"HFFont"] retain]; + _data = [[coder decodeObjectForKey:@"HFData"] retain]; + verticalOffset = (CGFloat)[coder decodeDoubleForKey:@"HFVerticalOffset"]; + horizontalContainerInset = (CGFloat)[coder decodeDoubleForKey:@"HFHorizontalContainerOffset"]; + defaultLineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFDefaultLineHeight"]; + bytesBetweenVerticalGuides = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesBetweenVerticalGuides"]; + startingLineBackgroundColorIndex = (NSUInteger)[coder decodeInt64ForKey:@"HFStartingLineBackgroundColorIndex"]; + rowBackgroundColors = [[coder decodeObjectForKey:@"HFRowBackgroundColors"] retain]; + _hftvflags.antialias = [coder decodeBoolForKey:@"HFAntialias"]; + _hftvflags.drawCallouts = [coder decodeBoolForKey:@"HFDrawCallouts"]; + _hftvflags.editable = [coder decodeBoolForKey:@"HFEditable"]; + return self; +} + +- (CGFloat)horizontalContainerInset { + return horizontalContainerInset; +} + +- (void)setHorizontalContainerInset:(CGFloat)inset { + horizontalContainerInset = inset; +} + +- (void)setBytesBetweenVerticalGuides:(NSUInteger)val { + bytesBetweenVerticalGuides = val; +} + +- (NSUInteger)bytesBetweenVerticalGuides { + return bytesBetweenVerticalGuides; +} + + +- (void)setFont:(NSFont *)val { + if (val != _font) { + [_font release]; + _font = [val retain]; + NSLayoutManager *manager = [[NSLayoutManager alloc] init]; + defaultLineHeight = [manager defaultLineHeightForFont:_font]; + [manager release]; + [self setNeedsDisplay:YES]; + } +} + +- (CGFloat)lineHeight { + return defaultLineHeight; +} + +/* The base implementation does not support font substitution, so we require that it be the base font. */ +- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx { + HFASSERT(idx == 0); + USE(idx); + return _font; +} + +- (NSRange)roundPartialByteRange:(NSRange)byteRange { + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + /* Get the left and right edges of the range */ + NSUInteger left = byteRange.location, right = NSMaxRange(byteRange); + + /* Round both to the left. This may make the range bigger or smaller, or empty! */ + left -= left % bytesPerCharacter; + right -= right % bytesPerCharacter; + + /* Done */ + HFASSERT(right >= left); + return NSMakeRange(left, right - left); + +} + +- (void)setNeedsDisplayForLinesInRange:(NSRange)lineRange { + // redisplay the lines in the given range + if (lineRange.length == 0) return; + NSUInteger firstLine = lineRange.location, lastLine = NSMaxRange(lineRange); + CGFloat lineHeight = [self lineHeight]; + CGFloat vertOffset = [self verticalOffset]; + CGFloat yOrigin = (firstLine - vertOffset) * lineHeight; + CGFloat lastLineBottom = (lastLine - vertOffset) * lineHeight; + NSRect bounds = [self bounds]; + NSRect dirtyRect = NSMakeRect(bounds.origin.x, bounds.origin.y + yOrigin, NSWidth(bounds), lastLineBottom - yOrigin); + [self setNeedsDisplayInRect:dirtyRect]; +} + +- (void)setData:(NSData *)val { + if (val != _data) { + NSUInteger oldLength = [_data length]; + NSUInteger newLength = [val length]; + const unsigned char *oldBytes = (const unsigned char *)[_data bytes]; + const unsigned char *newBytes = (const unsigned char *)[val bytes]; + NSUInteger firstDifferingIndex = HFIndexOfFirstByteThatDiffers(oldBytes, oldLength, newBytes, newLength); + if (firstDifferingIndex == NSUIntegerMax) { + /* Nothing to do! Data is identical! */ + } + else { + NSUInteger lastDifferingIndex = HFIndexOfLastByteThatDiffers(oldBytes, oldLength, newBytes, newLength); + HFASSERT(lastDifferingIndex != NSUIntegerMax); //if we have a first different byte, we must have a last different byte + /* Expand to encompass characters that they touch */ + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + firstDifferingIndex -= firstDifferingIndex % bytesPerCharacter; + lastDifferingIndex = HFRoundUpToMultipleInt(lastDifferingIndex, bytesPerCharacter); + + /* Now figure out the line range they touch */ + const NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger firstLine = firstDifferingIndex / bytesPerLine; + NSUInteger lastLine = HFDivideULRoundingUp(MAX(oldLength, newLength), bytesPerLine); + /* The +1 is for the following case - if we change the last character, then it may push the caret into the next line (even though there's no text there). This last line may have a background color, so we need to make it draw if it did not draw before (or vice versa - when deleting the last character which pulls the caret from the last line). */ + NSUInteger lastDifferingLine = (lastDifferingIndex == NSNotFound ? lastLine : HFDivideULRoundingUp(lastDifferingIndex + 1, bytesPerLine)); + if (lastDifferingLine > firstLine) { + [self setNeedsDisplayForLinesInRange:NSMakeRange(firstLine, lastDifferingLine - firstLine)]; + } + } + [_data release]; + _data = [val copy]; + [self _updateCaretTimer]; + } +} + +- (void)setStyles:(NSArray *)newStyles { + if (! [_styles isEqual:newStyles]) { + + /* Figure out which styles changed - that is, we want to compute those objects that are not in oldStyles or newStyles, but not both. */ + NSMutableSet *changedStyles = _styles ? [[NSMutableSet alloc] initWithArray:_styles] : [[NSMutableSet alloc] init]; + FOREACH(HFTextVisualStyleRun *, run, newStyles) { + if ([changedStyles containsObject:run]) { + [changedStyles removeObject:run]; + } + else { + [changedStyles addObject:run]; + } + } + + /* Now figure out the first and last indexes of changed ranges. */ + NSUInteger firstChangedIndex = NSUIntegerMax, lastChangedIndex = 0; + FOREACH(HFTextVisualStyleRun *, changedRun, changedStyles) { + NSRange range = [changedRun range]; + if (range.length > 0) { + firstChangedIndex = MIN(firstChangedIndex, range.location); + lastChangedIndex = MAX(lastChangedIndex, NSMaxRange(range) - 1); + } + } + + /* Don't need this any more */ + [changedStyles release]; + + /* Expand to cover all touched characters */ + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + firstChangedIndex -= firstChangedIndex % bytesPerCharacter; + lastChangedIndex = HFRoundUpToMultipleInt(lastChangedIndex, bytesPerCharacter); + + /* Figure out the changed lines, and trigger redisplay */ + if (firstChangedIndex <= lastChangedIndex) { + const NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger firstLine = firstChangedIndex / bytesPerLine; + NSUInteger lastLine = HFDivideULRoundingUp(lastChangedIndex, bytesPerLine); + [self setNeedsDisplayForLinesInRange:NSMakeRange(firstLine, lastLine - firstLine + 1)]; + } + + /* Do the usual Cocoa thing */ + [_styles release]; + _styles = [newStyles copy]; + } +} + +- (void)setVerticalOffset:(CGFloat)val { + if (val != verticalOffset) { + verticalOffset = val; + [self setNeedsDisplay:YES]; + } +} + +- (CGFloat)verticalOffset { + return verticalOffset; +} + +- (NSUInteger)startingLineBackgroundColorIndex { + return startingLineBackgroundColorIndex; +} + +- (void)setStartingLineBackgroundColorIndex:(NSUInteger)val { + startingLineBackgroundColorIndex = val; +} + +- (BOOL)isFlipped { + return YES; +} + +- (HFTextRepresenter *)representer { + return representer; +} + +- (void)dealloc { + HFUnregisterViewForWindowAppearanceChanges(self, _hftvflags.registeredForAppNotifications /* appToo */); + [caretTimer invalidate]; + [caretTimer release]; + [_font release]; + [_data release]; + [_styles release]; + [cachedSelectedRanges release]; + [callouts release]; + if(byteColoring) Block_release(byteColoring); + [super dealloc]; +} + +- (NSColor *)backgroundColorForEmptySpace { + NSArray *colors = [[self representer] rowBackgroundColors]; + if (! [colors count]) return [NSColor clearColor]; + else return colors[0]; +} + +- (NSColor *)backgroundColorForLine:(NSUInteger)line { + NSArray *colors = [[self representer] rowBackgroundColors]; + NSUInteger colorCount = [colors count]; + if (colorCount == 0) return [NSColor clearColor]; + NSUInteger colorIndex = (line + startingLineBackgroundColorIndex) % colorCount; + if (colorIndex == 0) return nil; //will be drawn by empty space + else return colors[colorIndex]; +} + +- (NSUInteger)bytesPerLine { + HFASSERT([self representer] != nil); + return [[self representer] bytesPerLine]; +} + +- (NSUInteger)bytesPerColumn { + HFASSERT([self representer] != nil); + return [[self representer] bytesPerColumn]; +} + +- (void)_drawDefaultLineBackgrounds:(NSRect)clip withLineHeight:(CGFloat)lineHeight maxLines:(NSUInteger)maxLines { + NSRect bounds = [self bounds]; + NSUInteger lineIndex; + NSRect lineRect = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), lineHeight); + if ([self showsFocusRing]) lineRect = NSInsetRect(lineRect, 2, 0); + lineRect.origin.y -= [self verticalOffset] * [self lineHeight]; + NSUInteger drawableLineIndex = 0; + NEW_ARRAY(NSRect, lineRects, maxLines); + NEW_ARRAY(NSColor*, lineColors, maxLines); + for (lineIndex = 0; lineIndex < maxLines; lineIndex++) { + NSRect clippedLineRect = NSIntersectionRect(lineRect, clip); + if (! NSIsEmptyRect(clippedLineRect)) { + NSColor *lineColor = [self backgroundColorForLine:lineIndex]; + if (lineColor) { + lineColors[drawableLineIndex] = lineColor; + lineRects[drawableLineIndex] = clippedLineRect; + drawableLineIndex++; + } + } + lineRect.origin.y += lineHeight; + } + + if (drawableLineIndex > 0) { + NSRectFillListWithColorsUsingOperation(lineRects, lineColors, drawableLineIndex, NSCompositeSourceOver); + } + + FREE_ARRAY(lineRects); + FREE_ARRAY(lineColors); +} + +- (HFTextVisualStyleRun *)styleRunForByteAtIndex:(NSUInteger)byteIndex { + HFTextVisualStyleRun *run = [[HFTextVisualStyleRun alloc] init]; + [run setRange:NSMakeRange(0, NSUIntegerMax)]; + [run setForegroundColor:[NSColor blackColor]]; + return [run autorelease]; +} + +/* Given a list of rects and a parallel list of values, find cases of equal adjacent values, and union together their corresponding rects, deleting the second element from the list. Next, delete all nil values. Returns the new count of the list. */ +static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) { + size_t trailing = 0, leading = 0; + while (leading < count) { + /* Copy our value left */ + valueList[trailing] = valueList[leading]; + rectList[trailing] = rectList[leading]; + + /* Skip one - no point unioning with ourselves */ + leading += 1; + + /* Sweep right, unioning until we reach a different value or the end */ + id targetValue = valueList[trailing]; + for (; leading < count; leading++) { + id testValue = valueList[leading]; + if (targetValue == testValue || (testValue && [targetValue isEqual:testValue])) { + /* Values match, so union the two rects */ + rectList[trailing] = NSUnionRect(rectList[trailing], rectList[leading]); + } + else { + /* Values don't match, we're done sweeping */ + break; + } + } + + /* We're done with this index */ + trailing += 1; + } + + /* trailing keeps track of how many values we have */ + count = trailing; + + /* Now do the same thing, except delete nil values */ + for (trailing = leading = 0; leading < count; leading++) { + if (valueList[leading] != nil) { + valueList[trailing] = valueList[leading]; + rectList[trailing] = rectList[leading]; + trailing += 1; + } + } + count = trailing; + + /* All done */ + return count; +} + +/* Draw vertical guidelines every four bytes */ +- (void)drawVerticalGuideLines:(NSRect)clip { + if (bytesBetweenVerticalGuides == 0) return; + + NSUInteger bytesPerLine = [self bytesPerLine]; + NSRect bounds = [self bounds]; + CGFloat advancePerCharacter = [self advancePerCharacter]; + CGFloat spaceAdvancement = advancePerCharacter / 2; + CGFloat advanceAmount = (advancePerCharacter + spaceAdvancement) * bytesBetweenVerticalGuides; + CGFloat lineOffset = (CGFloat)(NSMinX(bounds) + [self horizontalContainerInset] + advanceAmount - spaceAdvancement / 2.); + CGFloat endOffset = NSMaxX(bounds) - [self horizontalContainerInset]; + + NSUInteger numGuides = (bytesPerLine - 1) / bytesBetweenVerticalGuides; // -1 is a trick to avoid drawing the last line + NSUInteger guideIndex = 0, rectIndex = 0; + NEW_ARRAY(NSRect, lineRects, numGuides); + + while (lineOffset < endOffset && guideIndex < numGuides) { + NSRect lineRect = NSMakeRect(lineOffset - 1, NSMinY(bounds), 1, NSHeight(bounds)); + NSRect clippedLineRect = NSIntersectionRect(lineRect, clip); + if (! NSIsEmptyRect(clippedLineRect)) { + lineRects[rectIndex++] = clippedLineRect; + } + lineOffset += advanceAmount; + guideIndex++; + } + if (rectIndex > 0) { + [[NSColor colorWithCalibratedWhite:(CGFloat).8 alpha:1] set]; + NSRectFillListUsingOperation(lineRects, rectIndex, NSCompositePlusDarker); + } + FREE_ARRAY(lineRects); +} + +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount { + USE(byteCount); + UNIMPLEMENTED(); +} + +- (void)setByteColoring:(void (^)(uint8_t, uint8_t*, uint8_t*, uint8_t*, uint8_t*))coloring { + Block_release(byteColoring); + byteColoring = coloring ? Block_copy(coloring) : NULL; + [self setNeedsDisplay:YES]; +} + +- (void)drawByteColoringBackground:(NSRange)range inRect:(NSRect)rect { + if(!byteColoring) return; + + size_t width = (size_t)rect.size.width; + + // A rgba, 8-bit, single row image. + // +1 in case messing around with floats makes us overshoot a bit. + uint32_t *buffer = calloc(width+1, 4); + + const uint8_t *bytes = [_data bytes]; + bytes += range.location; + + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + CGFloat advancePerCharacter = [self advancePerCharacter]; + CGFloat advanceBetweenColumns = [self advanceBetweenColumns]; + + // For each character, draw the corresponding part of the image + CGFloat offset = [self horizontalContainerInset]; + for(NSUInteger i = 0; i < range.length; i++) { + uint8_t r, g, b, a; + byteColoring(bytes[i], &r, &g, &b, &a); + uint32_t c = ((uint32_t)r<<0) | ((uint32_t)g<<8) | ((uint32_t)b<<16) | ((uint32_t)a<<24); + memset_pattern4(&buffer[(size_t)offset], &c, 4*(size_t)(advancePerCharacter+1)); + offset += advancePerCharacter; + if(bytesPerColumn && (i+1) % bytesPerColumn == 0) + offset += advanceBetweenColumns; + } + + // Do a CGImage dance to draw the buffer + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, 4 * width, NULL); + CGColorSpaceRef cgcolorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); + CGImageRef image = CGImageCreate(width, 1, 8, 32, 4 * width, cgcolorspace, + (CGBitmapInfo)kCGImageAlphaLast, provider, NULL, false, kCGRenderingIntentDefault); + CGContextDrawImage([[NSGraphicsContext currentContext] graphicsPort], NSRectToCGRect(rect), image); + CGColorSpaceRelease(cgcolorspace); + CGImageRelease(image); + CGDataProviderRelease(provider); + free(buffer); +} + +- (void)drawStyledBackgroundsForByteRange:(NSRange)range inRect:(NSRect)rect { + NSRect remainingRunRect = rect; + NSRange remainingRange = range; + + /* Our caller lies to us a little */ + remainingRunRect.origin.x += [self horizontalContainerInset]; + + const NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + + /* Here are the properties we care about */ + struct PropertyInfo_t { + SEL stylePropertyAccessor; // the selector we use to get the property + NSRect *rectList; // the list of rects corresponding to the property values + id *propertyValueList; // the list of the property values + size_t count; //list count, only gets set after cleaning up our lists + } propertyInfos[] = { + {.stylePropertyAccessor = @selector(backgroundColor)}, + {.stylePropertyAccessor = @selector(bookmarkStarts)}, + {.stylePropertyAccessor = @selector(bookmarkExtents)}, + {.stylePropertyAccessor = @selector(bookmarkEnds)} + }; + + /* Each list has the same capacity, and (initially) the same count */ + size_t listCount = 0, listCapacity = 0; + + /* The function pointer we use to get our property values */ + id (* const funcPtr)(id, SEL) = (id (*)(id, SEL))objc_msgSend; + + size_t propertyIndex; + const size_t propertyInfoCount = sizeof propertyInfos / sizeof *propertyInfos; + + while (remainingRange.length > 0) { + /* Get the next run for the remaining range. */ + HFTextVisualStyleRun *styleRun = [self styleRunForByteAtIndex:remainingRange.location]; + + /* The length of the run is the end of the style run or the end of the range we're given (whichever is smaller), minus the beginning of the range we care about. */ + NSUInteger runStart = remainingRange.location; + NSUInteger runLength = MIN(NSMaxRange(range), NSMaxRange([styleRun range])) - runStart; + + /* Get the width of this run and use it to compute the rect */ + CGFloat runRectWidth = [self totalAdvanceForBytesInRange:NSMakeRange(remainingRange.location, runLength)]; + NSRect runRect = remainingRunRect; + runRect.size.width = runRectWidth; + + /* Update runRect and remainingRunRect based on what we just learned */ + remainingRunRect.origin.x += runRectWidth; + remainingRunRect.size.width -= runRectWidth; + + /* Do a hack - if we end at a column boundary, subtract the advance between columns. If the next run has the same value for this property, then we'll end up unioning the rects together and the column gap will be filled. This is the primary purpose of this function. */ + if (bytesPerColumn > 0 && (runStart + runLength) % bytesPerColumn == 0) { + runRect.size.width -= MIN([self advanceBetweenColumns], runRect.size.width); + } + + /* Extend our lists if necessary */ + if (listCount == listCapacity) { + /* Our list is too small, extend it */ + listCapacity = listCapacity + 16; + + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + struct PropertyInfo_t *p = propertyInfos + propertyIndex; + p->rectList = check_realloc(p->rectList, listCapacity * sizeof *p->rectList); + p->propertyValueList = check_realloc(p->propertyValueList, listCapacity * sizeof *p->propertyValueList); + } + } + + /* Now append our values to our lists, even if it's nil */ + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + struct PropertyInfo_t *p = propertyInfos + propertyIndex; + id value = funcPtr(styleRun, p->stylePropertyAccessor); + p->rectList[listCount] = runRect; + p->propertyValueList[listCount] = value; + } + + listCount++; + + /* Update remainingRange */ + remainingRange.location += runLength; + remainingRange.length -= runLength; + + } + + /* Now clean up our lists, to delete the gaps we may have introduced */ + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + struct PropertyInfo_t *p = propertyInfos + propertyIndex; + p->count = unionAndCleanLists(p->rectList, p->propertyValueList, listCount); + } + + /* Finally we can draw them! First, draw byte backgrounds. */ + [self drawByteColoringBackground:range inRect:rect]; + + const struct PropertyInfo_t *p; + + /* Draw backgrounds */ + p = propertyInfos + 0; + if (p->count > 0) NSRectFillListWithColorsUsingOperation(p->rectList, p->propertyValueList, p->count, NSCompositeSourceOver); + + /* Clean up */ + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + p = propertyInfos + propertyIndex; + free(p->rectList); + free(p->propertyValueList); + } +} + +- (void)drawGlyphs:(const struct HFGlyph_t *)glyphs atPoint:(NSPoint)point withAdvances:(const CGSize *)advances withStyleRun:(HFTextVisualStyleRun *)styleRun count:(NSUInteger)glyphCount { + HFASSERT(glyphs != NULL); + HFASSERT(advances != NULL); + HFASSERT(glyphCount > 0); + if ([styleRun shouldDraw]) { + [styleRun set]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + + /* Get all the CGGlyphs together */ + NEW_ARRAY(CGGlyph, cgglyphs, glyphCount); + for (NSUInteger j=0; j < glyphCount; j++) { + cgglyphs[j] = glyphs[j].glyph; + } + + NSUInteger runStart = 0; + HFGlyphFontIndex runFontIndex = glyphs[0].fontIndex; + CGFloat runAdvance = 0; + for (NSUInteger i=1; i <= glyphCount; i++) { + /* Check if this run is finished, or if we are using a substitution font */ + if (i == glyphCount || glyphs[i].fontIndex != runFontIndex || runFontIndex > 0) { + /* Draw this run */ + NSFont *fontToUse = [self fontAtSubstitutionIndex:runFontIndex]; + [[fontToUse screenFont] set]; + CGContextSetTextPosition(ctx, point.x + runAdvance, point.y); + + if (runFontIndex > 0) { + /* A substitution font. Here we should only have one glyph */ + HFASSERT(i - runStart == 1); + /* Get the advance for this glyph. */ + NSSize nativeAdvance; + NSGlyph nativeGlyph = cgglyphs[runStart]; + [fontToUse getAdvancements:&nativeAdvance forGlyphs:&nativeGlyph count:1]; + if (nativeAdvance.width > advances[runStart].width) { + /* This glyph is too wide! We'll have to scale it. Here we only scale horizontally. */ + CGFloat horizontalScale = advances[runStart].width / nativeAdvance.width; + CGAffineTransform textCTM = CGContextGetTextMatrix(ctx); + textCTM.a *= horizontalScale; + CGContextSetTextMatrix(ctx, textCTM); + /* Note that we don't have to restore the text matrix, because the next call to set the font will overwrite it. */ + } + } + + /* Draw the glyphs */ + CGContextShowGlyphsWithAdvances(ctx, cgglyphs + runStart, advances + runStart, i - runStart); + + /* Record the new run */ + if (i < glyphCount) { + /* Sum the advances */ + for (NSUInteger j = runStart; j < i; j++) { + runAdvance += advances[j].width; + } + + /* Record the new run start and index */ + runStart = i; + runFontIndex = glyphs[i].fontIndex; + HFASSERT(runFontIndex != kHFGlyphFontIndexInvalid); + } + } + } + } +} + + +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount { + USE(bytes); + USE(numBytes); + USE(offsetIntoLine); + USE(glyphs); + USE(advances); + USE(resultGlyphCount); + UNIMPLEMENTED_VOID(); +} + +- (void)extractGlyphsForBytes:(const unsigned char *)bytePtr range:(NSRange)byteRange intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances withInclusionRanges:(NSArray *)restrictingToRanges initialTextOffset:(CGFloat *)initialTextOffset resultingGlyphCount:(NSUInteger *)resultingGlyphCount { + NSParameterAssert(glyphs != NULL && advances != NULL && restrictingToRanges != nil && bytePtr != NULL); + NSRange priorIntersectionRange = {NSUIntegerMax, NSUIntegerMax}; + NSUInteger glyphBufferIndex = 0; + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger restrictionRangeCount = [restrictingToRanges count]; + for (NSUInteger rangeIndex = 0; rangeIndex < restrictionRangeCount; rangeIndex++) { + NSRange inclusionRange = [restrictingToRanges[rangeIndex] rangeValue]; + NSRange intersectionRange = NSIntersectionRange(inclusionRange, byteRange); + if (intersectionRange.length == 0) continue; + + NSUInteger offsetIntoLine = intersectionRange.location % bytesPerLine; + + NSRange byteRangeToSkip; + if (priorIntersectionRange.location == NSUIntegerMax) { + byteRangeToSkip = NSMakeRange(byteRange.location, intersectionRange.location - byteRange.location); + } + else { + HFASSERT(intersectionRange.location >= NSMaxRange(priorIntersectionRange)); + byteRangeToSkip.location = NSMaxRange(priorIntersectionRange); + byteRangeToSkip.length = intersectionRange.location - byteRangeToSkip.location; + } + + if (byteRangeToSkip.length > 0) { + CGFloat additionalAdvance = [self totalAdvanceForBytesInRange:byteRangeToSkip]; + if (glyphBufferIndex == 0) { + *initialTextOffset = *initialTextOffset + additionalAdvance; + } + else { + advances[glyphBufferIndex - 1].width += additionalAdvance; + } + } + + NSUInteger glyphCountForRange = NSUIntegerMax; + [self extractGlyphsForBytes:bytePtr + intersectionRange.location count:intersectionRange.length offsetIntoLine:offsetIntoLine intoArray:glyphs + glyphBufferIndex advances:advances + glyphBufferIndex resultingGlyphCount:&glyphCountForRange]; + HFASSERT(glyphCountForRange != NSUIntegerMax); + glyphBufferIndex += glyphCountForRange; + priorIntersectionRange = intersectionRange; + } + if (resultingGlyphCount) *resultingGlyphCount = glyphBufferIndex; +} + +- (void)drawTextWithClip:(NSRect)clip restrictingToTextInRanges:(NSArray *)restrictingToRanges { + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + NSRect bounds = [self bounds]; + CGFloat lineHeight = [self lineHeight]; + + CGAffineTransform textTransform = CGContextGetTextMatrix(ctx); + CGContextSetTextDrawingMode(ctx, kCGTextFill); + + NSUInteger lineStartIndex, bytesPerLine = [self bytesPerLine]; + NSData *dataObject = [self data]; + NSFont *fontObject = [[self font] screenFont]; + //const NSUInteger bytesPerChar = [self bytesPerCharacter]; + const NSUInteger byteCount = [dataObject length]; + + const unsigned char * const bytePtr = [dataObject bytes]; + + NSRect lineRectInBoundsSpace = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), lineHeight); + lineRectInBoundsSpace.origin.y -= [self verticalOffset] * lineHeight; + + /* Start us off with the horizontal inset and move the baseline down by the ascender so our glyphs just graze the top of our view */ + textTransform.tx += [self horizontalContainerInset]; + textTransform.ty += [fontObject ascender] - lineHeight * [self verticalOffset]; + NSUInteger lineIndex = 0; + const NSUInteger maxGlyphCount = [self maximumGlyphCountForByteCount:bytesPerLine]; + NEW_ARRAY(struct HFGlyph_t, glyphs, maxGlyphCount); + NEW_ARRAY(CGSize, advances, maxGlyphCount); + for (lineStartIndex = 0; lineStartIndex < byteCount; lineStartIndex += bytesPerLine) { + if (lineStartIndex > 0) { + textTransform.ty += lineHeight; + lineRectInBoundsSpace.origin.y += lineHeight; + } + if (NSIntersectsRect(lineRectInBoundsSpace, clip)) { + const NSUInteger bytesInThisLine = MIN(bytesPerLine, byteCount - lineStartIndex); + + /* Draw the backgrounds of any styles. */ + [self drawStyledBackgroundsForByteRange:NSMakeRange(lineStartIndex, bytesInThisLine) inRect:lineRectInBoundsSpace]; + + NSUInteger byteIndexInLine = 0; + CGFloat advanceIntoLine = 0; + while (byteIndexInLine < bytesInThisLine) { + const NSUInteger byteIndex = lineStartIndex + byteIndexInLine; + HFTextVisualStyleRun *styleRun = [self styleRunForByteAtIndex:byteIndex]; + HFASSERT(styleRun != nil); + HFASSERT(byteIndex >= [styleRun range].location); + const NSUInteger bytesInThisRun = MIN(NSMaxRange([styleRun range]) - byteIndex, bytesInThisLine - byteIndexInLine); + const NSRange characterRange = [self roundPartialByteRange:NSMakeRange(byteIndex, bytesInThisRun)]; + if (characterRange.length > 0) { + NSUInteger resultGlyphCount = 0; + CGFloat initialTextOffset = 0; + if (restrictingToRanges == nil) { + [self extractGlyphsForBytes:bytePtr + characterRange.location count:characterRange.length offsetIntoLine:byteIndexInLine intoArray:glyphs advances:advances resultingGlyphCount:&resultGlyphCount]; + } + else { + [self extractGlyphsForBytes:bytePtr range:NSMakeRange(byteIndex, bytesInThisRun) intoArray:glyphs advances:advances withInclusionRanges:restrictingToRanges initialTextOffset:&initialTextOffset resultingGlyphCount:&resultGlyphCount]; + } + HFASSERT(resultGlyphCount <= maxGlyphCount); + +#if ! NDEBUG + for (NSUInteger q=0; q < resultGlyphCount; q++) { + HFASSERT(glyphs[q].fontIndex != kHFGlyphFontIndexInvalid); + } +#endif + + if (resultGlyphCount > 0) { + textTransform.tx += initialTextOffset + advanceIntoLine; + CGContextSetTextMatrix(ctx, textTransform); + /* Draw them */ + [self drawGlyphs:glyphs atPoint:NSMakePoint(textTransform.tx, textTransform.ty) withAdvances:advances withStyleRun:styleRun count:resultGlyphCount]; + + /* Undo the work we did before so as not to screw up the next run */ + textTransform.tx -= initialTextOffset + advanceIntoLine; + + /* Record how far into our line this made us move */ + NSUInteger glyphIndex; + for (glyphIndex = 0; glyphIndex < resultGlyphCount; glyphIndex++) { + advanceIntoLine += advances[glyphIndex].width; + } + } + } + byteIndexInLine += bytesInThisRun; + } + } + else if (NSMinY(lineRectInBoundsSpace) > NSMaxY(clip)) { + break; + } + lineIndex++; + } + FREE_ARRAY(glyphs); + FREE_ARRAY(advances); +} + + +- (void)drawFocusRingWithClip:(NSRect)clip { + USE(clip); + [NSGraphicsContext saveGraphicsState]; + NSSetFocusRingStyle(NSFocusRingOnly); + [[NSColor clearColor] set]; + NSRectFill([self bounds]); + [NSGraphicsContext restoreGraphicsState]; +} + +- (BOOL)shouldDrawCallouts { + return _hftvflags.drawCallouts; +} + +- (void)setShouldDrawCallouts:(BOOL)val { + _hftvflags.drawCallouts = val; + [self setNeedsDisplay:YES]; +} + +- (void)drawBookmarksWithClip:(NSRect)clip { + if([self shouldDrawCallouts]) { + /* Figure out which callouts we're going to draw */ + NSRect allCalloutsRect = NSZeroRect; + NSMutableArray *localCallouts = [[NSMutableArray alloc] initWithCapacity:[callouts count]]; + FOREACH(HFRepresenterTextViewCallout *, callout, [callouts objectEnumerator]) { + NSRect calloutRect = [callout rect]; + if (NSIntersectsRect(clip, calloutRect)) { + [localCallouts addObject:callout]; + allCalloutsRect = NSUnionRect(allCalloutsRect, calloutRect); + } + } + allCalloutsRect = NSIntersectionRect(allCalloutsRect, clip); + + if ([localCallouts count]) { + /* Draw shadows first */ + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextBeginTransparencyLayerWithRect(ctx, NSRectToCGRect(allCalloutsRect), NULL); + FOREACH(HFRepresenterTextViewCallout *, callout, localCallouts) { + [callout drawShadowWithClip:clip]; + } + CGContextEndTransparencyLayer(ctx); + + FOREACH(HFRepresenterTextViewCallout *, newCallout, localCallouts) { + // NSRect rect = [callout rect]; + // [[NSColor greenColor] set]; + // NSFrameRect(rect); + [newCallout drawWithClip:clip]; + } + } + [localCallouts release]; + } +} + +- (void)drawRect:(NSRect)clip { + [[self backgroundColorForEmptySpace] set]; + NSRectFillUsingOperation(clip, NSCompositeSourceOver); + BOOL antialias = [self shouldAntialias]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + + [[self.font screenFont] set]; + + if ([self showsFocusRing]) { + NSWindow *window = [self window]; + if (self == [window firstResponder] && [window isKeyWindow]) { + [self drawFocusRingWithClip:clip]; + } + } + + NSUInteger bytesPerLine = [self bytesPerLine]; + if (bytesPerLine == 0) return; + NSUInteger byteCount = [_data length]; + + [self _drawDefaultLineBackgrounds:clip withLineHeight:[self lineHeight] maxLines:ll2l(HFRoundUpToNextMultipleSaturate(byteCount, bytesPerLine) / bytesPerLine)]; + [self drawSelectionIfNecessaryWithClip:clip]; + + NSColor *textColor = [NSColor blackColor]; + [textColor set]; + + if (! antialias) { + CGContextSaveGState(ctx); + CGContextSetShouldAntialias(ctx, NO); + } + [self drawTextWithClip:clip restrictingToTextInRanges:nil]; + if (! antialias) { + CGContextRestoreGState(ctx); + } + + // Vertical dividers only make sense in single byte mode. + if ([self _effectiveBytesPerColumn] == 1) { + [self drawVerticalGuideLines:clip]; + } + + [self drawCaretIfNecessaryWithClip:clip]; + + [self drawBookmarksWithClip:clip]; +} + +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forRange:(NSRange)byteRange { + HFASSERT(edge == NSMinXEdge || edge == NSMaxXEdge || edge == NSMinYEdge || edge == NSMaxYEdge); + const NSUInteger bytesPerLine = [self bytesPerLine]; + CGFloat lineHeight = [self lineHeight]; + CGFloat vertOffset = [self verticalOffset]; + NSUInteger firstLine = byteRange.location / bytesPerLine, lastLine = (NSMaxRange(byteRange) - 1) / bytesPerLine; + NSRect result = NSZeroRect; + + if (edge == NSMinYEdge || edge == NSMaxYEdge) { + /* This is the top (MinY) or bottom (MaxY). We only have to look at one line. */ + NSUInteger lineIndex = (edge == NSMinYEdge ? firstLine : lastLine); + NSRange lineRange = NSMakeRange(lineIndex * bytesPerLine, bytesPerLine); + NSRange intersection = NSIntersectionRange(lineRange, byteRange); + HFASSERT(intersection.length > 0); + CGFloat yOrigin = (lineIndex - vertOffset) * lineHeight; + CGFloat xStart = [self originForCharacterAtByteIndex:intersection.location].x; + CGFloat xEnd = [self originForCharacterAtByteIndex:NSMaxRange(intersection) - 1].x + [self advancePerCharacter]; + result = NSMakeRect(xStart, yOrigin, xEnd - xStart, 0); + } + else { + if (firstLine == lastLine) { + /* We only need to consider this one line */ + NSRange lineRange = NSMakeRange(firstLine * bytesPerLine, bytesPerLine); + NSRange intersection = NSIntersectionRange(lineRange, byteRange); + HFASSERT(intersection.length > 0); + CGFloat yOrigin = (firstLine - vertOffset) * lineHeight; + CGFloat xCoord; + if (edge == NSMinXEdge) { + xCoord = [self originForCharacterAtByteIndex:intersection.location].x; + } + else { + xCoord = [self originForCharacterAtByteIndex:NSMaxRange(intersection) - 1].x + [self advancePerCharacter]; + } + result = NSMakeRect(xCoord, yOrigin, 0, lineHeight); + } + else { + /* We have more than one line. If we are asking for the left edge, sum up the left edge of every line but the first, and handle the first specially. Likewise for the right edge (except handle the last specially) */ + BOOL includeFirstLine, includeLastLine; + CGFloat xCoord; + if (edge == NSMinXEdge) { + /* Left edge, include the first line only if it starts at the beginning of the line or there's only one line */ + includeFirstLine = (byteRange.location % bytesPerLine == 0); + includeLastLine = YES; + xCoord = [self horizontalContainerInset]; + } + else { + /* Right edge, include the last line only if it starts at the beginning of the line or there's only one line */ + includeFirstLine = YES; + includeLastLine = (NSMaxRange(byteRange) % bytesPerLine == 0); + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + /* Don't add in space for the advance after the last column, hence subtract 1. */ + NSUInteger numColumns = (bytesPerColumn ? (bytesPerLine / bytesPerColumn - 1) : 0); + xCoord = [self horizontalContainerInset] + ([self advancePerCharacter] * bytesPerLine / [self bytesPerCharacter]) + [self advanceBetweenColumns] * numColumns; + } + NSUInteger firstLineToInclude = (includeFirstLine ? firstLine : firstLine + 1), lastLineToInclude = (includeLastLine ? lastLine : lastLine - 1); + result = NSMakeRect(xCoord, (firstLineToInclude - [self verticalOffset]) * lineHeight, 0, (lastLineToInclude - firstLineToInclude + 1) * lineHeight); + } + } + return result; +} + +- (NSUInteger)availableLineCount { + CGFloat result = (CGFloat)ceil(NSHeight([self bounds]) / [self lineHeight]); + HFASSERT(result >= 0.); + HFASSERT(result <= NSUIntegerMax); + return (NSUInteger)result; +} + +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight { + return viewHeight / [self lineHeight]; +} + +- (void)setFrameSize:(NSSize)size { + NSUInteger currentBytesPerLine = [self bytesPerLine]; + double currentLineCount = [self maximumAvailableLinesForViewHeight:NSHeight([self bounds])]; + [super setFrameSize:size]; + NSUInteger newBytesPerLine = [self maximumBytesPerLineForViewWidth:size.width]; + double newLineCount = [self maximumAvailableLinesForViewHeight:NSHeight([self bounds])]; + HFControllerPropertyBits bits = 0; + if (newBytesPerLine != currentBytesPerLine) bits |= (HFControllerBytesPerLine | HFControllerDisplayedLineRange); + if (newLineCount != currentLineCount) bits |= HFControllerDisplayedLineRange; + if (bits) [[self representer] representerChangedProperties:bits]; +} + +- (CGFloat)advanceBetweenColumns { + UNIMPLEMENTED(); +} + +- (CGFloat)advancePerCharacter { + UNIMPLEMENTED(); +} + +- (CGFloat)advancePerColumn { + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + if (bytesPerColumn == 0) { + return 0; + } + else { + return [self advancePerCharacter] * (bytesPerColumn / [self bytesPerCharacter]) + [self advanceBetweenColumns]; + } +} + +- (CGFloat)totalAdvanceForBytesInRange:(NSRange)range { + if (range.length == 0) return 0; + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + HFASSERT(bytesPerColumn == 0 || [self bytesPerLine] % bytesPerColumn == 0); + CGFloat result = (range.length * [self advancePerCharacter] / [self bytesPerCharacter]) ; + if (bytesPerColumn > 0) { + NSUInteger numColumnSpaces = NSMaxRange(range) / bytesPerColumn - range.location / bytesPerColumn; //note that integer division does not distribute + result += numColumnSpaces * [self advanceBetweenColumns]; + } + return result; +} + +/* Returns the number of bytes in a character, e.g. if we are UTF-16 this would be 2. */ +- (NSUInteger)bytesPerCharacter { + return 1; +} + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth { + CGFloat availableSpace = (CGFloat)(viewWidth - 2. * [self horizontalContainerInset]); + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn], bytesPerCharacter = [self bytesPerCharacter]; + if (bytesPerColumn == 0) { + /* No columns */ + NSUInteger numChars = (NSUInteger)(availableSpace / [self advancePerCharacter]); + /* Return it, except it's at least one character */ + return MAX(numChars, 1u) * bytesPerCharacter; + } + else { + /* We have some columns */ + CGFloat advancePerColumn = [self advancePerColumn]; + //spaceRequiredForNColumns = N * (advancePerColumn) - spaceBetweenColumns + CGFloat fractionalColumns = (availableSpace + [self advanceBetweenColumns]) / advancePerColumn; + NSUInteger columnCount = (NSUInteger)fmax(1., HFFloor(fractionalColumns)); + return columnCount * bytesPerColumn; + } +} + + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + HFASSERT(bytesPerLine > 0); + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + CGFloat result; + if (bytesPerColumn == 0) { + result = (CGFloat)((2. * [self horizontalContainerInset]) + [self advancePerCharacter] * (bytesPerLine / [self bytesPerCharacter])); + } + else { + HFASSERT(bytesPerLine % bytesPerColumn == 0); + result = (CGFloat)((2. * [self horizontalContainerInset]) + [self advancePerColumn] * (bytesPerLine / bytesPerColumn) - [self advanceBetweenColumns]); + } + return result; +} + +- (BOOL)isEditable { + return _hftvflags.editable; +} + +- (void)setEditable:(BOOL)val { + if (val != _hftvflags.editable) { + _hftvflags.editable = val; + [self _updateCaretTimer]; + } +} + +- (BOOL)shouldAntialias { + return _hftvflags.antialias; +} + +- (void)setShouldAntialias:(BOOL)val { + _hftvflags.antialias = !!val; + [self setNeedsDisplay:YES]; +} + +- (BOOL)behavesAsTextField { + return [[self representer] behavesAsTextField]; +} + +- (BOOL)showsFocusRing { + return [[self representer] behavesAsTextField]; +} + +- (BOOL)isWithinMouseDown { + return _hftvflags.withinMouseDown; +} + +- (void)_windowDidChangeKeyStatus:(NSNotification *)note { + USE(note); + [self _updateCaretTimer]; + if ([[note name] isEqualToString:NSWindowDidBecomeKeyNotification]) { + [self _forceCaretOnIfHasCaretTimer]; + } + if ([self showsFocusRing] && self == [[self window] firstResponder]) { + [[self superview] setNeedsDisplayInRect:NSInsetRect([self frame], -6, -6)]; + } + [self setNeedsDisplay:YES]; +} + +- (void)viewDidMoveToWindow { + [self _updateCaretTimer]; + HFRegisterViewForWindowAppearanceChanges(self, @selector(_windowDidChangeKeyStatus:), ! _hftvflags.registeredForAppNotifications); + _hftvflags.registeredForAppNotifications = YES; + [super viewDidMoveToWindow]; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + HFUnregisterViewForWindowAppearanceChanges(self, NO /* appToo */); + [super viewWillMoveToWindow:newWindow]; +} + +/* Computes the character at the given index for selection, properly handling the case where the point is outside the bounds */ +- (NSUInteger)characterAtPointForSelection:(NSPoint)point { + NSPoint mungedPoint = point; + // shift us right by half an advance so that we trigger at the midpoint of each character, rather than at the x origin + mungedPoint.x += [self advancePerCharacter] / (CGFloat)2.; + // make sure we're inside the bounds + const NSRect bounds = [self bounds]; + mungedPoint.x = HFMax(NSMinX(bounds), mungedPoint.x); + mungedPoint.x = HFMin(NSMaxX(bounds), mungedPoint.x); + mungedPoint.y = HFMax(NSMinY(bounds), mungedPoint.y); + mungedPoint.y = HFMin(NSMaxY(bounds), mungedPoint.y); + return [self indexOfCharacterAtPoint:mungedPoint]; +} + +- (NSUInteger)maximumCharacterIndex { + //returns the maximum character index that the selection may lie on. It is one beyond the last byte index, to represent the cursor at the end of the document. + return [[self data] length] / [self bytesPerCharacter]; +} + +- (void)mouseDown:(NSEvent *)event { + HFASSERT(_hftvflags.withinMouseDown == 0); + _hftvflags.withinMouseDown = 1; + [self _forceCaretOnIfHasCaretTimer]; + NSPoint mouseDownLocation = [self convertPoint:[event locationInWindow] fromView:nil]; + NSUInteger characterIndex = [self characterAtPointForSelection:mouseDownLocation]; + + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); //characterIndex may be one beyond the last index, to represent the cursor at the end of the document + [[self representer] beginSelectionWithEvent:event forCharacterIndex:characterIndex]; + + /* Drive the event loop in event tracking mode until we're done */ + HFASSERT(_hftvflags.receivedMouseUp == NO); //paranoia - detect any weird recursive invocations + NSDate *endDate = [NSDate distantFuture]; + + /* Start periodic events for autoscroll */ + [NSEvent startPeriodicEventsAfterDelay:0.1 withPeriod:0.05]; + + NSPoint autoscrollLocation = mouseDownLocation; + while (! _hftvflags.receivedMouseUp) { + @autoreleasepool { + NSEvent *ev = [NSApp nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSPeriodicMask untilDate:endDate inMode:NSEventTrackingRunLoopMode dequeue:YES]; + + if ([ev type] == NSPeriodic) { + // autoscroll if drag is out of view bounds + CGFloat amountToScroll = 0; + NSRect bounds = [self bounds]; + if (autoscrollLocation.y < NSMinY(bounds)) { + amountToScroll = (autoscrollLocation.y - NSMinY(bounds)) / [self lineHeight]; + } + else if (autoscrollLocation.y > NSMaxY(bounds)) { + amountToScroll = (autoscrollLocation.y - NSMaxY(bounds)) / [self lineHeight]; + } + if (amountToScroll != 0.) { + [[[self representer] controller] scrollByLines:amountToScroll]; + characterIndex = [self characterAtPointForSelection:autoscrollLocation]; + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); + [[self representer] continueSelectionWithEvent:ev forCharacterIndex:characterIndex]; + } + } + else if ([ev type] == NSLeftMouseDragged) { + autoscrollLocation = [self convertPoint:[ev locationInWindow] fromView:nil]; + } + + [NSApp sendEvent:ev]; + } // @autoreleasepool + } + + [NSEvent stopPeriodicEvents]; + + _hftvflags.receivedMouseUp = NO; + _hftvflags.withinMouseDown = 0; +} + +- (void)mouseDragged:(NSEvent *)event { + if (! _hftvflags.withinMouseDown) return; + NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; + NSUInteger characterIndex = [self characterAtPointForSelection:location]; + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); + [[self representer] continueSelectionWithEvent:event forCharacterIndex:characterIndex]; +} + +- (void)mouseUp:(NSEvent *)event { + if (! _hftvflags.withinMouseDown) return; + NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; + NSUInteger characterIndex = [self characterAtPointForSelection:location]; + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); + [[self representer] endSelectionWithEvent:event forCharacterIndex:characterIndex]; + _hftvflags.receivedMouseUp = YES; +} + +- (void)keyDown:(NSEvent *)event { + HFASSERT(event != NULL); + [self interpretKeyEvents:@[event]]; +} + +- (void)scrollWheel:(NSEvent *)event { + [[self representer] scrollWheel:event]; +} + +- (void)insertText:(id)string { + if (! [self isEditable]) { + NSBeep(); + } + else { + if ([string isKindOfClass:[NSAttributedString class]]) string = [string string]; + [NSCursor setHiddenUntilMouseMoves:YES]; + [[self representer] insertText:string]; + } +} + +- (BOOL)handleCommand:(SEL)sel { + if (sel == @selector(insertTabIgnoringFieldEditor:)) { + [self insertText:@"\t"]; + } + else if ([self respondsToSelector:sel]) { + [self performSelector:sel withObject:nil]; + } + else { + return NO; + } + return YES; +} + +- (void)doCommandBySelector:(SEL)sel { + HFRepresenter *rep = [self representer]; + // NSLog(@"%s%s", _cmd, sel); + if ([self handleCommand:sel]) { + /* Nothing to do */ + } + else if ([rep respondsToSelector:sel]) { + [rep performSelector:sel withObject:self]; + } + else { + [super doCommandBySelector:sel]; + } +} + +- (IBAction)selectAll:sender { + [[self representer] selectAll:sender]; +} + +/* Indicates whether at least one byte is selected */ +- (BOOL)_selectionIsNonEmpty { + NSArray *selection = [[[self representer] controller] selectedContentsRanges]; + FOREACH(HFRangeWrapper *, rangeWrapper, selection) { + if ([rangeWrapper HFRange].length > 0) return YES; + } + return NO; +} + +- (SEL)_pasteboardOwnerStringTypeWritingSelector { + UNIMPLEMENTED(); +} + +- (void)paste:sender { + if (! [self isEditable]) { + NSBeep(); + } + else { + USE(sender); + [[self representer] pasteBytesFromPasteboard:[NSPasteboard generalPasteboard]]; + } +} + +- (void)copy:sender { + USE(sender); + [[self representer] copySelectedBytesToPasteboard:[NSPasteboard generalPasteboard]]; +} + +- (void)cut:sender { + USE(sender); + [[self representer] cutSelectedBytesToPasteboard:[NSPasteboard generalPasteboard]]; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)item { + SEL action = [item action]; + if (action == @selector(selectAll:)) return YES; + else if (action == @selector(cut:)) return [[self representer] canCut]; + else if (action == @selector(copy:)) return [self _selectionIsNonEmpty]; + else if (action == @selector(paste:)) return [[self representer] canPasteFromPasteboard:[NSPasteboard generalPasteboard]]; + else return YES; +} + +@end diff --git a/HexFiend/HFRepresenterTextViewCallout.h b/HexFiend/HFRepresenterTextViewCallout.h new file mode 100644 index 0000000..42ae7e3 --- /dev/null +++ b/HexFiend/HFRepresenterTextViewCallout.h @@ -0,0 +1,31 @@ +// +// HFRepresenterTextViewCallout.h +// HexFiend_2 +// +// Copyright 2011 ridiculous_fish. All rights reserved. +// + +#import + +@class HFRepresenterTextView; + +#define kHFRepresenterTextViewCalloutMaxGlyphCount 2u + +@interface HFRepresenterTextViewCallout : NSObject { + CGFloat rotation; + NSPoint tipOrigin; + NSPoint pinStart, pinEnd; +} + +@property(nonatomic) NSInteger byteOffset; +@property(nonatomic, copy) NSColor *color; +@property(nonatomic, copy) NSString *label; +@property(nonatomic, retain) id representedObject; +@property(readonly) NSRect rect; + ++ (void)layoutCallouts:(NSArray *)callouts inView:(HFRepresenterTextView *)textView; + +- (void)drawShadowWithClip:(NSRect)clip; +- (void)drawWithClip:(NSRect)clip; + +@end diff --git a/HexFiend/HFRepresenterTextViewCallout.m b/HexFiend/HFRepresenterTextViewCallout.m new file mode 100644 index 0000000..ae46bd8 --- /dev/null +++ b/HexFiend/HFRepresenterTextViewCallout.m @@ -0,0 +1,477 @@ +// +// HFRepresenterTextViewCallout.m +// HexFiend_2 +// +// Copyright 2011 ridiculous_fish. All rights reserved. +// + +#import "HFRepresenterTextViewCallout.h" +#import "HFRepresenterTextView.h" + +static const CGFloat HFTeardropRadius = 12; +static const CGFloat HFTeadropTipScale = 2.5; + +static const CGFloat HFShadowXOffset = -6; +static const CGFloat HFShadowYOffset = 0; +static const CGFloat HFShadowOffscreenHack = 3100; + +static NSPoint rotatePoint(NSPoint center, NSPoint point, CGFloat percent) { + CGFloat radians = percent * M_PI * 2; + CGFloat x = point.x - center.x; + CGFloat y = point.y - center.y; + CGFloat newX = x * cos(radians) + y * sin(radians); + CGFloat newY = x * -sin(radians) + y * cos(radians); + return NSMakePoint(center.x + newX, center.y + newY); +} + +static NSPoint scalePoint(NSPoint center, NSPoint point, CGFloat percent) { + CGFloat x = point.x - center.x; + CGFloat y = point.y - center.y; + CGFloat newX = x * percent; + CGFloat newY = y * percent; + return NSMakePoint(center.x + newX, center.y + newY); +} + +static NSBezierPath *copyTeardropPath(void) { + static NSBezierPath *sPath = nil; + if (! sPath) { + + CGFloat radius = HFTeardropRadius; + CGFloat rotation = 0; + CGFloat droppiness = .15; + CGFloat tipScale = HFTeadropTipScale; + CGFloat tipLengthFromCenter = radius * tipScale; + NSPoint bulbCenter = NSMakePoint(-tipLengthFromCenter, 0); + + NSPoint triangleCenter = rotatePoint(bulbCenter, NSMakePoint(bulbCenter.x + radius, bulbCenter.y), rotation); + NSPoint dropCorner1 = rotatePoint(bulbCenter, triangleCenter, droppiness / 2); + NSPoint dropCorner2 = rotatePoint(bulbCenter, triangleCenter, -droppiness / 2); + NSPoint dropTip = scalePoint(bulbCenter, triangleCenter, tipScale); + + NSBezierPath *path = [[NSBezierPath alloc] init]; + [path appendBezierPathWithArcWithCenter:bulbCenter radius:radius startAngle:-rotation * 360 + droppiness * 180. endAngle:-rotation * 360 - droppiness * 180. clockwise:NO]; + + [path moveToPoint:dropCorner1]; + [path lineToPoint:dropTip]; + [path lineToPoint:dropCorner2]; + [path closePath]; + + sPath = path; + } + return [sPath retain]; +} + + +@implementation HFRepresenterTextViewCallout + +/* A helpful struct for representing a wedge (portion of a circle). Wedges are counterclockwise. */ +typedef struct { + double offset; // 0 <= offset < 1 + double length; // 0 <= length <= 1 +} Wedge_t; + + +static inline double normalizeAngle(double x) { + /* Convert an angle to the range [0, 1). We typically only generate angles that are off by a full rotation, so a loop isn't too bad. */ + while (x >= 1.) x -= 1.; + while (x < 0.) x += 1.; + return x; +} + +static inline double distanceCCW(double a, double b) { return normalizeAngle(b-a); } + +static inline double wedgeMax(Wedge_t wedge) { + return normalizeAngle(wedge.offset + wedge.length); +} + +/* Computes the smallest wedge containing the two given wedges. Compute the wedge from the min of one to the furthest part of the other, and pick the smaller. */ +static Wedge_t wedgeUnion(Wedge_t wedge1, Wedge_t wedge2) { + // empty wedges don't participate + if (wedge1.length <= 0) return wedge2; + if (wedge2.length <= 0) return wedge1; + + Wedge_t union1 = wedge1; + union1.length = fmin(1., fmax(union1.length, distanceCCW(union1.offset, wedge2.offset) + wedge2.length)); + + Wedge_t union2 = wedge2; + union2.length = fmin(1., fmax(union2.length, distanceCCW(union2.offset, wedge1.offset) + wedge1.length)); + + Wedge_t result = (union1.length <= union2.length ? union1 : union2); + HFASSERT(result.length <= 1); + return result; +} + +- (instancetype)init { + self = [super init]; + if (self) { + // Initialization code here. + } + + return self; +} + +- (void)dealloc { + [_representedObject release]; + [_color release]; + [_label release]; + [super dealloc]; +} + +- (NSComparisonResult)compare:(HFRepresenterTextViewCallout *)callout { + return [_representedObject compare:callout.representedObject]; +} + +static Wedge_t computeForbiddenAngle(double distanceFromEdge, double angleToEdge) { + Wedge_t newForbiddenAngle; + + /* This is how far it is to the center of our teardrop */ + const double teardropLength = HFTeardropRadius * HFTeadropTipScale; + + if (distanceFromEdge <= 0) { + /* We're above or below. */ + if (-distanceFromEdge >= (teardropLength + HFTeardropRadius)) { + /* We're so far above or below we won't be visible at all. No hope. */ + newForbiddenAngle = (Wedge_t){.offset = 0, .length = 1}; + } else { + /* We're either above or below the bounds, but there's a hope we can be visible */ + + double invertedAngleToEdge = normalizeAngle(angleToEdge + .5); + double requiredAngle; + if (-distanceFromEdge >= teardropLength) { + // We're too far north or south that all we can do is point in the right direction + requiredAngle = 0; + } else { + // By confining ourselves to required angles, we can make ourselves visible + requiredAngle = acos(-distanceFromEdge / teardropLength) / (2 * M_PI); + } + // Require at least a small spread + requiredAngle = fmax(requiredAngle, .04); + + double requiredMin = invertedAngleToEdge - requiredAngle; + double requiredMax = invertedAngleToEdge + requiredAngle; + + newForbiddenAngle = (Wedge_t){.offset = requiredMax, .length = distanceCCW(requiredMax, requiredMin) }; + } + } else if (distanceFromEdge < teardropLength) { + // We're onscreen, but some angle will be forbidden + double forbiddenAngle = acos(distanceFromEdge / teardropLength) / (2 * M_PI); + + // This is a wedge out of the top (or bottom) + newForbiddenAngle = (Wedge_t){.offset = angleToEdge - forbiddenAngle, .length = 2 * forbiddenAngle}; + } else { + /* Nothing prohibited at all */ + newForbiddenAngle = (Wedge_t){0, 0}; + } + return newForbiddenAngle; +} + + +static double distanceMod1(double a, double b) { + /* Assuming 0 <= a, b < 1, returns the distance between a and b, mod 1 */ + if (a > b) { + return fmin(a-b, b-a+1); + } else { + return fmin(b-a, a-b+1); + } +} + ++ (void)layoutCallouts:(NSArray *)callouts inView:(HFRepresenterTextView *)textView { + + // Keep track of how many drops are at a given location + NSCountedSet *dropsPerByteLoc = [[NSCountedSet alloc] init]; + + const CGFloat lineHeight = [textView lineHeight]; + const NSRect bounds = [textView bounds]; + + NSMutableArray *remainingCallouts = [[callouts mutableCopy] autorelease]; + [remainingCallouts sortUsingSelector:@selector(compare:)]; + + while ([remainingCallouts count] > 0) { + /* Get the next callout to lay out */ + const NSInteger byteLoc = [remainingCallouts[0] byteOffset]; + + /* Get all the callouts that share that byteLoc */ + NSMutableArray *sharedCallouts = [NSMutableArray array]; + FOREACH(HFRepresenterTextViewCallout *, testCallout, remainingCallouts) { + if ([testCallout byteOffset] == byteLoc) { + [sharedCallouts addObject:testCallout]; + } + } + + /* We expect to get at least one */ + const NSUInteger calloutCount = [sharedCallouts count]; + HFASSERT(calloutCount > 0); + + /* Get the character origin */ + const NSPoint characterOrigin = [textView originForCharacterAtByteIndex:byteLoc]; + + Wedge_t forbiddenAngle = {0, 0}; + + // Compute how far we are from the top (or bottom) + BOOL isNearerTop = (characterOrigin.y < NSMidY(bounds)); + double verticalDistance = (isNearerTop ? characterOrigin.y - NSMinY(bounds) : NSMaxY(bounds) - characterOrigin.y); + forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(verticalDistance, (isNearerTop ? .25 : .75))); + + // Compute how far we are from the left (or right) + BOOL isNearerLeft = (characterOrigin.x < NSMidX(bounds)); + double horizontalDistance = (isNearerLeft ? characterOrigin.x - NSMinX(bounds) : NSMaxX(bounds) - characterOrigin.x); + forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(horizontalDistance, (isNearerLeft ? .5 : 0.))); + + + /* How much will each callout rotate? No more than 1/8th. */ + HFASSERT(forbiddenAngle.length <= 1); + double changeInRotationPerCallout = fmin(.125, (1. - forbiddenAngle.length) / calloutCount); + double totalConsumedAmount = changeInRotationPerCallout * calloutCount; + + /* We would like to center around .375. */ + const double goalCenter = .375; + + /* We're going to pretend to work on a line segment that extends from the max prohibited angle all the way back to min */ + double segmentLength = 1. - forbiddenAngle.length; + double goalSegmentCenter = normalizeAngle(goalCenter - wedgeMax(forbiddenAngle)); //may exceed segmentLength! + + /* Now center us on the goal, or as close as we can get. */ + double consumedSegmentCenter; + + /* We only need to worry about wrapping around if we have some prohibited angle */ + if (forbiddenAngle.length <= 0) { //never expect < 0, but be paranoid + consumedSegmentCenter = goalSegmentCenter; + } else { + + /* The consumed segment center is confined to the segment range [amount/2, length - amount/2] */ + double consumedSegmentCenterMin = totalConsumedAmount/2; + double consumedSegmentCenterMax = segmentLength - totalConsumedAmount/2; + if (goalSegmentCenter >= consumedSegmentCenterMin && goalSegmentCenter < consumedSegmentCenterMax) { + /* We can hit our goal */ + consumedSegmentCenter = goalSegmentCenter; + } else { + /* Pick either the min or max location, depending on which one gets us closer to the goal segment center mod 1. */ + if (distanceMod1(goalSegmentCenter, consumedSegmentCenterMin) <= distanceMod1(goalSegmentCenter, consumedSegmentCenterMax)) { + consumedSegmentCenter = consumedSegmentCenterMin; + } else { + consumedSegmentCenter = consumedSegmentCenterMax; + } + + } + } + + /* Now convert this back to an angle */ + double consumedAngleCenter = normalizeAngle(wedgeMax(forbiddenAngle) + consumedSegmentCenter); + + // move us slightly towards the character + NSPoint teardropTipOrigin = NSMakePoint(characterOrigin.x + 1, characterOrigin.y + floor(lineHeight / 8.)); + + // make the pin + NSPoint pinStart, pinEnd; + pinStart = NSMakePoint(characterOrigin.x + .25, characterOrigin.y); + pinEnd = NSMakePoint(pinStart.x, pinStart.y + lineHeight); + + // store it all, invalidating as necessary + NSInteger i = 0; + FOREACH(HFRepresenterTextViewCallout *, callout, sharedCallouts) { + + /* Compute the rotation */ + double seq = (i+1)/2; //0, 1, -1, 2, -2... + if ((i & 1) == 0) seq = -seq; + //if we've got an even number of callouts, we want -.5, .5, -1.5, 1.5... + if (! (calloutCount & 1)) seq -= .5; + // compute the angle of rotation + double angle = consumedAngleCenter + seq * changeInRotationPerCallout; + // our notion of rotation has 0 meaning pointing right and going counterclockwise, but callouts with 0 pointing left and going clockwise, so convert + angle = normalizeAngle(.5 - angle); + + + NSRect beforeRect = [callout rect]; + + callout->rotation = angle; + callout->tipOrigin = teardropTipOrigin; + callout->pinStart = pinStart; + callout->pinEnd = pinEnd; + + // Only the first gets a pin + pinStart = pinEnd = NSZeroPoint; + + NSRect afterRect = [callout rect]; + + if (! NSEqualRects(beforeRect, afterRect)) { + [textView setNeedsDisplayInRect:beforeRect]; + [textView setNeedsDisplayInRect:afterRect]; + } + + i++; + } + + + /* We're done laying out these callouts */ + [remainingCallouts removeObjectsInArray:sharedCallouts]; + } + + [dropsPerByteLoc release]; +} + +- (CGAffineTransform)teardropTransform { + CGAffineTransform trans = CGAffineTransformMakeTranslation(tipOrigin.x, tipOrigin.y); + trans = CGAffineTransformRotate(trans, rotation * M_PI * 2); + return trans; +} + +- (NSRect)teardropBaseRect { + NSSize teardropSize = NSMakeSize(HFTeardropRadius * (1 + HFTeadropTipScale), HFTeardropRadius*2); + NSRect result = NSMakeRect(-teardropSize.width, -teardropSize.height/2, teardropSize.width, teardropSize.height); + return result; +} + +- (CGAffineTransform)shadowTransform { + CGFloat shadowXOffset = HFShadowXOffset; + CGFloat shadowYOffset = HFShadowYOffset; + CGFloat offscreenOffset = HFShadowOffscreenHack; + + // Figure out how much movement the shadow offset produces + CGFloat shadowTranslationDistance = hypot(shadowXOffset, shadowYOffset); + + CGAffineTransform transform = CGAffineTransformIdentity; + transform = CGAffineTransformTranslate(transform, tipOrigin.x + offscreenOffset - shadowXOffset, tipOrigin.y - shadowYOffset); + transform = CGAffineTransformRotate(transform, rotation * M_PI * 2 - atan2(shadowTranslationDistance, 2*HFTeardropRadius /* bulbHeight */)); + return transform; +} + +- (void)drawShadowWithClip:(NSRect)clip { + USE(clip); + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + + // Set the shadow. Note that these shadows are pretty unphysical for high rotations. + NSShadow *shadow = [[NSShadow alloc] init]; + [shadow setShadowBlurRadius:5.]; + [shadow setShadowOffset:NSMakeSize(HFShadowXOffset - HFShadowOffscreenHack, HFShadowYOffset)]; + [shadow setShadowColor:[NSColor colorWithDeviceWhite:0. alpha:.5]]; + [shadow set]; + [shadow release]; + + // Draw the shadow first and separately + CGAffineTransform transform = [self shadowTransform]; + CGContextConcatCTM(ctx, transform); + + NSBezierPath *teardrop = copyTeardropPath(); + [teardrop fill]; + [teardrop release]; + + // Clear the shadow + CGContextSetShadowWithColor(ctx, CGSizeZero, 0, NULL); + + // Undo the transform + CGContextConcatCTM(ctx, CGAffineTransformInvert(transform)); +} + +- (void)drawWithClip:(NSRect)clip { + USE(clip); + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + // Here's the font we'll use + CTFontRef ctfont = CTFontCreateWithName(CFSTR("Helvetica-Bold"), 1., NULL); + if (ctfont) { + // Set the font + [(NSFont *)ctfont set]; + + // Get characters + NSUInteger labelLength = MIN([_label length], kHFRepresenterTextViewCalloutMaxGlyphCount); + UniChar calloutUniLabel[kHFRepresenterTextViewCalloutMaxGlyphCount]; + [_label getCharacters:calloutUniLabel range:NSMakeRange(0, labelLength)]; + + // Get our glyphs and advances + CGGlyph glyphs[kHFRepresenterTextViewCalloutMaxGlyphCount]; + CGSize advances[kHFRepresenterTextViewCalloutMaxGlyphCount]; + CTFontGetGlyphsForCharacters(ctfont, calloutUniLabel, glyphs, labelLength); + CTFontGetAdvancesForGlyphs(ctfont, kCTFontHorizontalOrientation, glyphs, advances, labelLength); + + // Count our glyphs. Note: this won't work with any label containing spaces, etc. + NSUInteger glyphCount; + for (glyphCount = 0; glyphCount < labelLength; glyphCount++) { + if (glyphs[glyphCount] == 0) break; + } + + // Set our color. + [_color set]; + + // Draw the pin first + if (! NSEqualPoints(pinStart, pinEnd)) { + [NSBezierPath setDefaultLineWidth:1.25]; + [NSBezierPath strokeLineFromPoint:pinStart toPoint:pinEnd]; + } + + CGContextSaveGState(ctx); + CGContextBeginTransparencyLayerWithRect(ctx, NSRectToCGRect([self rect]), NULL); + + // Rotate and translate in preparation for drawing the teardrop + CGContextConcatCTM(ctx, [self teardropTransform]); + + // Draw the teardrop + NSBezierPath *teardrop = copyTeardropPath(); + [teardrop fill]; + [teardrop release]; + + // Draw the text with white and alpha. Use blend mode copy so that we clip out the shadow, and when the transparency layer is ended we'll composite over the text. + CGFloat textScale = (glyphCount == 1 ? 24 : 20); + + // we are flipped by default, so invert the rotation's sign to get the text direction. Use a little slop so we don't get jitter. + const CGFloat textDirection = (rotation <= .27 || rotation >= .73) ? -1 : 1; + + CGPoint positions[kHFRepresenterTextViewCalloutMaxGlyphCount]; + CGFloat totalAdvance = 0; + for (NSUInteger i=0; i < glyphCount; i++) { + // make sure to provide negative advances if necessary + positions[i].x = copysign(totalAdvance, -textDirection); + positions[i].y = 0; + CGFloat advance = advances[i].width; + // Workaround 5834794 + advance *= textScale; + // Tighten up the advances a little + advance *= .85; + totalAdvance += advance; + } + + + // Compute the vertical offset + CGFloat textYOffset = (glyphCount == 1 ? 4 : 5); + // LOL + if ([_label isEqualToString:@"6"] || [_label isEqualToString:@"7"] == 7) textYOffset -= 1; + + + // Apply this text matrix + NSRect bulbRect = [self teardropBaseRect]; + CGAffineTransform textMatrix = CGAffineTransformMakeScale(-copysign(textScale, textDirection), copysign(textScale, textDirection)); //roughly the font size we want + textMatrix.tx = NSMinX(bulbRect) + HFTeardropRadius + copysign(totalAdvance/2, textDirection); + + + if (textDirection < 0) { + textMatrix.ty = NSMaxY(bulbRect) - textYOffset; + } else { + textMatrix.ty = NSMinY(bulbRect) + textYOffset; + } + + // Draw + CGContextSetTextMatrix(ctx, textMatrix); + CGContextSetTextDrawingMode(ctx, kCGTextClip); + CGContextShowGlyphsAtPositions(ctx, glyphs, positions, glyphCount); + + CGContextSetBlendMode(ctx, kCGBlendModeCopy); + CGContextSetGrayFillColor(ctx, 1., .66); //faint white fill + CGContextFillRect(ctx, NSRectToCGRect(NSInsetRect(bulbRect, -20, -20))); + + // Done drawing, so composite + CGContextEndTransparencyLayer(ctx); + CGContextRestoreGState(ctx); // this also restores the clip, which is important + + // Done with the font + CFRelease(ctfont); + } +} + +- (NSRect)rect { + // get the transformed teardrop rect + NSRect result = NSRectFromCGRect(CGRectApplyAffineTransform(NSRectToCGRect([self teardropBaseRect]), [self teardropTransform])); + + // outset a bit for the shadow + result = NSInsetRect(result, -8, -8); + return result; +} + +@end diff --git a/HexFiend/HFRepresenterTextView_Internal.h b/HexFiend/HFRepresenterTextView_Internal.h new file mode 100644 index 0000000..70eed23 --- /dev/null +++ b/HexFiend/HFRepresenterTextView_Internal.h @@ -0,0 +1,11 @@ +#import + +#define GLYPH_BUFFER_SIZE 16u + +@interface HFRepresenterTextView (HFInternal) + +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingLayoutManager:(NSLayoutManager *)textView glyphs:(CGGlyph *)glyphs; +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingTextView:(NSTextView *)textView glyphs:(CGGlyph *)glyphs; +- (NSUInteger)_getGlyphs:(CGGlyph *)glyphs forString:(NSString *)string font:(NSFont *)font; //uses CoreText. Here glyphs must have space for [string length] glyphs. + +@end diff --git a/HexFiend/HFRepresenter_Internal.h b/HexFiend/HFRepresenter_Internal.h new file mode 100644 index 0000000..9a0b704 --- /dev/null +++ b/HexFiend/HFRepresenter_Internal.h @@ -0,0 +1,7 @@ +#import + +@interface HFRepresenter (HFInternalStuff) + +- (void)_setController:(HFController *)controller; + +@end diff --git a/HexFiend/HFSharedMemoryByteSlice.h b/HexFiend/HFSharedMemoryByteSlice.h new file mode 100644 index 0000000..204492d --- /dev/null +++ b/HexFiend/HFSharedMemoryByteSlice.h @@ -0,0 +1,32 @@ +// +// HFSharedMemoryByteSlice.h +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFSharedMemoryByteSlice + @brief A subclass of HFByteSlice for working with data stored in memory. + + HFSharedMemoryByteSlice is a subclass of HFByteSlice that represents a portion of data from memory, e.g. typed or pasted in by the user. The term "shared" refers to the ability for mutiple HFSharedMemoryByteSlices to reference the same NSData; it does not mean that the data is in shared memory or shared between processes. + + Instances of HFSharedMemoryByteSlice are immutable (like all instances of HFByteSlice). However, to support efficient typing, the backing data is an instance of NSMutableData that may be grown. A referenced range of the NSMutableData will never have its contents changed, but it may be allowed to grow larger, so that the data does not have to be copied merely to append a single byte. This is implemented by overriding the -byteSliceByAppendingSlice: method of HFByteSlice. +*/ +@interface HFSharedMemoryByteSlice : HFByteSlice { + NSMutableData *data; + NSUInteger offset; + NSUInteger length; + unsigned char inlineTailLength; + unsigned char inlineTail[15]; //size chosen to exhaust padding of 32-byte allocator +} + +// copies the data +- (instancetype)initWithUnsharedData:(NSData *)data; + +// retains, does not copy +- (instancetype)initWithData:(NSMutableData *)data; +- (instancetype)initWithData:(NSMutableData *)data offset:(NSUInteger)offset length:(NSUInteger)length; + +@end diff --git a/HexFiend/HFSharedMemoryByteSlice.m b/HexFiend/HFSharedMemoryByteSlice.m new file mode 100644 index 0000000..fe7f43c --- /dev/null +++ b/HexFiend/HFSharedMemoryByteSlice.m @@ -0,0 +1,209 @@ +// +// HFSharedMemoryByteSlice.m +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import +#import + +#define MAX_FAST_PATH_SIZE (1 << 13) + +#define MAX_TAIL_LENGTH (sizeof ((HFSharedMemoryByteSlice *)NULL)->inlineTail / sizeof *((HFSharedMemoryByteSlice *)NULL)->inlineTail) + +@implementation HFSharedMemoryByteSlice + +- (instancetype)initWithUnsharedData:(NSData *)unsharedData { + self = [super init]; + REQUIRE_NOT_NULL(unsharedData); + NSUInteger dataLength = [unsharedData length]; + NSUInteger inlineAmount = MIN(dataLength, MAX_TAIL_LENGTH); + NSUInteger sharedAmount = dataLength - inlineAmount; + HFASSERT(inlineAmount <= UCHAR_MAX); + inlineTailLength = (unsigned char)inlineAmount; + length = sharedAmount; + if (inlineAmount > 0) { + [unsharedData getBytes:inlineTail range:NSMakeRange(dataLength - inlineAmount, inlineAmount)]; + } + if (sharedAmount > 0) { + data = [[NSMutableData alloc] initWithBytes:[unsharedData bytes] length:sharedAmount]; + } + return self; +} + +// retains, does not copy +- (instancetype)initWithData:(NSMutableData *)dat { + REQUIRE_NOT_NULL(dat); + return [self initWithData:dat offset:0 length:[dat length]]; +} + +- (instancetype)initWithData:(NSMutableData *)dat offset:(NSUInteger)off length:(NSUInteger)len { + self = [super init]; + REQUIRE_NOT_NULL(dat); + HFASSERT(off + len >= off); //check for overflow + HFASSERT(off + len <= [dat length]); + offset = off; + length = len; + data = [dat retain]; + return self; +} + +- (instancetype)initWithSharedData:(NSMutableData *)dat offset:(NSUInteger)off length:(NSUInteger)len tail:(const void *)tail tailLength:(NSUInteger)tailLen { + self = [super init]; + if (off || len) REQUIRE_NOT_NULL(dat); + if (tailLen) REQUIRE_NOT_NULL(tail); + HFASSERT(tailLen <= MAX_TAIL_LENGTH); + HFASSERT(off + len >= off); + HFASSERT(off + len <= [dat length]); + offset = off; + length = len; + data = [dat retain]; + HFASSERT(tailLen <= UCHAR_MAX); + inlineTailLength = (unsigned char)tailLen; + memcpy(inlineTail, tail, tailLen); + HFASSERT([self length] == tailLen + len); + return self; +} + +- (void)dealloc { + [data release]; + [super dealloc]; +} + +- (unsigned long long)length { + return length + inlineTailLength; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)lrange { + HFASSERT(HFSum(length, inlineTailLength) >= HFMaxRange(lrange)); + NSRange requestedRange = NSMakeRange(ll2l(lrange.location), ll2l(lrange.length)); + NSRange dataRange = NSMakeRange(0, length); + NSRange tailRange = NSMakeRange(length, inlineTailLength); + NSRange dataRangeToCopy = NSIntersectionRange(requestedRange, dataRange); + NSRange tailRangeToCopy = NSIntersectionRange(requestedRange, tailRange); + HFASSERT(HFSum(dataRangeToCopy.length, tailRangeToCopy.length) == lrange.length); + + if (dataRangeToCopy.length > 0) { + HFASSERT(HFSum(NSMaxRange(dataRangeToCopy), offset) <= [data length]); + const void *bytes = [data bytes]; + memcpy(dst, bytes + dataRangeToCopy.location + offset, dataRangeToCopy.length); + } + if (tailRangeToCopy.length > 0) { + HFASSERT(tailRangeToCopy.location >= length); + HFASSERT(NSMaxRange(tailRangeToCopy) - length <= inlineTailLength); + memcpy(dst + dataRangeToCopy.length, inlineTail + tailRangeToCopy.location - length, tailRangeToCopy.length); + } +} + +- (HFByteSlice *)subsliceWithRange:(HFRange)lrange { + if (HFRangeEqualsRange(lrange, HFRangeMake(0, HFSum(length, inlineTailLength)))) return [[self retain] autorelease]; + + HFByteSlice *result; + HFASSERT(lrange.length > 0); + HFASSERT(HFSum(length, inlineTailLength) >= HFMaxRange(lrange)); + NSRange requestedRange = NSMakeRange(ll2l(lrange.location), ll2l(lrange.length)); + NSRange dataRange = NSMakeRange(0, length); + NSRange tailRange = NSMakeRange(length, inlineTailLength); + NSRange dataRangeToCopy = NSIntersectionRange(requestedRange, dataRange); + NSRange tailRangeToCopy = NSIntersectionRange(requestedRange, tailRange); + HFASSERT(HFSum(dataRangeToCopy.length, tailRangeToCopy.length) == lrange.length); + + NSMutableData *resultData = NULL; + NSUInteger resultOffset = 0; + NSUInteger resultLength = 0; + const unsigned char *tail = NULL; + NSUInteger tailLength = 0; + if (dataRangeToCopy.length > 0) { + resultData = data; + HFASSERT(resultData != NULL); + resultOffset = offset + dataRangeToCopy.location; + resultLength = dataRangeToCopy.length; + HFASSERT(HFSum(resultOffset, resultLength) <= [data length]); + } + if (tailRangeToCopy.length > 0) { + tail = inlineTail + tailRangeToCopy.location - length; + tailLength = tailRangeToCopy.length; + HFASSERT(tail >= inlineTail && tail + tailLength <= inlineTail + inlineTailLength); + } + HFASSERT(resultLength + tailLength == lrange.length); + result = [[[[self class] alloc] initWithSharedData:resultData offset:resultOffset length:resultLength tail:tail tailLength:tailLength] autorelease]; + HFASSERT([result length] == lrange.length); + return result; +} + +- (HFByteSlice *)byteSliceByAppendingSlice:(HFByteSlice *)slice { + REQUIRE_NOT_NULL(slice); + const unsigned long long sliceLength = [slice length]; + if (sliceLength == 0) return self; + + const unsigned long long thisLength = [self length]; + + HFASSERT(inlineTailLength <= MAX_TAIL_LENGTH); + NSUInteger spaceRemainingInTail = MAX_TAIL_LENGTH - inlineTailLength; + + if (sliceLength <= spaceRemainingInTail) { + /* We can do our work entirely within the tail */ + NSUInteger newTailLength = (NSUInteger)sliceLength + inlineTailLength; + unsigned char newTail[MAX_TAIL_LENGTH]; + memcpy(newTail, inlineTail, inlineTailLength); + [slice copyBytes:newTail + inlineTailLength range:HFRangeMake(0, sliceLength)]; + HFByteSlice *result = [[[[self class] alloc] initWithSharedData:data offset:offset length:length tail:newTail tailLength:newTailLength] autorelease]; + HFASSERT([result length] == HFSum(sliceLength, thisLength)); + return result; + } + else { + /* We can't do our work entirely in the tail; see if we can append some shared data. */ + HFASSERT(offset + length >= offset); + if (offset + length == [data length]) { + /* We can append some shared data. But impose some reasonable limit on how big our slice can get; this is 16 MB */ + if (HFSum(thisLength, sliceLength) < (1ULL << 24)) { + NSUInteger newDataOffset = offset; + NSUInteger newDataLength = length; + unsigned char newDataTail[MAX_TAIL_LENGTH]; + unsigned char newDataTailLength = MAX_TAIL_LENGTH; + NSMutableData *newData = (data ? data : [[[NSMutableData alloc] init] autorelease]); + + NSUInteger sliceLengthInt = ll2l(sliceLength); + NSUInteger newTotalTailLength = sliceLengthInt + inlineTailLength; + HFASSERT(newTotalTailLength >= MAX_TAIL_LENGTH); + NSUInteger amountToShiftIntoSharedData = newTotalTailLength - MAX_TAIL_LENGTH; + NSUInteger amountToShiftIntoSharedDataFromTail = MIN(amountToShiftIntoSharedData, inlineTailLength); + NSUInteger amountToShiftIntoSharedDataFromNewSlice = amountToShiftIntoSharedData - amountToShiftIntoSharedDataFromTail; + + if (amountToShiftIntoSharedDataFromTail > 0) { + HFASSERT(amountToShiftIntoSharedDataFromTail <= inlineTailLength); + [newData appendBytes:inlineTail length:amountToShiftIntoSharedDataFromTail]; + newDataLength += amountToShiftIntoSharedDataFromTail; + } + if (amountToShiftIntoSharedDataFromNewSlice > 0) { + HFASSERT(amountToShiftIntoSharedDataFromNewSlice <= [slice length]); + NSUInteger dataLength = offset + length + amountToShiftIntoSharedDataFromTail; + HFASSERT([newData length] == dataLength); + [newData setLength:dataLength + amountToShiftIntoSharedDataFromNewSlice]; + [slice copyBytes:[newData mutableBytes] + dataLength range:HFRangeMake(0, amountToShiftIntoSharedDataFromNewSlice)]; + newDataLength += amountToShiftIntoSharedDataFromNewSlice; + } + + /* We've updated our data; now figure out the tail */ + NSUInteger amountOfTailFromNewSlice = sliceLengthInt - amountToShiftIntoSharedDataFromNewSlice; + HFASSERT(amountOfTailFromNewSlice <= MAX_TAIL_LENGTH); + [slice copyBytes:newDataTail + MAX_TAIL_LENGTH - amountOfTailFromNewSlice range:HFRangeMake(sliceLengthInt - amountOfTailFromNewSlice, amountOfTailFromNewSlice)]; + + /* Copy the rest, if any, from the end of self */ + NSUInteger amountOfTailFromSelf = MAX_TAIL_LENGTH - amountOfTailFromNewSlice; + HFASSERT(amountOfTailFromSelf <= inlineTailLength); + if (amountOfTailFromSelf > 0) { + memcpy(newDataTail, inlineTail + inlineTailLength - amountOfTailFromSelf, amountOfTailFromSelf); + } + + HFByteSlice *result = [[[[self class] alloc] initWithSharedData:newData offset:newDataOffset length:newDataLength tail:newDataTail tailLength:newDataTailLength] autorelease]; + HFASSERT([result length] == HFSum([slice length], [self length])); + return result; + } + } + } + return nil; +} + +@end diff --git a/HexFiend/HFStatusBarRepresenter.h b/HexFiend/HFStatusBarRepresenter.h new file mode 100644 index 0000000..e70b893 --- /dev/null +++ b/HexFiend/HFStatusBarRepresenter.h @@ -0,0 +1,31 @@ +// +// HFStatusBarRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @enum HFStatusBarMode + The HFStatusBarMode enum is used to describe the format of the byte counts displayed by the status bar. +*/ +typedef NS_ENUM(NSUInteger, HFStatusBarMode) { + HFStatusModeDecimal, ///< The status bar should display byte counts in decimal + HFStatusModeHexadecimal, ///< The status bar should display byte counts in hexadecimal + HFStatusModeApproximate, ///< The text should display byte counts approximately (e.g. "56.3 KB") + HFSTATUSMODECOUNT ///< The number of modes, to allow easy cycling +}; + +/*! @class HFStatusBarRepresenter + @brief The HFRepresenter for the status bar. + + HFStatusBarRepresenter is a subclass of HFRepresenter responsible for showing the status bar, which displays information like the total length of the document, or the number of selected bytes. +*/ +@interface HFStatusBarRepresenter : HFRepresenter { + HFStatusBarMode statusMode; +} + +@property (nonatomic) HFStatusBarMode statusMode; + +@end diff --git a/HexFiend/HFStatusBarRepresenter.m b/HexFiend/HFStatusBarRepresenter.m new file mode 100644 index 0000000..702dfea --- /dev/null +++ b/HexFiend/HFStatusBarRepresenter.m @@ -0,0 +1,266 @@ +// +// HFStatusBarRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +#define kHFStatusBarDefaultModeUserDefaultsKey @"HFStatusBarDefaultMode" + +@interface HFStatusBarView : NSView { + NSCell *cell; + NSSize cellSize; + HFStatusBarRepresenter *representer; + NSDictionary *cellAttributes; + BOOL registeredForAppNotifications; +} + +- (void)setRepresenter:(HFStatusBarRepresenter *)rep; +- (void)setString:(NSString *)string; + +@end + + +@implementation HFStatusBarView + +- (void)_sharedInitStatusBarView { + NSMutableParagraphStyle *style = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; + [style setAlignment:NSCenterTextAlignment]; + cellAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSColor colorWithCalibratedWhite:(CGFloat).15 alpha:1], NSForegroundColorAttributeName, [NSFont labelFontOfSize:10], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]; + cell = [[NSCell alloc] initTextCell:@""]; + [cell setAlignment:NSCenterTextAlignment]; + [cell setBackgroundStyle:NSBackgroundStyleRaised]; +} + +- (instancetype)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + [self _sharedInitStatusBarView]; + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + [self _sharedInitStatusBarView]; + return self; +} + +// nothing to do in encodeWithCoder + +- (BOOL)isFlipped { return YES; } + +- (void)setRepresenter:(HFStatusBarRepresenter *)rep { + representer = rep; +} + +- (void)setString:(NSString *)string { + [cell setAttributedStringValue:[[[NSAttributedString alloc] initWithString:string attributes:cellAttributes] autorelease]]; + cellSize = [cell cellSize]; + [self setNeedsDisplay:YES]; +} + +- (void)drawDividerWithClip:(NSRect)clipRect { + [[NSColor lightGrayColor] set]; + NSRect bounds = [self bounds]; + NSRect lineRect = bounds; + lineRect.size.height = 1; + NSRectFill(NSIntersectionRect(lineRect, clipRect)); +} + + +- (NSGradient *)getGradient:(BOOL)active { + static NSGradient *sActiveGradient; + static NSGradient *sInactiveGradient; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sActiveGradient = [[NSGradient alloc] initWithColorsAndLocations: + [NSColor colorWithCalibratedWhite:.89 alpha:1.], 0.00, + [NSColor colorWithCalibratedWhite:.77 alpha:1.], 0.9, + [NSColor colorWithCalibratedWhite:.82 alpha:1.], 1.0, + nil]; + + sInactiveGradient = [[NSGradient alloc] initWithColorsAndLocations: + [NSColor colorWithCalibratedWhite:.93 alpha:1.], 0.00, + [NSColor colorWithCalibratedWhite:.87 alpha:1.], 0.9, + [NSColor colorWithCalibratedWhite:.90 alpha:1.], 1.0, + nil]; + }); + return active ? sActiveGradient : sInactiveGradient; +} + + +- (void)drawRect:(NSRect)clip { + USE(clip); + NSRect bounds = [self bounds]; + // [[NSColor colorWithCalibratedWhite:(CGFloat).91 alpha:1] set]; + // NSRectFill(clip); + + NSWindow *window = [self window]; + BOOL drawActive = (window == nil || [window isMainWindow] || [window isKeyWindow]); + [[self getGradient:drawActive] drawInRect:bounds angle:90.]; + + [self drawDividerWithClip:clip]; + NSRect cellRect = NSMakeRect(NSMinX(bounds), HFCeil(NSMidY(bounds) - cellSize.height / 2), NSWidth(bounds), cellSize.height); + [cell drawWithFrame:cellRect inView:self]; +} + +- (void)mouseDown:(NSEvent *)event { + USE(event); + HFStatusBarMode newMode = ([representer statusMode] + 1) % HFSTATUSMODECOUNT; + [representer setStatusMode:newMode]; + [[NSUserDefaults standardUserDefaults] setInteger:newMode forKey:kHFStatusBarDefaultModeUserDefaultsKey]; +} + +- (void)windowDidChangeKeyStatus:(NSNotification *)note { + USE(note); + [self setNeedsDisplay:YES]; +} + +- (void)viewDidMoveToWindow { + HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !registeredForAppNotifications); + registeredForAppNotifications = YES; + [super viewDidMoveToWindow]; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + HFUnregisterViewForWindowAppearanceChanges(self, NO); + [super viewWillMoveToWindow:newWindow]; +} + +- (void)dealloc { + HFUnregisterViewForWindowAppearanceChanges(self, registeredForAppNotifications); + [cell release]; + [cellAttributes release]; + [super dealloc]; +} + +@end + +@implementation HFStatusBarRepresenter + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeInt64:statusMode forKey:@"HFStatusMode"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + statusMode = (NSUInteger)[coder decodeInt64ForKey:@"HFStatusMode"]; + return self; +} + +- (instancetype)init { + self = [super init]; + statusMode = [[NSUserDefaults standardUserDefaults] integerForKey:kHFStatusBarDefaultModeUserDefaultsKey]; + return self; +} + +- (NSView *)createView { + HFStatusBarView *view = [[HFStatusBarView alloc] initWithFrame:NSMakeRect(0, 0, 100, 18)]; + [view setRepresenter:self]; + [view setAutoresizingMask:NSViewWidthSizable]; + return view; +} + +- (NSString *)describeLength:(unsigned long long)length { + switch (statusMode) { + case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu byte%s", length, length == 1 ? "" : "s"]; + case HFStatusModeHexadecimal: return [NSString stringWithFormat:@"0x%llX byte%s", length, length == 1 ? "" : "s"]; + case HFStatusModeApproximate: return [NSString stringWithFormat:@"%@", HFDescribeByteCount(length)]; + default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; + } +} + +- (NSString *)describeOffset:(unsigned long long)offset { + switch (statusMode) { + case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu", offset]; + case HFStatusModeHexadecimal: return [NSString stringWithFormat:@"0x%llX", offset]; + case HFStatusModeApproximate: return [NSString stringWithFormat:@"%@", HFDescribeByteCount(offset)]; + default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; + } +} + +/* same as describeOffset, except we treat Approximate like Hexadecimal */ +- (NSString *)describeOffsetExcludingApproximate:(unsigned long long)offset { + switch (statusMode) { + case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu", offset]; + case HFStatusModeHexadecimal: + case HFStatusModeApproximate: return [NSString stringWithFormat:@"0x%llX", offset]; + default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; + } +} + +- (NSString *)stringForEmptySelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length { + return [NSString stringWithFormat:@"%@ out of %@", [self describeOffset:offset], [self describeLength:length]]; +} + +- (NSString *)stringForSingleByteSelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length { + return [NSString stringWithFormat:@"Byte %@ selected out of %@", [self describeOffset:offset], [self describeLength:length]]; +} + +- (NSString *)stringForSingleRangeSelection:(HFRange)range length:(unsigned long long)length { + return [NSString stringWithFormat:@"%@ selected at offset %@ out of %@", [self describeLength:range.length], [self describeOffsetExcludingApproximate:range.location], [self describeLength:length]]; +} + +- (NSString *)stringForMultipleSelectionsWithLength:(unsigned long long)multipleSelectionLength length:(unsigned long long)length { + return [NSString stringWithFormat:@"%@ selected at multiple offsets out of %@", [self describeLength:multipleSelectionLength], [self describeLength:length]]; +} + + +- (void)updateString { + NSString *string = nil; + HFController *controller = [self controller]; + if (controller) { + unsigned long long length = [controller contentsLength]; + NSArray *ranges = [controller selectedContentsRanges]; + NSUInteger rangeCount = [ranges count]; + if (rangeCount == 1) { + HFRange range = [ranges[0] HFRange]; + if (range.length == 0) { + string = [self stringForEmptySelectionAtOffset:range.location length:length]; + } + else if (range.length == 1) { + string = [self stringForSingleByteSelectionAtOffset:range.location length:length]; + } + else { + string = [self stringForSingleRangeSelection:range length:length]; + } + } + else { + unsigned long long totalSelectionLength = 0; + FOREACH(HFRangeWrapper *, wrapper, ranges) { + HFRange range = [wrapper HFRange]; + totalSelectionLength = HFSum(totalSelectionLength, range.length); + } + string = [self stringForMultipleSelectionsWithLength:totalSelectionLength length:length]; + } + } + if (! string) string = @""; + [[self view] setString:string]; +} + +- (HFStatusBarMode)statusMode { + return statusMode; +} + +- (void)setStatusMode:(HFStatusBarMode)mode { + statusMode = mode; + [self updateString]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & (HFControllerContentLength | HFControllerSelectedRanges)) { + [self updateString]; + } +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(0, -1); +} + +@end diff --git a/HexFiend/HFStringEncodingTextRepresenter.h b/HexFiend/HFStringEncodingTextRepresenter.h new file mode 100644 index 0000000..2c5da7b --- /dev/null +++ b/HexFiend/HFStringEncodingTextRepresenter.h @@ -0,0 +1,26 @@ +// +// HFASCIITextRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFStringEncodingTextRepresenter + + @brief An HFRepresenter responsible for showing data interpreted via an NSStringEncoding. + + HFHexTextRepresenter is an HFRepresenter responsible for showing and editing data interpreted via an NSStringEncoding. Currently only supersets of ASCII are supported. +*/ +@interface HFStringEncodingTextRepresenter : HFTextRepresenter { + NSStringEncoding stringEncoding; + +} + +/*! Get the string encoding for this representer. The default encoding is [NSString defaultCStringEncoding]. */ +@property (nonatomic) NSStringEncoding encoding; + +/*! Set the string encoding for this representer. */ + +@end diff --git a/HexFiend/HFStringEncodingTextRepresenter.m b/HexFiend/HFStringEncodingTextRepresenter.m new file mode 100644 index 0000000..27ea995 --- /dev/null +++ b/HexFiend/HFStringEncodingTextRepresenter.m @@ -0,0 +1,121 @@ +// +// HFASCIITextRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +@interface HFStringEncodingPasteboardOwner : HFPasteboardOwner { + NSStringEncoding encoding; +} +@property (nonatomic) NSStringEncoding encoding; +@end + +@implementation HFStringEncodingPasteboardOwner +- (void)setEncoding:(NSStringEncoding)val { encoding = val; } +- (NSStringEncoding)encoding { return encoding; } + +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { + HFASSERT([type isEqual:NSStringPboardType]); + HFByteArray *byteArray = [self byteArray]; + HFASSERT(length <= NSUIntegerMax); + NSUInteger dataLength = ll2l(length); + NSUInteger stringLength = dataLength; + NSUInteger offset = 0, remaining = dataLength; + unsigned char * restrict const stringBuffer = check_malloc(stringLength); + while (remaining > 0) { + NSUInteger amountToCopy = MIN(32u * 1024u, remaining); + [byteArray copyBytes:stringBuffer + offset range:HFRangeMake(offset, amountToCopy)]; + offset += amountToCopy; + remaining -= amountToCopy; + } + NSString *string = [[NSString alloc] initWithBytesNoCopy:stringBuffer length:stringLength encoding:encoding freeWhenDone:YES]; + [pboard setString:string forType:type]; + [string release]; +} + +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { + return dataLength; +} + +@end + +@implementation HFStringEncodingTextRepresenter + +- (instancetype)init { + self = [super init]; + stringEncoding = [NSString defaultCStringEncoding]; + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + stringEncoding = (NSStringEncoding)[coder decodeInt64ForKey:@"HFStringEncoding"]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeInt64:stringEncoding forKey:@"HFStringEncoding"]; +} + +- (Class)_textViewClass { + return [HFRepresenterStringEncodingTextView class]; +} + +- (NSStringEncoding)encoding { + return stringEncoding; +} + +- (void)setEncoding:(NSStringEncoding)encoding { + stringEncoding = encoding; + [[self view] setEncoding:encoding]; + [[self controller] representer:self changedProperties:HFControllerViewSizeRatios]; +} + +- (void)initializeView { + [[self view] setEncoding:stringEncoding]; + [super initializeView]; +} + +- (void)insertText:(NSString *)text { + REQUIRE_NOT_NULL(text); + NSData *data = [text dataUsingEncoding:[self encoding] allowLossyConversion:NO]; + if (! data) { + NSBeep(); + } + else if ([data length]) { // a 0 length text can come about via e.g. option-e + [[self controller] insertData:data replacingPreviousBytes:0 allowUndoCoalescing:YES]; + } +} + +- (NSData *)dataFromPasteboardString:(NSString *)string { + REQUIRE_NOT_NULL(string); + return [string dataUsingEncoding:[self encoding] allowLossyConversion:NO]; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(1, 0); +} + +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + HFByteArray *selection = [[self controller] byteArrayForSelectedContentsRanges]; + HFASSERT(selection != NULL); + if ([selection length] == 0) { + NSBeep(); + } + else { + HFStringEncodingPasteboardOwner *owner = [HFStringEncodingPasteboardOwner ownPasteboard:pb forByteArray:selection withTypes:@[HFPrivateByteArrayPboardType, NSStringPboardType]]; + [owner setEncoding:[self encoding]]; + [owner setBytesPerLine:[self bytesPerLine]]; + } +} + +@end diff --git a/HexFiend/HFTextRepresenter.h b/HexFiend/HFTextRepresenter.h new file mode 100644 index 0000000..306a197 --- /dev/null +++ b/HexFiend/HFTextRepresenter.h @@ -0,0 +1,39 @@ +// +// HFTextRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +/*! @class HFTextRepresenter + @brief An HFRepresenter that draws text (e.g. the hex or ASCII view). + + HFTextRepresenter is an abstract subclass of HFRepresenter that is responsible for displaying text. There are two concrete subclass, HFHexTextRepresenter and HFStringEncodingTextRepresenter. + + Most of the functionality of HFTextRepresenter is private, and there is not yet enough exposed to allow creating new representers based on it. However, there is a small amount of configurability. +*/ +@interface HFTextRepresenter : HFRepresenter {} +/*! Given a rect edge, return an NSRect representing the maximum edge in that direction, in the coordinate system of the receiver's view. The dimension in the direction of the edge is 0 (so if edge is NSMaxXEdge, the resulting width is 0). The returned rect is in the coordinate space of the receiver's view. If the byte range is not displayed, returns NSZeroRect. + + If range is entirely above the visible region, returns an NSRect whose width and height are 0, and whose origin is -CGFLOAT_MAX (the most negative CGFloat). If range is entirely below the visible region, returns the same except with CGFLOAT_MAX (positive). + + This raises an exception if range is empty. +*/ +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forByteRange:(HFRange)range; + +/*! Returns the origin of the character at the given byte index. The returned point is in the coordinate space of the receiver's view. If the character is not displayed because it would be above the displayed range, returns {0, -CGFLOAT_MAX}. If it is not displayed because it is below the displayed range, returns {0, CGFLOAT_MAX}. As a special affordance, you may pass a byte index one greater than the contents length of the controller, and it will return the result as if the byte existed. + */ +- (NSPoint)locationOfCharacterAtByteIndex:(unsigned long long)byteIndex; + +/*! The per-row background colors. Each row is drawn with the next color in turn, cycling back to the beginning when the array is exhausted. Any empty space is filled with the first color in the array. If the array is empty, then the background is drawn with \c clearColor. + */ +@property (nonatomic, copy) NSArray *rowBackgroundColors; + +/*! Whether the text view behaves like a text field (YES) or a text view (NO). Currently this determines whether it draws a focus ring when it is the first responder. +*/ +@property (nonatomic) BOOL behavesAsTextField; + +@end diff --git a/HexFiend/HFTextRepresenter.m b/HexFiend/HFTextRepresenter.m new file mode 100644 index 0000000..edc3515 --- /dev/null +++ b/HexFiend/HFTextRepresenter.m @@ -0,0 +1,373 @@ +// +// HFTextRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import +#import + +@implementation HFTextRepresenter + +- (Class)_textViewClass { + UNIMPLEMENTED(); +} + +- (instancetype)init { + self = [super init]; + + NSColor *color1 = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0]; + NSColor *color2 = [NSColor colorWithCalibratedRed:.87 green:.89 blue:1. alpha:1.]; + _rowBackgroundColors = [@[color1, color2] retain]; + + return self; +} + +- (void)dealloc { + if ([self isViewLoaded]) { + [[self view] clearRepresenter]; + } + [_rowBackgroundColors release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeBool:_behavesAsTextField forKey:@"HFBehavesAsTextField"]; + [coder encodeObject:_rowBackgroundColors forKey:@"HFRowBackgroundColors"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + _behavesAsTextField = [coder decodeBoolForKey:@"HFBehavesAsTextField"]; + _rowBackgroundColors = [[coder decodeObjectForKey:@"HFRowBackgroundColors"] retain]; + return self; +} + +- (NSView *)createView { + HFRepresenterTextView *view = [[[self _textViewClass] alloc] initWithRepresenter:self]; + [view setAutoresizingMask:NSViewHeightSizable]; + return view; +} + +- (HFByteArrayDataStringType)byteArrayDataStringType { + UNIMPLEMENTED(); +} + +- (HFRange)entireDisplayedRange { + HFController *controller = [self controller]; + unsigned long long contentsLength = [controller contentsLength]; + HFASSERT(controller != NULL); + HFFPRange displayedLineRange = [controller displayedLineRange]; + NSUInteger bytesPerLine = [controller bytesPerLine]; + unsigned long long lineStart = HFFPToUL(floorl(displayedLineRange.location)); + unsigned long long lineEnd = HFFPToUL(ceill(displayedLineRange.location + displayedLineRange.length)); + HFASSERT(lineEnd >= lineStart); + HFRange byteRange = HFRangeMake(HFProductULL(bytesPerLine, lineStart), HFProductULL(lineEnd - lineStart, bytesPerLine)); + if (byteRange.length == 0) { + /* This can happen if we are too small to even show one line */ + return HFRangeMake(0, 0); + } + else { + HFASSERT(byteRange.location <= contentsLength); + byteRange.length = MIN(byteRange.length, contentsLength - byteRange.location); + HFASSERT(HFRangeIsSubrangeOfRange(byteRange, HFRangeMake(0, [controller contentsLength]))); + return byteRange; + } +} + +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forByteRange:(HFRange)byteRange { + HFASSERT(byteRange.length > 0); + HFRange displayedRange = [self entireDisplayedRange]; + HFRange intersection = HFIntersectionRange(displayedRange, byteRange); + NSRect result; + if (intersection.length > 0) { + NSRange intersectionNSRange = NSMakeRange(ll2l(intersection.location - displayedRange.location), ll2l(intersection.length)); + if (intersectionNSRange.length > 0) { + result = [[self view] furthestRectOnEdge:edge forRange:intersectionNSRange]; + } + } + else if (byteRange.location < displayedRange.location) { + /* We're below it. */ + return NSMakeRect(-CGFLOAT_MAX, -CGFLOAT_MAX, 0, 0); + } + else if (byteRange.location >= HFMaxRange(displayedRange)) { + /* We're above it */ + return NSMakeRect(CGFLOAT_MAX, CGFLOAT_MAX, 0, 0); + } + else { + /* Shouldn't be possible to get here */ + [NSException raise:NSInternalInconsistencyException format:@"furthestRectOnEdge: expected an intersection, or a range below or above the byte range, but nothin'"]; + } + return result; +} + +- (NSPoint)locationOfCharacterAtByteIndex:(unsigned long long)index { + NSPoint result; + HFRange displayedRange = [self entireDisplayedRange]; + if (HFLocationInRange(index, displayedRange) || index == HFMaxRange(displayedRange)) { + NSUInteger location = ll2l(index - displayedRange.location); + result = [[self view] originForCharacterAtByteIndex:location]; + } + else if (index < displayedRange.location) { + result = NSMakePoint(-CGFLOAT_MAX, -CGFLOAT_MAX); + } + else { + result = NSMakePoint(CGFLOAT_MAX, CGFLOAT_MAX); + } + return result; +} + +- (HFTextVisualStyleRun *)styleForAttributes:(NSSet *)attributes range:(NSRange)range { + HFTextVisualStyleRun *run = [[[HFTextVisualStyleRun alloc] init] autorelease]; + [run setRange:range]; + [run setForegroundColor:[NSColor blackColor]]; + + return run; +} + +- (NSArray *)stylesForRange:(HFRange)range { + return nil; +} + +- (void)updateText { + HFController *controller = [self controller]; + HFRepresenterTextView *view = [self view]; + HFRange entireDisplayedRange = [self entireDisplayedRange]; + [view setData:[controller dataForRange:entireDisplayedRange]]; + [view setStyles:[self stylesForRange:entireDisplayedRange]]; + HFFPRange lineRange = [controller displayedLineRange]; + long double offsetLongDouble = lineRange.location - floorl(lineRange.location); + CGFloat offset = ld2f(offsetLongDouble); + [view setVerticalOffset:offset]; + [view setStartingLineBackgroundColorIndex:ll2l(HFFPToUL(floorl(lineRange.location)) % NSUIntegerMax)]; +} + +- (void)initializeView { + [super initializeView]; + HFRepresenterTextView *view = [self view]; + HFController *controller = [self controller]; + if (controller) { + [view setFont:[controller font]]; + [view setEditable:[controller editable]]; + [self updateText]; + } + else { + [view setFont:[NSFont fontWithName:HFDEFAULT_FONT size:HFDEFAULT_FONTSIZE]]; + } +} + +- (void)scrollWheel:(NSEvent *)event { + [[self controller] scrollWithScrollEvent:event]; +} + +- (void)selectAll:(id)sender { + [[self controller] selectAll:sender]; +} + +- (double)selectionPulseAmount { + return [[self controller] selectionPulseAmount]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & (HFControllerFont | HFControllerLineHeight)) { + [[self view] setFont:[[self controller] font]]; + } + if (bits & (HFControllerContentValue | HFControllerDisplayedLineRange | HFControllerByteRangeAttributes)) { + [self updateText]; + } + if (bits & (HFControllerSelectedRanges | HFControllerDisplayedLineRange)) { + [[self view] updateSelectedRanges]; + } + if (bits & (HFControllerEditable)) { + [[self view] setEditable:[[self controller] editable]]; + } + if (bits & (HFControllerAntialias)) { + [[self view] setShouldAntialias:[[self controller] shouldAntialias]]; + } + if (bits & (HFControllerShowCallouts)) { + [[self view] setShouldDrawCallouts:[[self controller] shouldShowCallouts]]; + } + if (bits & (HFControllerColorBytes)) { + if([[self controller] shouldColorBytes]) { + [[self view] setByteColoring: ^(uint8_t byte, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a){ + *r = *g = *b = (uint8_t)(255 * ((255-byte)/255.0*0.6+0.4)); + *a = (uint8_t)(255 * 0.7); + }]; + } else { + [[self view] setByteColoring:NULL]; + } + } + [super controllerDidChange:bits]; +} + +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight { + return [[self view] maximumAvailableLinesForViewHeight:viewHeight]; +} + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth { + return [[self view] maximumBytesPerLineForViewWidth:viewWidth]; +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + return [[self view] minimumViewWidthForBytesPerLine:bytesPerLine]; +} + +- (NSUInteger)byteGranularity { + HFRepresenterTextView *view = [self view]; + NSUInteger bytesPerColumn = MAX([view bytesPerColumn], 1u), bytesPerCharacter = [view bytesPerCharacter]; + return HFLeastCommonMultiple(bytesPerColumn, bytesPerCharacter); +} + +- (NSArray *)displayedSelectedContentsRanges { + HFController *controller = [self controller]; + NSArray *result; + NSArray *selectedRanges = [controller selectedContentsRanges]; + HFRange displayedRange = [self entireDisplayedRange]; + + HFASSERT(displayedRange.length <= NSUIntegerMax); + NEW_ARRAY(NSValue *, clippedSelectedRanges, [selectedRanges count]); + NSUInteger clippedRangeIndex = 0; + FOREACH(HFRangeWrapper *, wrapper, selectedRanges) { + HFRange selectedRange = [wrapper HFRange]; + BOOL clippedRangeIsVisible; + NSRange clippedSelectedRange; + /* Necessary because zero length ranges do not intersect anything */ + if (selectedRange.length == 0) { + /* Remember that {6, 0} is considered a subrange of {3, 3} */ + clippedRangeIsVisible = HFRangeIsSubrangeOfRange(selectedRange, displayedRange); + if (clippedRangeIsVisible) { + HFASSERT(selectedRange.location >= displayedRange.location); + clippedSelectedRange.location = ll2l(selectedRange.location - displayedRange.location); + clippedSelectedRange.length = 0; + } + } + else { + // selectedRange.length > 0 + clippedRangeIsVisible = HFIntersectsRange(selectedRange, displayedRange); + if (clippedRangeIsVisible) { + HFRange intersectionRange = HFIntersectionRange(selectedRange, displayedRange); + HFASSERT(intersectionRange.location >= displayedRange.location); + clippedSelectedRange.location = ll2l(intersectionRange.location - displayedRange.location); + clippedSelectedRange.length = ll2l(intersectionRange.length); + } + } + if (clippedRangeIsVisible) clippedSelectedRanges[clippedRangeIndex++] = [NSValue valueWithRange:clippedSelectedRange]; + } + result = [NSArray arrayWithObjects:clippedSelectedRanges count:clippedRangeIndex]; + FREE_ARRAY(clippedSelectedRanges); + return result; +} + +//maps bookmark keys as NSNumber to byte locations as NSNumbers. Because bookmark callouts may extend beyond the lines containing them, allow a larger range by 10 lines. +- (NSDictionary *)displayedBookmarkLocations { + NSMutableDictionary *result = nil; + HFController *controller = [self controller]; + NSUInteger rangeExtension = 10 * [controller bytesPerLine]; + HFRange displayedRange = [self entireDisplayedRange]; + + HFRange includedRange = displayedRange; + + /* Extend the bottom */ + unsigned long long bottomExtension = MIN(includedRange.location, rangeExtension); + includedRange.location -= bottomExtension; + includedRange.length += bottomExtension; + + /* Extend the top */ + unsigned long long topExtension = MIN([controller contentsLength] - HFMaxRange(includedRange), rangeExtension); + includedRange.length = HFSum(includedRange.length, topExtension); + + return result; +} + +- (unsigned long long)byteIndexForCharacterIndex:(NSUInteger)characterIndex { + HFController *controller = [self controller]; + HFFPRange lineRange = [controller displayedLineRange]; + unsigned long long scrollAmount = HFFPToUL(floorl(lineRange.location)); + unsigned long long byteIndex = HFProductULL(scrollAmount, [controller bytesPerLine]) + characterIndex * [[self view] bytesPerCharacter]; + return byteIndex; +} + +- (void)beginSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex { + [[self controller] beginSelectionWithEvent:event forByteIndex:[self byteIndexForCharacterIndex:characterIndex]]; +} + +- (void)continueSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex { + [[self controller] continueSelectionWithEvent:event forByteIndex:[self byteIndexForCharacterIndex:characterIndex]]; +} + +- (void)endSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex { + [[self controller] endSelectionWithEvent:event forByteIndex:[self byteIndexForCharacterIndex:characterIndex]]; +} + +- (void)insertText:(NSString *)text { + USE(text); + UNIMPLEMENTED_VOID(); +} + +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb { + USE(pb); + UNIMPLEMENTED_VOID(); +} + +- (void)cutSelectedBytesToPasteboard:(NSPasteboard *)pb { + [self copySelectedBytesToPasteboard:pb]; + [[self controller] deleteSelection]; +} + +- (NSData *)dataFromPasteboardString:(NSString *)string { + USE(string); + UNIMPLEMENTED(); +} + +- (BOOL)canPasteFromPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + if ([[self controller] editable]) { + // we can paste if the pboard contains text or contains an HFByteArray + return [HFPasteboardOwner unpackByteArrayFromPasteboard:pb] || [pb availableTypeFromArray:@[NSStringPboardType]]; + } + return NO; +} + +- (BOOL)canCut { + /* We can cut if we are editable, we have at least one byte selected, and we are not in overwrite mode */ + HFController *controller = [self controller]; + if ([controller editMode] != HFInsertMode) return NO; + if (! [controller editable]) return NO; + + FOREACH(HFRangeWrapper *, rangeWrapper, [controller selectedContentsRanges]) { + if ([rangeWrapper HFRange].length > 0) return YES; //we have something selected + } + return NO; // we did not find anything selected +} + +- (BOOL)pasteBytesFromPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + BOOL result = NO; + HFByteArray *byteArray = [HFPasteboardOwner unpackByteArrayFromPasteboard:pb]; + if (byteArray) { + [[self controller] insertByteArray:byteArray replacingPreviousBytes:0 allowUndoCoalescing:NO]; + result = YES; + } + else { + NSString *stringType = [pb availableTypeFromArray:@[NSStringPboardType]]; + if (stringType) { + NSString *stringValue = [pb stringForType:stringType]; + if (stringValue) { + NSData *data = [self dataFromPasteboardString:stringValue]; + if (data) { + [[self controller] insertData:data replacingPreviousBytes:0 allowUndoCoalescing:NO]; + } + } + } + } + return result; +} + +@end diff --git a/HexFiend/HFTextRepresenter_Internal.h b/HexFiend/HFTextRepresenter_Internal.h new file mode 100644 index 0000000..c1e9f01 --- /dev/null +++ b/HexFiend/HFTextRepresenter_Internal.h @@ -0,0 +1,33 @@ +#import + +@interface HFTextRepresenter (HFInternal) + +- (NSArray *)displayedSelectedContentsRanges; //returns an array of NSValues representing the selected ranges (as NSRanges) clipped to the displayed range. + +- (NSDictionary *)displayedBookmarkLocations; //returns an dictionary mapping bookmark names to bookmark locations. Bookmark locations may be negative. + +- (void)beginSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex; +- (void)continueSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex; +- (void)endSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex; + +// Copy/Paste methods +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb; +- (void)cutSelectedBytesToPasteboard:(NSPasteboard *)pb; +- (BOOL)canPasteFromPasteboard:(NSPasteboard *)pb; +- (BOOL)canCut; +- (BOOL)pasteBytesFromPasteboard:(NSPasteboard *)pb; + +// Must be implemented by subclasses +- (void)insertText:(NSString *)text; + +// Must be implemented by subclasses. Return NSData representing the string value. +- (NSData *)dataFromPasteboardString:(NSString *)string; + +// Value between [0, 1] +- (double)selectionPulseAmount; + +- (void)scrollWheel:(NSEvent *)event; + +- (void)selectAll:(id)sender; + +@end diff --git a/HexFiend/HFTextRepresenter_KeyBinding.m b/HexFiend/HFTextRepresenter_KeyBinding.m new file mode 100644 index 0000000..b6e16d6 --- /dev/null +++ b/HexFiend/HFTextRepresenter_KeyBinding.m @@ -0,0 +1,128 @@ +// +// HFTextRepresenter_KeyBinding.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +#define FORWARD(x) - (void)x : sender { USE(sender); UNIMPLEMENTED_VOID(); } + +@implementation HFTextRepresenter (HFKeyBinding) + +- (void)moveRight:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementByte andModifySelection:NO]; } +- (void)moveLeft:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementByte andModifySelection:NO]; } +- (void)moveUp:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementLine andModifySelection:NO]; } +- (void)moveDown:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementLine andModifySelection:NO]; } +- (void)moveWordRight:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementColumn andModifySelection:NO]; } +- (void)moveWordLeft:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementColumn andModifySelection:NO]; } + +- (void)moveRightAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementByte andModifySelection:YES]; } +- (void)moveLeftAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementByte andModifySelection:YES]; } +- (void)moveUpAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementLine andModifySelection:YES]; } +- (void)moveDownAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementLine andModifySelection:YES]; } +- (void)moveWordRightAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementColumn andModifySelection:YES]; } +- (void)moveWordLeftAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementColumn andModifySelection:YES]; } + +- (void)moveForward:unused { USE(unused); [self moveRight:unused]; } +- (void)moveBackward:unused { USE(unused); [self moveLeft:unused]; } + +- (void)moveWordForward:unused { USE(unused); [self moveWordRight:unused]; } +- (void)moveWordBackward:unused { USE(unused); [self moveWordLeft:unused]; } +- (void)moveForwardAndModifySelection:unused { USE(unused); [self moveRightAndModifySelection:unused]; } +- (void)moveBackwardAndModifySelection:unused { USE(unused); [self moveLeftAndModifySelection:unused]; } +- (void)moveWordForwardAndModifySelection:unused { USE(unused); [self moveForwardAndModifySelection:unused]; } +- (void)moveWordBackwardAndModifySelection:unused { USE(unused); [self moveBackwardAndModifySelection:unused]; } + +- (void)deleteBackward:unused { USE(unused); [[self controller] deleteDirection:HFControllerDirectionLeft]; } +- (void)deleteForward:unused { USE(unused); [[self controller] deleteDirection:HFControllerDirectionRight]; } +- (void)deleteWordForward:unused { USE(unused); [self deleteForward:unused]; } +- (void)deleteWordBackward:unused { USE(unused); [self deleteBackward:unused]; } + +- (void)delete:unused { USE(unused); [self deleteForward:unused]; } + + //todo: implement these + +- (void)deleteToBeginningOfLine:(id)sender { USE(sender); } +- (void)deleteToEndOfLine:(id)sender { USE(sender); } +- (void)deleteToBeginningOfParagraph:(id)sender { USE(sender); } +- (void)deleteToEndOfParagraph:(id)sender { USE(sender); } + +- (void)moveToBeginningOfLine:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionLeft andModifySelection:NO]; } +- (void)moveToEndOfLine:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionRight andModifySelection:NO]; } +- (void)moveToBeginningOfDocument:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementDocument andModifySelection:NO]; } +- (void)moveToEndOfDocument:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementDocument andModifySelection:NO]; } + +- (void)moveToBeginningOfLineAndModifySelection:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionLeft andModifySelection:YES]; } +- (void)moveToEndOfLineAndModifySelection:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionRight andModifySelection:YES]; } +- (void)moveToBeginningOfDocumentAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementDocument andModifySelection:YES]; } +- (void)moveToEndOfDocumentAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementDocument andModifySelection:YES]; } + +- (void)moveToBeginningOfParagraph:unused { USE(unused); [self moveToBeginningOfLine:unused]; } +- (void)moveToEndOfParagraph:unused { USE(unused); [self moveToEndOfLine:unused]; } +- (void)moveToBeginningOfParagraphAndModifySelection:unused { USE(unused); [self moveToBeginningOfLineAndModifySelection:unused]; } +- (void)moveToEndOfParagraphAndModifySelection:unused { USE(unused); [self moveToEndOfLineAndModifySelection:unused]; } + +- (void)scrollPageDown:unused { USE(unused); [[self controller] scrollByLines:[[self controller] displayedLineRange].length]; } +- (void)scrollPageUp:unused { USE(unused); [[self controller] scrollByLines: - [[self controller] displayedLineRange].length]; } +- (void)pageDown:unused { USE(unused); [self scrollPageDown:unused]; } +- (void)pageUp:unused { USE(unused); [self scrollPageUp:unused]; } + +- (void)centerSelectionInVisibleArea:unused { + USE(unused); + HFController *controller = [self controller]; + NSArray *selection = [controller selectedContentsRanges]; + unsigned long long min = ULLONG_MAX, max = 0; + HFASSERT([selection count] >= 1); + FOREACH(HFRangeWrapper *, wrapper, selection) { + HFRange range = [wrapper HFRange]; + min = MIN(min, range.location); + max = MAX(max, HFMaxRange(range)); + } + HFASSERT(max >= min); + [controller maximizeVisibilityOfContentsRange:HFRangeMake(min, max - min)]; +} + +- (void)insertTab:unused { + USE(unused); + [[[self view] window] selectNextKeyView:nil]; +} + +- (void)insertBacktab:unused { + USE(unused); + [[[self view] window] selectPreviousKeyView:nil]; +} + +FORWARD(scrollLineUp) +FORWARD(scrollLineDown) +FORWARD(transpose) +FORWARD(transposeWords) + +FORWARD(selectParagraph) +FORWARD(selectLine) +FORWARD(selectWord) +FORWARD(indent) +//FORWARD(insertNewline) +FORWARD(insertParagraphSeparator) +FORWARD(insertNewlineIgnoringFieldEditor) +FORWARD(insertTabIgnoringFieldEditor) +FORWARD(insertLineBreak) +FORWARD(insertContainerBreak) +FORWARD(changeCaseOfLetter) +FORWARD(uppercaseWord) +FORWARD(lowercaseWord) +FORWARD(capitalizeWord) +FORWARD(deleteBackwardByDecomposingPreviousCharacter) +FORWARD(yank) +FORWARD(complete) +FORWARD(setMark) +FORWARD(deleteToMark) +FORWARD(selectToMark) +FORWARD(swapWithMark) +//FORWARD(cancelOperation) + +@end + diff --git a/HexFiend/HFTextVisualStyleRun.h b/HexFiend/HFTextVisualStyleRun.h new file mode 100644 index 0000000..0fa2ea7 --- /dev/null +++ b/HexFiend/HFTextVisualStyleRun.h @@ -0,0 +1,23 @@ +// +// HFTextVisualStyle.h +// HexFiend_2 +// +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import + +@interface HFTextVisualStyleRun : NSObject {} + +@property (nonatomic, copy) NSColor *foregroundColor; +@property (nonatomic, copy) NSColor *backgroundColor; +@property (nonatomic) NSRange range; +@property (nonatomic) BOOL shouldDraw; +@property (nonatomic) CGFloat scale; +@property (nonatomic, copy) NSIndexSet *bookmarkStarts; +@property (nonatomic, copy) NSIndexSet *bookmarkExtents; +@property (nonatomic, copy) NSIndexSet *bookmarkEnds; + +- (void)set; + +@end diff --git a/HexFiend/HFTextVisualStyleRun.m b/HexFiend/HFTextVisualStyleRun.m new file mode 100644 index 0000000..e9675cd --- /dev/null +++ b/HexFiend/HFTextVisualStyleRun.m @@ -0,0 +1,79 @@ +// +// HFTextVisualStyleRun.m +// HexFiend_2 +// +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import "HFTextVisualStyleRun.h" + + +@implementation HFTextVisualStyleRun + +- (instancetype)init { + self = [super init]; + _scale = 1.; + _shouldDraw = YES; + return self; +} + +- (void)dealloc { + [_foregroundColor release]; + [_backgroundColor release]; + [_bookmarkStarts release]; + [_bookmarkExtents release]; + [super dealloc]; +} + +- (void)set { + [_foregroundColor set]; + if (_scale != (CGFloat)1.0) { + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + CGAffineTransform tm = CGContextGetTextMatrix(ctx); + /* Huge hack - adjust downward a little bit if we are scaling */ + tm = CGAffineTransformTranslate(tm, 0, -.25 * (_scale - 1)); + tm = CGAffineTransformScale(tm, _scale, _scale); + CGContextSetTextMatrix(ctx, tm); + } +} + +static inline NSUInteger flip(NSUInteger x) { + return _Generic(x, unsigned: NSSwapInt, unsigned long: NSSwapLong, unsigned long long: NSSwapLongLong)(x); +} +static inline NSUInteger rol(NSUInteger x, unsigned char r) { + r %= sizeof(NSUInteger)*8; + return (x << r) | (x << (sizeof(NSUInteger)*8 - r)); +} +- (NSUInteger)hash { + NSUInteger A = 0; + // All these hashes tend to have only low bits, except the double which has only high bits. +#define Q(x, r) rol(x, sizeof(NSUInteger)*r/6) + A ^= flip([_foregroundColor hash] ^ Q([_backgroundColor hash], 2)); // skew high + A ^= Q(_range.length ^ flip(_range.location), 2); // skew low + A ^= flip([_bookmarkStarts hash]) ^ Q([_bookmarkEnds hash], 3) ^ Q([_bookmarkExtents hash], 4); // skew high + A ^= _shouldDraw ? 0 : (NSUInteger)-1; + A ^= *(NSUInteger*)&_scale; // skew high + return A; +#undef Q +} + +- (BOOL)isEqual:(HFTextVisualStyleRun *)run { + if(![run isKindOfClass:[self class]]) return NO; + /* Check each field for equality. */ + if(!NSEqualRanges(_range, run->_range)) return NO; + if(_scale != run->_scale) return NO; + if(_shouldDraw != run->_shouldDraw) return NO; + if(!!_foregroundColor != !!run->_foregroundColor) return NO; + if(!!_backgroundColor != !!run->_backgroundColor) return NO; + if(!!_bookmarkStarts != !!run->_bookmarkStarts) return NO; + if(!!_bookmarkExtents != !!run->_bookmarkExtents) return NO; + if(!!_bookmarkEnds != !!run->_bookmarkEnds) return NO; + if(![_foregroundColor isEqual: run->_foregroundColor]) return NO; + if(![_backgroundColor isEqual: run->_backgroundColor]) return NO; + if(![_bookmarkStarts isEqual: run->_bookmarkStarts]) return NO; + if(![_bookmarkExtents isEqual: run->_bookmarkExtents]) return NO; + if(![_bookmarkEnds isEqual: run->_bookmarkEnds]) return NO; + return YES; +} + +@end diff --git a/HexFiend/HFTypes.h b/HexFiend/HFTypes.h new file mode 100644 index 0000000..b6ae681 --- /dev/null +++ b/HexFiend/HFTypes.h @@ -0,0 +1,13 @@ +/*! @brief HFRange is the 64 bit analog of NSRange, containing a 64 bit location and length. */ +typedef struct { + unsigned long long location; + unsigned long long length; +} HFRange; + +/*! @brief HFFPRange is a struct used for representing floating point ranges, similar to NSRange. It contains two long doubles. + + This is useful for (for example) showing the range of visible lines. A double-precision value has 53 significant bits in the mantissa - so we would start to have precision problems at the high end of the range we can represent. Long double has a 64 bit mantissa on Intel, which means that we would start to run into trouble at the very very end of our range - barely acceptable. */ +typedef struct { + long double location; + long double length; +} HFFPRange; diff --git a/HexFiend/HFVerticalScrollerRepresenter.h b/HexFiend/HFVerticalScrollerRepresenter.h new file mode 100644 index 0000000..a14881e --- /dev/null +++ b/HexFiend/HFVerticalScrollerRepresenter.h @@ -0,0 +1,21 @@ +// +// HFRepresenterVerticalScroller.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFVerticalScrollerRepresenter + @brief An HFRepresenter responsible for showing a vertical scroll bar. + + HFVerticalScrollerRepresenter is an HFRepresenter whose view is a vertical NSScroller, that represents the current position within an HFController "document." It has no methods beyond those of HFRepresenter. + + As HFVerticalScrollerRepresenter is an especially simple representer, it makes for good sample code. +*/ +@interface HFVerticalScrollerRepresenter : HFRepresenter { + +} + +@end diff --git a/HexFiend/HFVerticalScrollerRepresenter.m b/HexFiend/HFVerticalScrollerRepresenter.m new file mode 100644 index 0000000..371b568 --- /dev/null +++ b/HexFiend/HFVerticalScrollerRepresenter.m @@ -0,0 +1,133 @@ +// +// HFRepresenterVerticalScroller.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +/* Note that on Tiger, NSScroller did not support double in any meaningful way; [scroller doubleValue] always returns 0, and setDoubleValue: doesn't look like it works either. */ + +#import + + +@implementation HFVerticalScrollerRepresenter + +/* No special NSCoding support needed */ + +- (NSView *)createView { + NSScroller *scroller = [[NSScroller alloc] initWithFrame:NSMakeRect(0, 0, [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleLegacy], 64)]; + [scroller setTarget:self]; + [scroller setContinuous:YES]; + [scroller setEnabled:YES]; + [scroller setTarget:self]; + [scroller setAction:@selector(scrollerDidChangeValue:)]; + [scroller setAutoresizingMask:NSViewHeightSizable]; + return scroller; +} + +- (NSUInteger)visibleLines { + HFController *controller = [self controller]; + HFASSERT(controller != NULL); + return ll2l(HFFPToUL(ceill([controller displayedLineRange].length))); +} + +- (void)scrollByKnobToValue:(double)newValue { + HFASSERT(newValue >= 0. && newValue <= 1.); + HFController *controller = [self controller]; + unsigned long long contentsLength = [controller contentsLength]; + NSUInteger bytesPerLine = [controller bytesPerLine]; + HFASSERT(bytesPerLine > 0); + unsigned long long totalLineCountTimesBytesPerLine = HFRoundUpToNextMultipleSaturate(contentsLength - 1, bytesPerLine); + HFASSERT(totalLineCountTimesBytesPerLine == ULLONG_MAX || totalLineCountTimesBytesPerLine % bytesPerLine == 0); + unsigned long long totalLineCount = HFDivideULLRoundingUp(totalLineCountTimesBytesPerLine, bytesPerLine); + HFFPRange currentLineRange = [controller displayedLineRange]; + HFASSERT(currentLineRange.length < HFULToFP(totalLineCount)); + long double maxScroll = totalLineCount - currentLineRange.length; + long double newScroll = maxScroll * (long double)newValue; + [controller setDisplayedLineRange:(HFFPRange){newScroll, currentLineRange.length}]; +} + +- (void)scrollByLines:(long long)linesInt { + if (linesInt == 0) return; + + //note - this properly computes the absolute value even for LLONG_MIN + long double lines = HFULToFP((unsigned long long)llabs(linesInt)); + + HFController *controller = [self controller]; + HFASSERT(controller != NULL); + HFFPRange displayedRange = [[self controller] displayedLineRange]; + if (linesInt < 0) { + displayedRange.location -= MIN(lines, displayedRange.location); + } + else { + long double availableLines = HFULToFP([controller totalLineCount]); + displayedRange.location = MIN(availableLines - displayedRange.length, displayedRange.location + lines); + } + [controller setDisplayedLineRange:displayedRange]; +} + +- (void)scrollerDidChangeValue:(NSScroller *)scroller { + assert(scroller == [self view]); + switch ([scroller hitPart]) { + case NSScrollerDecrementPage: [self scrollByLines: -(long long)[self visibleLines]]; break; + case NSScrollerIncrementPage: [self scrollByLines: (long long)[self visibleLines]]; break; + case NSScrollerDecrementLine: [self scrollByLines: -1LL]; break; + case NSScrollerIncrementLine: [self scrollByLines: 1LL]; break; + case NSScrollerKnob: [self scrollByKnobToValue:[scroller doubleValue]]; break; + default: break; + } +} + +- (void)updateScrollerValue { + HFController *controller = [self controller]; + CGFloat value, proportion; + NSScroller *scroller = [self view]; + BOOL enable = YES; + if (controller == nil) { + value = 0; + proportion = 0; + } + else { + unsigned long long length = [controller contentsLength]; + HFFPRange lineRange = [controller displayedLineRange]; + HFASSERT(lineRange.location >= 0 && lineRange.length >= 0); + if (length == 0) { + value = 0; + proportion = 1; + enable = NO; + } + else { + long double availableLines = HFULToFP([controller totalLineCount]); + long double consumedLines = MAX(1., lineRange.length); + proportion = ld2f(lineRange.length / availableLines); + + long double maxScroll = availableLines - consumedLines; + HFASSERT(maxScroll >= lineRange.location); + if (maxScroll == 0.) { + enable = NO; + value = 0; + } + else { + value = ld2f(lineRange.location / maxScroll); + } + } + } + [scroller setDoubleValue:value]; + [scroller setKnobProportion:proportion]; + [scroller setEnabled:enable]; +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + USE(bytesPerLine); + return [NSScroller scrollerWidthForControlSize:[[self view] controlSize] scrollerStyle:NSScrollerStyleLegacy]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & (HFControllerContentLength | HFControllerDisplayedLineRange)) [self updateScrollerValue]; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(2, 0); +} + +@end diff --git a/HexFiend/HexFiend.h b/HexFiend/HexFiend.h new file mode 100644 index 0000000..60d69a7 --- /dev/null +++ b/HexFiend/HexFiend.h @@ -0,0 +1,78 @@ +/*! @mainpage HexFiend.framework + * + * @section intro Introduction + * HexFiend.framework (hereafter "Hex Fiend" when there is no risk of confusion with the app by the same name) is a framework designed to enable applications to support viewing and editing of binary data. The emphasis is on editing data in a natural way, following Mac OS X text editing conventions. + * + * Hex Fiend is designed to work efficiently with large amounts (64 bits worth) of data. As such, it can work with arbitrarily large files without reading the entire file into memory. This includes insertions, deletions, and in-place editing. Hex Fiend can also efficiently save such changes back to the file, without requiring any additional temporary disk space. + * + * Hex Fiend has a clean separation between the model, view, and controller layers. The model layer allows for efficient manipulation of raw data of mixed sources, making it useful for tools that need to work with large files. + * + * Both the framework and the app are open source under a BSD-style license. In summary, you may use Hex Fiend in any project as long as you include the copyright notice somewhere in the documentation. + * + * @section requirements Requirements + * Hex Fiend is only available on Mac OS X, and supported on Mountain Lion and later. + * + * @section getting_started Getting Started + * + * The Hex Fiend source code is available at http://ridiculousfish.com/hexfiend/ and on GitHub at https://github.com/ridiculousfish/HexFiend + * + * Hex Fiend comes with some sample code ("HexFiendling"), distributed as part of the project. And of course the Hex Fiend application itself is open source, acting as a more sophisticated sample code. +*/ + + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + + +/* The following is all for Doxygen */ + + +/*! @defgroup model Model + * Hex Fiend's model classes + */ +///@{ +///@class HFByteArray +///@class HFBTreeByteArray +///@class HFFullMemoryByteArray +///@class HFByteSlice +///@class HFFileByteSlice +///@class HFSharedMemoryByteSlice +///@class HFFullMemoryByteSlice + +///@} + + +/*! @defgroup view View + * Hex Fiend's view classes + */ +///@{ +///@class HFRepresenter +///@class HFHexTextRepresenter +///@class HFStringEncodingTextRepresenter +///@class HFLayoutRepresenter +///@class HFLineCountingRepresenter +///@class HFStatusBarRepresenter +///@class HFVerticalScrollerRepresenter +///@class HFLineCountingRepresenter + +///@} + +/*! @defgroup controller Controller + * Hex Fiend's controller classes + */ +///@{ +///@class HFController + +///@} diff --git a/HexFiend/HexFiend_2_Framework_Prefix.pch b/HexFiend/HexFiend_2_Framework_Prefix.pch new file mode 100644 index 0000000..96d0fb7 --- /dev/null +++ b/HexFiend/HexFiend_2_Framework_Prefix.pch @@ -0,0 +1,99 @@ +// +// Prefix header for all source files of the 'HexFiend_2' target in the 'HexFiend_2' project +// + +#ifdef __OBJC__ + #import + #import +#endif + +#define PRIVATE_EXTERN __private_extern__ + +#include + +#if ! NDEBUG +#define HFASSERT(a) assert(a) +#else +#define HFASSERT(a) if (0 && ! (a)) abort() +#endif + + +#define UNIMPLEMENTED_VOID() [NSException raise:NSGenericException \ + format:@"Message %@ sent to instance of class %@, "\ + @"which does not implement that method",\ + NSStringFromSelector(_cmd), [[self class] description]] + +#define UNIMPLEMENTED() UNIMPLEMENTED_VOID(); return 0 + +/* Macro to "use" a variable to prevent unused variable warnings. */ +#define USE(x) ((void)(x)) + +#define check_malloc(x) ({ size_t _count = (x); void *_result = malloc(_count); if(!_result) { fprintf(stderr, "Out of memory allocating %lu bytes\n", (unsigned long)_count); exit(EXIT_FAILURE); } _result; }) +#define check_calloc(x) ({ size_t _count = (x); void *_result = calloc(_count, 1); if(!_result) { fprintf(stderr, "Out of memory allocating %lu bytes\n", (unsigned long)_count); exit(EXIT_FAILURE); } _result; }) +#define check_realloc(p, x) ({ size_t _count = (x); void *_result = realloc((p), x); if(!_result) { fprintf(stderr, "Out of memory reallocating %lu bytes\n", (unsigned long)_count); exit(EXIT_FAILURE); } _result; }) + +#if ! NDEBUG +#define REQUIRE_NOT_NULL(a) do { \ + if ((a)==NULL) {\ + fprintf(stderr, "REQUIRE_NOT_NULL failed: NULL value for parameter " #a " on line %d in file %s\n", __LINE__, __FILE__);\ + abort();\ + }\ +} while (0) + +#define EXPECT_CLASS(e, c) do { \ + if (! [(e) isKindOfClass:[c class]]) {\ + fprintf(stderr, "EXPECT_CLASS failed: Expression " #e " is %s on line %d in file %s\n", (e) ? "(nil)" : [[e description] UTF8String], __LINE__, __FILE__);\ + abort();\ + }\ +} while (0) + +#else +#define REQUIRE_NOT_NULL(a) USE(a) +#define EXPECT_CLASS(e, c) USE(e) +#endif + +#define FOREACH(type, var, exp) for (type var in (exp)) + +#define NEW_ARRAY(type, name, number) \ + type name ## static_ [256];\ + type * name = ((number) <= 256 ? name ## static_ : check_malloc((number) * sizeof(type))) + +#define FREE_ARRAY(name) \ + if (name != name ## static_) free(name) + +#if !defined(MIN) + #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#endif + +#if !defined(MAX) + #define MAX(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) +#endif + +//How many bytes should we read at a time when doing a find/replace? +#define SEARCH_CHUNK_SIZE 32768 + +//What's the smallest clipboard data size we should offer to avoid copying when quitting? This is 5 MB +#define MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT (5UL << 20) + +//What's the largest clipboard data size we should support exporting (at all?) This is 500 MB. Note that we can still copy more data than this internally, we just can't put it in, say, TextEdit. +#define MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT (500UL << 20) + +// When we save a file, and other byte arrays need to break their dependencies on the file by copying some of its data into memory, what's the max amount we should copy (per byte array)? We currently don't show any progress for this, so this should be a smaller value +#define MAX_MEMORY_TO_USE_FOR_BREAKING_FILE_DEPENDENCIES_ON_SAVE (16 * 1024 * 1024) + +#ifdef __OBJC__ + #import + #import "HFFunctions_Private.h" +#endif + +#ifndef __has_feature // Optional. +#define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_RETURNS_RETAINED +#if __has_feature(attribute_ns_returns_retained) +#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained)) +#else +#define NS_RETURNS_RETAINED +#endif +#endif diff --git a/HexFiend/License.txt b/HexFiend/License.txt new file mode 100644 index 0000000..7760edb --- /dev/null +++ b/HexFiend/License.txt @@ -0,0 +1,21 @@ +Copyright (c) 2005-2009, Peter Ammon +* All rights reserved. +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile index 033a62c..7ad7c87 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ OBJ := build/obj CC := clang -CFLAGS += -Werror -Wall -std=gnu11 -ICore -D_GNU_SOURCE -DVERSION="$(VERSION)" +CFLAGS += -Werror -Wall -std=gnu11 -ICore -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. SDL_LDFLAGS := -lSDL LDFLAGS += -lc -lm CONF ?= debug @@ -32,7 +32,7 @@ endif ifeq ($(CONF),debug) CFLAGS += -g else ifeq ($(CONF), release) -CFLAGS += -O3 -flto +CFLAGS += -O3 -flto -DNDEBUG LDFLAGS += -flto else $(error Invalid value for CONF: $(CONF). Use "debug" or "release") @@ -46,7 +46,7 @@ CORE_SOURCES := $(shell echo Core/*.c) SDL_SOURCES := $(shell echo SDL/*.c) ifeq ($(shell uname -s),Darwin) -COCOA_SOURCES := $(shell echo Cocoa/*.m) +COCOA_SOURCES := $(shell echo Cocoa/*.m) $(shell echo HexFiend/*.m) SDL_SOURCES += $(shell echo SDL/*.m) endif @@ -75,6 +75,11 @@ $(OBJ)/%.c.o: %.c -@mkdir -p $(dir $@) $(CC) $(CFLAGS) -c $< -o $@ +# HexFiend requires more flags +$(OBJ)/HexFiend/%.m.o: HexFiend/%.m + -@mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(OCFLAGS) -c $< -o $@ -fno-objc-arc -include HexFiend/HexFiend_2_Framework_Prefix.pch + $(OBJ)/%.m.o: %.m -@mkdir -p $(dir $@) $(CC) $(CFLAGS) $(OCFLAGS) -c $< -o $@