//
// INewsBaseText extends Text with the following capabilities:
//
//  1. drag and drop of media files. e.g. .tiff, .snd and .mtif files.
//  2. copy and paste of NewsBasePboardType pasteboard type
//     NewsBasePboardType is a new pasteboard type  that allows you to
//     copy and paste the NewsBase Rich Text Format with embedded objects
//  3. support of selected Emacs-style editing commands
//  4. size dynamically updated.
//  5. scrolling using the space bar
//  6. find modified to work with embedded media objects
//
//  The NewsBasePboardType is a new private pasteboard type. The format
//  is header, body, header, body, ... header, body.  The header is a
//  struct mediaObjectDescriptor.  The body is the media object data as
//  written by the writeFromStream: method of the media object.  Struct
//  mediaObjectDescriptor is defined by:
//
//    struct mediaObjectDescriptor {
//        int magic;
//        char key[256];
//        char type[16];
//        int size;
//    };
//
//  where type is the same as the file extension fof the media object.
//  

#import "INewsBaseText.h"
#import <appkit/nextstd.h>
#import <appkit/Application.h>
#import <appkit/Pasteboard.h>
#import <appkit/Panel.h>
#import <appkit/Font.h>
#import <streams/streams.h>
#import <libc.h>
#import <strings.h>
#import <mach/mach.h>
#import "IMMEditor.h"
#import "IGraphicImage.h"
#import "IMediaTable.h"
#import "IMediaD.h"
#import "CopyIcon.h"
#import "INewScroller.h"
#import "IAppDelegate.h"
#import <ctype.h>
#import "data_types.h"
#import "errdebug.h"

#import "Localization.h"

#define LoStr(key)      doLocalString(NULL,key,NULL)


@implementation INewsBaseText

NXAtom NewsBasePboardType;		// new pasteboard type for NewsBase
					// RTF with embedded objects
static NXAtom *types;			// NULL terminated list of pasteboard
					// types in order by preference.

// initialize registers the rtf control word for embedded media objects and
// initializes the list of pasteboard types that InewsBaseText will write to
// the pasteboard.

+ initialize
{
    NXAtom *typesPtr;
    Class MediaClass;
    int i;

    [Text registerDirective:RTF_CONTROL_WORD forClass:[IGraphicImage class]];
    NewsBasePboardType = NXUniqueString(NEWSBASE_PBOARD_TYPE);
    typesPtr = types = malloc(([IMediaTable count] + 4) * sizeof(NXAtom));
    *typesPtr++ = NewsBasePboardType;
    // add types for supported media classes
    for (i = 0; (MediaClass = [IMediaTable mediaClassAt:i]) != Nil; ++i) {
        if ((*typesPtr = [(id)MediaClass pasteboardType]) != NULL) {
            ++typesPtr;
        }
    }
    *typesPtr++ = NXRTFPboardType;
    *typesPtr++ = NXAsciiPboardType;
    *typesPtr = NULL;
    return(self);
}

// [INewsBaseText initFrame:text:aligment:] sets special character and text
// filters then calls [Text initFrame:text:aligment:].

- initFrame:(const NXRect *)frameRect text:(const char *)theText
    alignment:(int)mode
{
    unsigned short emacsEditorFilter(unsigned short charCode, int flags,
        unsigned short charSet);
    char *textFilter(id sender, unsigned char *insertText,
        int *insertLength, int position);

    [super initFrame:frameRect text:theText alignment:mode];
    [NXApp registerServicesMenuSendTypes:types andReturnTypes:types];
#ifdef KANJI
    inputManager = [NXApp inputManager];
#endif

    [self setTextFilter:textFilter];
    // must keep track of embedded views in order to free them when text
    // is deleted first
    embeddedViewControllers = [List allocFromZone:[self zone]];
    return(self);
}

//*****************************************************************************
//   Pasteboard methods
//*****************************************************************************

// validRequestorForSendType:andReturnType: will always return self.
// This is incorrect but neccessary as determining the correct answer is
// computationally intensive and this false answer does not turn out to 
// be a problem in reality.

- validRequestorForSendType:(NXAtom)sendType andReturnType:(NXAtom)returnType
{
    return(self);
}

- copy:sender
{
    [self writeSelectionToPasteboard:[Pasteboard new] types:NULL];
    return(self);
}

// writeSelectionToPasteboard: will write the current selection to the
// pasteboard in the following types: NewsbasePboardType, media
// types (e.g., tiff, jpeg, snd, ...) for media objects present in the
// selection, NXRTFPboardType and NXAsciiPboardType

