# *** $Id: mcterm.w,v 1.10 1997/04/11 14:25:53 forrest Rel $ *** # This file is a heavily modified version of the AnsiTerm widget # (Copyright 1995 Bert Bos ) from the # Free Widget Foundation (FWF Version 3.9, 6 Sep 1995). @CLASS XMcTerm (Core) @file=mcterm @ The McTerm widget emulates an ANSI terminal in an X Window. It displays lines of text and scrolls them if necessary. It displays a cursor, to indicate where the next text will appear and it interprets the ANSI escape codes to position the cursor or highlight the text. Text is written to the widget by calling |XMcTermWrite| (a convenience function which calls the |write| method). A callback is provided for reacting to key presses. The widget also supports the selection mechanism, the same way the xterm program does: select by dragging with mouse button 1 pressed, paste by clicking with mouse button 2. Another callback is provided for reacting to selection pasting. Another callback is provided to notify the application when the number of rows or columns changes. If the application runs a shell or some other program in the background, it can use this callback to send a |SIGWINCH| signal to the child process. @PUBLIC @ The |rows| resource is used to set the number of lines of text. The widget will try to set its |height| to match the number of rows. Note that the reverse is not true: if the height changes for any other reason, the |rows| resource is not changed. @def DEFAULT_ROWS = (long) 24 @var int rows = DEFAULT_ROWS @ The |columns| resource sets the width of the widget to a certain number of characters. The widget will try to set its |width| larger enough for that many columns. The reverse is not true: if the |width| changes, |columns| is not adjusted. @def DEFAULT_COLUMNS = (long) 80 @var int columns = DEFAULT_COLUMNS @ The widget uses one font. It should usually be a monospaced font. @var XFontStruct *font = XtDefaultFont @ The margin between the border and the text, on all sides. @def DEFAULT_MARGIN = (long) 2 @var int margin = DEFAULT_MARGIN @ The possible text colors. The foregroundNormalBlack..foregroundNormalWhite colors are the normal-intensity renditions of ANSI colors 30-37. The foregroundBrightBlack..foregroundBrightWhite colors are the bright-intensity renditions of ANSI colors 30-37. The backgroundBlack..backgroundWhite colors are ANSI colors 40-47. Note that they default to the same colors as the normal foreground colors. @var Pixel backgroundBlack = "black" @var Pixel backgroundRed = "red4" @var Pixel backgroundGreen = "green4" @var Pixel backgroundYellow = "yellow4" @var Pixel backgroundBlue = "blue4" @var Pixel backgroundMagenta = "magenta4" @var Pixel backgroundCyan = "cyan4" @var Pixel backgroundWhite = "grey80" @var Pixel foregroundNormalBlack = "black" @var Pixel foregroundNormalRed = "red4" @var Pixel foregroundNormalGreen = "green4" @var Pixel foregroundNormalYellow = "yellow4" @var Pixel foregroundNormalBlue = "blue4" @var Pixel foregroundNormalMagenta = "magenta4" @var Pixel foregroundNormalCyan = "cyan4" @var Pixel foregroundNormalWhite = "grey80" @var Pixel foregroundBrightBlack = "grey50" @var Pixel foregroundBrightRed = "red" @var Pixel foregroundBrightGreen = "green" @var Pixel foregroundBrightYellow = "yellow" @var Pixel foregroundBrightBlue = "blue" @var Pixel foregroundBrightMagenta = "magenta" @var Pixel foregroundBrightCyan = "cyan" @var Pixel foregroundBrightWhite = "white" @var Pixel foreground = XtDefaultForeground @var Pixel foregroundBright = XtDefaultForeground @ The |keyCallback| is called whenever the widget receives a key press. The |call_data| argument contains a pointer to the event that was sent (type: |XKeyEvent *|). @var XtCallbackList keyCallback = NULL @ The |pasteCallback| is called whenever the user pastes into the widget. The |call_data| argument contains a pointer to the character that was pasted (type: |char *|). @var XtCallbackList pasteCallback = NULL @ The |resizeCallback| is invoked when the widget changes the number of rows or columns. The |call_data| argument holds a pointer to a structure containing two ints (type: |XMcTermWindowSizeInfo *|). The first int is the number of rows, the second int is the number of columns. @var XtCallbackList resizeCallback = NULL @ Debugging flags. @var Bool debug_query_geometry = False @var Bool debug_set_values = False @var Bool debug_resize = False @var Bool debug_set_resize_increments = False @CLASSVARS @ Set a few class variables for optimizing event processing: multiple consecutive events of the same type are added together by Xt and delivered as one event. This applies to motion, expose and enter/leave events. @var compress_motion = True @var compress_enterleave = True @var compress_exposure = XtExposeCompressMultiple @EXPORTS @incl "m0tcolor.h" @ The |XMcTermCursor*| macros define the values that this widget recognizes in its change-cursor-type escape sequence. @def XMcTermCursorBlank = 0 @def XMcTermCursorUnderline = 1 @def XMcTermCursorFullHollow = 2 @def XMcTermCursorFullSolid = 3 @def XMcTermCursorHalfHollow = 4 @def XMcTermCursorHalfSolid = 5 @def XMcTermCursorQuarterHollow = 6 @def XMcTermCursorQuarterSolid = 7 @def XMcTermCursorMin = XMcTermCursorBlank @def XMcTermCursorMax = XMcTermCursorQuarterSolid @ The |resizeCallback| is passed a pointer to an |XMcTermWindowSizeInfo| struct. @type XMcTermWindowSizeInfo = struct { int rows, columns; } @ The |XMcTermCellSize| function is passed a pointer to an |XMcTermCellSizeInfo| struct. @type XMcTermCellSizeInfo = struct { int cellwidth, cellheight; } @ The |XMcTermWrite| function sends characters to the McTerm widget. It is a convenience function that simply calls the |write| method of the widget. @proc void XMcTermWrite($, char *buf, size_t nbytes) { if (! XtIsSubclass($, xmcTermWidgetClass)) XtAppError(XtWidgetToApplicationContext($), "XMcTermWrite may only be called for McTerm widgets."); $write($, buf, nbytes); } @ The |XMcTermCellSize| function gets the size of the McTerm widget. It is a convenience function that simply reports the values of the |rows| and |columns| resources. @proc void XMcTermCellSize($, XMcTermCellSizeInfo *buf) { if (! XtIsSubclass($, xmcTermWidgetClass)) XtAppError(XtWidgetToApplicationContext($), "XMcTermCellSize may only be called for McTerm widgets."); buf->cellwidth = $cellwidth; buf->cellheight = $cellheight; } @PRIVATE @ The size of a character cell is assumed to be constant. It is stored in |cellwidth| and |cellheight|. @var int cellwidth @var int cellheight @var int uline_pos @var int uline_thick @ Two GC's are used for drawing the Two possible faces: normal/reverse. @ One GC is used for drawing the cursor. @var GC defgc @var GC revgc @var GC cursorgc @ The contents of the widget are held in an array of lines. Each character also has an attrib_t value with attributes (bold, reverse, underlined) in a separate array. |allocated_lines| is the number of elements in the arrays. It is used in the |allocate_contents| function to know the number of lines to deallocate. The |ATTRIB_DIRTY| attribute is used by the |write| method and its utility functions to mark lines that have changed, so that they can all be redrawn at the end of the |write| function. This flag is only ever set on the first character of a line. @type attrib_t = int @var char **contents @var attrib_t **attribs @var int allocated_rows @var int allocated_columns @def ATTRIB_REV_SHIFT = 0 @def ATTRIB_REV_WIDTH = 1 @def ATTRIB_REV = (1 << ATTRIB_REV_SHIFT) @def ATTRIB_ULINE_SHIFT = (ATTRIB_REV_SHIFT + ATTRIB_REV_WIDTH) @def ATTRIB_ULINE_WIDTH = 1 @def ATTRIB_ULINE = (1 << ATTRIB_ULINE_SHIFT) @def ATTRIB_BRIGHT_SHIFT = (ATTRIB_ULINE_SHIFT + ATTRIB_ULINE_WIDTH) @def ATTRIB_BRIGHT_WIDTH = 1 @def ATTRIB_BRIGHT = (1 << ATTRIB_BRIGHT_SHIFT) @def ATTRIB_DIRTY_SHIFT = (ATTRIB_BRIGHT_SHIFT + ATTRIB_BRIGHT_WIDTH) @def ATTRIB_DIRTY_WIDTH = 1 @def ATTRIB_DIRTY = (1 << ATTRIB_DIRTY_SHIFT) @def ATTRIB_FOREGROUND_SHIFT = (ATTRIB_DIRTY_SHIFT + ATTRIB_DIRTY_WIDTH) @def ATTRIB_FOREGROUND_WIDTH = 4 @def ATTRIB_FOREGROUND_MASK = (0xF << ATTRIB_FOREGROUND_SHIFT) @def ATTRIB_BACKGROUND_SHIFT = (ATTRIB_FOREGROUND_SHIFT + ATTRIB_FOREGROUND_WIDTH) @def ATTRIB_BACKGROUND_WIDTH = 4 @def ATTRIB_BACKGROUND_MASK = (0xF << ATTRIB_BACKGROUND_SHIFT) @ The scrolling region parameters are kept in |scroll_top_row| and |scroll_bot_row|. The scrolling region affects the portion of the screen scrolled by |cursor_down|, |delete_line|, and |insert_line|. @var int scroll_top_row @var int scroll_bot_row @ The cursor position is held in |cursorx| and |cursory|. Because the cursor might move between being drawn and being removed, the timer procedure that blinks the cursor keeps the position where the cursor is drawn in a separate pair of coordinates |xor_cursorx| and |xor_cursory|. The variable |cursortype| indicates the type of cursor to draw. Because the cursor type might have changed between being drawn and being removed, the timer procedure that blinks the cursor keeps the type of the cursor it drew in |xor_cursortype|. The XMcTermCursor* macros give the supported cursor types. They are in the exports section since client applications might use them in constructing escape sequences. @var int cursorx @var int cursory @var int cursortype @var int xor_cursorx @var int xor_cursory @var int xor_cursortype @var Bool cursor_on @ The |do_wrap| flag is true if the cursor is in the last column and a character was just written there. @var Bool do_wrap @ Another pair of coordinates, |savedx| and |savedy|, is used to implement the `ESC 7' or `ESC [ s' (save cursor) and `ESC 8' or `ESC [ u' (restore cursor) control sequences. @var int savedx @var int savedy @ The next text to be added at the cursor position has attributes |cur_attrib|. The value of this variable can only be changed through ANSI escapes, that are sent to the |write| method. |insert_mode| is likewise only set by escape sequences; it determines whether new characters on a line push existing characters to the right and newlines push lower lines down. |last_char| is the most recently written character. @var attrib_t cur_attrib @var Bool insert_mode @var char last_char @ The |write| method interprets escape using an FSM, the state of which is recorded in |state|. Escape sequences can have up to 5 numeric parameters, which are stored by |write| in 5 registers. @def Init = 0 @def Esc = 1 @def EscLB = 2 @def EscLBRegister0 = 3 @def EscLBRegister1 = 4 @def EscLBRegister2 = 5 @def EscLBRegister3 = 6 @def EscLBRegister4 = 7 @def EscRB = 8 @def EscRBRegister0 = 9 @var int state @var int escparm[5] @ The |timer| variable holds the ID of the timer function that handles the blinking of the cursor. @var XtIntervalId timer @ The current selection is kept in a buffer. @var int start_x @var int start_y @var int end_x @var int end_y @var char *selection @var int selection_len @ A press of Button1 without a subsequent motion is not enough to start a selection. The |drag_started| variable is set to |True| by |extend_selection| and is inspected by |end_selection| to decide whether a selection has taken place. If |drag_started| is |False| the event was a simple click. @var Bool drag_started @METHODS @ The |initialize| method initializes the private variables and sets the geometry resources to the values indicated by |rows| and |columns|. An event handler for the Map and Unmap events is registered, which will in turn install or remove a timer that makes the cursor blink. The event handler for key presses is installed, because we don't want translations to change the meaning of key presses. Also we don't want other widgets to steal our keyboard input, not even for keyboard traversal. @proc initialize { compute_cell_size($); set_resize_increments($); if ($columns < 1) $columns = 1; if ($rows < 1) $rows = 1; $width = 2 * $margin + $columns * $cellwidth; $height = 2 * $margin + $rows * $cellheight; $allocated_columns = $allocated_rows = 0; $contents = NULL; $attribs = NULL; allocate_contents($); XtAddEventHandler($, StructureNotifyMask, False, map_handler, NULL); XtInsertEventHandler($, KeyPressMask, False, handle_keys, NULL, XtListHead); $defgc = $revgc = $cursorgc = NULL; $savedx = $savedy = $cursorx = $cursory = 0; $cursor_on = False; $do_wrap = False; $cursortype = XMcTermCursorFullSolid; $cur_attrib = 0; $insert_mode = False; $last_char = ' '; $state = Init; $start_x = 0; $start_y = 0; $end_x = 0; $end_y = 0; $selection = NULL; $selection_len = 0; $drag_started = False; } @ The |realize| method uses the parent widget's |realize| method and creates the GCs for this widget. We can't call |make_gc| in the |initialize| method because we don't yet have a window that |make_gc| can pass to |XCreateGC|. Note that |make_gc| uses |uline_thick|, which is set by |compute_cell_size|. @proc realize { #realize($, mask, attributes); make_gc($); } @ If the font, the margin or the number of rows/columns changes, the |set_values| method will resize the widget in order to keep the requested number of rows/columns. Only if |width| or |height| is changed explicitly (and |rows| or |columns| is not changed at the same time), will the |rows| or |columns| be recomputed and changed. @proc set_values { Bool redraw = False; Bool need_gc = False; Dimension new_width = $width; Dimension new_height = $height; if ($font != $old$font) { compute_cell_size($); set_resize_increments($); need_gc = True; } if ($foreground != $old$foreground || $foregroundBright != $old$foregroundBright || $background_pixel != $old$background_pixel || $foregroundNormalBlack != $old$foregroundNormalBlack || $foregroundNormalRed != $old$foregroundNormalRed || $foregroundNormalGreen != $old$foregroundNormalGreen || $foregroundNormalYellow != $old$foregroundNormalYellow || $foregroundNormalBlue != $old$foregroundNormalBlue || $foregroundNormalMagenta != $old$foregroundNormalMagenta || $foregroundNormalCyan != $old$foregroundNormalCyan || $foregroundNormalWhite != $old$foregroundNormalWhite || $foregroundBrightBlack != $old$foregroundBrightBlack || $foregroundBrightRed != $old$foregroundBrightRed || $foregroundBrightGreen != $old$foregroundBrightGreen || $foregroundBrightYellow != $old$foregroundBrightYellow || $foregroundBrightBlue != $old$foregroundBrightBlue || $foregroundBrightMagenta != $old$foregroundBrightMagenta || $foregroundBrightCyan != $old$foregroundBrightCyan || $foregroundBrightWhite != $old$foregroundBrightWhite || $backgroundBlack != $old$backgroundBlack || $backgroundRed != $old$backgroundRed || $backgroundGreen != $old$backgroundGreen || $backgroundYellow != $old$backgroundYellow || $backgroundBlue != $old$backgroundBlue || $backgroundMagenta != $old$backgroundMagenta || $backgroundCyan != $old$backgroundCyan || $backgroundWhite != $old$backgroundWhite) { need_gc = True; } if ($columns < 1) $columns = 1; /* Silently increase */ if ($rows < 1) $rows = 1; /* * If the width did not change, but the columns or margin or cellwidth * did, recompute the width. */ if ($width == $old$width) { if ($columns != $old$columns || $margin != $old$margin || $cellwidth != $old$cellwidth) { new_width = 2 * $margin + $columns * $cellwidth; } } /* * If the height did not change, but the rows or margin or cellheight * did, recompute the height. */ if ($height == $old$height) { if ($rows != $old$rows || $margin != $old$margin || $cellheight != $old$cellheight) { new_height = 2 * $margin + $rows * $cellheight; } } /* * If we want to change our own size, try to do it. */ if (new_width != $width || new_height != $height) { request_resize($, new_width, new_height); } if (need_gc) { make_gc($); redraw = True; } /* * Now make sure that $rows and $columns are in sync with * our current $width and $height. */ synchronize_dims($); clip_gcs($); return redraw; } @ When the parent widget asks for the preferred size, the McTerm widget replies with the size that is needed for the current rows and columns. @proc query_geometry { XtGeometryMask whmask = CWWidth | CWHeight; /* Compute our preferred geometry */ reply->request_mode = whmask; reply->width = 2 * $margin + $columns * $cellwidth; reply->height = 2 * $margin + $rows * $cellheight; if ($debug_query_geometry) { fprintf(stderr, "query_geometry:\n"); fprintf(stderr, "requested geometry:"); print_geometry(stderr, request); fprintf(stderr, "preferred geometry:"); print_geometry(stderr, request); } /* * If the proposed geometry is acceptable without * modification, return XtGeometryYes. */ if (((request->request_mode & whmask) == whmask) && request->width == reply->width && request->height == reply->height) { if ($debug_query_geometry) { fprintf(stderr, "query_geometry: returning XtGeometryYes\n"); } return XtGeometryYes; } /* * If the proposed geometry is the same as the current * geometry, return XtGeometryNo, which indicates that * no change should be made. */ if ($width == reply->width && $height == reply->height) { if ($debug_query_geometry) { fprintf(stderr, "query_geometry: returning XtGeometryNo\n"); } return XtGeometryNo; } /* * If the proposed geometry is not acceptable, store * the suggested modifications in the reply structure * and return XtGeometryAlmost. */ if ($debug_query_geometry) { fprintf(stderr, "query_geometry: returning XtGeometryAlmost\n"); } return XtGeometryAlmost; } @ The |resize| method keeps the |rows| and |columns| resources synchronized with the new |width| and |height|. It also sets the new clip regions in all GCs. @proc resize { if ($debug_resize) { fprintf(stderr, "resize A: $width, $height, $rows, $columns == %ld, %ld, %ld, %ld\n", (long)$width, (long)$height, (long)$rows, (long)$columns); } synchronize_dims($); clip_gcs($); if ($debug_resize) { fprintf(stderr, "resize B: $width, $height, $rows, $columns == %ld, %ld, %ld, %ld\n", (long)$width, (long)$height, (long)$rows, (long)$columns); } } @ The |destroy| method frees the memory occupied by the |contents| and |attribs| arrays. It also frees the GCs and the fonts. @proc destroy { int i; if ($allocated_rows != 0) { for (i = 0; i < $allocated_rows; i++) { XtFree($contents[i]); XtFree((char *)$attribs[i]); } XtFree((char *) $contents); XtFree((char *) $attribs); } XFreeGC(XtDisplay($), $defgc); XFreeGC(XtDisplay($), $revgc); XFreeGC(XtDisplay($), $cursorgc); } @ The |expose| method draws only the characters in the exposed region. (More precisely: in the smallest rectangle enclosing the region, as given by the fields of the event structure.) QUESTION: is it useful to maintain an off-screen copy of the text in a Pixmap and redraw the screen from that? It might speed up the redrawing, but it takes up server memory and some servers may already have their own `save-under' optimizations. @proc expose { int top, bot, lft, rgt, y, x, i, w, h; XExposeEvent *ev = (XExposeEvent *) event; Region region1; XRectangle rect; if (! XtIsRealized($)) return; /* the inside of our window */ rect.x = 0; rect.y = 0; rect.width = $width; rect.height = $height; region1 = XCreateRegion(); XUnionRectWithRegion(&rect, region1, region1); if (region) XIntersectRegion(region, region1, region1); XSetRegion(XtDisplay($), $defgc, region1); XSetRegion(XtDisplay($), $revgc, region1); XSetRegion(XtDisplay($), $cursorgc, region1); x = ev ? ev->x : rect.x; y = ev ? ev->y : rect.y; w = ev ? ev->width : rect.width; h = ev ? ev->height : rect.height; find_cell($, x, y, &lft, &top); find_cell($, x + w, y + h, &rgt, &bot); if (lft < $columns && top < $rows) for (i = top; i <= bot; i++) draw_line($, i, lft, rgt + 1); XDestroyRegion(region1); clip_gcs($); /* Reset clipping region */ } @ The |write| method is the means by which characters are added to the display. The function adds characters to the |contents| array, wrapping and scrolling if necessary. It also interprets ANSI escape codes. It implements a finite state machine to keep track of its position in an escape sequence. Actual drawing is postponed until all |n| characters have been processed. At that point all lines with `dirty' flags on their first character are redrawn. This is only efficient when |n| is greater than one, it becomes more efficient with larger |n|. The reason lies in the expectation that programs generate large numbers of characters at once, but one character at a time is usually the result of a user typing. In the latter case the small performance penalty is probably not noticable. This hypothesis has not been tested in practice, however. (Note that the `dirty' flag is not reset when the widget is not realized. This shouldn't matter much.) @proc write($, char *text, size_t n) { size_t i; for (i = 0; i < n; i++) { switch ($state) { case Init: switch (text[i]) { case '\007': XBell(XtDisplay($), 0); break; /* Bell */ case '\015': $cursorx = 0; $do_wrap = False; break; /* CR */ case '\011': next_tabstop($); break; /* Tab */ case '\012': cursor_down($); break; /* LF */ case '\010': if ($cursorx) { $cursorx--; $do_wrap = False; } break; /* Backspace */ case '\033': $state = Esc; break; /* Esc */ default: add_char($, text[i]); } break; case Esc: switch (text[i]) { case '[': $state = EscLB; break; case ']': $state = EscRB; break; case '2': $state = Init; break; /* Set tab stop ignored */ case '7': $savedx = $cursorx; $savedy = $cursory; $state = Init; break; case '8': goto_xy($, $savedx, $savedy); $state = Init; break; default: /* Unrecognized sequence */ add_char($, '\033'); add_char($, text[i]); $state = Init; } break; case EscLB: switch (text[i]) { case 'H': goto_xy($, 0, 0); $state = Init; break; case 'J': clear_eos($); $state = Init; break; case 'K': clear_eol($), $state = Init; break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[0] = text[i] - '0'; $state = EscLBRegister0; break; case 'C': if ($cursorx < $columns - 1) $cursorx++; $state = Init; break; case 'A': if ($cursory) $cursory--; $state = Init; break; case 'P': delete_char($); $state = Init; break; case 'M': delete_line($); $state = Init; break; case 'm': $cur_attrib = 0; $state = Init; break; case '@': insert_chars($, 1); $state = Init; break; case 'L': insert_line($); $state = Init; break; case 's': $savedx = $cursorx; $savedy = $cursory; $state = Init; break; case 'u': goto_xy($, $savedx, $savedy); $state = Init; break; case 'r': set_scroll($, 0, $rows-1); $state = Init; break; default: /* Unrecognized sequence */ add_char($, '\033'); add_char($, '['); add_char($, text[i]); $state = Init; } break; case EscLBRegister0: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[0] = 10 * $escparm[0] + text[i] - '0'; break; case ';': $escparm[1] = 0; $state = EscLBRegister1; break; case 'm': set_attrib($, 1); $state = Init; break; case 'h': $insert_mode = True; $state = Init; break; case 'l': $insert_mode = False; $state = Init; break; case '@': insert_chars($, $escparm[0]); $state = Init; break; case 'b': repeat_char($, $escparm[0] - 1); $state = Init; break; case 'H': case 'f': goto_xy($, 0, $escparm[0] - 1); $state = Init; break; case 'n': $state = Init; break; /* Report cursor pos ignored */ case 'j': $cursorx = $cursory = 0; clear_eos($); $state = Init; break; case 'r': set_scroll($, $escparm[0]-1, $rows-1); $state = Init; break; default: $state = Init; /* Ignore unknown sequence */ } break; case EscLBRegister1: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[1] = 10 * $escparm[1] + text[i] - '0'; break; case ';': $escparm[2] = 0; $state = EscLBRegister2; break; case 'm': set_attrib($, 2); $state = Init; break; case 'H': case 'f': goto_xy($, $escparm[1] - 1, $escparm[0] - 1); $state = Init; break; case 'r': set_scroll($, $escparm[0]-1, $escparm[1]-1); $state = Init; break; default: $state = Init; /* Ignore unknown sequence */ } break; case EscLBRegister2: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[2] = 10 * $escparm[2] + text[i] - '0'; break; case ';': $escparm[3] = 0; $state = EscLBRegister3; break; case 'm': set_attrib($, 3); $state = Init; break; default: $state = Init; /* Ignore unknown sequence */ } break; case EscLBRegister3: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[3] = 10 * $escparm[3] + text[i] - '0'; break; case ';': $escparm[4] = 0; $state = EscLBRegister4; break; case 'm': set_attrib($, 4); $state = Init; break; default: $state = Init; /* Ignore unknown sequence */ } break; case EscLBRegister4: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[4] = 10 * $escparm[4] + text[i] - '0'; break; case 'm': set_attrib($, 5); $state = Init; break; default: $state = Init; /* Ignore unknown sequence */ } break; case EscRB: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[0] = text[i] - '0'; $state = EscRBRegister0; break; default: /* Unrecognized sequence */ add_char($, '\033'); add_char($, ']'); add_char($, text[i]); $state = Init; } break; case EscRBRegister0: switch (text[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': $escparm[0] = 10 * $escparm[0] + text[i] - '0'; break; #if 0 case ';': $escparm[1] = 0; $state = EscRBRegister1; break; #endif case 'X': set_cursortype($, $escparm[0]); $state = Init; break; default: $state = Init; /* Ignore unknown sequence */ } break; default: assert(0); /* cannot happen */ } } /* Finally redraw all lines that are marked as `dirty' */ if (XtIsRealized($)) { int j; for (j = 0; j < $rows; j++) if ($attribs[j][0] & ATTRIB_DIRTY) { $attribs[j][0] &= ~ATTRIB_DIRTY; draw_line($, j, 0, $columns); } } } @TRANSLATIONS @trans Shift: extend_selection() @trans : start_selection() @trans : extend_selection() @trans : end_selection() @trans : paste_selection() @ACTIONS @ The selection mechanism consist of three action procedures for highlighting a selection and one for pasting text into the widget. |start_selection| establishes the start of the highlighted selection from the mouse position. |extend_selection| highlights the text between the start position and the current mouse position. |end_selection| copies the highlighted part to a buffer and notifies the X server that this widget wants to be the current selection owner. |paste_selection| requests the current selection from whatever application has it and calls the |pasteCallback| for every character. @proc start_selection { find_cell($, event->xbutton.x, event->xbutton.y, &$start_x, &$start_y); $end_x = $start_x; $end_y = $start_y; $drag_started = False; /* No selection yet */ } @ |extend_selection| is called when the mouse is dragged over the text. It finds the cell that the mouse is on and highlights all cells from the one where the drag started to the current one (both inclusive). The function only draws the lines that the mouse passed between the previous and the current event. @proc extend_selection { int i, j, sx, sy, ex, ey, x, y; $drag_started = True; find_cell($, event->xbutton.x, event->xbutton.y, &x, &y); if ($start_y < y || ($start_y == y && $start_x < x)) { sy = $start_y; ey = y; sx = $start_x; ex = x; } else { /* Swap start and end */ sy = y; ey = $start_y; sx = x; ex = $start_x; } /* Draw the lines that the mouse passed over since last event */ for (i = min(y, $end_y); i <= max(y, $end_y); i++) { /* Toggle REV attribute on cells within selected region */ if (i == sy && i == ey) for (j = sx; j <= ex; j++) $attribs[i][j] ^= ATTRIB_REV; else if (i == sy) for (j = sx; j < $columns; j++) $attribs[i][j] ^= ATTRIB_REV; else if (i == ey) for (j = 0; j < ex; j++) $attribs[i][j] ^= ATTRIB_REV; else if (sy < i && i < ey) for (j = 0; j < $columns; j++) $attribs[i][j] ^= ATTRIB_REV; draw_line($, i, 0, $columns); /* Draw the line */ /* Toggle REV attribute on cells in region again */ if (i == sy && i == ey) for (j = sx; j <= ex; j++) $attribs[i][j] ^= ATTRIB_REV; else if (i == sy) for (j = sx; j < $columns; j++) $attribs[i][j] ^= ATTRIB_REV; else if (i == ey) for (j = 0; j < ex; j++) $attribs[i][j] ^= ATTRIB_REV; else if (sy < i && i < ey) for (j = 0; j < $columns; j++) $attribs[i][j] ^= ATTRIB_REV; } $end_x = x; $end_y = y; } @ When the mouse button is released, the selection is complete and the highlighted part is copied to a buffer. The widget then tells the X server that it wants to become the selection owner for the PRIMARY selection. A simple click should not be taken as a selection. If there has been no movement between the mouse press and release, the |end_selection| action simply returns without doing anything. The highlight is immediately removed from the screen. This is easy to implement but it removes a visual indicator that some people may want to leave there while the selection is active. Something to reconsider for the next version? @proc end_selection { int sx, sy, ex, ey, k, i, j; if (! $drag_started) return; /* No movement since BtnDown */ if ($start_y < $end_y || ($start_y == $end_y && $start_x < $end_x)) { sy = $start_y; ey = $end_y; sx = $start_x; ex = $end_x; } else { /* Swap start & end */ sy = $end_y; ey = $start_y; sx = $end_x; ex = $start_x; } /* Unhighlight the selection */ if (sy == ey) { draw_line($, sy, sx, ex + 1); } else { draw_line($, sy, sx, $columns); /* First (partial) line */ for (i = sy + 1; i <= ey - 1; i++) /* Middle lines */ draw_line($, i, 0, $columns); draw_line($, ey, 0, ex + 1); /* Last (partial) line */ } /* Copy selection to buffer */ if (sy == ey) { $selection = XtMalloc(ex - sx + 2); for (k = 0, j = sx; j <= ex; k++, j++) $selection[k] = $contents[sy][j]; $selection[k] = '\0'; $selection_len = k; } else { $selection = XtMalloc($columns - sx + 1 + (ey - sy - 1) * ($columns + 1) + ex + 3); k = 0; for (j = sx; j < $columns; j++, k++) $selection[k] = $contents[sy][j]; $selection[k++] = '\n'; for (i = sy + 1; i <= ey - 1; i++) { for (j = 0; j < $columns; j++, k++) $selection[k] = $contents[i][j]; $selection[k++] = '\n'; } for (j = 0; j <= ex; j++, k++) $selection[k] = $contents[ey][j]; $selection[k] = '\0'; $selection_len = k; } /* Now ask the X server for ownership of the selection */ if (XtOwnSelection($, XA_PRIMARY, event->xbutton.time, convert_proc, NULL, NULL) == False) XtAppWarning(XtWidgetToApplicationContext($), "Failed to become selection owner"); } @proc paste_selection { XtGetSelectionValue($, XA_PRIMARY, XA_STRING, paste_callback, NULL, event->xbutton.time); } @UTILITIES @def max(a, b) = ((a) > (b) ? (a) : (b)) @def min(a, b) = ((a) < (b) ? (a) : (b)) @ The |draw_line| function is called by |expose| to draw the characters in line |row| between columns |lft| and |rgt|. It draws runs of characters with the same attributes. @proc draw_line($, int row, int lft, int rgt) { Display *dpy = XtDisplay($); Window win = XtWindow($); int x, y; Bool undid_cursor = False; x = $margin + lft * $cellwidth; y = $margin + row * $cellheight + $font->ascent; /* * disable the cursor if we are about to draw over it */ if ($cursor_on && row == $xor_cursory && lft <= $xor_cursorx && $xor_cursorx < rgt) { disable_cursor($); undid_cursor = True; } /* * draw the requested text */ while (lft < rgt) { int i, n; GC use_gc; i = lft + 1; while (i < rgt && $attribs[row][i] == $attribs[row][lft]) i++; attrib_gcs($, $attribs[row][lft]); switch ($attribs[row][lft] & ATTRIB_REV) { case 0: use_gc = $defgc; break; case ATTRIB_REV: use_gc = $revgc; break; } n = i - lft; XDrawImageString(dpy, win, use_gc, x, y, &$contents[row][lft], n); if ($attribs[row][lft] & ATTRIB_ULINE) { /* * We want to draw a line of n * $cellwidth pixels * beginning at [x,y]. Since the endpoint of the * line is included in the line, the endpoint is * [x+n*$cellwidth-1,y] */ XDrawLine(dpy, win, use_gc, x, y + $uline_pos, x + n * $cellwidth - 1, y + $uline_pos); } lft = i; x += n * $cellwidth; } /* * restore the cursor if we had to disable it */ if (undid_cursor) enable_cursor($); } @ The |convert_proc| function is a callback called by Xt when some application requests the selection. It checks if the requested type is |XA_STRING|, since it can only give data in that format. @proc Boolean convert_proc($, Atom *selection, Atom *target, Atom *type_return, XtPointer *value_return, unsigned long *length_return, int *format_return) { static Atom xa_targets = None; if (xa_targets == None) { xa_targets = XInternAtom(XtDisplay($), "TARGETS", False); assert(xa_targets != None); } if (*target == xa_targets) { /* We support this many conversion atoms. */ const unsigned long num_atoms = 2; /* * On some systems, the Atom typedef is 64 bits wide, so * we need an Atom typedef that is guaranteed to be 32 bits * wide. * * This is because *format_return MUST be set to one of 8, 16, * or 32. 64 is not allowed. */ typedef CARD32 Atom32; Atom32 *valueP; int i; /* Create the list of conversion atoms we support */ valueP = (Atom32 *) XtMalloc((Cardinal)(sizeof(Atom32) * (num_atoms))); assert(valueP); i = 0; valueP[i++] = (Atom32) XA_STRING; valueP[i++] = (Atom32) xa_targets; assert(i == num_atoms); /* Arrange to return it */ *type_return = XA_ATOM; *value_return = (XtPointer) valueP; *length_return = num_atoms; *format_return = sizeof(Atom32) * 8; assert(*format_return == 32); return True; } else if (*target == XA_STRING) { *type_return = XA_STRING; *value_return = $selection; *length_return = $selection_len; *format_return = 8; return True; } else { return False; } } @ The |paste_callback| is installed by the |paste_selection| action procedure. It receives the selection data and `pastes' it into the McTerm. In this case that means it is passed character for character to the |pasteCallback| function. @proc paste_callback($, XtPointer client_data, Atom *selection, Atom *type, XtPointer value, unsigned long *length, int *format) { /* if it's what we're expecting, process it */ if(*selection == XA_PRIMARY && *type == XA_STRING && *format == 8 && value && *length > 0) { char *text = (char *) value; int i; for (i = 0; i < *length; i++) XtCallCallbackList($, $pasteCallback, (XtPointer) &text[i]); if (value) XtFree(value); return; } /* * *format == 32 for XA_ATOM, even though Atom is 64 bits on some * platforms... */ #if 0 if(*selection == XA_PRIMARY && *type == XA_ATOM && *format == 32) { Atom *list = (Atom *) value; int i; fprintf(stderr, "paste_callback: %lu targets\n", *length); for (i = 0; i < *length; i++) { char *name = "!!!NAME NOT RETRIEVED!!!"; if (list[i] == XT_CONVERT_FAIL) { name = "XT_CONVERT_FAIL"; } else if (list[i] == 0) { name = "!!!ZERO ATOM!!!"; } else { name = XGetAtomName(XtDisplay($), (Atom) list[i]); if (!name) name = "!!!NULL!!!"; } fprintf(stderr, "paste_callback: targets[%d] == 0x%08X (%s)\n", i, (unsigned int) list[i], name); } fflush(stderr); if (value) XtFree(value); return; } #endif /* if we get to here, it's an error */ XBell(XtDisplay($), 0); if (value) XtFree(value); } @ |compute_cell_size| compares the character sizes from the normal font and sets |cellwidth| and |cellheight| to the character size of the normal font. @proc compute_cell_size($) { unsigned long h1; if (! XGetFontProperty($font, XA_QUAD_WIDTH, &h1)) h1 = XTextWidth($font, "M", 1); $cellwidth = (int) h1; h1 = $font->ascent + $font->descent; $cellheight = (int) h1; if (! XGetFontProperty($font, XA_UNDERLINE_POSITION, &h1)) h1 = 2; $uline_pos = (int) h1; if (! XGetFontProperty($font, XA_UNDERLINE_THICKNESS, &h1)) h1 = 1; $uline_thick = (int) h1; } @ If our parent is a WMShell widget, |set_resize_increments| sets its |baseWidth|, |baseHeight|, |widthInc|, and |heightInc| resources according to our margin and cell size. @proc set_resize_increments($) { Widget top; top = XtParent($); if (!top) return; if (!XtIsWMShell(top)) return; if ($debug_set_resize_increments) { fprintf(stderr, "Setting baseWidth = %ld\n", (long)(2 * $margin)); fprintf(stderr, "Setting baseHeight = %ld\n", (long)(2 * $margin)); fprintf(stderr, "Setting widthInc = %ld\n", (long)($cellwidth)); fprintf(stderr, "Setting heightInc = %ld\n", (long)($cellheight)); } XtVaSetValues(top, XtNbaseWidth, 2 * $margin, XtNbaseHeight, 2 * $margin, XtNwidthInc, $cellwidth, XtNheightInc, $cellheight, /* * Set some reasonable minimum sizes. */ XtNminWidth, 2 * $margin + 8 * $cellwidth, XtNminHeight, 2 * $margin + 4 * $cellheight, NULL); } @ The |allocate_contents| function first deallocates the memory for the lines that are no longer needed or allocates memory if the number of lines has grown. It then lengthens or shortens each line to the new number of columns. New lines and columns are initialized to spaces, with default (=zero) attributes. If the number of rows changes, it resets the scroll region to the entire screen. @proc allocate_contents($) { int i, j; /* Remove superfluous lines */ for (i = $rows; i < $allocated_rows; i++) { XtFree($contents[i]); XtFree((char *)$attribs[i]); } /* Allocate and initialize new lines */ $contents = (char **)XtRealloc((char *)$contents, $rows*(int)sizeof(char *)); $attribs = (attrib_t **)XtRealloc((char *)$attribs, $rows*(int)sizeof(attrib_t*)); for (i = $allocated_rows; i < $rows; i++) { $contents[i] = XtMalloc($columns*(int)sizeof(char)); $attribs[i] = (attrib_t *)XtMalloc($columns*(int)sizeof(attrib_t)); for (j = 0; j < $columns; j++) { $contents[i][j] = ' '; $attribs[i][j] = 0; } } /* Lengthen or shorten existing lines */ if ($allocated_columns != $columns) { for (i = 0; i < min($allocated_rows, $rows); i++) { $contents[i] = XtRealloc($contents[i], $columns*(int)sizeof(char)); $attribs[i] = (attrib_t *)XtRealloc((char *)$attribs[i], $columns*(int)sizeof(attrib_t)); for (j = $allocated_columns; j < $columns; j++) { $contents[i][j] = ' '; $attribs[i][j] = 0; } } } if ($allocated_rows != $rows) { set_scroll($, 0, $rows-1); } if ($allocated_columns != $columns) { $do_wrap = False; } $allocated_rows = $rows; $allocated_columns = $columns; } @ The |xor_cursor| function xors a cursor of the given type at the given character cell. @proc xor_cursor($, int type, int row, int col) { Display *dpy = XtDisplay($); Window win = XtWindow($); int x, y; int half_height; int three_quarter_height; unsigned int width, height; x = $margin + col * $cellwidth; y = $margin + row * $cellheight; width = $cellwidth; height = $cellheight; half_height = $cellheight/2; three_quarter_height = (3 * $cellheight) / 4; attrib_gcs($, $attribs[row][col]); switch (type) { case XMcTermCursorBlank: /* don't draw anything */ break; case XMcTermCursorUnderline: y += $cellheight-1; height -= $cellheight-1; XFillRectangle(dpy, win, $cursorgc, x, y, width, height); break; case XMcTermCursorFullHollow: XDrawRectangle(dpy, win, $cursorgc, x, y, width-1, height-1); break; case XMcTermCursorFullSolid: XFillRectangle(dpy, win, $cursorgc, x, y, width, height); break; case XMcTermCursorHalfHollow: y += half_height; height -= half_height; XDrawRectangle(dpy, win, $cursorgc, x, y, width-1, height-1); break; case XMcTermCursorHalfSolid: y += half_height; height -= half_height; XFillRectangle(dpy, win, $cursorgc, x, y, width, height); break; case XMcTermCursorQuarterHollow: y += three_quarter_height; height -= three_quarter_height; XDrawRectangle(dpy, win, $cursorgc, x, y, width-1, height-1); break; case XMcTermCursorQuarterSolid: y += three_quarter_height; height -= three_quarter_height; XFillRectangle(dpy, win, $cursorgc, x, y, width, height); break; } } @ The |enable_cursor| function draws the cursor if it is not already drawn. It saves the cursor type it drew and the coordinates it drew it on, so that |disable_cursor| can remove it regardless of where it is supposed to be drawn next. @proc enable_cursor($) { if ($cursor_on) { /* Restore to normal */ return; } xor_cursor($, $cursortype, $cursory, $cursorx); $xor_cursorx = $cursorx; $xor_cursory = $cursory; $xor_cursortype = $cursortype; $cursor_on = True; } @ The |disable_cursor| function undraws the cursor if it is already drawn. @proc disable_cursor($) { if (! $cursor_on) { return; } xor_cursor($, $xor_cursortype, $xor_cursory, $xor_cursorx); $cursor_on = False; } @ The |toggle_cursor| function either draws the cursor (if the cursor is not drawn) or erases the cursor (if the cursor is drawn). @proc toggle_cursor($) { if ($cursor_on) { disable_cursor($); } else { enable_cursor($); } } @ The |blink_handler| function is called every 500 milliseconds to blink the cursor. It draws the character cell under the cursor alternately in its normal face and in reverse. It reinstalls itself. @proc blink_handler(XtPointer client_data, XtIntervalId *id) { Widget $ = (Widget) client_data; toggle_cursor($); $timer = XtAppAddTimeOut(XtWidgetToApplicationContext($), 500, blink_handler, $); } @ The |map_handler| function is installed by |initialize| as an event handler for Map and Unmap events. It will install (on Map) or remove (on Unmap) the timer function that blinks the cursor. TO DO: this can be made a little more efficient by checking the widget's |visibilty| field (after setting the class variable |visible_interest| to |True|) and installing a timer for blinking only when the widget is visible and a timer for periodically checking |visible| otherwise. @proc map_handler($, XtPointer client_data, XEvent *event, Boolean *cont) { if (event->type == MapNotify) $timer = XtAppAddTimeOut(XtWidgetToApplicationContext($), 500, blink_handler, $); else if (event->type == UnmapNotify) XtRemoveTimeOut($timer); } @ |synchronize_dims| keeps the |rows| and |columns| resources synchronized with the new |width| and |height|. If the |rows| or |columns| have changed, it reallocates the storage for the contents of the widget and calls the |resizeCallback| with the new dimensions. @proc synchronize_dims($) { XMcTermWindowSizeInfo cb_info; int oldcolumns = $columns, oldrows = $rows; $columns = max(1, ((int)$width - 2 * $margin)/$cellwidth); $rows = max(1, ((int)$height - 2 * $margin)/$cellheight); if ($cursorx >= $columns) { $cursorx = $columns - 1; $do_wrap = False; } if ($cursory >= $rows) $cursory = $rows - 1; if ($rows != oldrows || $columns != oldcolumns) { Bool undid_cursor = False; /* * Disable the cursor before resizing the window in case * the old cursor position will be off the new screen. */ if ($cursor_on) { disable_cursor($); undid_cursor = True; } allocate_contents($); cb_info.rows = $rows; cb_info.columns = $columns; XtCallCallbackList($, $resizeCallback, &cb_info); /* * Enable the cursor if we disabled it. */ if (undid_cursor) enable_cursor($); } } @ |clip_gcs| sets the clipping regions in all GCs. It is called wenever the GC's change or the widget resizes. @proc clip_gcs($) { XRectangle rect; /* * If the user specifies an initial geometry with -geometry, * the resize() method gets called before the window is * realized. In that case, we need to avoid modifying the * GCs because they have not been created yet. */ if (! XtIsRealized($)) return; /* the inside of our window */ rect.x = 0; rect.y = 0; rect.width = $width; rect.height = $height; XSetClipRectangles(XtDisplay($), $defgc, 0, 0, &rect, 1, YXSorted); XSetClipRectangles(XtDisplay($), $revgc, 0, 0, &rect, 1, YXSorted); XSetClipRectangles(XtDisplay($), $cursorgc, 0, 0, &rect, 1, YXSorted); } @ The |make_gc| function is called by |realize| and |set_values| every time the fonts or the colors change. These GCs are always clipped to the part inside the border. We do not use XtGetGC() to create these GCs because we need to modify the clip mask later on. We do not use XtAllocateGC() to create these GCs because we don't want to have to reset the clip mask every time we are about to use the GCs. Hence, we are left with XCreateGC(). @proc make_gc($) { XGCValues values; unsigned long mask; if (! XtIsRealized($)) return; mask = GCFont | GCLineWidth; if ($defgc) XFreeGC(XtDisplay($), $defgc); values.font = $font->fid; values.line_width = $uline_thick; $defgc = XCreateGC(XtDisplay($), XtWindow($), mask, &values); if ($revgc) XFreeGC(XtDisplay($), $revgc); values.font = $font->fid; values.line_width = $uline_thick; $revgc = XCreateGC(XtDisplay($), XtWindow($), mask, &values); /* * To quote from the X FAQ: * * You might expect that when using GXxor to draw black on black you * would get white. But this is not necessarily the case, since * GXxor does not operate on RGB values but on colormap indices. * * If you want to use GXxor simply to switch between two colors, * then you can take the shortcut of setting the background color * in the GC (graphics context) to 0 and the foreground color to a * value such that when it draws over red, say, the result is blue, * and when it draws over blue the result is red. This foreground * value is itself the XOR of the colormap indices of red and blue. * * Hence the peculiar settings for the background and foreground * pixels in |cursorgc|. */ mask = GCFunction | GCLineWidth; if ($cursorgc) XFreeGC(XtDisplay($), $cursorgc); values.function = GXxor; values.line_width = 0; $cursorgc = XCreateGC(XtDisplay($), XtWindow($), mask, &values); attrib_gcs($, 0); clip_gcs($); } @ The |attrib_gcs| function is called to set the foreground and background colors in the GCs as required. It sets them unconditionally, and relies on Xlib's caching to keep round trip activity to the server to a minimum. @proc attrib_gcs($, attrib_t a) { Pixel fg; Pixel bg; if (! XtIsRealized($)) return; switch(attrib_get_foreground(a)) { case XMcTermForegroundBlack: fg = a & ATTRIB_BRIGHT ? $foregroundBrightBlack : $foregroundNormalBlack; break; case XMcTermForegroundRed: fg = a & ATTRIB_BRIGHT ? $foregroundBrightRed : $foregroundNormalRed; break; case XMcTermForegroundGreen: fg = a & ATTRIB_BRIGHT ? $foregroundBrightGreen : $foregroundNormalGreen; break; case XMcTermForegroundYellow: fg = a & ATTRIB_BRIGHT ? $foregroundBrightYellow : $foregroundNormalYellow; break; case XMcTermForegroundBlue: fg = a & ATTRIB_BRIGHT ? $foregroundBrightBlue : $foregroundNormalBlue; break; case XMcTermForegroundMagenta: fg = a & ATTRIB_BRIGHT ? $foregroundBrightMagenta : $foregroundNormalMagenta; break; case XMcTermForegroundCyan: fg = a & ATTRIB_BRIGHT ? $foregroundBrightCyan : $foregroundNormalCyan; break; case XMcTermForegroundWhite: fg = a & ATTRIB_BRIGHT ? $foregroundBrightWhite : $foregroundNormalWhite; break; default: fg = a & ATTRIB_BRIGHT ? $foregroundBright : $foreground; break; } switch(attrib_get_background(a)) { case XMcTermBackgroundBlack: bg = $backgroundBlack; break; case XMcTermBackgroundRed: bg = $backgroundRed; break; case XMcTermBackgroundGreen: bg = $backgroundGreen; break; case XMcTermBackgroundYellow: bg = $backgroundYellow; break; case XMcTermBackgroundBlue: bg = $backgroundBlue; break; case XMcTermBackgroundMagenta: bg = $backgroundMagenta; break; case XMcTermBackgroundCyan: bg = $backgroundCyan; break; case XMcTermBackgroundWhite: bg = $backgroundWhite; break; default: bg = $background_pixel; break; } XSetForeground(XtDisplay($), $defgc, fg); XSetBackground(XtDisplay($), $defgc, bg); XSetForeground(XtDisplay($), $revgc, bg); XSetBackground(XtDisplay($), $revgc, fg); /* * To quote from the X FAQ: * * You might expect that when using GXxor to draw black on black you * would get white. But this is not necessarily the case, since * GXxor does not operate on RGB values but on colormap indices. * * If you want to use GXxor simply to switch between two colors, * then you can take the shortcut of setting the background color * in the GC (graphics context) to 0 and the foreground color to a * value such that when it draws over red, say, the result is blue, * and when it draws over blue the result is red. This foreground * value is itself the XOR of the colormap indices of red and blue. * * Hence the peculiar settings for the background and foreground * pixels in |cursorgc|. */ XSetForeground(XtDisplay($), $cursorgc, fg ^ bg); XSetBackground(XtDisplay($), $cursorgc, 0); } @ The |add_char| function is called by the |write| method to add a character at the current cursor position. The cursor is then moved to the right (unless it is already at the right margin). When |insert_mode| is on, the characters that are on the same line to the right of the cursor are shifted to the right. The first character of the modified line is marked with the |ATTRIB_DIRTY| flag, to notify the |write| that this line should be redrawn. @proc add_char($, int c) { if ($do_wrap) { $cursorx = 0; /* CR */ cursor_down($); /* LF */ $do_wrap = False; } if ($insert_mode) { memmove(&$contents[$cursory][$cursorx+1], &$contents[$cursory][$cursorx], ($columns - $cursorx - 1)); memmove(&$attribs[$cursory][$cursorx+1], &$attribs[$cursory][$cursorx], sizeof(attrib_t)*($columns - $cursorx - 1)); $contents[$cursory][$cursorx] = c; $attribs[$cursory][$cursorx] = $cur_attrib; } else { $contents[$cursory][$cursorx] = c; $attribs[$cursory][$cursorx] = $cur_attrib; } $attribs[$cursory][0] |= ATTRIB_DIRTY; if ($cursorx < $columns - 1) { $cursorx++; } else { /* Margin reached */ $do_wrap = True; } $last_char = c; /* Save for repeat_char() */ } @ The |repeat_char| functions inserts |n| copies of the most recently inserted character. @proc repeat_char($, int n) { for (; n > 0; n--) add_char($, $last_char); } @ |next_tabstop| moves the cursor to the next tab stop. In overwrite mode, the cursor is moved and the line is not changed; in insert mode up to 7 spaces are inserted and the line is marked for redrawing. @proc next_tabstop($) { if (! $insert_mode) { $cursorx = (($cursorx + 8)/8) * 8; } else { int x = (($cursorx + 8)/8) * 8; if (x >= $columns) x = $columns - 1; while ($cursorx != x) add_char($, ' '); $attribs[$cursory][0] |= ATTRIB_DIRTY; } } @ The |delete_line| function deletes the line on which the cursor is, shifting all lower lines up. The cursor doesn't move. All lines that changed are marked for redrawing. The deleted line is reinserted at the bottom and cleared. (This way no memory is lost.) NOTE: This only has an effect when the current line is in the scrolling region, and it only scrolls up lines that are in the scrolling region. If the current line is at the bottom of the scrolling region, it simply clears it. @proc delete_line($) { int i; char *swap_contents; attrib_t *swap_attribs; /* if the cursor is not in the scroll region, do nothing */ if ($cursory < $scroll_top_row || $scroll_bot_row < $cursory) { return; } $do_wrap = False; swap_contents = $contents[$cursory]; swap_attribs = $attribs[$cursory]; for (i = $cursory; i < $scroll_bot_row; i++) { $contents[i] = $contents[i+1]; $attribs[i] = $attribs[i+1]; $attribs[i][0] |= ATTRIB_DIRTY; } $contents[$scroll_bot_row] = swap_contents; $attribs[$scroll_bot_row] = swap_attribs; memset(&$contents[$scroll_bot_row][0], ' ', $columns); memset(&$attribs[$scroll_bot_row][0], 0, sizeof(attrib_t)*$columns); $attribs[$scroll_bot_row][0] |= ATTRIB_DIRTY; } @ |insert_line| inserts a blank line at the cursor's line, moving the existing line and all lines below it down. The line that is pushed off the bottom is reinserted at the cursor and cleared. NOTE: This only has an effect when the current line is in the scrolling region, and it only scrolls down lines that are in the scrolling region. If the current line is at the bottom of the scrolling region, it simply clears it. @proc insert_line($) { int i; char *swap_contents; attrib_t *swap_attribs; /* if the cursor is not in the scroll region, do nothing */ if ($cursory < $scroll_top_row || $scroll_bot_row < $cursory) { return; } $do_wrap = False; swap_contents = $contents[$scroll_bot_row]; swap_attribs = $attribs[$scroll_bot_row]; for (i = $scroll_bot_row; i > $cursory; i--) { $contents[i] = $contents[i-1]; $attribs[i] = $attribs[i-1]; $attribs[i][0] |= ATTRIB_DIRTY; } $contents[$cursory] = swap_contents; $attribs[$cursory] = swap_attribs; memset($contents[$cursory], ' ', $columns); memset($attribs[$cursory], 0, sizeof(attrib_t)*$columns); $attribs[$cursory][0] |= ATTRIB_DIRTY; } @ The |cursor_down| function moves the cursor to the next lower line or scrolls the text up. NOTE: It scrolls the text up only if the current line is the bottom line of the scroll region. @proc cursor_down($) { $do_wrap = False; if ($cursory == $scroll_bot_row) { $cursory = $scroll_top_row; delete_line($); $cursory = $scroll_bot_row; } else if ($cursory < $rows - 1) { $cursory++; } } @ The |clear_eol| function clears the text from the cursor to the end of the line. @proc clear_eol($) { $do_wrap = False; memset(&$contents[$cursory][$cursorx], ' ', ($columns - $cursorx)); memset(&$attribs[$cursory][$cursorx], 0, sizeof(attrib_t) * ($columns - $cursorx)); $attribs[$cursory][0] |= ATTRIB_DIRTY; } @ The |clear_eos| function clears the text from the cursor to the end of the screen. @proc clear_eos($) { int i, save_x, save_y; $do_wrap = False; clear_eol($); save_x = $cursorx; save_y = $cursory; $cursorx = 0; for (i = save_y + 1; i < $rows; i++) { $cursory = i; clear_eol($); } $cursorx = save_x; $cursory = save_y; } @ |delete_char| removes the character under the cursor and shifts the rest of the line one position to the left. @proc delete_char($) { $do_wrap = False; memmove(&$contents[$cursory][$cursorx], &$contents[$cursory][$cursorx+1], ($columns - $cursorx - 1)); memmove(&$attribs[$cursory][$cursorx], &$attribs[$cursory][$cursorx+1], sizeof(attrib_t)*($columns - $cursorx - 1)); $contents[$cursory][$columns-1] = ' '; $attribs[$cursory][$columns-1] = 0; $attribs[$cursory][0] |= ATTRIB_DIRTY; } @ |insert_chars| inserts |n| spaces, shifting the rest of the line to the right. @proc insert_chars($, int n) { $do_wrap = False; if (n > $columns - $cursorx) n = $columns - $cursorx; memmove(&$contents[$cursory][$cursorx+n], &$contents[$cursory][$cursorx], ($columns - $cursorx - n)); memmove(&$attribs[$cursory][$cursorx+n], &$attribs[$cursory][$cursorx], sizeof(attrib_t)*($columns - $cursorx - n)); memset(&$contents[$cursory][$cursorx], ' ', n); memset(&$attribs[$cursory][$cursorx], 0, sizeof(attrib_t)*n); $attribs[$cursory][0] |= ATTRIB_DIRTY; } @ |set_cursortype| sets the current cursor type. @proc set_cursortype($, int type) { if (type < XMcTermCursorMin || XMcTermCursorMax < type) return; $cursortype = type; } @ |set_scroll| sets the current scroll region. The parameters |toprow| and |botrow| have been adjusted to be zero-based. @proc set_scroll($, int toprow, int botrow) { if (toprow < 0 || botrow < toprow || botrow > $rows-1) return; assert(0 <= toprow); assert(toprow <= botrow); assert(botrow <= $rows-1); $scroll_top_row = toprow; $scroll_bot_row = botrow; } @ |set_attrib| sets the current text attributes. The parameter |n| tells the function how many of the registers contain useful codes. Each of these registers is interpreted. @proc set_attrib($, int n) { int i; int val; for (i = 0; i < n; i++) { val = $escparm[i]; switch(val) { case 0: $cur_attrib = 0; break; case 4: $cur_attrib |= ATTRIB_ULINE; break; case 7: /* turn on reverse */ $cur_attrib |= ATTRIB_REV; break; case 27: /* turn off reverse */ $cur_attrib &= ~ATTRIB_REV; break; case XMcTermForegroundBright: /* turn on bright */ $cur_attrib |= ATTRIB_BRIGHT; break; case XMcTermForegroundNormal: /* turn off bright */ $cur_attrib &= ~ATTRIB_BRIGHT; break; case XMcTermForegroundBlack: case XMcTermForegroundRed: case XMcTermForegroundGreen: case XMcTermForegroundYellow: case XMcTermForegroundBlue: case XMcTermForegroundMagenta: case XMcTermForegroundCyan: case XMcTermForegroundWhite: case XMcTermForegroundDefault: attrib_set_foreground(&$cur_attrib, val); break; case XMcTermBackgroundBlack: case XMcTermBackgroundRed: case XMcTermBackgroundGreen: case XMcTermBackgroundYellow: case XMcTermBackgroundBlue: case XMcTermBackgroundMagenta: case XMcTermBackgroundCyan: case XMcTermBackgroundWhite: case XMcTermBackgroundDefault: attrib_set_background(&$cur_attrib, val); break; } } } @ We encode the default foreground (39) as 0, and the other foreground colors (30-37) as 1-8. @proc attrib_set_foreground(attrib_t *a, int n) { *a &= ~ATTRIB_FOREGROUND_MASK; if (n == XMcTermForegroundDefault) return; *a |= (((n-XMcTermForegroundBlack)+1) << ATTRIB_FOREGROUND_SHIFT); } @proc int attrib_get_foreground(attrib_t a) { a &= ATTRIB_FOREGROUND_MASK; if (a == 0) return XMcTermForegroundDefault; return (a>>ATTRIB_FOREGROUND_SHIFT)-1 + XMcTermForegroundBlack; } @ We encode the default background (49) as 0, and the other background colors (40-47) as 1-8. @proc attrib_set_background(attrib_t *a, int n) { *a &= ~ATTRIB_BACKGROUND_MASK; if (n == XMcTermBackgroundDefault) return; *a |= (((n-XMcTermBackgroundBlack)+1) << ATTRIB_BACKGROUND_SHIFT); } @proc int attrib_get_background(attrib_t a) { a &= ATTRIB_BACKGROUND_MASK; if (a == 0) return XMcTermBackgroundDefault; return (a>>ATTRIB_BACKGROUND_SHIFT)-1 + XMcTermBackgroundBlack; } @ |goto_xy| positions the cursor at the given coordinates, making sure they fall within the current extend. @proc goto_xy($, int x, int y) { $do_wrap = False; $cursorx = max(0, min($columns - 1, x)); $cursory = max(0, min($rows - 1, y)); } @ |find_cell| computes the coordinates of the cell that contains (cx,cy) or that is closest to it. (cx,cy) are relative to the real window. @proc find_cell($, int cx, int cy, int *col, int *row) { int i, j, x, y; for (x = $margin + $cellwidth, i = 0; i < $columns - 1 && x < cx; i++, x += $cellwidth) ; for (y = $margin + $cellheight, j = 0; j < $rows - 1 && y < cy; j++, y += $cellheight) ; *col = i; *row = j; } @ |handle_keys| is an event handler that is installed by the |initialize| method to handle all key presses. When a key press could be interpreted, the |cont| parameter is set to |False|, so that the key is not used again for other purposes (such as keyboard traversal, in the case of the Tab key). @proc handle_keys($, XtPointer client_data, XEvent *event, Boolean *cont) { assert(event->type == KeyPress); XtCallCallbackList($, $keyCallback, &event->xkey); *cont = False; } @proc print_geometry(FILE *f, XtWidgetGeometry *g) { fprintf(f, "request_mode:"); if (g->request_mode & CWX) fprintf(f, " CWX"); if (g->request_mode & CWY) fprintf(f, " CWY"); if (g->request_mode & CWWidth) fprintf(f, " CWWidth"); if (g->request_mode & CWHeight) fprintf(f, " CWHeight"); if (g->request_mode & CWBorderWidth) fprintf(f, " CWBorderWidth"); if (g->request_mode & CWSibling) fprintf(f, " CWSibling"); if (g->request_mode & CWStackMode) fprintf(f, " CWStackMode"); fprintf(f, "\n"); if (g->request_mode & CWX) fprintf(f, " CWX = %ld\n", (long) g->x); if (g->request_mode & CWY) fprintf(f, " CWY = %ld\n", (long) g->y); if (g->request_mode & CWWidth) fprintf(f, " CWWidth = %ld\n", (long) g->width); if (g->request_mode & CWHeight) fprintf(f, " CWHeight = %ld\n", (long) g->height); if (g->request_mode & CWBorderWidth) fprintf(f, " CWBorderWidth = %ld\n", (long) g->border_width); if (g->request_mode & CWSibling) fprintf(f, " CWSibling = %ld\n", (long) g->sibling); if (g->request_mode & CWStackMode) fprintf(f, " CWStackMode = %ld\n", (long) g->stack_mode); /* CWX CWY CWWidth CWHeight CWBorderWidth CWSibling CWStackMode */ } @ if the request would succeed, request a resize. @proc request_resize($, Dimension new_width, Dimension new_height) { XtWidgetGeometry request; XtWidgetGeometry compromise_return; XtGeometryResult ans; if ($debug_set_values) { fprintf(stderr, "set_values A: $width, $height == %ld, %ld\n", (long)$width, (long)$height); } request.request_mode = CWWidth | CWHeight | XtCWQueryOnly; request.width = new_width; request.height = new_height; ans = XtMakeGeometryRequest($, &request, &compromise_return); if (ans == XtGeometryNo) { if ($debug_set_values) { fprintf(stderr, "set_values B: aborting geometry request\n"); } return; } request.request_mode = CWWidth | CWHeight; ans = XtMakeGeometryRequest($, &request, &compromise_return); if ($debug_set_values) { fprintf(stderr, "XtMakeResizeRequest(%ld, %ld) -> %ld, %ld, %s\n", (long)request.width, (long)request.height, (long)compromise_return.width, (long)compromise_return.height, ans == XtGeometryYes ? "Yes" : ans == XtGeometryNo ? "No" : ans == XtGeometryAlmost ? "Almost" : "Unknown return value!" ); } switch(ans) { case XtGeometryAlmost: request.width = compromise_return.width; request.height = compromise_return.height; XtMakeGeometryRequest($, &request, &compromise_return); break; } /* * At this point, $width and $height might have been * changed. */ if ($debug_set_values) { fprintf(stderr, "set_values B: $width, $height == %ld, %ld\n", (long)$width, (long)$height); } } @IMPORTS @incl @incl @incl @ The file is for the definition of CARD32 @incl @ The file is for the definitions of XtNbaseWidth, XtNbaseHeight, XtNwidthInc, XtNheightInc. @incl