在关于C64的GeoWrite WYSIWYG文本编辑器内部结构的系列文章中,本文讨论了该应用程序如何整合键盘输入以跟上快速打字员的步伐。
GEOS不使用C64的ROM中的键盘驱动程序,但有自己的实现,使用基于ASCII的8位键码:代码$20到$7F是常规的ASCII可打印字符。所有控制键(如光标键)以及非ASCII键(如“GB”)都映射到控制代码空间$00到$1F:
所有这些定义了代码$00-$7F,它占7位。最上面的位(值$80)表示是否已经用键按下了“Commodore”修改键。在GEOS中,Commodore键用于键盘快捷键,与Macintosh上的Command键非常相似。
键盘驱动程序在中断上下文中运行,该中断上下文由定时器每秒触发60次。它扫描键盘,并将新的按键代码放在16字节队列的末尾:这样,如果应用程序不能立即处理传入的按键,则不会丢失任何按键-只要输入的按键不超过16个键。
应用程序可以通过调用GetNextChar从队列中获取下一个键。如果队列中有密钥,则会将其从队列中移除并返回。否则,返回值为0。
所有这一切几乎都是在任何操作系统上发生的-包括C64在基本模式下的原始ROM。
GEOS是一个事件驱动的环境:系统的“主循环”控制所有执行,并根据鼠标点击、按键和计时器等事件回调应用程序。
因此,在GEOS上处理键盘的惯用方式是注册键盘回调。在每个主循环迭代中,系统将检查队列中是否至少有一个键。如果是这种情况,它将使最旧的键出列,将其存储在系统变量keydata中,并调用应用程序可以设置的函数指针keyVector。如果向量为0(默认值),则不会进行回调,密钥代码将被丢弃。
然后,应用程序可以从KeyData读取密钥代码,处理它并返回。如果队列中有更多的键,主循环将在下一次迭代中使用下一个键代码再次调用向量。
或者,回调函数可以通过重复调用GetNextChar从队列中读取更多键。
GeoWrite的核心功能是文本输入,因此需要确保它具有响应性,并且不会丢失任何按键。
这很棘手:在1 MHz的CPU上,重新绘制几行比例字体可能需要几秒钟的时间,所以应用程序落后于用户的键入是很常见的。因此,如果用户键入导致重画几行的字符,并且用户同时键入三个字符,则这三个字符可能会导致三个重画,在此期间用户可以再键入九个字符。这款应用程序会越来越落后,最终会丢掉字符。
为了避免这种情况,应用程序必须跟上用户的打字速度。好消息是,一旦应用程序在按键上落后,它就有几个按键代码要处理,所以可以进行一些优化:
插入单个字符时,光标后的文本数据上移一个字节,并添加新字符。然后,更新屏幕:重新绘制光标右侧的当前行的一部分,如果该行的最后一个单词溢出到下一行,则该行也必须重新绘制,依此类推。
与单独为键盘缓冲区中的每个字符执行此工作不同,一次为所有字符执行此操作可以节省大量工作:
这基本上与从文本废料粘贴文本时发生的情况相同。
您可以在上面的动画中看到此策略的效果。按住键盘上的“a”键,生成快速的“a”键码流。
与此同时,又进了两个字,一口气加进去了。这会触发整个段落的重绘,在本例中,因为“准将”一词必须移到下一行。
这张重画的费用太高了,所以在此期间又有七个角色加入进来。重新绘制了这条线,这次它不会溢出。
因为这次重画很便宜,所以只多了两个角色进来了。同样,它会导致仅重画当前行。
我们在实践中看到,每当需要重画更多文本时,都会缓冲更多的按键,但系统很快就会跟上。
所有可打印字符以及换行符和制表符字符都可以组合成要插入的单个字符串。所有其他钥匙都必须有特殊外壳。
如果用户键入一个字符,然后按DEL键,文档实际上将保持不变,但内存中的文本首先向上移动一个字节,然后再次向下移动一个字节,并且屏幕更新了两次。
因此,如果键盘缓冲区包含Del字符,则GeoWrites会从缓冲区中删除前面的字符。这两个角色相互抵消,不需要做更多的工作。
如果在缓冲区为空时遇到DEL,则不能删除任何字符,但也不能插入字符。然后,GeoWrite递增它在键盘缓冲区中看到的剩余DEL键计数。下一段后面有一些例子。
像光标键和键盘快捷键这样的不可打印的按键代码也是特殊的。如果队列包含“abc”,后跟C=T(Commodore key+T;粘贴文本)和“def”,则GeoWrite必须插入“abc”,将文本粘贴到文本碎片中,然后插入“def”。它不能组合这两个文本字符串。
这就是每当遇到控制键或快捷键时,GeoWrite停止处理队列的原因。将计算Del字符、目前为止的字符串和控制键,并在下次调用keyVector后处理键盘队列的其余部分。
以下是键盘队列的内容、开头的Del字符数、要插入的字符串、检测到的控制键以及此处理后的键盘队列内容的一些示例:
示例显示,字符后面的DEL键将有效地删除该键,但如果DEL键代码的数量超过其前面的字符数,则将计算额外的DEL键。并且如果存在控制键或键盘快捷键,则键盘队列的处理在此停止。
这是处理键盘队列并返回要删除的字符数、要插入的字符串和检测到的控制键的代码:
ProcessKbdQueue: LDA#0 STA delCount;到目前为止没有多余的DEL键 Sta kbdStringCnt;kbd字符串为空 STA curControlKey;无Ctrl键 LDA Keydata;传入keyVector的第一个字符 @再次:bmi@ctrl;";C=";快捷方式,请停止处理 CMP#KEY_DELETE;DEL键? Beq@del;是 CMP#KEY_INSERT;SHIFT+DEL? Beq@del;是(与GEOS中的Del相同) CMP#CR;回车键? Beq@nctrl;是的,不算控制键 CMP#TAB;Tab键? Beq@nctrl;是的,不算控制键 CMP#$20;低于$20,即不可打印 Bcc@ctrl;是的,它是一个控制键,停止处理 @nctrl:ldx kbdStringCnt Sta kbdString,x;添加到kbd字符串 Inc.kbdStringCnt @Next:JSR GetNextChar;kbd队列中是否还有字符? 税费 Beq@rts;否,返回 Bne@再一次;是的,重复一遍 @del:ldx kbdStringCnt;kbd字符串中是否有字符? Beq@excss;否,没有要删除的字符 12月kbdStringCnt;删除前一个字符 Bra@Next;并继续 @excss:inc delCount;计算删除的多余字符数 Bne@Next;并继续 @ctrl:sta curControlKey;保存控制键 @rts:rts。
GeoWrite的keyVector处理程序调用此函数,然后将三个输出(DELS、STRING、CONTROL KEY)应用到文档:
首先,如果有多余的Del,它会根据Del的数量删除当前光标位置上的字符。它只是将缓冲区的剩余部分向下移动Del数。
然后,如果有字符串,它会将缓冲区向上移动字符串的长度,并将字符串复制到文本中,从而一次性插入该字符串。
有两种特殊情况:如果当前页面上的Del多于字符,则必须以不同方式处理额外的Del。如果有DELs和字符串,则在前两个步骤中执行优化:不需要移动缓冲区两次。例如。
如果字符串中有一个DEL和两个字符,则缓冲区必须上移一个字节,插入位置在游标之前一个字节。
如果字符串中有两个DL和一个字符,则缓冲区必须下移一个字节,插入位置在游标之前两个字节。
如果字符串中有两个DL和两个字符,则根本不需要移动缓冲区,并且插入位置在光标之前两个字节。
在设计软件时,如果有大量内存,则可以弥补CPU速度慢的问题,例如,通过缓存数据来避免计算数据。快速的CPU可以弥补太少的内存。GEOS和GeoWrite两者都没有。它们是为具有64KB RAM的8位CPU编写的。
内存约束要求对所有代码的大小进行优化,这会使其变得更慢,并且需要将GeoWrite拆分成9个部分,这些部分从慢速磁盘驱动器加载。
如果字体渲染器有足够的空间来存放FastPath代码和缓存,那么它的速度可能会快得多。
缓慢的字体渲染要求GeoWrite包含大量额外的逻辑,以便能够跟上快速打字员的步伐。
额外的代码增加了内存压力,需要更密集地编写其他代码或将其推送到不同的记录中。
GEOS工程师似乎找到了一个合理的折衷方案。是的,GeoWrite在未扩展的C64上速度很慢,但对于64KB的计算机来说,它是一个有用且非常强大的工具。