- (BOOL)writeSelectionToPasteboard:(Pasteboard *)pasteboard
    types:(NXAtom *)requestedTypes
{
    int typesNum;
    NXAtom types[16] = {NewsBasePboardType}, *typesPtr, type;
    NXSelPt start, end;
    NXStream *rtfStream, *stream;
    int length, realLength, maxLength;
    char *data;
    List *list;
    int n;
    IMediaD *mediaObject;
    struct mediaObjectDescriptor mediaObjectDescriptor;

    [self getSel:&start :&end];
    rtfStream = NXOpenMemory(NULL, 0, NX_READWRITE);
    // list will contain media objects in the selection
    [IGraphicImage setList:list = [[List alloc] init]];
    [self writeRichText:rtfStream from:start.cp to:end.cp];
    // add types for media objects in selection
    typesNum = 1;
    typesPtr = &types[1];
    for (n = 0; (mediaObject = [list objectAt:n]) != nil; ++n) {
        if ((*typesPtr = [[mediaObject class] pasteboardType]) != NULL) {
            ++typesNum;
            ++typesPtr;
        } 
    }
    // add NXRTFPboardType and NXAsciiPboardType
    *typesPtr = NXRTFPboardType;
    *++typesPtr = NXAsciiPboardType;
    typesNum += 2;
    [pasteboard declareTypes:types num:typesNum owner:NULL];
   
    // write NewsbasePboardType to pasteboard
    stream = NXOpenMemory(NULL, 0, NX_READWRITE);
    mediaObjectDescriptor.magic = MEDIA_OBJECT_DESCRIPTOR_MAGIC;
    NXGetMemoryBuffer(rtfStream, &data, &length, &maxLength);
    NXSeek(rtfStream, (long)0, NX_FROMEND);
    strncpy(mediaObjectDescriptor.key, NewsBasePboardType,
        sizeof(mediaObjectDescriptor.key));
    mediaObjectDescriptor.key[sizeof(mediaObjectDescriptor.key) - 1] = '\0';
    strncpy(mediaObjectDescriptor.type, "rtf",
        sizeof(mediaObjectDescriptor.type));
    mediaObjectDescriptor.type[sizeof(mediaObjectDescriptor.type) - 1] = '\0';
    length = NXTell(rtfStream);
    mediaObjectDescriptor.size = length;
    NXWrite(stream, &mediaObjectDescriptor, sizeof(mediaObjectDescriptor));
    NXWrite(stream, data, length);
    // now write all media objects to pasteboard in NewsbasePboardType
    for (n = 0; (mediaObject = [list objectAt:n]) != nil; ++n) {
        strncpy(mediaObjectDescriptor.key, [mediaObject key],
            sizeof(mediaObjectDescriptor.key));
        mediaObjectDescriptor.key[sizeof(mediaObjectDescriptor.key) - 1]
            = '\0';
        strncpy(mediaObjectDescriptor.type, [[mediaObject class] fileExtension],
            sizeof(mediaObjectDescriptor.type));
        mediaObjectDescriptor.type[sizeof(mediaObjectDescriptor.type) - 1]
            = '\0';
        mediaObjectDescriptor.size = [mediaObject size];
        NXWrite(stream, &mediaObjectDescriptor, sizeof(mediaObjectDescriptor));
        if (mediaObjectDescriptor.size > 0) {
            length = NXTell(stream);
            [mediaObject writeToStream:stream];
            NX_ASSERT(NXTell(stream) - length == mediaObjectDescriptor.size,
                "[mediaObject size] does not give correct length of output of "
                "[mediaObject writeToStream:]");
        }
    }
    NXGetMemoryBuffer(stream, &data, &length, &maxLength);
    NXSeek(stream, (long)0, NX_FROMEND);
    length = NXTell(stream);
    [pasteboard writeType:NewsBasePboardType data:data length:length];
    NXCloseMemory(stream, NX_FREEBUFFER);
   
    // write media objects to pasteboard in there natural pasteboard type
    for (n = 0; (mediaObject = [list objectAt:n]) != nil; ++n) {
        if ((type = [[mediaObject class] pasteboardType]) != NULL) {
            stream = NXOpenMemory(NULL, 0, NX_READWRITE);
            [mediaObject writeToStream:stream];
            NXGetMemoryBuffer(stream, &data, &length, &maxLength);
            NXSeek(stream, (long)0, NX_FROMEND);
            length = NXTell(stream);
            [pasteboard writeType:type data:data length:length];
            NXCloseMemory(stream, NX_FREEBUFFER);
        }
    }
    [IGraphicImage setList:nil];
    [list free];

    // do NXRTFPboardType
    NXGetMemoryBuffer(rtfStream, &data, &length, &maxLength);
    NXSeek(rtfStream, (long)0, NX_FROMEND);
    length = NXTell(rtfStream);
    NXPutc(rtfStream, '\0');
    [pasteboard writeType:NXRTFPboardType data:data length:length];
    NXCloseMemory(rtfStream, NX_FREEBUFFER);

    // finally do NXAsciiPboardType
    length = end.cp - start.cp;
    data = NXZoneMalloc([self zone], 3 * length + 1);
    realLength = [self getSubstring:data start:start.cp length:length];
    NX_ASSERT(realLength <= 3 * length, "[Text getSubstring:start:length:]"
        "copied more than specified length");
    [pasteboard writeType:NXAsciiPboardType data:data length:realLength];
    NXZoneFree([self zone], data);
    return(TRUE);
}

- paste:sender
{
    return([self readSelectionFromPasteboard:[Pasteboard new]]);
}

// readSelectionFromPasteboard reads the pasteboard.  It will read all
// standard types as well as the proprietary NewsbasePboardType.  If there
// are multiple representations on the pasteboard then the priority is 
// arranged as follows NewsbasePboardType, media types, NXRTFPboardType
// and finally NXAsciiPboardType

- readSelectionFromPasteboard:(Pasteboard *)pasteboard
{
    const NXAtom *type;
    NXStream *stream;
    char *data, *dataPtr, *dataEnd;
    int length;
    IGraphicImage *graphicObject;
    id mediaObject;
    Class mediaClass;
    id loadingAlertPanel;
    NXModalSession loadingModalSession;

    if (isEditable == NO || [self hasEmbeddedView] == YES) {
        if ([[self delegate] textWillChange:self] == YES) {
            return(nil);
        }
    }
    // If NewsBasePboardType is on the pasteboard then use that.
    for (type = [pasteboard types]; *type != NULL; ++type) {
        if (*type == NewsBasePboardType) {
            if ([pasteboard readType:NewsBasePboardType data:&data
                length:&length] != nil) {
                if (((struct mediaObjectDescriptor *)data)->magic !=
                    MEDIA_OBJECT_DESCRIPTOR_MAGIC) {
                    NXRunAlertPanel(LoStr("NewsBase"),
			LoStr("Paste of %s failed.Bad magic Number"),
			NULL,NULL,NULL, NewsBasePboardType);
                    vm_deallocate(task_self(), (vm_address_t)data, length);
                    return(nil);
                }
                dataPtr = data + sizeof(struct mediaObjectDescriptor) + 
                    ((struct mediaObjectDescriptor *)data)->size;
                dataEnd = data + length;

                // show alert panel for long paste
                if (length > 4096) {
                    loadingAlertPanel = NXGetAlertPanel(LoStr("NewsBase"),
			LoStr("Loading from %s pasteboard..., please wait"),
			NULL, NULL, NULL, NewsBasePboardType);
                    [NXApp beginModalSession:&loadingModalSession
                        for:loadingAlertPanel];
                } else {
                    loadingAlertPanel = nil;
                }

                // loop over all objects 
                while (dataPtr < dataEnd) {
                    if (((struct mediaObjectDescriptor *)dataPtr)->magic !=
                        MEDIA_OBJECT_DESCRIPTOR_MAGIC) {
                        NXRunAlertPanel(LoStr("NewsBase"),
			LoStr("Paste of %s failed.Bad magic Number"),
			NULL,NULL,NULL, NewsBasePboardType);
                        vm_deallocate(task_self(), (vm_address_t)data, length);
                        if (loadingAlertPanel != nil) {
                            [NXApp endModalSession:&loadingModalSession];
                            [loadingAlertPanel orderOut:self];
                            NXFreeAlertPanel(loadingAlertPanel);
                        }
                        return(nil);
                    }
                    if (isMultimedia == NO) {
                        switch(NXRunAlertPanel(LoStr("NewsBase"),
	LoStr("This is not a multimedia article. Change to multimedia?"),
	LoStr("YES"),LoStr("NO"),NULL)) {
                        case NX_ALERTDEFAULT:
                            isMultimedia = YES;
                            [plainMultimediaButton  setIcon:"multi_icon"];
                            [self setMonoFont:NO];
                            break;
                        case NX_ALERTALTERNATE:
                            vm_deallocate(task_self(), (vm_address_t)data,
                                length);
                            if (loadingAlertPanel != nil) {
                                [NXApp endModalSession:&loadingModalSession];
                                [loadingAlertPanel orderOut:self];
                                NXFreeAlertPanel(loadingAlertPanel);
                            }
                            return(nil);
                        }
                    }
                    // create object if it doesn't already exists
                    mediaClass = [IMediaTable mediaClassForFileExtension:
                        ((struct mediaObjectDescriptor *)dataPtr)->type];
                    if ([[self article] dataForKey:
                        ((struct mediaObjectDescriptor *)dataPtr)->key] ==
                        nil) {
                        mediaObject = [[(id)mediaClass allocFromZone:
                            [[self article] zone]] initWithKey:
                            ((struct mediaObjectDescriptor *)dataPtr)->key];
                        // ## REWRITE! ## need to implement our own stream
                        // package which will map arbritrary memory block
                        // not just vm_allocate memory block
                        stream = NXOpenMemory(NULL, 0, NX_READWRITE);
                        NXWrite(stream, dataPtr +
                            sizeof(struct mediaObjectDescriptor),
                            ((struct mediaObjectDescriptor *)dataPtr)->size);
                        NXSeek(stream, (long)0, NX_FROMSTART);
                        if ([mediaObject readFromStream:stream] == YES) {
                            [[self article] insertKeyedObject:mediaObject];
                        } else {
                            [mediaObject free];
                        }
                        NXCloseMemory(stream, NX_FREEBUFFER);
                    }
                    dataPtr += sizeof(struct mediaObjectDescriptor) + 
                        ((struct mediaObjectDescriptor *)dataPtr)->size;
                }
                stream = NXOpenMemory(data, 
                    sizeof(struct mediaObjectDescriptor) + 
                    ((struct mediaObjectDescriptor *)data)->size,
                    NX_READONLY);
                NXSeek(stream, sizeof(struct mediaObjectDescriptor),
                    NX_FROMSTART);
                [self replaceSelWithRichText:stream];
                NXCloseMemory(stream, NX_SAVEBUFFER);
                needsSaving = YES;
                [[self window] setDocEdited:needsSaving == YES];
                [[article external] markAsDirty];
                vm_deallocate(task_self(), (vm_address_t)data, length);
                if (loadingAlertPanel != nil) {
                    [NXApp runModalSession:&loadingModalSession];
                    [NXApp endModalSession:&loadingModalSession];
                    [loadingAlertPanel orderOut:self];
                    NXFreeAlertPanel(loadingAlertPanel);
                }
		[self drainEventQueue];
                [self display];
                return(self);
            } else {
                NXRunAlertPanel(LoStr("NewsBase"),LoStr("Paste of %s failed.")
			,NULL,NULL,NULL, NewsBasePboardType);
                return(nil);
            }
        }
    }
   
    // If no NewsBasePboardType is on the pasteboard then try media objects.
    // This responsibility is delegated to IGraphicImage which will return
    // IGraphicImage if a valid media pasteboard type exists
    if ((graphicObject = [[IGraphicImage allocFromZone:[article zone]]
        initFromPasteboard:pasteboard forView:self]) != nil) {
        [self replaceSelWithCell:graphicObject];
        needsSaving = YES;
        [[self window] setDocEdited:needsSaving == YES];
        [[article external] markAsDirty];
        NXPing();
        [self drainEventQueue];
        return(self);
    }

    // If no NewsBasePboardType and no media objects try NXRTFPboardType
    // then NXAsciiPboardType
    for (type = [pasteboard types]; *type != NULL; ++type) {
        if (*type == NXRTFPboardType) {
            if ([pasteboard readType:NXRTFPboardType data:&data
                length:&length] != nil) {
                stream = NXOpenMemory(data, length, NX_READONLY);
                [self replaceSelWithRichText:stream];
                NXCloseMemory(stream, NX_FREEBUFFER);
                [articleSizeField setIntValue:[self byteLength] + size];
                needsSaving = YES;
                [[self window] setDocEdited:needsSaving == YES];
                [[article external] markAsDirty];
                return(self);
            } else {
                NXRunAlertPanel(LoStr("NewsBase"),
			LoStr("Paste of %s failed."),
			NULL,NULL,NULL,NXRTFPboardType);
                return(nil);
            }
        }
    }
    for (type = [pasteboard types]; *type != NULL; ++type) {
        if (*type == NXAsciiPboardType) {
            if ([pasteboard readType:NXAsciiPboardType data:&data
                length:&length] != nil) {
                [self replaceSel:data length:length];
                vm_deallocate(task_self(), (vm_address_t)data, length);
                [articleSizeField setIntValue:[self byteLength] + size];
                needsSaving = YES;
                [[self window] setDocEdited:needsSaving == YES];
               [[article external] markAsDirty];
                return(self);
            } else {
                NXRunAlertPanel(LoStr("NewsBase"),LoStr("Paste of %s failed."),
			NULL,NULL,NULL,NXAsciiPboardType);
                return(nil);
            }
        }
    }
    return(nil);
}

//*****************************************************************************
//   Drag and Drop Interface methods
//*****************************************************************************

static const char *pathList = NULL;      // path of dragged file
static NXImage *icon = nil;              // icon of dragged file

- (int)iconEntered:(int)windowNum at:(double)x :(double)y
    iconWindow:(int)iconWindowNum iconX:(double)iconX iconY:(double)iconY
    iconWidth:(double)iconWidth iconHeight:(double)iconHeight
    pathList:(const char *)iconPathList
{
    NXSize iconSize = {48.0, 48.0};

    DBG(1, ;)

    // free previous path if any
    if (pathList != NULL) {
        NXZoneFree([self zone], (void *)pathList);
    }

    // save path    
    pathList = NXCopyStringBufferFromZone(iconPathList, [self zone]);

    // free previous icon if any
    if (icon != nil) {
        [icon free];
    }

    // save icon
    icon = [[NXImage allocFromZone:[article zone]] initSize:&iconSize];
    [icon lockFocus];
    copyIconPicture(iconWindowNum, (float)iconX, (float)iconY,
        (float)iconWidth, (float)iconHeight);
    [icon unlockFocus];
    return(0);
}

- (int)iconReleasedAt:(double)x :(double)y ok:(int *)flag
{
    const char *stringPosition, *ptr;
    int files=1;
    struct stat statData;
    NXSelPt start, end;
    IGraphicImage *newGraphic;

    if (icon == nil || pathList == NULL) {
        // this should not happen
        *flag = 0;
        return(0);
    }
    if (draggedIconBelongsToMe == YES) {
        draggedIconBelongsToMe = NO;
        NXZoneFree([self zone], (void *)pathList);
        pathList = NULL;
        [icon free];
        icon = nil;
        *flag = 1;
        return(0);
    }

    // The dragged and dropped file will replace the current selection or be
    // inserted at the current cursor;  If there is no selection or cursor
    // the operation is aborted.
    [self getSel:&start :&end];
    DBG(1, fprintf(stderr, "%d - %d", start.cp, end.cp);)
    if (start.cp == -1) {
        if ([self textLength] == 0) {
            [self setSel:0 :0];
        } else {
            NXRunAlertPanel(LoStr("NewsBase"),
     LoStr("Please specify destination by making a selection in article body.")
   		,NULL,NULL,NULL);
            NXZoneFree([self zone], (void *)pathList);
            pathList = NULL;
            [icon free];
            icon = nil;
            *flag = 0;
            return(0);
        }
    }

    // check if article is editable
    if (isEditable == NO || [self hasEmbeddedView] == YES) {
        if ([[self delegate] textWillChange:self] == YES) {
            NXZoneFree([self zone], (void *)pathList);
            pathList = NULL;
            [icon free];
            icon = nil;
            *flag = 0;
            return(0);
        }
    }

    // check if article is multimedia
    if (isMultimedia == NO) {
        switch(NXRunAlertPanel(LoStr("NewsBase"),
	LoStr("This is not a multimedia article.change to multimedia?"),
	LoStr("YES"),LoStr("NO"),NULL)) {
        case NX_ALERTDEFAULT:
            isMultimedia = YES;
            [plainMultimediaButton  setIcon:"multi_icon"];
            [self setMonoFont:NO];
            break;
        case NX_ALERTALTERNATE:
            NXZoneFree([self zone], (void *)pathList);
            pathList = NULL;
            [icon free];
            icon = nil;
            *flag = 0;
            return(0);
        }
    }
    /* the number of tabs + 1 equals the number of files dragged in */
    stringPosition = pathList;
    while (stringPosition = index(stringPosition, '\t')) {
        files++;
        stringPosition++;
    }

    /* make sure the user dragged one file in (and it wasn't root) */
    if ((files > 1) || !(*pathList)) {
        NXZoneFree([self zone], (void *)pathList);
        pathList = NULL;
        [icon free];
        icon = nil;
        *flag = 0;
        return(0);
    }

    /* punt if a directory */
    stat(pathList, &statData);
    if (statData.st_mode & S_IFDIR && ((ptr = rindex(pathList, '/'), 
        ptr = rindex(ptr, '.')) == 0 ||
        strcmp(ptr + 1, MM_FILE_EXTENSION) != 0)) {
        NXZoneFree([self zone], (void *)pathList);
        pathList = NULL;
        [icon free];
        icon = nil;
        *flag = 0;
        return(0);
    }

    // create a new IGraphicImage object and initialize that object from
    // the dragged and dropped file.  The IGraphicImage object is 
    // responsible for providing the image to be displayed
    if ((newGraphic = [[IGraphicImage allocFromZone:[article zone]]
        initFromFile:pathList forView:self withIcon:icon]) != nil) {
        [self replaceSelWithCell:newGraphic];
        needsSaving = YES;
        [[self window] setDocEdited:needsSaving == YES];
        [[article external] markAsDirty];
    }
    // ownership of icon transferred to newGraphic
    icon = nil;
    NXZoneFree([self zone], (void *)pathList);
    pathList = NULL;
    *flag = 1;
    return(0);
}

- (int)iconExitedAt:(double)x :(double)y
{
    DBG(1, ;)
    if (pathList != NULL) {
        NXZoneFree([self zone], (void *)pathList);
        pathList = NULL;
    }
    if (icon != nil) {
        [icon free];
        icon = nil;
    }
    draggedIconBelongsToMe = NO;
    return(0);
}

- setArticle:(IArticleD *)anArticle
{
    article = anArticle;
    return(self);
}

- (IOrderedListD *)article
{
    return(article);
}

- setIsMultimedia:(BOOL)flag
{
    id font_obj;

    isMultimedia = flag;
    if (isMultimedia == YES) {
        [plainMultimediaButton setIcon:"multi_icon"];
        [self setMonoFont:NO];
    } else {
        [plainMultimediaButton setIcon:"plain_icon"];
        font_obj = [Font newFont:"RyuminTimes-Light" size:14.0];
        [self setFont:font_obj];
    }
    return(self);
}

- (BOOL)isMultimedia
{
    return(isMultimedia);
}

- setIsEditable:(BOOL)flag
{
    isEditable = flag;
    return(self);
}

- (BOOL)isEditable
{
    return(isEditable);
}

//*****************************************************************************
//   Embedded View methods
//*****************************************************************************

// An embedded view is view that is overlayed on a portion of the text view.
// Currently the only embedded view belongs to a video view.

- (BOOL)hasEmbeddedView
{
    if ([embeddedViewControllers count] > 0) {
        return(YES);
    } else {
        return(NO);
    }
}

- addEmbeddedViewController:controller
{
    [embeddedViewControllers addObject:controller];
    // setup the textWillChange: message
    [[[self window] delegate] toggleFirstResponder];
    return(self);
}

- removeEmbeddedViewController:controller
{
    [embeddedViewControllers removeObject:controller];
    return(self);
}


- (List *)embeddedViewControllers
{
    return(embeddedViewControllers);
}

- setNeedsSaving:(BOOL)flag;
{
    if (flag != 0) {
        flag = YES;
    }
    needsSaving = flag;
    [[self window] setDocEdited:needsSaving == YES];
    [[article external] markAsDirty];
    return(self);
}

- (BOOL)needsSaving
{
    return(needsSaving);
}

- setArticleSize:(int)newSize
{
     size = newSize;
    [articleSizeField setIntValue:[self byteLength] + size];
    return(self);
}

- (int)addToArticleSize:(int)increment
{
    size += increment;
    [articleSizeField setIntValue:[self byteLength] + size];
    return(size);
}

- displaySize
{
    [articleSizeField setIntValue:[self byteLength] + size];
    return(self);
}

- displaySize:dummy
{
    [articleSizeField setIntValue:[self byteLength] + size];
    return(self);
}

- setArticleSizeField:(Form *)aForm
{
    articleSizeField = aForm;
    return(self);
}

- setPlainMultimediaButton:(Button *)aButton
{
    plainMultimediaButton = aButton;
    return(self);
}

- drainEventQueue
{
    NXEvent *event;

    while ((event = [NXApp getNextEvent:NX_ALLEVENTS waitFor:0.0 threshold:0])
        != NULL) {
        DBG(1, fprintf(stderr, "event type = %d", event->type);)
    }
    return(self);
}

- setSelectionFileName:(const char *)fileName
{
    selectionFileName = fileName;
    return(self);
}

- setOwnerOfDraggedIconToMe:(BOOL)flag
{
    draggedIconBelongsToMe = YES;
    return(self);
}

//*****************************************************************************
//   Editing methods (also support for scroll by space bar)
//*****************************************************************************

// The following Emacs-style commands are supported.

#define CONTROL_A   0x01	// Emacs command to move to beginning of line
#define CONTROL_B   0x02	// Emacs command to move back one character
#define CONTROL_D   0x04	// Emacs command to delete next character
#define CONTROL_E   0x05	// Emacs command to move to end of line
#define CONTROL_F   0x06	// Emacs command to move back one character
#define CONTROL_H   0x08	// Emacs command to delete previous character
#define CONTROL_K   0x0b	// Emacs command to delete to end of line
#define CONTROL_N   0x0e	// Emacs command to move down one line
#define CONTROL_P   0x10	// Emacs command to move up one line
#define CONTROL_Y   0x19	// Emacs command to paste line
#define ALTERNATE_B 0xe5	// Emacs command to move back one word
#define ALTERNATE_D 0x44	// Emacs command to delete to end of word
#define ALTERNATE_F 0xa6	// Emacs command to move forward one word
#define ALTERNATE_H 0xe3	// Emacs command to delete to beginning of word
#define ALTERNATE_LT 0xa3	// Emacs command to move beginning of text
#define ALTERNATE_GT 0xb3	// Emacs command to move end of text

// emacs key command
//   
- keyDown:(NXEvent *)theEvent
{
    NXSelPt	start, end;
    int		textEnd;
    int		line;
    char	buffer[8];
    Scroller    *vertScroller;


    // First, catch scroll by space bar events and switch to broswer return
    if (isEditable == NO && theEvent->data.key.charCode == '\r') {
        [[NXApp delegate] setMyContext:[NXApp activate:[[NXApp delegate]
            previousContext]]];
        return(self);
    }
    if (isEditable == NO && (theEvent->data.key.charCode == ' ' ||
        theEvent->data.key.charCode == 128)) {
        vertScroller = [[[self superview] superview] vertScroller];
        // alternate key specifies page or line mode
        // alpha shift specifies up or down mode
        if ((theEvent->flags & NX_ALTERNATEMASK) == 0) {
            if ((theEvent->flags & NX_ALPHASHIFTMASK) == 0) {
                [vertScroller setHitPart:NX_INCPAGE];
            } else {
                [vertScroller setHitPart:NX_DECPAGE];
            }
        } else {
//            [[[self superview] superview] setLineScroll:1.0];
            if ((theEvent->flags & NX_ALPHASHIFTMASK) == 0) {
                [vertScroller setHitPart:NX_INCLINE];
            } else {
                [vertScroller setHitPart:NX_DECLINE];
            }
        }
        [[vertScroller target] perform:[vertScroller action]
            with:vertScroller];
        return(self);
    }

    // 1. ClientInputManager will take the key event and handle it
    // 2. if ClientInputManager process it, return
    // 3. if not process, check the key event is equivalent to emacs key
    // 4. if emacs key binding, process and return
    // 5. hand the event to -keyDown of "super" class

#ifdef KANJI
    if ([inputManager imProcessEvent:theEvent from:self] != nil) {
	return self;
    }
#endif /* KANJI */

    // alt+space(128) will be input by mistake often, and it cause 
    // problem in kanji converter(EUC->JIS). so we remove it.
    if (theEvent->data.key.charCode == 128) {
	return self;
    }
    
    // the key event is not processed in InputManager
    switch (theEvent->data.key.charCode) {
	case CONTROL_F:
	    [self moveCaret:NX_RIGHT];
	    return self;
	    break;
	case CONTROL_B:
	    [self moveCaret:NX_LEFT];
	    return self;
	    break;
	case CONTROL_P:
	    [self moveCaret:NX_UP];
	    return self;
	    break;
	case CONTROL_N:
	    [self moveCaret:NX_DOWN];
	    return self;
	    break;
	case CONTROL_A:
	    // move to head of line
	    [self getSel:&start :&end];
	    start.cp = [self positionFromLine:
					[self lineFromPosition:start.cp]];
	    [self setSel:start.cp :start.cp];
	    return self;
	    break;
	case CONTROL_D:
	    // delete next character
	    textEnd = [self textLength];
	    [self getSel:&start :&end];
	    if (start.cp < textEnd) {
		[self setSel:start.cp :start.cp + 1];
		[self delete:nil];
	    }
	    return self;
	    break;
	case CONTROL_E:
	    // move to end of line
	    [self getSel:&start :&end];
	    line = [self lineFromPosition:start.cp];
	    textEnd = [self textLength];
	    if (line < [self lineFromPosition:textEnd]) {
		start.cp = [self positionFromLine:line + 1] - 1;
	    } else {
		start.cp = textEnd;
	    }
	    [self setSel:start.cp :start.cp];
	    return self;
	    break;
	case CONTROL_K:
	    // delete to end of line
	    [self getSel:&start :&end];
	    line = [self lineFromPosition:start.cp];
	    textEnd = [self textLength];
	    if (line < [self lineFromPosition:textEnd]) {
		// this line is not the last line, so can calculate next line
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer == '\n') {
		    // if next char == '\n', delete '\n'
		    end.cp = [self positionFromLine:line + 1];
		} else {
		    // cut this line
		    end.cp = [self positionFromLine:line + 1] - 1;
		}
	    } else {
		end.cp = textEnd;
	    }
	    [self setSel:start.cp :end.cp];
	    [self cut:nil];
	    return self;
	    break;
	case CONTROL_Y:
	    // paste line
	    [self paste:nil];
	    return self;
	    break;
	case ALTERNATE_B:
	    // move back one word
	    [self getSel:&start :&end];
	    if (start.cp > 0) {
		--start.cp;
	    }
	    for (; start.cp > 0; --start.cp) {
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer != ' ' && *buffer != '\t' && *buffer != '\n') {
		    break;
		}
	    }
	    for (; start.cp > 0; --start.cp) {
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer == ' ' || *buffer == '\t' || *buffer == '\n') {
		    ++start.cp;
		    break;
		}
	    }
	    [self setSel:start.cp :start.cp];
	    return self;
	    break;
	case ALTERNATE_D:
	    if (theEvent->data.key.charSet != 1) {
		break;
	    }
	    // delete to end of current word
	    textEnd = [self textLength];
	    [self getSel:&start :&end];
	    for (end.cp = start.cp; end.cp < textEnd; ++end.cp) {
		[self getSubstring:buffer start:end.cp length:1];
		if (*buffer != ' ' && *buffer != '\t' && *buffer != '\n') {
		    break;
		}
	    }
	    for (; end.cp < textEnd; ++end.cp) {
		[self getSubstring:buffer start:end.cp length:1];
		if (*buffer == ' ' || *buffer == '\t' || *buffer == '\n') {
		    break;
		}
	    }
	    [self setSel:start.cp :end.cp];
	    [self delete:nil];
	    return self;
	    break;
	case ALTERNATE_F:
	    // move forward one word
	    textEnd = [self textLength];
	    [self getSel:&start :&end];
	    for (; start.cp < textEnd; ++start.cp) {
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer == ' ' || *buffer == '\t' || *buffer == '\n') {
		    break;
		}
	    }
	    for (; start.cp < textEnd; ++start.cp) {
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer != ' ' && *buffer != '\t' && *buffer != '\n') {
		    break;
		}
	    }
	    [self setSel:start.cp :start.cp];
	    return self;
	    break;
	case ALTERNATE_H:
	    // delete to beginning of current word
	    [self getSel:&start :&end];
	    end.cp = start.cp;
	    for (--start.cp; start.cp >= 0; --start.cp) {
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer != ' ' && *buffer != '\t' && *buffer != '\n') {
		    break;
		}
	    }
	    for (; start.cp >= 0; --start.cp) {
		[self getSubstring:buffer start:start.cp length:1];
		if (*buffer == ' ' || *buffer == '\t' || *buffer == '\n') {
		    break;
		}
	    }
	    ++start.cp;
	    [self setSel:start.cp :end.cp];
	    [self delete:nil];
	    return self;
	    break;
	case ALTERNATE_LT:
	    // Emacs command to move beggining of text
	    start.cp = 0;
	    [self setSel:start.cp :start.cp];
	    return self;
	    break;
	case ALTERNATE_GT:
	    // Emacs command to move end of text
	    textEnd = [self textLength];
	    start.cp = textEnd;
	    [self setSel:start.cp :start.cp];
	    return self;
	    break;
	default:
	    break;
    }
    return ([super keyDown:theEvent]);
}
    
// textIsFreeing should be set just before self is freeded.
// It is neccessary to control the freeing of media objects
// which should not be freed when the self is freeded.

- setTextIsFreeing
{
    textIsFreeing = YES;
    return(self);
}

- (BOOL)textIsFreeing
{
    return(textIsFreeing);
}

//*****************************************************************************
//   Find methods
//*****************************************************************************

// find is messy because of the embedded graphics

- find:(const char *)string forward:(BOOL)dirFlag ignoreCase:(BOOL)caseFlag
{
    NXStream *stream;
    char *buffer;
    int len, maxlen;
    NXSelPt start, end;
    char *ptr, *bufferEnd, *ptr1;
    int stringLen;
    BOOL found;
    int graphicImageCount;
    int	kanjiCount;
    int	startPtr;

    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    [self writeText:stream];
    NXGetMemoryBuffer(stream, &buffer, &len, &maxlen);
    bufferEnd = buffer + NXTell(stream);
    // translate all to lower if ignore case
    if (caseFlag == YES) {
        for (ptr = buffer; ptr < bufferEnd; ++ ptr) {
            if (isupper(*ptr) != 0) {
                *ptr = tolower(*ptr);
            }
        }
    }
    [self getSel:&start :&end];
    if (start.cp == -1) {
        start.cp = 1;
    }
    // grahic images '\240' and kanji code (>0x80)
    //  are doubly counted so make adjustment
    for (ptr = buffer; start.cp > 0; ++ptr, --start.cp) {
        if (*ptr & 0x80 || *ptr == '\240') {
            ++ptr;
        }
    }
    stringLen = strlen(string);
    if (dirFlag == YES) {
        // forward
        for (++ptr; ptr < bufferEnd; ++ptr) {
            if (*ptr == string[0] && strncmp(ptr, string, stringLen) == 0) {
               found = YES;
               break;
            }
        }
    } else {
        // backward
        for (--ptr; ptr >= buffer; --ptr) {
            if (*ptr == string[0] && strncmp(ptr, string, stringLen) == 0) {
               found = YES;
               break;
           }
        }
    }
    if (found == YES) {
        // grahic images are doubly counted so make adjustment
        graphicImageCount = 0;
	kanjiCount = 0;
        for (ptr1 = buffer; ptr1 < ptr; ++ptr1) {
	    if (*ptr1 & 0x80) {
		if (*ptr1 == '\240') {
		    ++graphicImageCount;
		} else {
		    ++kanjiCount;
		}
	    }
        }
	if (*ptr & 0x80) {
	    // kanji code is 2bytes
	    stringLen = (int)floor(strlen(string) / 2);
	} else {
	    stringLen = strlen(string);
	}
	startPtr = ptr - buffer - graphicImageCount 
					- (int)floor(kanjiCount / 2);
        [self setSel:startPtr :startPtr + stringLen];
        [self scrollSelToVisible];
    }
    NXCloseMemory(stream, NX_FREEBUFFER);
    return(self);
}

- (const char *)selection
{
    NXSelPt start, end;
    char buffer[128];
    int len;

    [self getSel:&start :&end];
    len = end.cp - start.cp;
    if (len > sizeof(buffer)) {
        len = sizeof(buffer) - 1;
    }
    len = [self getSubstring:buffer start:start.cp length:len];
    buffer[len] = '\0';
    return(buffer);
}

- free
{
    [embeddedViewControllers free];
    return([super free]);
}

#undef __OBJC__
#include "errdebug.h"

// textFilter() is also used to trigger [INewsBaseText displaySize:] which
// dynamically updates the size
char *textFilter(id sender, unsigned char *insertText,
    int *insertLength, int position)
{
    // count charactor and display the size of document 
    [sender perform:@selector(displaySize:) with:nil afterDelay:1
        cancelPrevious:YES];
    [sender setNeedsSaving:YES];
    
    return ((char *)insertText);
}

#define __OBJC__
#include "errdebug.h"

@end
