{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540 {\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fmodern\fcharset0 Courier;} {\colortbl;\red255\green255\blue255;} \margl1440\margr1440\vieww13040\viewh15960\viewkind0 \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\b\fs30 \cf0 Cocoa Interfaces Using the Apple Interface Builder (IB) and Clozure Common Lisp (CCL)\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \fs28 \cf0 \ Version 2.1 April 2010 \fs30 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b0\fs24 \cf0 \ Copyright \'a9 2010 Paul L. Krueger All rights reserved.\ \ Paul Krueger, Ph.D.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Introduction \b0\fs24 \ \ This tutorial provides a guide to creating Cocoa interfaces for Clozure Common Lisp (CCL) programs. It is not a comprehensive guide to Cocoa; that is simply too big an undertaking and there is ample documentation from many other sources. What I've tried to do is gradually introduce you to the Cocoa/Lisp development world so that you can go explore the many possibilities on your own with a good sense of how those things might be integrated into Lisp. For the sake of simplicity I have taken a fairly narrow path and not tried to explain or use every possible bell and whistle that Cocoa provides. Hopefully I will have provided enough of a foundation that each of you can individually explore the possibilities on your own. \ \ Although I have tried to be as factual as possible, it is entirely likely that there are things here which are in error. Please notify me of any such things (email plkrueger (at) comcast.net) and I will make corrections as quickly as possible. All of this was done with the Leopard operating system (Mac OSX version 10.5.8), Developer Tools version 3.1.4 and CCL version 1.5-dev-r13388M-trunk (DarwinX8664). Since all of this is a moving target, the diagrams and examples may look slightly different on your system.\ \ Whenever I add a new project I will change the major version number of this document. Minor version number changes indicated corrections without adding a new project. There is also an associated document called "Revision Notes" that has a synopsis of the content changes for each revision.\ \ There are a number of different objectives that you might have for development of graphical user interfaces for CCL programs and it's important to understand what the goals were for this work so that you know whether to keep reading or look for an alternative. \ The opinions expressed below are just that, opinions, and others will certainly disagree. So with that in mind, in order of importance to me, the goals that I had for selecting an approach to user interface development were:\ 1. Usable either to create stand-alone executables or interactively from within a standard \ Lisp Read/Eval/Print/Loop (REPL)\ 2. Looks native to the platform\ 3. Easy blending with Lisp code\ 4. Development effort commensurate with interface complexity\ 5. Cross-platform/OS portability\ \ \i Goal 1: Usable for stand-alone executables or within standard Lisp REPL \i0 \ \ One of the primary reasons that I find Lisp to be such a productive language is the ability to try snippets of code and quickly modify/correct them. Standard non-interpretted languages, for the most part, require a sort of code/build/execute/debug loop that I find to be a productivity killer. Modern IDE's like Apple's Xcode can make this somewhat tolerable, but it would never be my first choice. User interfaces, like most code, require debugging and being able to modify them on the fly from within a REPL is simply the easiest method I know for rapid deployment. Given all that, I don't want to do anything that would preclude creating stand-alone executables that use my interfaces. I want the best of both worlds.\ \ \i Goal 2: Looks native to platform \i0 \ \ I've been developing user interfaces of one sort or another for about four decades. One thing that I've observed over that time is that users of systems get used to a particular look and feel on whatever platform they have and won't readily tolerate an interface that departs too much from that. I personally find it annoying when an application behaves in a way that is different from others on the same platform. I don't want my own code to generate that same feeling of annoyance. I suppose this is changing somewhat as web/browser-based applications become more prevalent, but I still believe that for anything that runs natively, as my code will, adherence to standards is a must.\ \ \i Goal 3: Easy blending with Lisp code \i0 \ \ There are various ways that interfaces can be created on each platform. There are innumerable packages that exist to make this easy. For example X-windows, TCL/TK, Java Swing, and many more. None of these is particularly easy to use with Lisp although there certainly have been credible attempts to make them so (e.g. CLX and Garnet). One reason that none of these attempts has been widely adopted is that they often become obsolete rather quickly. That is because making those interface packages easily accessible to Lisp developers often entails a fair amount of bridge or translation code which becomes a burden for anyone to maintain. User interface packages tend to change rather rapidly and as a consequence the Lisp interface can quickly become out-of-date. The alternative is often to make a bridge that is NOT easy for Lisp developers and that's not much better.\ \ \i Goal 4: Development effort matches user interface complexity \i0 \ \ Developing user interfaces is generally not the main focus of my work. Sometimes I just need something quick and making format calls that print in the listener is just fine. But as the application gets more complex so do my needs for visualization, for changing control parameters, seeing output, etc. I want the effort that needs to be put into those interfaces to be as little as possible. I want intuitive easy-to-use tools to assist me in their development.\ \ \i Goal 5: Cross-platform/OS portability \i0 \ \ So basically I'm a Macintosh developer. This goal almost isn't on my radar, but I certainly recognize that for others it is a must. While I didn't go looking for a solution that made this a high priority I was pleased to find that there is some potential for cross-platform portability with the approach that I have taken although it is clearly not available today (January 2010). More on that later.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 The Search for a Solution \b0 \ \ I thought it might be useful to recount my thinking and processes for deciding how I wanted to build user interfaces and use them within Lisp. If you don't care how I got to my approach, you can skip to the next section.\ \ My platform of choice is the Apple Macintosh. So I started my search by trying to understand how user interfaces are constructed in that environment. That very quickly led me to Cocoa and I decided early on that any reasonable approach must use it. I learned much of what I know about Cocoa by working my way through \ "Cocoa Programming for MAC\super \'ae \nosupersub OS X", Third Edition, by Aaron Hillegass \ and I strongly urge you to buy the book and do the same. This book uses Apple's Xcode and Interface Builder (IB) tools to create progressively more complex user interfaces using Objective-C. Each of his projects introduces one or more new Cocoa and/or Objective-C concepts. What I've tried to do here is to adopt a similar approach, but from a Lisp perspective.\ \ Although I hope my examples and this accompanying documentation will give people a head-start, there just isn't any substitute for understanding how Cocoa works and there are many resources for doing so. I expect that many or most of the people reading this may not have any knowledge of Objective-C and will, therefore, shy away from this approach. All I can say to persuade you is that as much as I like Lisp (at last count I've developed programs in 29 different languages and Lisp is still my favorite) Objective-C is a pretty nice language that you should be able to pick up reasonably easy if you have any C background whatsoever. I assure you that it is nothing like C++ if that helps any.\ \ As I worked through the Hillegass book I became more and more fond of the way that Apple's Interface Builder helped with the design of user interfaces. I didn't need to worry about screen layout or any number of different things that I've always had to consider previously while creating user interfaces. It was, all things considered, pretty easy.\ \ So having zeroed in on Cocoa and having become somewhat familiar with how things worked, I went back to CCL's release to see what they had done to make this easy to use. I first found the easygui code in the CCL release: ... /ccl/examples/cocoa/easygui. In the previous version of Lisp that I used for Macintosh development (MCL), there was a rather nice assortment of user interface objects that you could put together for your own use and it seems to me that the easygui interfaces are an attempt to do something similar for CCL. But as I started to use it, I became convinced that it isn't the best approach either for a couple of reasons. \ \ In some sense the easygui approach represented a step backwards from the easy-to-use drag-and-drop Xcode/IB environment that I had been using. For anything more than a fairly simple window I once again had to think about all the myriad placement and relationship factors that were so easy to do using IB. I didn't really want to go back to a world where I had to \i compute \i0 every aspect of the user interface rather than just dragging and dropping and clicking check-boxes to define behavior. The nature of user interfaces has become enormously more capable and commensurately complex over time. That means that to use Cocoa as intended to develop full-featured interfaces would require mastering an enormous number of classes, methods, and interactions between them. \ \ The second problem I had with the easygui approach is that I found myself spending time trying to figure out whether and how easygui had implemented one or another of the classes or features that are described in the massive amount of Cocoa documentation that Apple provides. That mapping challenge seemed like it would be an ongoing problem for anyone who aspired to make more complex user interfaces. When you couple that with the fact that Cocoa is a constantly changing target, it just seemed to me that Easygui would require a very substantial amount of ongoing maintenance and documentation. \ \ So I went back to Xcode thinking that perhaps I could integrate CCL into that environment. I made some progress in this direction, but a little voice kept nagging at me that I wasn't going to be able to meet my #1 goal with this approach. Xcode is entirely built around the idea of the code/build/execute/debug loop and that just wasn't what I wanted. So after a short time looking at this approach I abandoned it as well.\ \ One day it occurred to me that I didn't need to use Xcode in order to get the advantages of IB. I don't claim this as an entirely new thought, I was just a bit slow in coming to the realization that this was not only possible, but could be done in a way that satisfied all of my goals. I quickly found that I could design my interfaces within IB and save them to NIB files that could be loaded into Lisp rather easily. But what about the ease of integration of this approach with Lisp? What would the Lisp code have to look like as I implemented more complex interfaces?\ \ It is at this point that I have to stand up and applaud Randall Beer, who originally created the Objective-C bridge code in the CCL release, as well as all those who have made it what it is today. This is simply a stupendous achievement that deserves all the recognition anyone can give it. I suspect that it wouldn't have been all that difficult to just use FFI to access the libraries, but that alone would not have made it easy to integrate into Lisp. The real achievements were:\ 1. The ability to create Lisp classes that inherit from Objective-C classes\ 2. Creating slots in these classes that are directly accessible to Objective-C code\ 3. Permitting the creation of Lisp methods that are callable from Objective-C\ 4. Making it easy to call Objective-C methods using Lisp syntax\ 5. Permitting Lisp to create, store, and access Objective-C objects just as you would Lisp objects\ 6. Making it easy to access Objective-C runtime constants and variables\ 7. Being able to instantiate Lisp classes that inherit from Objective-C classes from Objective-C\ 8. Automatic name translation between Objective-C and Lisp\ 9. Being able to call "make-instance" for Objective-C classes\ 10. Doing many type translations automatically\ and I'm sure there are other features that I'm missing. \ \ Initially I would build one of the Hillegas projects using Xcode and Objective-C and then go back to Lisp and create something similar. After doing just a few of these it became apparent that I no longer had to do the Xcode version. I could read the book and go directly to Lisp to create a counterpart. I could modify or augment those behaviors by going directly to the relevant Apple documentation and using it to inform my Lisp development. I didn't have to worry about what the mapping was between Objective-C and Lisp because for the most part it is a 1-1 mapping that uses relatively simple name translation rules.\ \ So let's review what we have against my goals:\ \ 1. I can clearly define interfaces that I load into a standard CCL IDE and modify in a REPL environment. I routinely redefine methods while a window is up and immediately see the modified behavior. To change the look of an interface I switch to IB, modify and save, go back to Lisp and create a new instance of the window to see the changes. That's faster than I could figure out how to modify code to generate a new interface as we did with MCL. At this point in time (January 2010) I have not yet made a stand-alone program, but it is clear to me that nothing I have done precludes this possibility. If you want to do this I suggest that you download Mikel Evins' APIS code and start with that: \ http://explorersguild.com/mikelevins/Apis_1_0.zip\ Integrating my examples into a stand-alone program using Mikel's framework would mostly consist of creating a main menu nib I think, but until I do everything I can't say what else might be required. I expect to create a project or two to do this before I'm done with this effort.\ \ 2. Clearly Cocoa is the way to get a native look and feel on Macintosh systems and I can now access all of Cocoa's capabilities without wondering how some intermediate layer might have implemented those capabilities.\ \ 3. The CCL-supplied Objective-C bridge code makes it a pleasure to integrate Lisp with the Objective-C runtime environment. Again, I can't say enough about how well this was done. We have a true Lisp-friendly \i interface \i0 to Objective-C rather than a \i new layer on top of it \i0 . So as the underlying Cocoa libraries and documentation change over time, we can make immediate use of them without waiting for some maintainer to figure out how to incorporate those changes into a Lisp layer.\ \ 4. I find that developing user interfaces can be done very quickly. The easy part is the initial layout using IB and specification of Lisp classes to interface to it. Depending on the complexity of what you are trying to do this is minutes to hours in duration. Typically I require a longer amount of time to debug the behavior that I want, but this is where the Lisp REPL environment proves its worth. Rather than finding a bug and rebuilding as would be required in an Xcode world, I can very quickly examine objects, modify methods, and generally do the sorts of things that developers always do in Lisp. \ \ 5. As I said initially, portability was not my main criteria, but as I browsed though some of the CCL Objective-C code I found references to "Cocotron", so I did a bit of research to see what that was all about. It turns out that this is an open-source project that aims to provide an Objective-C compiler and runtime environment for a multitude of platforms and OS types. It also has implementations of many of the same Cocoa classes and methods provided for the Macintosh, but as Gary Byers said in an email (12/3/09) to openmcl-devel@clozure.com: \ "Cocotron does a fairly good job of implementing a large subset of Cocoa and it's under active development, but it's not too hard to run into things that're implemented in Cocoa but not (yet) present in Cocotron or to run into things that're implemented differently (and in some cases incorrectly.)"\ That same email provides instruction about how to load Cocotron and try it with CCL if you're really brave. So I think there is some promise of future portability, but as of January 2010 it isn't there yet. For more information about Cocotron see: \ http://www.cocotron.org/Info.\ \ \b Background Reading\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b0 \cf0 CCl provides very nice documentation with their distribution that describes (among other things) their Objective-C bridge code. Assuming you have CCL installed in your applications folder as I do, you can find it at: \ file:///Applications/ccl/doc/ccl-documentation.html\ \ In addition to reading the Hillegas book, if you are not already familiar with Apple's Interface Builder, it would be a good idea to read:\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \fs22 \cf0 http://developer.apple.com/documentation/DeveloperTools/Conceptual/IB_UserGuide/IB_UserGuide.pdf \fs24 \ although if you work through the examples that I have here you should be able to pick up what's going on fairly quickly. You will have to download Apple's developer tools to get IB. I'm not going to describe how to do that here. Apple's website can tell you how to do that and in all likelihood if you're building your own CCL environment you've already done that anyway.\ \ Apple's developer website has an enormous wealth of information. I find it useful to get the latest documentation and have it on my own system. You can do this by running Xcode and using the help facility to subscribe and download the documentation sets. Although this works I would recommend that you get the AppKiDo application. AppKiDo is a reference tool for Cocoa programmers. The latest release, with source code, can be downloaded from \ http://homepage.mac.com/aglee/downloads\ AppKiDo is free. It creates a very easy way to access the reference documentation that is included with the Apple's developer tools (you will have to run Xcode at least once and use the help facility to subscribe to and force a download of the documents). I have found this to be an extremely easy way to find out more about specific classes or methods or constants that might otherwise be difficult to locate within Apple's documentation.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Setting up your CCL environment \b0\fs24 \ \ I'm going to presume that you have downloaded the CCL distribution and built the Cocoa-based CCL IDE. To make life a bit easier you may want to set up your environment to make it simple to tell Lisp where to find files. There are some simple things you can do in a ccl-init.Lisp file that you put into your home directory that will make it easier to use my examples. First you can create a logical host directory that points either to my InterfaceProjects directory or to an equivalent directory of yours where you will build up your own versions of my examples. Second, you can tell CCL to search within that directory when we "require" something. Mine looks like the following:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 ;; ccl-init.Lisp\ ;;\ ;; loaded by ccl at startup\ \ ;; Set up the logical host named "ip" so I can easily use it in pathnames\ ;; This is basically my Interface Projects" directory. Within this directory I\ ;; create various project directories and within them I put Lisp code that I might want\ ;; to "require" in Lisp.\ \ (setf (logical-pathname-translations "ip")\ '(("**;*.*" "ccl:contrib;krueger;InterfaceProjects;**;*.*")))\ \ ;; extend the module search path so that we can just "require" a module and ccl will find it\ ;; among our various projects. "InterfaceProjects" (now logical host "ip:") is a \ ;; special sub-directory within which I put all my cocoa interface examples, so I put that \ ;; in the module search path specifically.\ \ (setf *module-search-path* (nconc *module-search-path*\ (list (pathname "ip:*;")))) \f0 \ \ So once that's all done you can either start with my code and read about what I did, try to develop your own from scratch, or modify mine.\ \ If you just want to see what all the fully functional project windows will look like before beginning you can, at this point, start CCL and have a dialog like the following in the listener:\ \ \f1 Welcome to Clozure Common Lisp Version 1.5-dev-r13174M-trunk (DarwinX8664)!\ ? (require :simplesum)\ :SIMPLESUM\ ("NIB" "SIMPLESUM")\ ? (ss:test-sum)\ # (#x127B8170)>\ ? (require :speech-controller)\ :SPEECH-CONTROLLER\ ("SPEECH-CONTROLLER")\ ? (spc:test-speech)\ # (#x127C44C0)>\ ? (require :package-view)\ :PACKAGE-VIEW\ ("PACKAGE-VIEW")\ ? (pv:test-package)\ # (#x13F68BC0)>\ ? (require :loan-calc)\ :LOAN-CALC\ ("DATE" "LOAN-CALC")\ ? (lnc:test-loan)\ # (#x13F9FA90)>\ ? (require :loan-document)\ :LOAN-DOCUMENT\ ("MENU-UTILS" "LISP-DOC-CONTROLLER" "NIB" "DATE" "DECIMAL" "LOAN-WINDOW-CONTROLLER" "NS-STRING-UTILS" "NSLOG-UTILS" "LOAN-PRINT-VIEW" "LOAN-CALC" "LOAN-DOCUMENT")\ ? \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ The loan-document project has no test function per se. It adds a few menu items that you can use to test it. See a much longer discussion for Project 7. Feel free to play around with them; it may help you understand what is happening when we get to the development details.\ \ Each of these projects is defined within its own package and a separate package is also defined for various utility functions that are common to several projects. So it is safe to load any of these into your environment without fear of name conflicts with anything you have done.\ \ From this point on I will assume that you have an "ip" logical host defined that resolves to some directory with subdirectories for each of the projects we'll discuss. If you do something different you'll have to modify my instructions accordingly. In all cases below where I describe how to build something using IB, you can just open up the corresponding nib file from my example directory if you would prefer.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 Warnings \b0 :\ \ There are a few things that a lisp user might expect to do which are a bit different when you have to interact with the Objective-C runtime. At least one person tripped across one of these while following along in version 1.0 of this tutorial, so it seemed prudent to provide some warnings before you get started.\ \ The first warning is that we will be defining a number of Objective-C classes as we go on. In Lisp you can define a class, modify the source for it in some way, and then redefine it with the new description. Common Lisp explicitly handles this in very nice ways. Objective-C does essentially nothing here. You just can't define an Objective-C class, modify it, and then redefine it at runtime. Sorry, but if you need to do this you'll need to restart Lisp and reevaluate the source to get the new definition. Bummer. On the bright side, you CAN dynamically redefine Objective-C method definitions at runtime and get the effect that you expect (mostly). You may find that the fact that a class has a method defined has been squirreled away somewhere, so if you define a new method you may need to create a new instance of that class to get it to find your new method. If this is confusing, don't worry about it right now. All will become clear later.\ \ Although all files load fairly quickly, you may decide to compile them. If you do, you will note that the compiler will generate a warning for any Objective-C method that we define that is not a standard one from the library. Example:\ \ ;Compiler warnings for "ip:Simple Sum;simplesum.lisp.newest" :\ ; In SIMPLESUM::|-[SumWindowOwner doFirstThing]|: Undefined function NEXTSTEP-FUNCTIONS:|doSum:|\ \ This in fact occurs in a method that is in the process of defining that doFirstThing: method, but the compiler chooses to warn us that it is not a preexisting function name from the Objective-C library.\ \ Let's get started!\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 1: Hello World \b0\fs24 \ \ Key Concepts: Interface Builder, NIB and XIB files, NIB loading, NIB file owner\ \ We must begin by talking generally about what IB does. Basically it is a graphical design tool that lets you put together standard Apple interface objects and to specify how those objects will interact with other classes that a program defines. It packages up that design into a file that can be loaded at runtime by an application. When the file is loaded, instances of all of the objects it describes will be created and linked together to recreate the interface that you designed. The objects that it creates can include instances of classes that you defined in your application. \ \ Start by executing IB. You will see three windows:\ 1. An inspector window that will initially be titled "Attributes".\ 2. A Library window that allows you to select interface elements and drag them onto your design.\ 3. A window titled "Choose a template"\ \ The third window presents you with a dialog that asks you what sort of interface you want to build. Select "Cocoa" in the leftmost column and "Window" from the options provided in the rightmost column. When you select this the dialog will disappear and two additional windows will come up:\ 4. An untitled \i nib document window \i0 that represents all of the objects that will be contained in the NIB file that you \ will load into Lisp.\ 5. A window titled "Window" that is the main window object that you will design and depicts will be displayed in \ Lisp.\ \ If you click on window #5, the inspector window (#1) will now show the attributes of your window object. Click in the "title" box in the inspector window and change the title to something you like, say "Hi there!". You will see that change immediately reflected in window #5 which now has your title. Now in the Library window (#2) click on the "Inputs & Values" folder. Click and drag a "Label" object from the library window to your "Hi there!" window. Double-click it and type "Hello World!". That's it we're done with this interface. Now we just have to save it and load it into Lisp.\ \ There is the normal sort of "Save as ..." dialog that you might expect, but we need to talk about the difference between XIB files and NIB files. XIB files are XML files which IB uses to represent this interface. XIB files are nice because you can look at them and see what's there pretty directly and even modify them if you're brave enough. But applications can't directly load XIB files. Instead, they must load a compiled form of them called NIB files. For our purposes we never need XIB files, so I typically save as a NIB file initially and forget about it. In the Save dialog you will see a "File Type" choice. I typically use "NIB 3.x" because I don't have to worry about supporting an older version of OSX. Save it to someplace that you can easily access it or use the one that I provided in my example directory. I suggest that you un-check the box that hides the file extension. That way you won't be confused about what sort of file it is (NIB or XIB) and will know the right name to provide to Lisp.\ \ Navigate to the "Hello World" subdirectory of your InterfaceProjects directory and save the file as "hello.nib". Once you have saved the NIB, the name of the nib document window will change to reflect the saved name.\ \ Start the CCL IDE and follow along with the listener dialog shown below:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 Welcome to Clozure Common Lisp Version 1.5-dev-r13174M-trunk (DarwinX8664)!\ ? (require :nib)\ :NIB\ ("NIB")\ ? (iu:load-nibfile (truename "ip:Hello World;hello.nib"))\ (# (#x1CEC80)> # (#x14220420)>)\ T\ ? \f0 \ \ Your "Hello World" window will pop up. Congratulations! you just built your first Lisp Cocoa user interface.\ \ When the NIB file that we created is loaded, all of the user interface objects that we defined using IB are created. In this case the window and the label within it are created and linked together properly so that they display in exactly the same way that we defined. Also note that you can resize the window, that it will respond to the close menu and otherwise acts just as you would expect of any window. Also note that when this window is selected that any menu items that are NOT relevant are automatically grey-ed out. This is just what you would want without ever having to specify anywhere what are and are not relevant menu items for this window.\ \ Let's look under the hood at the load-nibfile function since that is all that we used for this project. This is my modified version of some example code that CCL provides.\ \ \f1 ;; nib.Lisp\ ;; Start with some corrected functions from .../ccl/examples/cocoa/nib-loading/HOWTO.html\ \ (defpackage :interface-utilities\ (:nicknames :iu)\ (:export load-nibfile))\ \ (in-package :iu)\ \ ;; Note that callers of this function are responsible for retaining top-level objects if\ ;; they're going to be around for a while and them releasing them when no longer needed.\ \ (defun load-nibfile (nib-path &key (nib-owner #&NSApp) (retain-top-objs nil))\ (let* ((app-zone (#/zone nib-owner))\ (nib-name (ccl::%make-nsstring (namestring nib-path)))\ (objects-array (#/arrayWithCapacity: ns:ns-mutable-array 16))\ (toplevel-objects (list))\ (dict (#/dictionaryWithObjectsAndKeys: ns:ns-mutable-dictionary\ nib-owner #&NSNibOwner\ objects-array #&NSNibTopLevelObjects\ (%null-ptr)))\ (result (#/loadNibFile:externalNameTable:withZone: ns:ns-bundle\ nib-name\ dict\ app-zone)))\ (when result\ (dotimes (i (#/count objects-array))\ (setf toplevel-objects \ (cons (#/objectAtIndex: objects-array i)\ toplevel-objects)))\ (when retain-top-objs\ (dolist (obj toplevel-objects)\ (#/retain obj))))\ (#/release nib-name)\ ;; Note that both dict and objects-array are temporary (i.e. autoreleased objects)\ ;; so don't need to be released by us\ (values toplevel-objects result)))\ \ (provide :NIB) \f0 \ \ You can find a good description of the function that this was based upon in CCL's documentation: \ "file:///Applications/ccl/examples/cocoa/nib-loading/HOWTO.html"\ so I won't reproduce that detail here, but only talk about a few differences. I have provided a couple of &key parameters to this function that were not in CCL's example code. The "nib-owner" key permits you to specify what object will "own" the nib objects that are loaded. You'll learn much more about that in the next project, but for now we just used the default owner which is the NSApplication object that was created for the Lisp IDE. Cocoa applications will have just one such object and the Objective-C variable denoted by #&NSApp contains a reference to it. The "retain-top-objs" key tells the function whether to "retain" the objects. You'll learn more about that in the next project as well. Since we don't care about keeping track of the objects created when the NIB was loaded both of the defaults are perfectly reasonable choices.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 2: simplesum \b0\fs24 \ \ Key Concepts: Lisp defined File's Owner class, Outlets, TextFields, Buttons, Actions and Targets, foreign slots, Lisp action functions\ \ In this project we'll create a fairly simple interface that lets a user enter two numbers and click a button to add them and display the result in the window. You may already know something (and perhaps be confused about) Apple objects like documents and window controllers. For this project we won't be using any of those. We'll just create the interface graphically and create a single simple Lisp class that owns it. Even with this simple example there are lots of little things to learn about how this all works.\ \ Let's begin as we did for the first project by opening up IB and selecting "New..." from the File menu. Select the Cocoa Window template. To be consistent with the names that I have in my example code, name the new file "sumLisp" in the "Simple Sum" project directory within the "InterfaceProjects" directory. Remember to save it as a "NIB 3.x" file rather than as a XIB file (which is the default).\ \ Recall that when a file is loaded at runtime the application must specify an object that "owns" it. Within IB you will see a \i proxy \i0 object called "File's Owner". This object is NOT created by the NIB loading function as other objects are. Instead, the object specified as the owner when the file is loaded is used in its place. What this lets us do is create relationships between interface objects and the proxy File's Owner object within IB that are recreated at NIB loading time between our real File's Owner object and the newly instantiated interface objects. There are various sorts of relationships that can be created and we'll be exploring more of them as we do different projects. One of the simplest sorts of relationships is a simple reference link so that, for example, we can initialize a slot in our File's Owner instance that contains a reference to a particular text box in the interface.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 Note: I've discovered that if you are using Interface Builder version 3.2 or later, then the process for adding an object that will be defined in Lisp is somewhat different than it is documented below. As currently shown in this tutorial, the process is to put a new generic object in the document window and then change its class name in the identity inspector before adding outlets and actions via the inspector window. Starting in IB version 3.2 that functionality has been moved to the Library window. There you should select the "Classes" tab and select the class you want to subclass. Then from the action menu select "New Subclass" and enter the name of the Lisp subclass. Choose not to generate source code. Then you can use controls in the library window to add outlets and actions as discussed in the rest of this tutorial. \b0 \ \ For this project we will be using a Lisp class that inherits from an Objective-C class as the File's Owner. We'll see later how to define such a class in Lisp. It is easy to tell IB what the class of the "File's Owner " object will be when the NIB is loaded. Click on the "File's Owner" object and then in the inspector window click on the Identity icon (looks like a circle with a small "i" in it) so that information about the object's identity is shown. I called this class "SumWindowOwn" and if you want to follow along you should type in the same name. See Figure 2.1 Below:\ \ {{\NeXTGraphic Pasted Graphic.tiff \width5740 \height13360 }¬}\ Figure 2.1 SumWindowOwn identity information\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Warning: if you later decide that you want to change the name of your owner class (or any Lisp class used in IB), you will lose most of the information that you entered in the inspector window because it now thinks that you just changed the class of the File's Owner. So pick a class name and don't change it. \i0 \ \ I suppose this is a good point to talk about naming in Objective-C and the translation of those names to Lisp. Objective-C uses a set of conventions to automatically generate names for various purposes. You will learn those conventions as we go along. Since Objective-C names are case-sensitive and Lisp names are not, some sort of translation is required in order to integrate the two environments. The CCL Objective-C bridge does those translations automatically for you whenever needed, but you need to understand how they will be done so that you can specify names appropriately.\ \ Our first example is the class name that we specified: "SumWindowOwn" (note the capitalization). Lisp will translate that to the case insensitive Lisp name "sum-window-own". You can read the CCL documentation for more detailed information about the translation rules, but gist of it is that except at the beginning of a name, wherever there is a capital letter the Lisp version will insert a hyphen.\ \ Now let's design the interface. As we did before, select your window and give it a title in the inspector window. From the library window select and drag three "Text Fields" and one Button of your choice to your window and arrange them any way that pleases you. Double-click on your button to rename it to "Sum" If you like, select a text field and use the inspector to explore various options you can select for its appearance. When you get done it might look something like the Figure 2.2 below:\ \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural \cf0 {{\NeXTGraphic Pasted Graphic 1.tiff \width9600 \height5840 }¬}\ Figure 2.2 Our Sum application window\ \ Note that my third text field is barely visible (only visible at all because I clicked on it) because I used the inspector to make it's border invisible and deselected the "Draws Background" box. I also deselected the "Selectable" and "Editable" boxes so that the user cannot click within it to do anything. When you change an attribute of a view object, IB tries to make it look exactly the way that it will at runtime so that you can easily visualize it. That's true even if it makes your object invisible. Sometimes that can make it hard to find when you need it, so you may want to wait until the design is mostly complete before setting attributes that make something invisible.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic.tiff \width5740 \height12320 }¬}\ Figure 2.3 Setting attributes for the output text field\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \cf0 \ Next we'll set up a couple of different relationships between the File's Owner object to these fields. The first type of relationship is the reference link that I mentioned above. Apple has a special name for slots that contain such references; they are called "Outlets". So the idea is that we will specify to IB what outlets the File's Owner class will have and then link those to the objects that we want linked at runtime. Refer back to Figure 2.1. \ \ In the Class Outlets part of the window click the "+" button to add a new outlet. Edit the name to be "input1" and the type to be NSTextField. Note that IB will help with the latter by giving you selectable options after you've typed the first few letters. Do the same for the input2 and sum outlets. Remember that later we will make slots in our Lisp sum-window-own class that correspond to these names. Our objective is that at runtime our input1 slot will contain a reference to the first text field, our input2 slot will contain a reference to the second, and our sum slot will contain a reference to the third.\ \ So far we have just created the outlets, now we need to cause them to be linked to their corresponding text fields. You do that by control-clicking on the "File's Owner" object in the nib document window (Figure 2.4) and dragging to the first edit field in the application window (Figure 2.2). When you release the button IB will let you select which of the File's Owner outlets should be linked to this text box. Select "input1". Now control-click and drag to link the other two text boxes to their respective outlets.\ \ {{\NeXTGraphic Pasted Graphic 3.tiff \width7640 \height8020 }¬}\ Figure 2.4 Project window for the sumLisp NIB\ \ We need to make one more sort of link and that is so that when the button is clicked something actually happens. In later projects we'll see other ways to accomplish this, but for now it's good to understand just what happens when a button gets pushed. It's basically pretty simple; the button sends an action message that we define to a target that we define. Refer once again to Figure 2.1. The "Action" sub-window is where we tell IB what action messages our class will respond to. Click the "+" button and add a new action. Edit the "MyAction1:" name that is generated and change it to "doSum:". Pay attention to the capitalization and to the use of the ":" at the end of the name. The capitalization is only important insomuch as we need to duplicate it when we define a corresponding method in Lisp. The ":" is required. This is a rule in Objective-C. I don't want to discuss the way that Objective-C functions are named here, but it is important that you understand it before going much further. This discussion can be found in the CCL documentation or any number of books about Cocoa. You can just follow along blindly and you may pick up on what's going on, but feel free to stop and read up on it before proceeding.\ \ Now that we have told IB about our action we need to tell the button what action message to send and where to send it. This is fairly simple. Control-click on the button and drag to our File Owner's object in the nib document window. A dialog will pop up that shows the possible received actions that we might want the button to send. We have only one, namely "doSum:", so select it. This has set the button's target to our File's Owner object and indicated that it should send a "doSum:" message when the button is pushed. When the action message is sent, the button will also pass a pointer to itself as an argument. In this case we don't need that, but in future projects we'll see how this can be helpful.\ \ That's pretty much it on the IB side of things. Save your file and let's go to Lisp. In general I suggest that you just open my source files and follow along with the discussion (simplesum.Lisp in this case), but feel free to type it all in yourself. I find that I sometimes learn things more thoroughly when I do them myself. If you do that, just make sure that you include things that may be in my source (such as "require" statements) that I may not talk specifically about.\ \ At this point, if you have not already done so, I urge you to read section 13 of the CCL documentation:\ file:///Applications/ccl/doc/ccl-documentation.html#The-Objective-C-Bridge\ Actually the whole document is a pretty good read and contains lots of goodies that you should know about.\ \ Let's start with the class definition for the object that will be our File's Owner when we load the NIB file:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass sum-window-owner (ns:ns-object)\ ((input1 :foreign-type :id\ :accessor input1)\ (input2 :foreign-type :id\ :accessor input2)\ (sum :foreign-type :id\ :accessor sum)\ (nib-objects :accessor nib-objects :initform nil))\ (:metaclass ns:+ns-object)) \f0 \ \ You'll see that the class name is the Lisp version of the name that we specified in the IB inspector window for the File's Owner classname (SumWindowOwner). This class inherits from the root class for all Objective-C objects; namely NSObject. Again, we have used the Lisp version of that name. The :metaclass specification is per CCL's recommendation and you can read their documentation for a discussion of it. \ \ We have four slots and the first three correspond to the outlets that we specified to IB. We have declared them to be a :foreign-type and take a value of :id. This makes them accessible to Objective-C just as if they were a slot defined for some native Objective-C class. The ":id" part of the specification just says that the slot will contain a generic pointer to an Objective-C object of some sort (which you should already know if you really read the CCL documentation!) Note that CCL is handling the name translation for you. We specified the Outlet names in lower case to IB and Lisp effectively upper-cases all names, so even though it may not be readily apparent, there is a name mapping going on here. When the NIB file is loaded using an Objective-C function it will look for a slot in File's Owner named "input1" and will find ours and set the value. And we can access that slot from Lisp in the normal way. Nice!\ \ The fourth slot, nib-objects, is there to contain the top-level objects that are instantiated by the NIB loading function. We'll see how we use that in just a bit. Next let's look at some of the things we do during initialization of this class:\ \ \f1 (defmethod initialize-instance :after ((self sum-window-owner) \ &key &allow-other-keys)\ (setf (nib-objects self)\ (load-nibfile \ (truename "ip:Simple Sum;sumLisp.nib") \ :nib-owner self\ :retain-top-objs t))\ ;; we do the following so that ccl:terminate will be called before we are garbage \ ;; collected and we can release the top-level objects from the NIB that we retained\ ;; when loaded\ (ccl:terminate-when-unreachable self)) \f0 \ \ As in the Hello World project we call the load-nibfile function, but we do a few things differently. First we set the nib-owner to the object we are creating so that it becomes the "File's Owner" object that is linked to the interface objects, just as we specified to IB. We save the list of objects that it returns in our nib-objects slot. As the File's Owner this object is responsible for retaining these top-level objects so that they don't get deallocated at runtime and also releasing them when we're done with them. We told the load-nibfile function to retain them for us by setting the key parameter :retain-to-objs to t. To assure that they are released properly we must ensure that before this object is garbage collected a method is called to do the release. We tell CCL to do that with the call to ccl:terminate-when-unreachable. The function that is called is:\ \ \f1 (defmethod ccl:terminate ((self sum-window-owner))\ (dolist (top-obj (nib-objects self))\ (unless (eql top-obj (%null-ptr))\ (#/release top-obj)))) \f0 \ \ and this simply calls the Objective-C method #/release on each of the objects that we had previously retained. If you don't do this everything may appear to work properly, but at the least the CCL IDE will terminate abnormally when you quit it. That's usually a sign that there are memory management problems (typically a memory leak) that could bite you, so it's best to do everything properly. In future projects we typically won't discuss this function, but be assured that it is there.\ \ The last method we need to define is the action method that we told the button to call when it was pushed. You'll recall that in IB we set that to "doSum:". It is defined as follows:\ \ \f1 (objc:defmethod (#/doSum: :void) \ ((self sum-window-owner) (s :id))\ (declare (ignore s))\ (with-slots (input1 input2 sum) self\ (#/setIntValue: sum (+ (#/intValue input1) (#/intValue input2))))) \f0 \ \ There are a few things to note about this definition. First, there are two different macros that can be used to define Objective-C methods. There are also two ways to invoke Objective-C functions and I will use the one shown here. If I had to characterize the differences I suppose I would say that one form is more consistent with the way that Objective-C programmers would call the functions and one seems somewhat more like natural Lisp syntax. It is not difficult to translate and after trying both I am simply more comfortable with the more Lisp-like syntax and will use that consistently throughout this tutorial. If you feel more comfortable with the other syntax you should be able to translate what I provide quite easily.\ \ Note that the function defined here is named #/doSum:. The #/ reader macro honors the case of the name that follows and does whatever is necessary to register the name with the Objective-C runtime system. In this way I can be sure of defining functions that have exactly the name that I want. Note the ":" at the end of the name. This is part of every Objective-C function that passes one or more parameters to the target (in addition to the target argument itself). No return value is required so the return value is specified as :void. The method is defined for the sum-window-owner so that is the first argument and a pointer to the sending object is also passed as an argument that we called "s" and defined to be a generic Objective-C pointer (which we don't need and therefore ignore).\ \ The function itself simply sets the value for the interface object that is pointed to by the pointer in our sum slot. How did I know to call "#/setIntValue:"? That is easily found by looking at the instance methods available to NSTextField objects. If you haven't done so yet, this is a good time to start the AppKiDo application and search for NSTextField. Click on it and then click on the "ALL Instance Methods" line to see every possible thing that you could do. Impressive, isn't it?\ \ An astute programmer might be wondering what happens when there isn't anything in those input fields. What will the #/intValue calls return? Initially I put all sorts of additional code here to initialize the fields to 0 if they were blank. But after experimenting a bit I discovered that what you get in that case is 0 so it was all unnecessary. Cocoa does lots of things that make life easy and this is just one small one.\ \ If you are following along in simplesum.lisp you will see the function #/doFirstThing next. Ignore this function for now. It is actually used as the target of a menu-item in project #3 and will be explained when we get to that point.\ \ Finally I typically create a test function for classes that I define and for this one it's pretty simple:\ \ \f1 (defun test-sum ()\ (make-instance 'sum-window-owner)) \f0 \ \ If you execute this in the REPL by typing (ss:test-sum) your interface will pop up and you can add numbers to your heart's content. Project 2 is complete.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 3: menus \b0\fs24 \ \ Key Concepts: First Responder and responder chains, delegates, menu actions, menu creation\ \ It's actually fairly straight-forward to add a menu object to your IB definition and load it into the CCL IDE. The problem is that it will \i replace \i0 the existing menu which is not exactly what we're trying to accomplish. Nevertheless, there are times when we would like to add our own menu to the menubar or modify menus that already exist. That's what we'll explore in this project.\ \ We've already seen that our new windows will automatically respond to existing menu choices whenever they are supported. How exactly does that work? You may have noticed a red box object in your nib document window called "First Responder". This is another proxy object somewhat like File's Owner. But in this case the actual first responder object is dynamically determined while your application is running. All windows and view objects within them are organized as a hierarchical set of containers. Every time the application user clicks on the screen somewhere the lowest-level user interface element in that location is given the opportunity to become the first responder. If it chooses to decline the responsibility then it's superview is given the opportunity and so on up the line until some object accepts. This chain of responsibility called the Responder Chain is dynamically redefined every time a click is made. Why do we care about this? There are many reasons, but in this case it is because the action messages sent by menu items are often targeted at the First Responder proxy. At runtime the actual messages are sent to whichever object is currently the First Responder. If it has a method defined for that action, it performs it. If not, the message is passed up the responder chain until some object does respond to it. \ \ Responder chains also can include \i delegate \i0 objects so we need to understand what these are. Many classes, such as NSWindow and others have a delegate outlet. When a delegate-owning object (that is an object with a delegate outlet that contains a pointer to some real object) is passed an action message it will first check to see whether a method corresponding to that message has been implemented by the delegate. If so, it passes the message to it. (i.e. it delegates responsibility for the response). If no such method exists either the object will respond itself or pass the message along to the next object in the responder chain. \ \ You may notice that menu-items which are not relevant for our windows are disabled when our window is active. That is because every menu-item checks to see whether ANY object in the current responder chain can respond to its action message. If none of them can, then it is disabled. \ \ What we're going to do here is dynamically add a menu with sub-menus to the menubar and then set one of our objects to respond to one of those menu items. Go back to IB and open the "sumLisp.nib" file that we used for project #2. Locate the Window object in the nib document window. Control-click on this object and drag to the "File's Owner" object just above it. When the pop-up appears select the "delegate" outlet. What we have now done is make the File's Owner a delegate for the window. If you now control-click on the window object you can see the link that has been made (Figure 3.1).\ \ {{\NeXTGraphic Pasted Graphic 4.tiff \width7180 \height3400 }¬}\ Figure 3.1 Window outlets showing that File's Owner is the delegate\ \ There is a bit of complexity to adding new menus and sub-menus, but thanks to Apple's sample code (Objective-C of course) it turned out to be not all that difficult. I suppose the key things to understand is that there are NSMenu objects and NSMenuItem objects. NSMenu's may only contain NSMenuItem's never subordinate NSMenu objects. NSMenuItem's may contain subordinate NSMenu objects. So in order to create a new menu, put it into the menubar (which is just the main menu), and have subordinate menu items we must create a new NSMenuItem that we make subordinate to the main NSMenu object, then make a new NSMenu object subordinate to the new NSMenuitem we created, and finally add a series of subordinate NSMenuItem's to our new NSMenu. It might be easier to see what the code is doing than it is to parse that last paragraph!\ \ I created a make-and-install-menu function to make this all a bit simpler:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defun make-and-install-menu (menu-name &rest menu-item-specs)\ (let* ((ns-menu-name (ccl::%make-nsstring menu-name))\ (menuitem (#/initWithTitle:action:keyEquivalent: \ (#/allocWithZone: ns:ns-menu-item \ (#/menuZone ns:ns-menu))\ ns-menu-name\ (%null-ptr)\ #@""))\ (menu (#/initWithTitle: (#/allocWithZone: \ ns:ns-menu (#/menuZone ns:ns-menu))\ ns-menu-name))\ (main-menu (#/mainMenu #&NSApp)))\ (dolist (mi menu-item-specs)\ (destructuring-bind (mi-title mi-selector &optional (mi-key "") mi-target) mi\ (let* ((ns-title (ccl::%make-nsstring (string mi-title)))\ (action-selector (get-selector (string mi-selector)))\ (ns-key (ccl::%make-nsstring (string mi-key)))\ (men-item (#/addItemWithTitle:action:keyEquivalent: menu \ ns-title \ action-selector\ ns-key)))\ (when mi-target\ (#/setTarget: men-item mi-target))\ (#/release ns-title)\ (#/release ns-key))))\ ;; Link up the new menuitem and new menu\ (#/setSubmenu: menuitem menu)\ (#/release menu)\ ;; Now tell the main menu to make this a sub-menu\ (#/addItem: main-menu menuitem)\ (#/release ns-menu-name)\ (#/release menuitem)\ menu))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ A menu-item-spec is a list of the form:\ (menu-item-name menu-item-action menu-item-key-equivalent menu-item-target)\ where the first three are all strings and the last must be an object that will be the target of the menuitem's action message when it is selected. Normally the target is left nil and the message is sent up the first responder chain, but for some menuitems that we will create later we want it to be sent to a specific target that we will specify.\ \ I also created the function make-and-install-menuitems-after to install new menuitems at a specific\ location within an existing menu. This will be used in Project #7.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defun make-and-install-menuitems-after (menu-name menu-item-name &rest menu-item-specs)\ (let* ((ns-menu-name (ccl::%make-nsstring menu-name))\ (main-menu (#/mainMenu #&NSApp))\ (menuitem (or (#/itemWithTitle: main-menu ns-menu-name) \ (error "~s is not a valid menu title" menu-name)))\ (sub-menu (#/submenu menuitem))\ (ns-menu-item-name (ccl::%make-nsstring menu-item-name))\ (insert-index (#/indexOfItemWithTitle: sub-menu ns-menu-item-name)))\ (dolist (mi menu-item-specs)\ (destructuring-bind (mi-title mi-selector &optional (mi-key "") mi-target) mi\ (let* ((ns-title (ccl::%make-nsstring (string mi-title)))\ (action-selector (get-selector (string mi-selector)))\ (ns-key (ccl::%make-nsstring (string mi-key)))\ (men-item (#/insertItemWithTitle:action:keyEquivalent:atIndex: \ sub-menu \ ns-title \ action-selector\ ns-key\ (incf insert-index))))\ (when mi-target\ (#/setTarget: men-item mi-target))\ (#/release ns-title)\ (#/release ns-key))))\ (#/release ns-menu-item-name)\ (#/release ns-menu-name))) \f0 \ \ If you have all the paths to my code set up as previously described then from the listener you can type:\ \ \f1 ? (require :menu-utilities) \f0 \ \ and this will be loaded. To demonstrate how to call this function you can use the following test-menu function:\ \f1 \ (defun test-menu ()\ (make-and-install-menu "New App Menu" \ '("Menu Item1" "doFirstThing")\ '("Menu Item2" "doSecondThing"))) \f0 \ \ When you execute this in the listener by typing (iu:test-menu) a new menu titled "New App Menu" will be added to the menubar with two sub-items titled "Menu Item1" and "Menu Item2". If you click on the menu you will see that both menu items are disabled. That is because no object in the responder chain knows anyting about the action messages "doFirstThing" or "doSecondThing". \ \ Recall that we previously linked our the simplesum File's Owner object as the delegate for the Sum Window object. What that means is our Lisp File's Owner object is now in the responder chain for any click within our window. So let's go back to our Lisp code and add a method that will respond to the action method "doFirstThing" that we defined for the first menu item:\ \ \f1 (objc:defmethod (#/doFirstThing :void) \ ((self sum-window-owner) (s :id))\ (declare (ignore s))\ ;;; test function for menu tests\ (#/doSum: self (%null-ptr))) \f0 \ \ This simple function just calls the same action function that is invoked by clicking on the "Sum" button, namely it adds the two input values and displays the new sum. Once this function is defined you will see that if you first click in a Sum window and then click on the "New App Menu" that "Menu Item1" is now enabled and that if you select it the display will act just as if you had clicked on the Sum button. \ \ There is also a second test function:\ \ \f1 (defun test2-menu ()\ (make-and-install-menuitems-after "File" "New"\ '("New myDoc" "newMyDoc"))) \f0 \ \ which installs a "New myDoc" menuitem immediately after the "New" menuitem within the "File" menu. You can experiment with adding key equivalents and specific targets as well although you will also do some of that in a later project.\ \ These are obviously pretty simple examples, but they demonstrate what is possible. For the ambitious developer I would refer you to Apple's example xcode project called "MenuMadness". It pretty thoroughly shows almost every sort of menu option and possibility that anyone could ever want. I think that you'll find that the functions there are readily ported to Lisp.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 4: Speech \b0\fs24 \ \ Key Concepts: Speech controller, Radio Buttons, Memory management, runtime view modification\ \ At this point the use of IB should be somewhat more intuitive, so this fun little project should be a piece of cake. Basically it demonstrates how to configure and use radio buttons and gives a small taste of how views can be modified at runtime. \ \ Start a new window NIB in IB or open my nib file: ...InterfaceProjects/Speech/SpeechView.nib. Define an interface that has a NSTextView field where we will type in things to be said by the speech synthesizer. Add a couple of buttons to start and stop speaking and label them as you wish. Add a "Radio Group" that you will find in the "Inputs & Values" folder in the Library window. Also add a label above the radio group and change it to say "Voice" (or whatever you'd like it to say). Initially the radio group will have two buttons, but we need 24 to accommodate all of the possible voices of Apple's speech synthesizer (You can discover that quite easily for yourself. Go into CCl and in the listener type:\ \f1 ? (#/count (#/availableVoices ns:ns-speech-synthesizer))\ 24 \f0 \ If yours says something different you may need a different number of radio buttons and you should modify this project appropriately.\ \ So we want to change the Radio Group to have 24 buttons and to be arranged nicely. 24 is a nice number because we can create a nice four-column six-row rectangular array. To do that click on the button group to select it. You need to be a bit careful here because this object is actually an arrangement of nested views and it's easy to select something other than what you want. It took me a while to figure out how IB decides what you are trying to select, but it's really quite a simple rule. The first time you click IB will select the outermost container at that point. If you click again within that region it selects the next most deeply embedded view object. Click again and you will get something even more deeply embedded if it exists. If you accidentally get too deep, just click outside the object somewhere and then start over. \ \ In this case, we want to select the "Matrix" object that contains all of the radio buttons so click accordingly and make sure that the title in the inspector window reflects the fact that you have selected the matrix. In that window click on the "Attributes" icon (far left) and change the number of rows and columns to suit your needs. You can drag and resize the matrix in the design window to change its visual characteristics. If you click on the "Size" icon in the inspector window you can change many things including how the matrix reacts to dynamic window-resizing. Play with the options a bit to see how this works and pick something that suits your personal sense of esthetics.\ \ \ {{\NeXTGraphic Pasted Graphic 6.tiff \width5740 \height13360 }¬}\ Figure 4.1 Radio Button Matrix attributes\ \ When you get done you newly designed window might look something like mine (Figure 4.2):\ \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural \cf0 {{\NeXTGraphic Pasted Graphic 5.tiff \width9500 \height9020 }¬}\ Figure 4.2 Speech window designed in IB\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \cf0 \ What about the labels on the radio buttons? Well, we certainly could go ahead and put labels on them within IB, but that would require us to figure out how to map them to the real voices in our Lisp code. It's easier to set those titles from within Lisp in a way that makes it easy to do this.\ \ Next we need to specify, as we have done previously, what sort of object will be the File's Owner and what relationships it will have with other interface objects. Click on the File's Owner object in the nib document window and on the identity icon in the inspector window. Change the class name to "SpeechController" and add the actions and outlets as shown in Figure 4.3.\ \ {{\NeXTGraphic Pasted Graphic 7.tiff \width5740 \height13360 }¬}\ Figure 4.3 SpeechController Identity Inspector\ \ Next we'll want to link our File's Owner object appropriately. Control-click and drag from your "Start Speaking" button to the File's Owner object in the nib document window. Select the startSpeaking action from the window that pops up. Similarly link the "StopSpeaking" button. Finally do the same for the Radio Button Matrix and link it to the "buttonPushed" action. To make sure that the outlet slots are set up in our real File's Owner object when the NIB is loaded we need to link them here. So control-click and drag from the File's Owner object to the Radio Button Matrix. In the pop-up window select the buttonMatrix outlet. Similarly link from the File's Owner to the text box where the user will type what they want said to populate the "speechText" outlet. When you get done you can control-click on the File's Owner object to see the links or click on the "Connections" icon in the inspector window to see all the connections you have made. It should look something like Figure 4.4 below:\ \ {{\NeXTGraphic Pasted Graphic 9.tiff \width5740 \height5740 }¬}\ Figure 4.4 Inspector Connections Window\ \ That's all we need for this project, so save the NIB file and we'll work on the Lisp code in CCL. You can open up my example file:\ ...InterfaceProjects/Speech/speech-controller.Lisp\ or create your own if you like. As before I'm only going to discuss new concepts, so if you do start with your own you may want to compare with mine when you're done to make sure you haven't overlooked something. Let's start with the class definition:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass speech-controller (ns:ns-object)\ ((speech-text :foreign-type :id :accessor speech-text)\ (button-matrix :foreign-type :id :accessor button-matrix)\ (speech-synth :accessor speech-synth \ :initform (make-instance ns:ns-speech-synthesizer))\ (voices :accessor voices \ :initform (#/retain (#/availableVoices ns:ns-speech-synthesizer)))\ (nib-objects :accessor nib-objects :initform nil))\ (:metaclass ns:+ns-object))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ As before, we translate the classname that we gave to IB from SpeechController to speech-controller. We created :foreign slots that are equivalent to the outlets that we defined in IB, again with appropriate name translation. In addition we create a normal Lisp slot to contain a pointer to our speech synthesizer instance and one to contain a pointer to an Objective-C array that contains all of the possible voices for the synthesizer. Memory management is often confusing so I think it's worth a short discussion about why the #/retain call was used for the voices slot, but not for the speech-synth slot initialization. The difference is that when you call make-instance the resulting object is already retained as part of the creation process. You still own it and must release it when done, but you don't have to retain it a second time. Conversely, functions that return things like arrays typically don't retain the objects they are returning. They leave it up to the caller to decide whether to do that or not. Instead they will #/autorelease the object before returning it to you. What this does is tell the Objective-C runtime to release the object sometime later (typically during the next event loop). So it's safe to use for a while, but if you want to keep it around long-term, you must #/retain it yourself to avoid having it disappear suddenly. In future projects you'll see a few instances where our Lisp-defined functions that are callable from Objective-C will #/autorelease the object that they return for exactly the same reason. \f1 \ \ (defmethod initialize-instance :after ((self speech-controller) \ &key &allow-other-keys)\ (setf (nib-objects self)\ (load-nibfile \ (truename "myLisp:InterfaceProjects;Speech;SpeechView.nib") \ :nib-owner self\ :retain-top-objs t))\ ;; get all of the voice strings and set the names of the radio buttons\ (dotimes (i (#/count (voices self)))\ (multiple-value-bind (col row) (floor i 6)\ (#/setTitle: (#/cellAtRow:column: (button-matrix self) row col)\ (#/objectForKey: \ (#/attributesForVoice: ns:ns-speech-synthesizer \ (#/objectAtIndex: (voices self) i)) \ #&NSVoiceName))))\ ;; Make sure that the initial voice selected for the speech syntesizer matches \ ;; the radio button that is selected at startup. To do that we'll just call our\ ;; own buttonPushed: method.\ (#/buttonPushed: self (button-matrix self))\ ;; we do the following so that ccl:terminate will be called before we are\ ;; garbage collected and we can release the top-level objects from the NIB \ ;; that we retained when loaded\ (ccl:terminate-when-unreachable self))\ \f0 \ The initialize-instance :after method is similar to previous ones with the exception that we also initialize all of the titles of those radio buttons that we defined in IB. We create a straightforward mapping from the linear voice index to the two-dimensional array of radio buttons. We go down each column and then across rows. One of the design choices we could have made would have been to translate all those voices to Lisp strings and save them that way rather than keeping around a pointer to an NSMutableArray that contains NSStrings. The reasons that I chose not to do that were that Lisp no longer needed those strings so there was no point in converting them to Lisp strings and we continued to need them when a radio button was pushed to set the new voice. I also could have used a Lisp array to contain those NSStrings, but then I would have had to worry about appropriate #/retain and #/release calls that are handled automatically by NSMutableArray. Deciding how to represent data is one of those ongoing tasks that must be done.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (defmethod ccl:terminate ((self speech-controller))\ (when (speech-synth self)\ (#/release (speech-synth self)))\ (when (voices self)\ (#/release (voices self)))\ (dolist (top-obj (nib-objects self))\ (unless (eql top-obj (%null-ptr))\ (#/release top-obj))))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ Note that the ccl:terminate method calls #/release for both the speech-synth and voices objects.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (objc:defmethod (#/startSpeaking: :void) \ ((self speech-controller) (s :id))\ (declare (ignore s))\ (with-slots (speech-text speech-synth) self\ (let ((stxt (#/stringValue speech-text)))\ (when (zerop (#/length stxt))\ (setf stxt #@"I have nothing to say"))\ (#/startSpeakingString: speech-synth stxt))))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ In our #/startSpeaking method we just get the string from the text box and give it to the speech synthesizer (unless it's blank in which case we provide a bit of humor). Note the use of the #@ reader macro. This creates a Objective-C string that is a constant. So we don't have to #/retain or #/release or #/autorelease it. It will be around for the duration of our program and can be re-used as many times as needed.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (objc:defmethod (#/stopSpeaking: :void) \ ((self speech-controller) (s :id))\ (declare (ignore s))\ (with-slots (speech-synth) self\ (#/stopSpeaking speech-synth)))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ In this method we tell the synthesizer to stop speaking. In that way the user can abort ongoing speaking.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (objc:defmethod (#/buttonPushed: :void) \ ((self speech-controller) (button-matrix :id))\ (let ((row (#/selectedRow button-matrix))\ (col (#/selectedColumn button-matrix)))\ (#/setVoice: (speech-synth self) \ (#/objectAtIndex: (voices self) (+ row (* col 6))))))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ This method is called when a radio button is pushed. We just locate the button within the matrix and convert that row and column into an offset into the voices array. We then set the voice for the speech synthesizer accordingly.\ \ As before we define a test function and after you eval'ed all the code you can run it: \f1 \ \ (defun test-speech ()\ (make-instance 'speech-controller)) \f0 \ \ Type (spc:test-speech) in the listener to run it.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Challenges: \i0 \ \ If you play around with this program a bit you may notice a bug. If you push a radio button while the synthesizer is talking the new voice will not be set properly. Clearly you can't do two things simultaneously with the synthesizer, so the #/setVoice call is ignored. How do we fix this? The right way is to ensure that it doesn't happen in the first place by disabling the radio buttons while the synthesizer is talking and then re-enabling them when it's done. After project #6 you'll know more about how to enable and disable controls, but feel free to work it out on your own.\ \ You've probably also noticed that using radio buttons to list the names of all the voices isn't a particularly good design. If the number of voices is changed at some future time then the interface is broken and must be fixed. A much better alternative might be to have a scrollable list of voices from which to pick. After project #5 you'll know something about how to create such a view and select from it. Feel free to come back and modify this project.\ \ If you really like the idea of radio buttons, then there is another alternative that could be tried. Here I'll admit that when I started this project I intended to do something a little more ambitious. Rather than just changing button titles, I intended to dynamically define how many radio buttons should be made and where to put them within a space that I defined within IB. See Apple's demonstration code called "ButtonMadness" for an example of this technique. I'll confess laziness and a desire to get on to other sorts of interfaces that were of more interest to me. But if you'd like to do something like this I believe it will provide both a good challenge and a good example of the sort of runtime alteration of the interface that is possible.\ \ \b\fs26 Project 5: PackageView \b0\fs24 \ \ Key Concepts: TableViews, Lisp Data Sources. Accessor functions accessible from Objective-C objects, window controllers\ \ So far the projects that we've done have simply used user input to do some easy things. In this project we'll be creating an interface to show data that originates in the Lisp system; namely information about packages that have been created and what other packages either use them or are used by them. You'll learn how easy it is create Lisp functions that provide data for NSTableViews that are part of our display window. We've previously seen how you can create :foreign slots that are directly accessible from Objective-C. For this project you'll see how to create accessor functions that create something like a \i virtual \i0 slot, which gives us an enormous amount of flexibility in how we provide data to interface objects.\ \ Let's start with the interface design. Open IB and either create a new window project or load my example NIB:\ ...InterfaceProjects/PackageView/packageview.nib\ If you created your own, then save it as packageview.nib as I did.\ \ Click on the design window and in the attributes view of the inspector window rename it as: "Lisp Packages" (or something you like better). We'll be adding three "Table Views" to the window which you can find by selecting the "Data Views" folder in the library window. Click and drag three of them to the Lisp Packages window and use the handles on those objects to resize and arrange them roughly as shown in Figure 5.1 below. The first window will have two columns and the other two will have one. To set the number of columns click on the view and ... \ \ Oops, be careful here. When you click on the table view note that the inspector says that you've selected a scroll view. That's because a Table View is actually composed of a moderately complex hierarchy objects that have been design to work together seamlessly. Click multiple times in various spots within the Table View and you'll see these different objects reflected in the inspector window's title. Cocoa defines a large number of reusable object classes. IB prototypes that we include in our interface are often composed of several of these classes combined into a useful pattern. This re-use of primitive classes makes for a very consistent look and feel. It also facilitates easy maintenance because bugs need only be fixed in one place rather than in many. Once this is recognized it becomes easy to see why we don't really want to have to set up all of these relationships programmatically. Cocoa provides ways to do that if you really want to, but personally I prefer to drag and drop a prototype interface element that is composed of a set of fully debugged objects.\ \ OK, so what you have to do to edit the number of columns in a Table View is to click twice on it so that the inspector window shows that you're looking at a "Table View". The attributes view within the inspector window will allow you to set the number of columns that you want. To change the column titles you should double-click on each column header and edit its title directly there.\ \ \ {{\NeXTGraphic Pasted Graphic 10.tiff \width11100 \height10840 }¬}\ Figure 5.1 Window design for the PackageView project\ \ At this point your window should look pretty much as shown in Figure 5.1 above. Now we need to set up the relationships that will allow each table view to retrieve the data that it will display. It's important that you re-read and understand that last sentence. In the Cocoa paradigm display objects that are not used for user input generally retrieve their data automatically. It's not typically the case that a function executing somewhere tells the display how to change. Instead, if a program knows that the data displayed in some view is no longer valid it will tell the view to reload its data. This permits very fast updates. For example if you are displaying a very large table where most of it is currently invisible due to the position of the scrollbar, then the Table View object is smart enough to only reload the visible portions of its data. This also relieves the application code from having to be aware of how the scrollbar is set at each moment in time (although as we'll see in the next project it can certainly make itself aware if that's desirable).\ \ Now we need to make connections so that our Lisp File's Owner class will know how to locate the Table Views in our window (to tell them to reload occasionally) and so that the Table Views know where to go to get their data. Let's start with the File's Owner object. In the Inspector identity view change the name of the class to "PackageViewController". Remember that this must be the first thing you do and don't change it unless you want to re-enter everything else later.\ \ Now add three outlets and title them as shown in Figure 5.2 below:\ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural \cf0 \ \ {{\NeXTGraphic Pasted Graphic 13.tiff \width5740 \height12420 }¬}\ Figure 5.2 PackageViewController Identity\ \ In previous projects we did a control-click and drag to set up a relationship between two objects. Here we'll do an entirely equivalent thing using a process that can sometimes be faster when you have lots of things to connect up to each other. \ \ First click twice on the first Table View (so that the inspector shows that you have selected the Table View rather than the Scroll View). Now control-click on that field. A pop-up window will show you all of the outlets for the scroll view. it will look like Figure 5.3 below:\ \ {{\NeXTGraphic Pasted Graphic 14.tiff \width7180 \height3400 }¬}\ Figure 5.3 Pop-up outlets for the first Table View\ \ If you move your cursor over any of the little circles corresponding to an outlet a "+" sign will appear. To link that outlet simply click within the circle and drag to the object you want to link it with. We'll link both the delegate and data source outlets for each Table View to the File's Owner object. Go ahead and make all of those links. \ \ Next we will make the File's Owner object the window's delegate. Either control-click on the window and then click-and-drag from the delegate outlet to the File's Owner or directly control-click and drag from the window to the File's Owner and then select the delegate outlet. The methods are equivalent.\ \ Now we'll create links from the File's Owner object. Control click on it to bring up a window that shows all of its outlets. Link the three outlets to their corresponding Table View objects. When you get all done the pop-up display for the File's Owner should look like Figure 5.4. Note that this also shows the outlets in other object that reference the File's Owner. Since everything we have done creates links to or from the File's Owner we can verify that we have done everything correctly all in this one place. It should look like Figure 5.4 below:\ \ \ {{\NeXTGraphic Pasted Graphic 15.tiff \width6340 \height5400 }¬}\ Figure 5.4 File's Owner Outlet pop-up\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \cf0 \ Note that this is pretty much identical to what you would see in the Connections view of the Inspector window when looking at the File's Owner object, so there are multiple ways to see the same data.\ \ We're going to do one last thing in IB before we move to the Lisp code and that is to set up column ids for the two columns in the top package table. We do this because when the table asks our code for data it passes in a column object rather than just a column number. This done because tables have the capability to re-order their columns, so a simple number just doesn't suffice. So to know which of the two columns is asking for data, we need to set up an identifier that our Lisp code can query when it gets the request.\ \ To do this click on the top Table View, once to select the Scrollable View, a second time to select the Table View, and a third time somewhere inside the first column but below where the words "Text Cell" are displayed. You have now selected the Table Column object. In the Attributes view of the inspector window you can now set the "identifier" string. We will just set it to "1". This will look as follows:\ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic.tiff \width5740 \height6660 }¬}\ Figure 5.5 Setting the Table Column Identifier to 1\ \ Similarly, set the identifier for the Nicknames column to 2.\ \ Save the NIB file and let's go create some Lisp code to work with this interface.\ \ As before you can open up my sample code and follow along or make your own. The source file for this project is:\ "...InterfaceProjects/PackageView/package-view.Lisp"\ \ The class definition is straight-forward. We have slots that correspond to the outlets that we created in IB and slots to contain the data that will be displayed in the Table Views. All of the latter are normal Lisp slots that will contain normal Lisp data. But instead of having a slot to keep track of the top-level objects from the NIB file, we have a slot to keep track of a NSWindowController object. Note that we could easily continue to track the NIB objects as we have in previous projects, but we are setting the stage for more complex relationships that will come in future projects. For this project we will use a generic NSWindowController to load the NIB for us and to keep track of the objects loaded. When the window is closed the NSWindowController will take care of releasing them. Of course, since we created the NSWindowController, we will have to take care of releasing it when we are garbage-collected.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass package-view-controller (ns:ns-object)\ ((package-table :foreign-type :id :accessor package-table)\ (use-table :foreign-type :id :accessor use-table)\ (used-by-table :foreign-type :id :accessor used-by-table)\ (current-package :accessor current-package :initform nil)\ (current-nicknames :accessor current-nicknames :initform nil)\ (all-packages :accessor all-packages :initform nil)\ (current-package-use-list :accessor current-package-use-list :initform nil)\ (current-package-used-by-list :accessor current-package-used-by-list :initform nil)\ (window-controller :accessor window-controller :initform nil))\ (:metaclass ns:+ns-object))\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 The initialize-instance :after method sets up the list of all packages. Note that we really wouldn't have to save it here, but doing so prevents having to call the list-all-packages over and over again. And creating an array to hold it means that we can access it faster when responding to a request from a Table View for that data.\ \ Normally window controllers are made to be the owner of the windows that they load, but in this case we specify that the package-view-controller entity is the owner. That way, all the links that we set up in IB to the File's Owner object will be references to the package-view-controller and not to the ns-window-controller. \ \ There are many ways to skin a cat and we could just as easily have made package-view-controller be a subclass of NSWindowController. Arguably for this simple example that might be the way to arrange things. The lesson I'm beginning to teach here is about the normal division of responsibilities that Apple suggests when using Cocoa for more complex interfaces and more complex data. They promote a Model/View/Controller (MVC) paradigm where Models are data models, Views are user interfaces, and Controllers map back and forth between the two. For very simple applications such as we have seen so far it is typically possible to combine the Model and Controller functionality into a single object class. When we get to the Document architecture in project #7 and beyond you will see a more distinct division of labor between models and controllers. If we want to present multiple views for a given set of data (perhaps with multiple windows), then this division becomes almost mandatory. As the data itself becomes even more complex, there may be reasons for creating multiple ways to organize it (confusingly these are referred to "views" in the database literature), thus adding another layer to the abstraction.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (defmethod initialize-instance :after ((self package-view-controller) \ &key &allow-other-keys)\ (let ((pkgs (list-all-packages)))\ (setf (all-packages self) (make-array (list (list-length pkgs)) \ :initial-contents pkgs)))\ (let ((nib-name (ccl::%make-nsstring \ (namestring (truename "ip:PackageView;packageview.nib")))))\ (setf (window-controller self)\ (make-instance ns:ns-window-controller\ :with-window-nib-path nib-name\ :owner self))\ ;; Now make the controller load the nib file and make the window visible\ (#/window (window-controller self))\ (#/release nib-name))\ ;; we do the following so that ccl:terminate will be called before we are garbage\ ;; collected and we can release the window-controller that we created\ (ccl:terminate-when-unreachable self))\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 The ccl:terminate function is similar to those we have seen previously, but only release the window controller.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (defmethod ccl:terminate ((self package-view-controller))\ (#/release (window-controller self)))\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 In previous projects we just let the nib loading function find and initialize our slots directly. We didn't have to define a function to do so. But here we are going to do things a bit differently. When the nib-loading function wants to set one of our slots it will first look for an appropriately named accessor function to do the job. The naming convention is critical and must be adhered to if we want the Objective-C runtime to locate our function. If the slot is named "mySlot" in IB, then the nib-loading function will look for an accessor function named "setMySlot:" All of the capitalization is critical. A read accessor for that slot must be named "mySlot" although for this project we won't need a read accessor.\ \ So why do we need a slot-write-accessor for this project. It turns out that sometimes Table View objects can be initialized and begin to make requests to their data sources before all of the links specified in the NIB file have been instantiated (I found this out the hard way). So our methods that respond to those requests have to do some something benign. In our case we will just tell the Table View that it doesn't have any data to display. That's all well and good, but then we have to correct that after the links ARE created by telling the Table View that it needs to reload its data. That is, we want a side-effect to setting the link. So we'll create methods to have that effect:\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (objc:defmethod (#/setPackageTable: :void) \ ((self package-view-controller) (tab :id))\ (setf (package-table self) tab)\ ;; Table may already have initialized before this link was set. Tell it to reload \ ;; just in case.\ (#/reloadData tab))\ \ (objc:defmethod (#/setUseTable: :void) \ ((self package-view-controller) (tab :id))\ (setf (use-table self) tab)\ ;; Table may already have initialized before this link was set. Tell it to reload \ ;; just in case\ (#/reloadData tab))\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 When we set the dataSource outlet in each Table View to point to our File's Owner object we were, in effect, promising that this object would conform to a data source protocol that Cocoa defines: NSTableDataSource. If you look at the documentation for this protocol (AppKiDo works well for this) you will see all of the required and optional methods that must be implemented. Since we are not allowing columns to be sorted and the user cannot edit the columns, this becomes a pretty simple protocol. We must respond to a request for the number of rows in the table and another for the value to be displayed in a particular row and column of the table. \ \ Let's first talk about the request for the number of rows. Our method is as follows: \f1 \ \ (objc:defmethod (#/numberOfRowsInTableView: #>NSInteger) \ ((self package-view-controller) (tab :id))\ (cond ((eql tab (package-table self))\ (array-dimension (all-packages self) 0))\ ((eql tab (use-table self))\ (if (current-package-use-list self)\ (array-dimension (current-package-use-list self) 0)\ 0))\ ((eql tab (used-by-table self))\ (if (current-package-used-by-list self)\ (array-dimension (current-package-used-by-list self) 0)\ 0))\ (t\ ;; We can get called before the links are initialized. If so, return 0\ 0)))\ \ \f0 This method will be called for each of the tables so we need to determine which table called us and respond appropriately. Making this determination is one of the two reasons why we needed those links to the Table View objects in the first place. If this had been the only reason then we might have elected to do things a bit differently. We could, for example, have set a tag for each table in IB and then queried the object that called this function for its tag to find out which one it was. But since we occasionally want to tell a specific table when to reload, we needed the link anyway and it constitutes an easy identifier.\ \ The second function we need to create is the "tableView:objectValueForTableColumn:row:". Each of the colons in the method name corresponds to another parameter that we need to accept (in addition to the first target parameter). Note that the row parameter returns an Objective-C NSInteger object, but that we use it later just as if it was a Lisp number. The type translation is done for us automatically.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 \ (objc:defmethod (#/tableView:objectValueForTableColumn:row: :id) \ ((self package-view-controller) \ (tab :id)\ (col :id)\ (row #>NSInteger))\ (let ((ret-str nil))\ (cond ((eql tab (package-table self))\ (let ((col-id (ccl::Lisp-string-from-nsstring (#/identifier col))))\ (setf ret-str (ccl::%make-nsstring \ (if (string= col-id "1") \ (package-name (svref (all-packages self) row))\ (format nil \ "~\{~a~^,~\}" \ (package-nicknames \ (svref (all-packages self) row))))))))\ ((eql tab (use-table self))\ (setf ret-str (ccl::%make-nsstring \ (if (current-package-use-list self)\ (package-name (svref (current-package-use-list self) row))\ ""))))\ ((eql tab (used-by-table self))\ (setf ret-str (ccl::%make-nsstring \ (if (current-package-used-by-list self)\ (package-name (svref (current-package-used-by-list self) row))\ ""))))\ (t\ (error "~s is not a linked view (~s, ~s, or ~s)" \ tab\ (package-table self)\ (use-table self)\ (used-by-table self))))\ (#/autorelease ret-str)\ ret-str))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ This method will return an NSString object that the table can use to set the value to be displayed. So we need to create one. But now we need to think about memory management. We can't release it before we return it obviously because it might be reclaimed before the Table gets it. So instead we "autorelease" it, which basically marks the object to be released sometime later. This gives the Table that made this call a chance to do whatever it wants with the NSString. Whether that object retains it itself or copies it or whatever is irrelevant to us; we have done our part to make sure that the reference count for it is correct.\ \ Other things to note in this function are that we don't have to worry about being asked for an invalid array reference because we already told the table how many rows it had. Also, we use a standard CL format function to create a string with all the class nicknames.\ \ You will also recall that we made our FIle's Owner object the delegate for each of the tables. We only care about one of the delegate methods: "tableViewSelectionDidChange:". This is called whenever the user clicks on an object in the table. When the user clicks in the top table we want the other two tables to immediately reflect that choice. So we use this method to find out when that happens, to find out what was selected, and to update the other two tables and tell them to reload themselves.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (objc:defmethod (#/tableViewSelectionDidChange: :void) \ ((self package-view-controller) (notif :id))\ (let ((tab (#/object notif)))\ (when (eql tab (package-table self))\ ;; change the other two tables to reflect the package selected\ (let* ((pkg (svref (all-packages self) (#/selectedRow (package-table self))))\ (pkgs-used (package-use-list pkg))\ (pkgs-using (package-used-by-list pkg)))\ (setf (current-package-use-list self)\ (make-array (list (list-length pkgs-used)) :initial-contents pkgs-used))\ (setf (current-package-used-by-list self)\ (make-array (list (list-length pkgs-using)) :initial-contents pkgs-using))\ (#/reloadData (use-table self))\ (#/reloadData (used-by-table self))))))\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 You may be wondering at this point why we made our File's Owner object the delegate of the other two tables. Good question! In fact it isn't necessary for the way we have defined things so far. At one point I considered adding the ability for a user to select a package in either of the two bottom windows to make that the selection in the top one. I decided that was a bit of overkill, but if you like the idea it could easily be implemented within this function. Have at it!\ \ And, as always, I defined a simple test function:\ \ \f1 (defun test-package ()\ (make-instance 'package-view-controller)) \f0 \ \ If you eval all of this or just do (require :package-view) in the browser to load my code and then do (pv:test-package) in the browser you should see a nice interactive display of all the Lisp packages and their use connections.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 6: Loan Calc \b0\fs24 \ \ Key Concepts: Bindings, Number formatters, Slider Controls, control enabling/disabling, control hiding/showing, continuous updating, window controller functionality\ \ In this project we will develop something that begins to look like a complete application. It will do various sorts of loan calculations. Loans can be characterized by the starting value, the interest rate, the duration, and the monthly payment. Given any three of these you can calculate the fourth and our project will do exactly that. Ok, actually there are some combinations of three of these that result in no possible value for the fourth, but we'll discuss and manage those cases a bit later.\ \ We will continue to explore the division of responsibility advocated by the \i model/view/controller (MVC) \i0 paradigm. In the last project we used a window controller to keep track of window objects, but nothing else. In this project it will become the NIB File owner and will assume responsibility for various ongoing window operations. As the File's Owner object, the window controller must provide an access path to the data (i.e. the model). To do that we will create our own window-controller subclass which contains a pointer to a loan object which maintains the data.\ \ This project will take advantage of \i bindings \i0 that we can set up between interface fields and class slots. Bindings are created using Objective-C's \i Key Value Coding (KVC) \i0 mechanism. I'll provide a short introduction to it here, but I'd strongly suggest that you consult other resources for more information. Assuming you have Apple developer tools loaded onto your system in the normal location you can look at:\ file:///Developer/Documentation/DocSets/com.apple.ADC_Reference_Library.CoreReference.docset/Contents/Resources/Documents/documentation/Cocoa/Reference/CocoaBindingsRef/CocoaBindingsRef.html\ or online you can access the same document at:\ \fs22 http://developer.apple.com/mac/library/documentation/Cocoa/Reference/CocoaBindingsRef/CocoaBindingsRef.pdf \fs24 \ and from either of these you'll see references to other documents. \ \ KVC requires that two methods be callable for any KVC-compliant object: \ (id)valueForKey:(NSString *)key\ (void)setValue:(id)value forKey:(NSString *)key\ If you think of this as access to object slot values by passing in the name of the slot as a parameter (i.e. as if by calling #'slot-value in Lisp) you won't be too far off the mark although there are differences in syntax and semantics as we'll see.\ \ The class NSObject (from which all of our classes so far inherit) has a default implementation of these methods which simply calls the corresponding accessor methods. That is, for a call to valueForKey: to an object, obj, passing it an NSString with the value "mySlot" it will make a call equivalent to (#/mySlot obj). For a call to setValue:ForKey: to an object obj passing it an NSObject reference, say objref, and an NSString with the value "mySlot" it will make a call equivalent to (#/setMySlot: obj objref). Again note that all of the name conventions must by adhered to so that the methods will be invoked correctly. In the absence of accessor methods, KVC will try to find a slot of the same name as the key and access it directly. In our classes this would work with slots that are declared to be :foreign. If there are no slots or accessors with the name specified then the receiver calls itself with the function #'valueForUndefinedKey:. It is, therefore, possible to handle such exceptions in any way we want by defining our own version of that function.\ \ What this practically means for us is that we can define Lisp slots for our classes (i.e. not :foreign) and provide Objective-C accessor methods for them and make our Lisp slots KVC compliant. We could even define accessor functions for a non-existent slot to create something like a \i virtual slot \i0 if desired.\ \ KVC also supports access via a \i key path \i0 . A key path is basically a dot-separated list of successive keys. Often objects are connected via links between their slots and this provides a mechanism to follow those links to a final destination. We will use that mechanism here to link the values of user interface elements through the window controller and via its link to the Loan object to "slots" in the Loan object. KVC additionally supports access to and through collection objects like arrays and sets, but we'll have to wait for a future project to see uses for that.\ \ Now we're ready to go build our interface. In IB you can create a new window project or open up "...InterfaceProjects/Loan Calc/loan.nib". Make your interface window look like the following:\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 13.tiff \width10540 \height10400 }¬}\ Figure 6.1 The Loan Calculator window\ \ To do that you will need to place 7 Text Fields 11 Labels, 3 Horizontal Sliders (which you can find by clicking on the "Inputs & Values" folder in the Library window), and one Radio Group. Change the window title to something you like as we did previously. Click on the radio group once and in the Attributes view of the inspector window change the number of rows in the matrix to 4. To change the name of the first button click on it (once or twice depending on what is currently selected) until you have selected the "Button Cell". Change its title using the attributes view of the inspector window as shown in Figure 6.2 below.\ \ {{\NeXTGraphic Pasted Graphic 2.tiff \width5740 \height11400 }¬}\ Figure 6.2 Radio Button Attributes\ \ Similarly change the titles for the other radio buttons. \ \ The three long labels with a smaller font that are situated under the Loan Duration and Monthly Payment boxes require some explanation. At runtime these will be conditionally displayed when certain combinations of loan variables occur. We'll talk about how to make that happen in a bit. For now place the labels, click on them, and in the Size view of the Inspector window (looks like a small ruler) select "Small" for the size. You can actually position the two labels under the Loan Duration box on top of each other since we will assure that only one of them is visible at any given time.\ \ When you've completed this the overall look of the interface should be pretty much what you want. \ \ Next we'll make sure that the text boxes operate the way we want them to. The text boxes to the right of each slider will show a numerical representation of the slider's value. I'll refer to each Text Field using the label that we gave it in the interface or the label of the corresponding slider.\ \ Let's start with the Origination Date Text Field. This will contain a date and we want it to be easily editable by the user. We are going to make our life a little easier by attaching a \i formatter \i0 to this text box. A formatter is basically what it sounds like, namely a process that intervenes in the display of a value to make it look the way we want and assures that what a user enters into this field conforms to the format that we desire. Click on the Formatters folder in the library window and drag a Date Formatter over the Origination Text Field and drop it. Now when you click on that text field you will see below it a small version of the date formatter icon that you saw in the library window. When you click on that icon you have selected the Date Formatter object and can modify its characteristics in the inspector window. Select it now and modify the Date Style to be Short Style as shown in Figure 6.3 below.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 3.tiff \width5740 \height4800 }¬}\ Figure 6.3 Modifying the Date Formatter\ \ Similarly, add a Date Formatter to the First Payment Text Field and also change its Date Style to Short Style.\ \ The Loan Amount Text Field will be a dollar amount. Drag a Number Formatter from the library window and drop it on top of this text field. Then edit it in the inspector attributes window. Change its Style to Currency as shown in Figure 6.4.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 14.tiff \width5740 \height14420 }¬}\ Figure 6.4 Modifying the number formatter\ \ You will also check the boxes "Generate Decimal Numbers" and "Always Shows Decimal" for this formatter. The second of these requires that a decimal point always be displayed and entered to have a valid amount. If you don't want this, it won't change the application code at all, so you can omit that if you want to allow values without a decimal point. The first checkbox causes the formatter to create and pass NSDecimalNumber objects to Lisp when the field is modified. The normal default is to pass NSNumber objects. So what's the difference?\ \ Whenever an application works with relatively large dollar amounts it can run into problems with the number of resolvable digits if it chooses to represent them using floating point values. You tend to lose cents or have them rounded in funny ways when you translate to or from a string representation such as we see in the user interface Text Fields. Lisp users have a relatively easy resolution to this problem because we can simply represent currency amounts as "the number of cents" and use very large fixnums. There are almost always enough digits to represent any value we might need. C programmers are not so lucky and have to create arrays of shorts or some such thing to represent these values and then create special functions to operate on them. Ugly, but what can you do? Apple decided to make this easier for developers by defining a class called NSDecimalNumber and providing functions to operate on instances of it. While we could just use such objects directly within Lisp, it would be a real pain to have to use the relevant Objective-C functions to manipulate them for every arithmetic operation that we wanted to use.\ \ Instead we will define some Lisp functions to translate back and forth from NSDecimalNumbers to Lisp integers. We'll examine them in detail when we get to the discussion of Lisp functions. But for now we have to assure that the dollar values that we enter and display are accurately moved between the user interface and Lisp by causing the formatter to generate NSDecimalNumber objects. So make sure that this box is checked.\ \ Also add Number Formatters to the Monthly Payment and Total Interest Paid Text Fields and change their style to Currency. Be sure to check the same boxes for "Generate Decimal Numbers" and "Always Shows Decimal". Don't be confused by the "Allows Floats" checkbox. That does not refer to the type of object passed back and forth. Rather, that indicates whether the display will show values with a decimal point or only whole numbers. Since dollar values definitely have decimal values, you still want to select "Allows Floats".\ \ Next add a Number Formatter to the Annual Interest Rate % Text Field and edit it in the inspector attributes window. First change its Style to be Percent. Note the Multiplier field is set to 100 by default. This will multiply any value that we set for this field by 100 before displaying it and divide it by 100 before returning it to any call that asks for the numerical value of the text field. For example, if we set the value to .10 it will be displayed as 10% just as we intend. Next we have a design decision to make as to how many decimal digits (if any) we want to allow. I have seen some credit card interest rates recently shown as 4 digit numbers so that's what I used, but do whatever seems reasonable to you. Set that in the "Maximum Integer Digits" field in the Constraints field of the Number Formatter attributes view. When you get done this should look as shown in Figure 6.5.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 5.tiff \width5740 \height13460 }¬}\ Figure 6.5 Annual Interest Rate Formatter\ \ Although we could have used NSDecimalNumbers for this object too, there typically are not enough digits to cause a problem when translating back and forth to Lisp. But if you're concerned about that, feel free to modify the formatter and the relevant Lisp code to use them here too.\ \ Finally add a number formatter to the Loan Duration Text Field. We want these values to always be integers, so we de-select the "Allows Floats" check box. In mine I also de-selected the other text boxes since we never need a separator and don't need to use NSDecimalNumbers to accurately transmit integers back and forth.\ \ The look of our interface is complete and now we have to make connections similar to those we have made in previous projects. For the loan calculator we want our user to be able to select which of the four loan variables to compute and input values for the other three to compute it. When the user selects the "Interest Rate" radio button, for example, we would like to disable input for both the Horizontal Slider and the Text Field that correspond to that parameter. To do that our File's Owner object will need to have pointers to those controls that we want to enable and disable. First click on the File's Owner object in the nib document window and in the identify view of the inspector window change the name of the class to "LoanController". Now add the following outlets to contain those links: durSlider, durText, intSlider, intText, loanText, paySlider, and payText. Note that although we could be precise about identifying the type of objects that these outlets will point to (as we did in previous projects), it is not necessary. We can use the "id" type which just means that it will be an NSObject of some kind and everything will work just fine. \ \ Also add an outlet titled "window". We will use this as a link to the window object.\ \ We'll also add an action method to be called when a radio button is pushed. Call it "buttonPushed". When you get done the identity view should look as in Figure 6.6 below.\ \ {{\NeXTGraphic Pasted Graphic 20.tiff \width5740 \height10920 }¬}\ Figure 6.6 Loan Controller Identity\ \ Now connect up those outlets to their corresponding display objects just as we have done previously. Control-click on the File's Owner object and you will see all of the outlets that you just added in the inspector window. For each outlet, click and drag from the circle next to it to the corresponding Text Field or Horizontal Slider in the Loan Calc window.\ \ Similarly control-click and drag from the Radio Group to the File's Owner object and connect it to the buttonPushed: received action that we defined.\ \ Similarly control-click and drag from the File's Owner object to the window object in the document window and connect it to the "window" outlet that we defined. While we're at it, control-click and drag from the window object to the File's Owner object and connect it to the window's "delegate" outlet. This will permit our loan-controller object to receive delegate messages. In particular, we will want to know when the window is closed by some means such as the user clicking on the window's close button or selecting "Close" from the file menu.\ \ Next we need to make connections that are used by the display elements to report changes in their values that the user makes. But for this project we are going to take advantage of KVC and use \i bindings \i0 rather than setting targets and action messages. What we will do is bind the value of a display field (e.g. a Text Field) to the value of a slot in a Loan object which is pointed to by the "loan" slot in the File's Owner (which at runtime will of course be an instance of our Lisp loan-controller class). If a binding such as this exists, then whenever KVC becomes aware that a value has changed on either side (i.e. either in the interface field or in the Loan class slot that it is bound to) it causes the value in the other bound object to be changed accordingly. This will save us a bunch of coding because we no longer have to be explicitly notified when something changes and then go query its value to see what the change was so that we can reflect it within our own structures in some way. We also don't have to explicitly set the value for the interface object when we compute some new value that should be displayed. In effect, the Loan object is now largely oblivious to the interface objects. As long as it is KVC compliant for all the slots that some interface might want that's all it needs to worry about. As we will see shortly, we can even bind multiple user interface objects to the same slot and they will both reflect the value in the slot.\ \ Let's start by clicking on the Origination Date Text Field. In the Inspector window select the bindings view (little green square and circle joined together). If you can't already see the Value binding fields, click on the arrow to the left of the word "Value" to cause them to appear. Then click on the "Bind to:" box and select "File's Owner". In the Model Key Path field type in "loan.originationDate". As always capitalization must be precise. "loan" is the name of the slot in the File's Owner in which there is a link to a loan object. "originationDate" must be a KVC compliant "slot" in that loan object to which this Text Field will be bound. As we will see when we get to the Lisp code, we won't actually use an Objective-C slot, but instead will create accessor functions that make our class KVC-compliant for originationDate. \ \ There is one more thing that we will do for any of our input objects and that is to click on the "Continuously Updates Value" box. This causes the bound value to be changed in the Loan object anytime the user makes any valid change to the field (i.e. one that the field's formatter accepts). Why would we do this rather than wait until the user is done editing? The main reason is to make our interface more responsive. Text Fields don't end editing until a user types a return in the field to indicate completion or clicks within another editable field. Leaving the field (even to click on a slider for example) is not enough to signify that editing is complete. So you can end up with one value appearing in the display and another value in your slot even though they are bound together. Trust me, this can lead to confusion. When you are done with the Origination Date Text Field it should look as in Figure 6.7 below.\ \ {{\NeXTGraphic Pasted Graphic 16.tiff \width5740 \height14420 }¬}\ Figure 6.7 Origination Date Text Field Bindings\ \ The First Payment Text Field is a bit different in that we don't want it to be editable. We will set it to be one month after the loan origination date. You may not want to make such a restriction, but the loan calculation will become somewhat more complex. Feel free to change that if you want to experiment and understand how to change the lisp code. Assuming you want to do it my way, select the Loan Payment Text Field and in the inspector attributes view deselect the "Selectable" and "Editable" check boxes so that this is merely a display field. Then in the bindings view bind its value to the "loan.firstPayment" slot of the Loan object (i.e. set the Model Key Path to "loan.firstPayment"). It is not necessary to check the "Continuously Updates Value" checkbox of course since this isn't an input field.\ \ Similarly bind the values for the other Text Fields to a File's Owner slot:\ "Loan Amount" Text Field to "loan.loanAmt" slot\ "Annual Interest Rate" Text Field to "loan.interestRate" slot\ "Loan Duration" Text Field to "loan.loanDuration" slot\ "Monthly Payment" Text Field to "loan.monthlyPayment" slot\ "Total Interest Paid" Text Field to "loan.totInterest" slot\ For the first four check the "Continuously Updates Value" checkbox and for the last make it an un-editable display-only field, just as we did for the First Payment Text Field.\ \ We have three more bindings to do for those three small labels (one "Reached max ..." and two "Reached min ... " ) that will only be displayed when a particular relationship holds among our loan variables. Under normal circumstances we want to hide these fields. We will only make them visible when Lisp decides that the conditions are right . Every view object has a boolean "hidden" attribute that causes the object to be invisible when that attribute is "Yes" and visible when it is "No". Cocoa with IB provides a very easy way to do this and that is to bind the "hidden" attribute of the Text Field to some slot. So we'll do just that; we'll bind them to appropriate slots in the Loan object which indicate that the corresponding condition exists. Click on the "Reached max ..." Text Field and in the Inspector window click on the bindings view. Then click the arrow next to "hidden" to expose the binding fields for the hidden attribute. Check the "Bind to:" box and select File's Owner. In the Model Key Path field enter "loan.maxDur". \ \ When the condition is #&YES for the loan we want the Text Field to be displayed, but that corresponds to a hidden value of #$NO. So we need to negate the value of the loan object field in order to make it the value of the Text Field's hidden attribute. To do that select the "NSNegateBoolean" choice for the Value Transformer field. This will do exactly what you might expect and hide the field when the corresponding loan condition is false and display it when true.\ \ Finally select "Yes" in the Null Placeholder field so that in the absence of information from our File's Owner object it will assume that the field is hidden. This is probably overkill since these fields will always be initialized whenever the Loan object exists, but in theory that need not be the case, so it's a good idea to think about what default value makes sense when creating bindings. When you get done, that binding information should look like Figure 6.8 below. Similarly bind the "Reached min payment ..." Text Field to the slot "loan.minPay" and the "Reached min duration ..." Text Field to the slot "loan.minDur".\ \ {{\NeXTGraphic Pasted Graphic 22.tiff \width5740 \height11180 }¬}\ Figure 6.8\ \ Now let's consider the Horizontal Sliders. We want them to reflect the same values as their corresponding Text Fields. We want changes to either control to be immediately reflected in the value displayed by the other as well as in the slot value in our File's Owner object. Making this happen is rather easy. We simply bind the value of the Horizontal Slider to the same slot as its corresponding Text Field. Then we have three things bound to a common value (the Horizontal Slider, the Text View, and the slot). When the value is changed in any of these, it will be immediately changed in the other two.\ \ Start by clicking on the Annual Interest Rate slider. In the Attributes view of the Inspector window set its minimum and maximum values to 0 and .5 respectively (unless you want your annual interest rate to go above 50%, in which case I'll be happy to loan you some money). Set the current value to any initial value that you would like. Then click on the bindings view and bind the value of the slider to the "loan.interestRate" key path of File's Owner, just as we did for the corresponding Annual Interest Rate text field. \ \ At this point we would love to click on the "Continuously Updates Value" checkbox if it existed, but it doesn't. Without that the slider value won't be reflected either in the text box or in the loan interestRate slot until you complete the movement of the slider and are no longer clicking on it. This is disconcerting for users because they would have no idea what value they would be setting the slider to as they moved it. Checking the technical documentation for sliders we discover that this attribute is available for sliders, so at least we know that what we want to do is possible. As you'll see a bit later, we will set this attribute explicitly in our Lisp code. Why IB (at least the version that I have) doesn't provide this capability I have no idea, but it's no problem to get the effect that we want.\ \ In a similar fashion set the minimum and maximum values for the "Loan Duration" slider to 0 and 500 respectively (or whatever values you deem reasonable.) Again set the current value to some good initial value. Next set the minimum and maximum values for the "Monthly Payment" slider to 0 and 10000 respectively and the current value to your desired starting value.\ \ Note that we are not using NSDecimalNumbers to transfer information between the slider and Lisp. There are a couple of reasons for this. The first is that it doesn't seem to work. You can actually attach a formatter to a slider (and then select the checkbox that tells it to generate NSDecimalNumbers), but it doesn't seem to actually do anything and what Lisp gets is an NSNumber. So the first reason is that we can't use them and luckily the second reason is that we don't need them. That's because slider representations are crude at best and there is no confusion if the slider position is slightly different than what is actually in the Lisp class slot. What is important is keeping text strings that the user can see consistent with what is in corresponding Lisp slots.\ \ To double-check that we've linked everything correctly, control-click on the File's Owner object. The pop-up should like very much like Figure 6.9 below.\ \ {{\NeXTGraphic Pasted Graphic 21.tiff \width6340 \height13900 }¬}\ Figure 6.9 LoanController links\ \ If everything looks ok, we can save the NIB file and move on to some Lisp code. You can open up my example file: "...InterfaceProjects/Loan Calc/loan-calc.lisp" or start your own.\ \ \i Program Flow \i0 \ \ The overall control flow of this application is something like the following:\ \ {{\NeXTGraphic loan10.tiff \width10320 \height12400 }¬}\ Figure 6.10 Loan Program flow\ \ Users make changes in the loan window are first converted and saved as Lisp values. Depending on the current compute mode we re-compute the variable that is free to move based on the combination of other values. Then we compute the whole pay schedule.\ \ There are various places in this process where we may change a value displayed in the loan window. It should be obvious that anytime we compute the new value of a loan parameter then we will cause the display to be updated. What is less obvious is that there are various combinations of parameters that may cause us to change a loan parameter other than the one we were explicitly directed to compute. For example, suppose we are told to compute the loan duration while the user manipulates the loan amount, interest rate, and monthly payment. Suppose the user increases the interest rate to a point where the monthly payment is no longer even as large as the first month's interest? Clearly the user is adjusting the interest rate, so they must want it higher. But if we left the monthly payment alone then the loan could never be paid off. Not the least of our problems would be an infinite loop when we tried to compute a payment schedule. So in this case we choose to arbitrarily adjust the monthly payment so that it minimally covers the first month's interest plus $1 to start paying off the loan principle. There are other cases where we must adjust the loan duration either up (to pay off a loan) or down (because the loan is paid off earlier than the duration the user requested). So we change these values dynamically as necessary to maintain a consistent set of loan parameters. In these cases where we adjust a value that wasn't dictated directly by the user, we will display a small banner explaining why we did so. These are the banners that we created that are conditionally hidden in the loan window.\ \ \i Utility Functions\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i0 \cf0 \ The first thing to note in loan-calc.lisp is that there is a:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (require :date) \f0 \ \ statement. This includes a utility file that I have built up over a long time which does various things with Lisp dates. Rather than isolate out just what is necessary for this project I've included all of it. But there are only a few functions that you will use and I won't discuss them much. Their names pretty much indicate what they do. The curious should be able to figure them out just by reading the code. Most are very simple.\ \ Next there is a:\ \ \f1 (require :decimal) \f0 \ \ statement. This is a utility file that provides the two functions shown below.\ \ \f1 (defun lisp-to-ns-decimal (int-val &key (decimals 2))\ ;; construct an NSDecimalNumber object with the given int-val and number of decimals\ ;; For example if you have a dollar amount 123.45 represented by the fixnum 12345\ ;; you would call (make-ns-decimal 12345 :decimals 2) to get a corresponding\ ;; NSDecimalNumber object. This object is the responsibility of the caller and a \ ;; call to #/release must be made when the caller is done with it.\ (unless (typep int-val 'fixnum)\ (error "Input must be a fixnum"))\ (#/decimalNumberWithMantissa:exponent:isNegative:\ ns:ns-decimal-number\ (abs int-val)\ (- decimals)\ (if (minusp int-val) #$YES #$NO))) \f0 \ \ The lisp-to-ns-decimal function simply creates an instance of NSDecimalNumber. The input must be a fixnum and if it represents a number with other than 2 decimal digits you should specify the number using the :decimals keyword argument. Note that the function called to create the NSDecimal number is a class function of the NSDecimalNumber class. It is one of the so-called \i convenience \i0 functions that are provided by some cocoa classes. According to Apple's documentation:\ http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/MemoryMgmt/MemoryMgmt.pdf\ "Many classes provide methods of the form +className... that you can use to obtain a new instance of\ the class. Often referred to as \'93convenience constructors\'94, these methods create a new instance of the class,\ initialize it, and return it for you to use. You do not own objects returned from convenience constructors, or\ from other accessor methods."\ \ So the upshot of this is that the NSDecimalNumber object that is returned is a temporary object. If you want to keep it longer than the span of the function that made the call you must retain it yourself and release it whenever you're done with it. Later discussion in this project will show other ways that we use other similar convenience functions.\ \ \f1 (defun lisp-from-ns-decimal (ns-dec-obj &key (decimals 2))\ ;; This function returns a fixnum that corresponds to the NSDecimalNumber \ ;; or NSNumber that is passed in as the first argument. \ ;; The result will be scaled and rounded to represent the desired\ ;; number of decimal digits as specified by the :decimals keyword argument.\ ;; For example, if an NSNumber is passed in which is something like 123.45678\ ;; and you ask for 2 decimal digits, the returned value will be the integer 12346.\ (let* ((loc (#/currentLocale ns:ns-locale))\ (lisp-str (ccl::lisp-string-from-nsstring \ (#/descriptionWithLocale: ns-dec-obj loc)))\ (str-len-1 (1- (length lisp-str)))\ (dec-pos (or (position #\\. lisp-str) str-len-1))\ (dec-digits (- str-len-1 dec-pos))\ (dec-diff (- decimals dec-digits))\ (mantissa-str (delete #\\. lisp-str)))\ (cond ((zerop dec-diff)\ (read-from-string mantissa-str))\ ((plusp dec-diff)\ (read-from-string (concatenate 'string \ mantissa-str\ (make-string dec-diff :initial-element #\\0))))\ (t ;; minusp dec-diff\ (let ((first-dropped (+ (length mantissa-str) dec-diff)))\ (+ (if (> (char-code (elt mantissa-str first-dropped)) (char-code #\\4)) 1 0)\ (read-from-string (subseq mantissa-str 0 first-dropped)))))))) \f0 \ \ The lisp-from-ns-decimal function takes either an NSDecimalNumber or an NSNumber as input and converts it to a fixnum that is returned. This asks the object for its string representation and then reads in the string after removing the ".". Finally it is scaled and rounded to set the number of implicit decimal digits in the returned fixnum to whatever was requested by the :decimals keyword argument. There is a fair amount of string manipulation going on here just to avoid the use of any floating point operations. There may be a more efficient process than this and the reader is invited to provide a better alternative. I suspect that a more efficient function would necessarily have to differentiate between NSDecimalNumber and NSNumber inputs. In any case, this seems to work fast enough to avoid any interface lag.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Loan-Controller Functionality \i0 \ \ The next section of loan-calc.lisp concerns the loan-controller class and its functionality. First is the class declaration:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass loan-controller (ns:ns-window-controller)\ ((loan :foreign-type :id :accessor loan)\ (loan-text :foreign-type :id :accessor loan-text)\ (int-text :foreign-type :id :accessor int-text)\ (dur-text :foreign-type :id :accessor dur-text)\ (pay-text :foreign-type :id :accessor pay-text)\ (int-slider :foreign-type :id :accessor int-slider)\ (dur-slider :foreign-type :id :accessor dur-slider)\ (pay-slider :foreign-type :id :accessor pay-slider))\ (:metaclass ns:+ns-object)) \f0 \ \ The first slot, "loan", will link the loan-controller instance to an instance of the loan class that we will discuss below. After that are the normal sort of :foreign slots that point to interface objects we've come to expect. They are used to contain the links that we created in IB from the File's Owner object to the various Text Fields and Horizontal Sliders. We'll use those to selectively enable and disable those controls at appropriate times.\ \ Next we define a custom initialization function #/initWithLoan for ourselves:\ \ \f1 (objc:defmethod (#/initWithLoan: :id)\ ((self loan-controller) (ln :id))\ (setf (loan self) ln)\ (let* ((nib-name (ccl::%make-nsstring \ (namestring (truename "ip:Loan Calc;loan.nib"))))\ (init-self (#/initWithWindowNibPath:owner: self nib-name self)))\ init-self)) \f0 \ \ The caller of this function must supply the loan object pointer that will be used. This init function first calls the\ #/initWithWindowNibPath:owner: method to set the path to our custom NIB file. When we discuss the loan methods below we will explain how the loan and loan-controller objects cooperate to open and close windows. This will also help you understand what Document objects do when we get to the next project.\ \ When a radio button is pushed we will change which of the loan variables is computed from the others. The function that is called by the radio button matrix is shown below.\ \ \f1 (objc:defmethod (#/buttonPushed: :void) \ ((self loan-controller) (button-matrix :id))\ (with-slots (loan loan-text int-text dur-text pay-text int-slider \ dur-slider pay-slider) self\ (let ((cm (#/selectedRow button-matrix)))\ (unless (eql cm (compute-mode loan))\ (case (compute-mode loan)\ (0 (#/setEnabled: loan-text #$YES))\ (1 (#/setEnabled: int-text #$YES)\ (#/setEnabled: int-slider #$YES))\ (2 (#/setEnabled: dur-text #$YES)\ (#/setEnabled: dur-slider #$YES))\ (3 (#/setEnabled: pay-text #$YES)\ (#/setEnabled: pay-slider #$YES)))\ (setf (compute-mode loan) cm)\ (case cm\ (0 (#/setEnabled: loan-text #$NO))\ (1 (#/setEnabled: int-text #$NO)\ (#/setEnabled: int-slider #$NO))\ (2 (#/setEnabled: dur-text #$NO)\ (#/setEnabled: dur-slider #$NO))\ (3 (#/setEnabled: pay-text #$NO)\ (#/setEnabled: pay-slider #$NO)))\ (compute-new-loan-values loan))))) \f0 \ \ We first determine which user interface objects are currently disabled and re-enable them. Then we disable those that correspond to the value that we are now being asked to compute. Note that KVC will know what we did because we called the appropriate Objective-C #/set... accessors. So we do not have to explicitly call #/willChangeValueForKey: and #/didChangeValueforKey:. And of course we set the slot value that contains the compute mode in the attached loan object. Note that the Loan object (which controls data) does not have to be aware of which radio button is pushed or even the fact that we're using radio buttons to select the compute mode.\ \ The next method we'll discuss, #/awakeFromNib, is called as a result of us setting the window's delegate outlet to point to the File's Owner class in IB. As its name implies, this method is called after the window has been loaded from the nib and initialized. \ \ \f1 (objc:defmethod (#/awakeFromNib :void) \ ((self loan-controller))\ (#/setEnabled: (loan-text self) #$NO)\ ;; set the sliders to update continuously so that the text boxes reflect the current value\ ;; Note that we can set this in IB for text boxes, but not, apparently, for sliders\ (#/setContinuous: (int-slider self) #$YES)\ (#/setContinuous: (dur-slider self) #$YES)\ (#/setContinuous: (pay-slider self) #$YES)) \f0 \ \ Here we initialize some interface attributes. Since we have set our default radio button to be to calculate the loan value we will disable input into that Text Field for now. As discussed above, when the user selects a different radio button we'll change which controls are enabled and disabled.\ \ Perhaps this is a good point to discuss another subject. When you were binding the values of various display fields you may have noticed that in addition to binding their values you can bind to a slot that specifies whether they are enabled or disabled. We could certainly have used that mechanism here either by defining a :foreign slot for each control and making sure that it was initialized properly or by providing accessor functions. But then when a button was pushed we would have to set those slot values rather than just directly setting the enabled property of the object. The point is that whether the object should be enabled or not is entirely a property of the interface state and not the data, so there really isn't any advantage of binding that property to a slot rather than just setting it directly.\ \ Next the function sets that Continuous update property for the three sliders that we discussed previously. If you want to see the difference in functionality, omit those calls and observe how the interface operates when a slider is moved.\ \ The next loan-controller method we'll discuss is also called as a result of its being the window delegate.\ \ \f1 (objc:defmethod (#/windowWillClose: :void) \ ((self loan-controller) (notif :id))\ (declare (ignore notif))\ (when (loan self)\ ;; Tell the loan that the window is closing\ ;; It will #/autorelease this window-controller)\ (window-closed (loan self)))) \f0 \ \ Once the window is closed we want the loan-controller object to go away as well (unless you want to re-show the window at some later time). When a window controller object that loaded a nib file is deallocated it then releases all of the top-level nib objects that were created when the nib file was loaded. These notably include the window object which then releases all of the interface objects that it points to. These may, in turn, release more deeply embedded objects. So if we want all of that memory to be reclaimed, it is important to make sure that the loan-controller object goes away. So the first thing we do is call the loan's "window-closed" method to tell it what is happening. We'll talk about that method later, but at this point just recognize that it has a link to this loan-controller object which must be released in order for it to be garbage collected and that's one of the things that the window-closed method will do.\ \ That's the end of the loan-controller functionality. We'll now switch from talking about the loan-controller class to discussing the loan class and other subsidiary loan functions. \ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Loan Functionality \i0 \ \ Most of the loan computation code is built around a basic loan equation: \ MonthlyPayment = Loan * (MonthlyInterest + (MonthlyInterest / ((1 + MonthlyInterest)^LoanDuration - 1))).\ We can easily rearrange this to derive the loan value from the other parameters. To compute interest from the other values, the code searches because there isn't an algebraic solution for it. To compute the loan duration we create a complete payout schedule and just look at its length.\ \ First we define a utility function that helps with these calculations:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defun pay-to-loan-ratio (mo-int loan-dur)\ ;; Just computes the MonthlyPayment/Loan ratio from the basic loan equation given above\ (if (zerop mo-int) 0 \ (+ mo-int (/ mo-int (1- (expt (1+ mo-int) loan-dur)))))) \f0 \ \ With a little mathematical manipulation you can see that this is derived from the basic loan equation above.\ \ The loan class is defined as follows:\ \ \f1 (defclass loan (ns:ns-object)\ ((loan-amount :accessor loan-amount :initform 0)\ (interest-rate :accessor interest-rate :initform 0)\ (loan-duration :accessor loan-duration :initform 0)\ (monthly-payment :accessor monthly-payment :initform 0)\ (origination-date :accessor origination-date :initform (now))\ (first-payment :accessor first-payment :initform (next-month (now)))\ (pay-schedule :accessor pay-schedule :initform nil)\ (window-controller :accessor window-controller :initform nil)\ (compute-mode :accessor compute-mode :initform 0)\ (max-dur :foreign-type #>BOOL :accessor max-dur)\ (min-dur :foreign-type #>BOOL :accessor min-dur)\ (min-pay :foreign-type #>BOOL :accessor min-pay))\ (:metaclass ns:+ns-object)) \f0 \ \ You'll see several Lisp slots (i.e. not defined as :foreign-type) defined first. At first blush they seem to correspond to the slot names that we bound to the various IB view object values. That was an intentional naming choice on my part to keep them straight, but as Lisp slots they are not accessible to Objective-C functions. Later I will discuss how we make use of them.\ \ After that is the "window-controller" slot in which we will keep a pointer to the instance which controls the window as described previously.\ \ That is followed by the compute-mode slot which keeps track of which variable is being computed given the values of the others.\ \ Next are three slots with a :foreign-type of #>BOOL. These are the slots that we bound to the "hidden" attribute of the three longer messages that we only want to appear when we detect certain conditions. In some sense they indicate the presence or absence of those conditions so we have not really exposed the user interface to the loan object.\ \ \f1 (defmethod initialize-instance :after ((self loan) \ &key &allow-other-keys)\ (setf (window-controller self)\ (make-instance (find-class 'loan-controller)\ :with-loan self))\ (#/showWindow: (window-controller self) self)) \f0 \ \ This function simply creates the loan-controller object that will manage the display window and then tells it to show its window.\ \ Recall that we used bindings for those three condition messages. Initially we want them all to be hidden because the corresponding conditions haven't been detected yet (because we don't even have values to calculate anything with yet). All :foreign-type slots are always initialized to 0, which conveniently corresponds to #$NO, so we don't have to do anything special to initialize those conditions.\ \ If we DID have to initialize those conditions, we would also have to make sure that KVC was made aware of the changes, so that they would be given to the bound user interface objects. Generally we can assure that KVC is aware of changes made to slots in any of three ways. First we can explicitly call #/setValue:forKey: for these slots. Second, we can define appropriately named Objective-C accessor functions for these slots and use them. Third, we can surround the Lisp call which sets the slot with a prior call to #/willChangeValueForKey: and a subsequent call to #/didChangeValueForKey:. Later we will see how that third method is used both for changes to these condition slots and also for the value bindings that we made even though there is no actual :foreign-type slot slot that contains those values.\ \ The next methods we will discuss are those accessor methods which are called when KVC detects that a user interface object has changed its value or that one of our slot values has changed. Let's start with the former set. When either a slider is moved or a value in a Text Field is changed KVC will call the corresponding #/setValue:forKey: method for the bound "slot" that we specified in IB. The default implementation of that method will call an appropriately named Objective-C accessor method. If the slot was named "mySlot", then the method called would be "setMySlot:". So we first define a group of these accessors.\ \ \f1 (objc:defmethod (#/setLoanAmt: :void)\ ((self loan-controller) (amt :id))\ (setf (loan-amount self) (lisp-from-ns-decimal amt))\ (compute-new-loan-values self)) \f0 \ \ Some things to note here. In IB we bound the Loan Amount Text Field to the slot "loanAmt" in File's Owner. An appropriately named write accessor for that slot is "setLoanAmt". We know that the object passed in as the new value will be an NSDecimalNumber because we told the formatter for that Text Field to generate such numbers. So we can just take it and convert it to a Lisp fixnum and put it in a conveniently named slot in our loan-controller object. It is now a Lisp value and can be manipulated normally. After doing this we trigger the side-effect of computing any new loan values. What gets computed will depend on the compute-mode field. As you may recall, this value is set by the loan-controller object when a new radio button is selected in the Loan window.\ \ \f1 (objc:defmethod (#/setInterestRate: :void)\ ((self loan-controller) (rate :id))\ (setf (interest-rate self) (#/floatValue rate))\ (compute-new-loan-values self)) \f0 \ \ In this case we will be passed an NSNumber object and we will ask for its float value which we put into our Lisp slot before computing new loan values.\ \ \f1 (objc:defmethod (#/setLoanDuration: :void)\ ((self loan-controller) (dur :id))\ (setf (loan-duration self) (#/longValue dur))\ (compute-new-loan-values self)) \f0 \ \ Durations are never float values (remember that we de-selected the "Allows Floats" checkbox in the formatter for this field in IB), so we can ask for a long from the NSNumber and get it.\ \ \f1 (objc:defmethod (#/setMonthlyPayment: :void)\ ((self loan-controller) (pay :id))\ (setf (monthly-payment self) (lisp-from-ns-decimal pay))\ (compute-new-loan-values self))\ \ The monthly payment is another dollar amount, so we use NSDecimal numbers to move values from the Text Field. But wait a minute, we also bound the monthly payment slider to this same value and it cannot provide NSDecimal numbers. Well, as we mentioned when we discussed the lisp-from-ns-decimal function, it can also handle NSNumber objects and has the nice side-effect of rounding the floating point input value that we get to two decimals and putting it into the same Lisp internal format that we want to use for currency values. \f0 \ \ \f1 (objc:defmethod (#/setOriginationDate: :void)\ ((self loan-controller) (dt :id))\ (let ((new-dt (ns-to-lisp-date dt)))\ (setf (origination-date self) new-dt)\ (#/willChangeValueForKey: self #@"firstPayment")\ (setf (first-payment self) (next-month new-dt))\ (#/didChangeValueForKey: self #@"firstPayment"))\ (compute-new-loan-values self)) \f0 \ \ When we set the loan origination date, we convert it to the corresponding Lisp data format. We also want to set the first payment date to be exactly one month later so we use the "next-month" function from the date.lisp file. Note that we have to notify KVC that we are changing the firstPayment "slot" so that will change the value of the First Payment Text Field in the Loan window. Note that an alternative here would have been to just directly call the write accessor:\ (#/setFirstPayment self dt)\ which would have let KVC know what was going on. That would have required two separate conversions of the date to Lisp so I elected not to do that.\ \ \f1 (objc:defmethod (#/setFirstPayment: :void) \ ((self loan-controller) (pay :id))\ (let ((new-pay (ns-to-lisp-date pay)))\ (setf (first-payment self) new-pay))\ (compute-new-loan-values self)) \f0 \ \ This method is not, strictly speaking, necessary. We made the First Payment Text Field un-editable so this should never be called. But who knows how we might decide to change things in the future, so we created it just in case it is needed.\ \ The corresponding reader accessor functions are shown below and are fairly straight-forward. We just have to make sure that the Objective-C objects that we create to represent values are released at some future time so we don't have a memory leak. So in all cases we use the same class convenience functions that we discussed in the context of the lisp-to-ns-decimal code (or in some cases functions that return the results of these functions). These all return objects that are not owned by us, so we do not need to do anything about releasing them here. There is also no point in trying to cache values here for future use because the only time that these methods will be called is when KVC becomes aware that they have changed. \ \ \f1 (objc:defmethod (#/loanAmt :id)\ ((self loan))\ (lisp-to-ns-decimal (loan-amount self))) \f0 \ \ \f1 (objc:defmethod (#/interestRate :id)\ ((self loan))\ (#/numberWithFloat: ns:ns-number (float (interest-rate self)))) \f0 \ \ \f1 (objc:defmethod (#/loanDuration :id)\ ((self loan))\ (#/numberWithInt: ns:ns-number (loan-duration self))) \f0 \ \ \f1 (objc:defmethod (#/monthlyPayment :id)\ ((self loan))\ (lisp-to-ns-decimal (monthly-payment self))) \f0 \ \ \f1 (objc:defmethod (#/originationDate :id)\ ((self loan))\ (lisp-to-ns-date (origination-date self))) \f0 \ \ \f1 (objc:defmethod (#/firstPayment :id)\ ((self loan))\ (lisp-to-ns-date (first-payment self))) \f0 \ \ There is one last reader accessor to discuss and that is for the Total Interest Text Field. We created Lisp slots that corresponded to each of the other Text Fields, but for this one we did not (mostly just to illustrate that you don't actually need to do so.) \ \ \f1 (objc:defmethod (#/totInterest :id)\ ((self loan))\ (lisp-to-ns-decimal (reduce #'+ (pay-schedule self) \ :key #'seventh \ :initial-value 0))) \f0 \ \ The #/totInterest accessor function computes the total interest from the current payment schedule and returns it as a NSDecimalNumber object whenever the Total Interest Text Field requests it. As always, the call to lisp-to-ns-decimal uses two decimal digits as the default assumption and encodes the value that we give it accordingly.\ \ As shown in the simple flowchart for our loan application and in the set... accessor code above, virtually any change the user makes results in a call to compute-new-loan-values. Here is that function:\ \ \f1 (defmethod compute-new-loan-values ((self loan))\ ;; For the sake of expediency we assume monthly componding\ ;; The basic equation governing these computations is \ (with-slots (compute-mode interest-rate loan-duration monthly-payment \ loan-amount pay-schedule) self\ (case compute-mode\ (0\ ;; compute the loan amount\ (unless (or (zerop interest-rate)\ (zerop loan-duration)\ (zerop monthly-payment))\ (#/willChangeValueForKey: self #@"loanAmt")\ (setf loan-amount \ (round (/ monthly-payment \ (pay-to-loan-ratio (/ interest-rate 12)\ loan-duration))))\ (set-pay-schedule self)\ (#/didChangeValueForKey: self #@"loanAmt")))\ (1\ ;; compute the interest rate\ (unless (or (zerop loan-amount)\ (zerop loan-duration)\ (zerop monthly-payment))\ (#/willChangeValueForKey: self #@"interestRate")\ (setf interest-rate \ (* 12 (/ (floor (* 1000000 (compute-int-rate self)))\ 1000000)))\ (set-pay-schedule self)\ (#/didChangeValueForKey: self #@"interestRate")))\ (2\ ;; compute the loan duration\ (unless (or (zerop interest-rate)\ (zerop loan-amount)\ (zerop monthly-payment))\ (#/willChangeValueForKey: self #@"loanDuration")\ (set-pay-schedule self)\ (setf loan-duration\ (list-length pay-schedule))\ (#/didChangeValueForKey: self #@"loanDuration")))\ (3\ ;; compute the monthly payment\ (unless (or (zerop interest-rate)\ (zerop loan-amount)\ (zerop loan-duration))\ (#/willChangeValueForKey: self #@"monthlyPayment")\ (setf monthly-payment\ (round (* loan-amount \ (pay-to-loan-ratio (/ interest-rate 12) \ loan-duration))))\ (set-pay-schedule self)\ (#/didChangeValueForKey: self #@"monthlyPayment")))))) \f0 \ \ When the program is first started, all of the input fields have 0 values and we cannot compute a loan value. So the first thing we do, for whatever compute mode the user has selected, is to make sure that we have non-zero values for the variables that are needed to compute it. Another thing that we do for all compute modes is let KVC know that we have changed the value that should be displayed in the corresponding Text Field by calling the appropriate #/willChange... and #/didChange methods. In all compute modes we will also call the set-pay-schedule method that we will discuss below. \ \ The computations for loan amount or monthly payment have easy closed-form solutions that are derived from the basic loan equation. The loan duration can be directly derived from the length of the payment schedule so we just call that first and then set the value of the loan-duration slot. We use a search algorithm to compute the interest rate and round the value returned to the number of digits we want to display. There is a separate method to do the search (compute-int-rate) which is shown below.\ \ \f1 (defmethod compute-int-rate ((self loan))\ ;; Find a monthly interest rate that makes the rest of the values work.\ ;; There isn't an algebraic solution for the interest rate, so let's search for it.\ ;; Find a suitable search range and do a binary search for it. Even for large interest \ ;; rates the number of search iterations should be minimal.\ \ (with-slots (loan-amount monthly-payment loan-duration interest-rate) self\ \ ;; First we'll check to see whether the monthly payment is great than the loan amount.\ ;; If so we'll set the interest rate directly so that the loan is paid off in one month.\ ;; This avoids some ugly arithmetic overflow things that can happen when interest rates\ ;; go off the charts\ (let ((max-monthly-rate (/ $max-interest-rate$ 12)))\ (if (>= monthly-payment loan-amount)\ (min max-monthly-rate (1- (/ monthly-payment loan-amount)))\ (let ((imin (max 0 (min max-monthly-rate\ (/ (- (* monthly-payment loan-duration) loan-amount) \ (* loan-duration loan-amount)))))\ ;; imin is basically a rate that would result in the first month's interest as \ ;; the average interest paid for all months. Since we know it must be greater \ ;; than this, we have a guaranteed lower bound. But we cap it at our allowed \ ;; monthly maximum interest.\ (imax (min max-monthly-rate \ (- (/ monthly-payment loan-amount) .000008333)))\ ;; imax is a rate that would result in the first month's interest being \ ;; minimally smaller than the payment. Since we must pay off in a finite\ ;; duration, this is a guaranteed maximum. We cap it the allowed maximum \ ;; monthly rate.\ (target-p-l-ratio (/ monthly-payment loan-amount)))\ (unless (>= imax imin)\ (error "Max int = ~8,4f, Min int = ~8,4f" imax imin))\ (do* ((i (/ (+ imin imax) 2) \ (/ (+ imin imax) 2))\ (p-l-ratio (pay-to-loan-ratio i loan-duration) \ (pay-to-loan-ratio i loan-duration)))\ ((<= (- imax imin) .000001) imax)\ (if (>= target-p-l-ratio p-l-ratio)\ (setf imin i)\ (setf imax i)))))))) \f0 \ \ This method uses a binary search to find the interest rate that produces something very close to the target payment to loan ratio. We can compute that target from the user-specified payment and loan value parameters. The initial bounds for the interest rate are computed as discussed in the code comments. \ \ At one time I used a more precise upper bound that for some reason resulted in extremely slow performance in circumstances where the computed rate eventually reached the upper bound. I decided to not chase that down, but I expect that it was caused by deriving a rate that resulted in an extremely long loan payout schedule. The method here seems to work pretty well and responsively. Consider it a challenge to do something more precise.\ \ The final method we will discuss computes a detailed payment schedule given all the loan parameters. Since we never display this anywhere you may wonder why it exists. There are four reasons. First it makes it very easy to determine the loan duration when we need to compute that from the other values. Second, I found it to be a useful debugging tool. I could print out the payment schedule in the listener just to make sure that everything looked good. I uncovered several bugs that way. Third, I wanted to provide a challenge for you to add functionality to this application. And fourth, I wanted to have something more substantial to print out when we get to Project #7.\ \ \f1 (defmethod set-pay-schedule ((self loan))\ ;; create a detailed payment schedule for the loan using daily compounding of interest \ ;; Payments are on the same date of each month, but the number of days between payments\ ;; varies because the number of days in each month varies.\ ;; We compute accurate interest compounded daily for the actual number of days.\ (let ((monthly-interest (/ (interest-rate self) 12))\ (payment (monthly-payment self))\ (sched nil)\ (display-min-pay-banner nil))\ (prog1\ (do* ((begin (loan-amount self) end)\ (begin-date (first-payment self) end-date)\ (end-date (next-month begin-date) (next-month begin-date))\ (int (round (* begin monthly-interest))\ (round (* begin monthly-interest)))\ (end (- (+ begin int) payment) (- (+ begin int) payment)))\ ((not (plusp end)) \ (progn\ (push (list (short-date-string begin-date) \ (/ begin 100)\ (/ int 100)\ (/ payment 100)\ (short-date-string end-date) \ (/ end 100)\ int) \ sched)\ (setf (pay-schedule self) (nreverse sched))))\ (when (>= end begin)\ ;; oops, with this combination of values the loan will never \ ;; be paid off, so let's set a minimum payment required\ ;; Display a field that tells user the minimum payment was reached \ (setf display-min-pay-banner t)\ (#/willChangeValueForKey: self #@"monthlyPayment")\ (setf (monthly-payment self) (1+ int))\ (#/didChangeValueForKey: self #@"monthlyPayment")\ ;; now patch up our loop variables and keep going\ (setf payment (monthly-payment self))\ (setf end (1- begin)))\ ;; put the last payment into the list\ (push (list (short-date-string begin-date) \ (/ begin 100)\ (/ int 100)\ (/ payment 100)\ (short-date-string end-date) \ (/ end 100)\ int)\ sched))\ (#/willChangeValueForKey: self #@"totInterest")\ ;; we'll make the total interest field call our accessor \ ;; to generate a new amount\ (#/didChangeValueForKey: self #@"totInterest")\ (if display-min-pay-banner\ (progn\ ;; Set a condition that says the minimum payment was reached \ (setf display-min-pay-banner t)\ (#/willChangeValueForKey: self #@"minPay")\ (setf (min-pay self) #$YES)\ (#/didChangeValueForKey: self #@"minPay"))\ (progn\ ;; otherwise reset that condition\ (#/willChangeValueForKey: self #@"minPay")\ (setf (min-pay self) #$NO)\ (#/didChangeValueForKey: self #@"minPay")))\ ;; If we happen to be computing the interest rate, then \ ;; the combination of loan-amount and monthly payment will\ ;; determine a maximum interest rate. This, in turn, \ ;; determines a maximum loan duration. If the duration was set\ ;; longer than this by the user, we will reset the \ ;; lone duration value to the maximum needed.\ ;; If, on the other hand, the monthly payment is set so low that\ ;; the interest rate approaches 0, then we may have to adjust the\ ;; loan duration up to the minimum needed to pay the loan.\ ;; Let's start by resetting our two "duration" conditions and then we'll\ ;; set them if conditions dictate.\ ;; Reset a condition that indicates the max duration was reached \ (#/willChangeValueForKey: self #@"maxDur")\ (setf (max-dur self) #$NO)\ (#/didChangeValueForKey: self #@"maxDur")\ ;; Reset a condition that indicates the min duration was reached \ (#/willChangeValueForKey: self #@"minDur")\ (setf (min-dur self) #$NO)\ (#/didChangeValueForKey: self #@"minDur"))\ (let ((duration-diff (- (loan-duration self) (list-length (pay-schedule self)))))\ (unless (or (eql (compute-mode self) 2) (zerop duration-diff))\ ;; i.e. we're not calling this function just to determine the loan duration\ ;; and we have to adjust the loan duration\ (if (plusp duration-diff)\ (progn\ ;; change the loan-duration value to what it must be\ (#/willChangeValueForKey: self #@"loanDuration")\ (setf (loan-duration self) (list-length (pay-schedule self)))\ (#/didChangeValueForKey: self #@"loanDuration")\ (when (> duration-diff 2)\ ;; If we're one-off just fix it and don't post a message\ ;; This can occur almost anytime because of numerical issues\ ;; Display a field that tells user the max duration was reached \ (#/willChangeValueForKey: self #@"maxDur")\ (setf (max-dur self) #$YES)\ (#/didChangeValueForKey: self #@"maxDur")))\ (progn\ ;; change the oan-duration value to what it must be\ (#/willChangeValueForKey: self #@"loanDuration")\ (setf (loan-duration self) (list-length (pay-schedule self)))\ (#/didChangeValueForKey: self #@"loanDuration")\ (when (< duration-diff -2)\ ;; If we're one-off just fix it and don't post a message\ ;; This can occur almost anytime because of numerical issues\ ;; Display a field that tells user the min duration was reached \ (#/willChangeValueForKey: self #@"minDur")\ (setf (min-dur self) #$YES)\ (#/didChangeValueForKey: self #@"minDur")))))))) \f0 \ \ This is a pretty simple function that starts with the initial loan value, calculates and adds the interest on that amount for one month, subtracts the payment to derive a new loan value and iterates until the loan value drops to zero or below. As it proceeds it keeps a record of all the payments. You may note that we put the monthly interest paid into each record twice; once as a float value that we can print directly and once as internally represented with a fixnum. The former is used in our payment schedule print function and the latter is used to compute the total interest for the interface. We previously discussed how this value is used to compute the total interest paid whenever the Total Interest Text Field asks for it.\ \ Computing the pay schedule itself is the easy part of this function and is taken care of by the "do*" function. The more difficult aspects of this method concern the detection of conditions that require that we limit or change some of the user-specified values. Under these circumstances we need to let the user know what we did and why so they don't get frustrated by a seemingly unresponsive control. \ \ One condition will be detected the first time through the do loop; namely whether the loan payment is less than or equal to the amount needed to pay off the first month's interest. Typically this will occur as the user increases the interest rate for a fixed loan value.If we allowed that condition to continue we would be in an infinite loop because the outstanding loan value would not decrease over time. If this case is detected, the monthly payment will be set to an amount that guarantees the loan will be paid off eventually and we set a flag indicating that we have set the minimum monthly value. After we have computed the whole schedule we will use that flag to set the condition value in the Loan object. That will, in turn, trigger the display of a banner in the user interface.\ \ The second condition that we detect is when the loan duration specified by the user is longer than necessary to pay off the loan. This can arise as the user increases the monthly loan payment for a fixed loan value. To be as informative as possible to users, we show how the duration is modified as the monthly payment is adjusted and set a conditional value in the loan object to indicate what is being done. That value, as we have seen, is used to display an informational message in the Loan window.\ \ The third condition is very similar to the second, but occurs when the payment is reduced to the point where the duration specified by the user is insufficient for the combination of other user-defined and computed parameters. So the code automatically increases the duration and sets the relevant condition in the loan object. This results in the display of an appropriate banner to explain what is being done.\ \ That's all the code that is necessary. As has been typical of our projects so far, our test function is pretty simple:\ \ \f1 (defun test-loan ()\ ;; up to caller to #/release the returned loan instance\ ;; but only after window is closed or crash will occur\ (make-instance 'loan)) \f0 \ \ Type (lnc:test-loan) in the listener to run this. Note that this function returns a loan object that has a retain count of 1, so it is the responsibility of the user to call the #/release function on this object.\ \ Give it a try.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Challenges: \i0 \ \ One of the things that might be useful for users is to see the whole loan payout schedule. Add a button to the window (or a menu item or both) that the user can select to open up a whole new window in which to display the loan-payout schedule. Create a separate NIB file for this window and load it on demand.\ \ In Canada and other countries installment loan interest charges are not compounded monthly. Modify the code to allow for a different compounding schedule.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 Project 7: Loan Document \b0 \ \ Key Concepts: Documents, Document archiving, Undo manager, Printing, Open Panels\ \ This project will be a straight-forward extension of the previous one. But we will transform loans into a standard document that can be opened, saved to a file, printed, and closed as one would expect. It will support "undo" and "redo" operations in a normal way. Most of the complications are caused by trying to add a new type of document into the existing CCL IDE without having to change it any way. You will see that for the most part we will meet this goal. The one way in which our documents will be different from others is it will not be possible to double-click on them in the finder and get them to open up in Lisp. We will, of course, be able to open them from within lisp. We could remedy this by appropriate editing of resources within the CCL IDE's bundle and also assuring that our lisp code is always loaded at runtime, but that would violate our rule for not making changes to the standard environment. The next project will be to make a stand-alone application that implements this same loan document functionality. At that time it will be possible to double-click .loan documents to open them.\ \ The user interface for this project is basically identical to that of Project 6. We will make one small addition that will be explained when we talk about supporting "undo" and "redo" functionality. As before, either create your own project named "Loan Document" or just follow along with mine. Copy the nib file from the previous project and save it as "loandoc.nib" within the project. Open it up and add a new outlet to the "File's Owner" object (loan-controller). Name that outlet OrigDateField. Control-click and drag from the File's Owner object to the loan origination text field and link it to that new outlet. That's it for NIB file changes. Save it and we'll move on the the lisp code.\ \ It's first necessary to understand the relationship between various classes in a normal application that supports documents. I will provide a brief introduction here, but if you want the whole story I suggest that you read Apple's "Document-Based Applications Overview". If you have the documentation sets downloaded as discussed previously you can find this on your own system in:\ file:///Developer/Documentation/DocSets/com.apple.ADC_Reference_Library.CoreReference.docset/Contents/Resources/Documents/documentation/Cocoa/Conceptual/Documents/Concepts/WindowClosingBehav.html#//apple_ref/doc/uid/20000027\ Or on Apple's web-site at:\ http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Documents/Documents.html\ \ Every document-based application will create a single instance of an NSDocumentController. The first instance created will be used as the shared instance for any later reference. Most document-based applications (including the CCL IDE) make a subclass of NSDocumentController that is unique to the application. They arrange to create an instance of it as one of the first things that the application does when it starts up. Since we do not want to interfere in any way with the functionality of that class while operating within the IDE, we will leave that class alone. When a user requests that a file be opened or that a new file be created, it is functionality within the NSDocumentController that handles that request. For the most part it knows what to do because of a resource file that is put into the application's bundle. Once again, we don't want to modify that file, so we can't directly make use of that functionality to open or create a new type of document. You will see below that I instead created a sort of pseudo document controller which does many of the same things that a normal document controller would do, but only for our new class. I tried to do that in such a way that if we decide to create a stand-alone application, we can replace our custom document controller with the more standard one and everything will work just as one would normally expect. See the next project for how that is done. We use our pseudo document controller to do a few things that we can't otherwise do and then register our documents with the standard NSDocumentController and let it do the rest of the things that it can do just fine (like saving and closing files).\ \ The NSDocumentController instance manages instances of NSDocument. Each type of document will have its own defined subclass and we will do the same for our loan document. NSDocument objects in turn manage NSWindowController objects (one per window needed to display a single document). We will have a single loan window, so we will have only a single NSWindowController (just as we did for the previous project). Each NSWindowController will manage a single window that is defined in a NIB file (again, just as we did previously).\ \ We will discuss the flow of control for various processes as we go through the lisp code, but for a more complete discussion, refer to the document cited previously.\ \ For this project I refactored all the code into separate source files for each major class. The "loan-document.lisp" file contains code relevant to the loan-document class. This is basically the same as the loan class in the previous project, but now inherits from the NSDocument class and supports functionality that traditional NSDocument classes support. The "lisp-document-controller.lisp" source file supports the creation of the pseudo document controller class discussed above. As previously stated, a stand-alone application would not require this. The "loan-window-controller.lisp" source file contains code relevant to our window controller class. Finally, the "loan-print-view.lisp" file contains code for a custom view that is used for printing loan documents.\ \ We will start with "lisp-document-controller.lisp". One design goal for this class was to make it as easy as possible to later migrate from "within-IDE" document functionality to "stand-alone" document functionality. Another was to be able to support multiple different types of documents with this one single class. To address the second goal, this class is "document-class agnostic". It does not know or care what class of document is being created. If you wanted to support two or more different types of document you could do so with this class. In a standard stand-alone document application, only a single instance of NSDocumentController is used. For our purposes we will use a single instance of the lisp-doc-controller for each \i class \i0 of document that is to be supported. For this project we will need only one instance that will be created when the loan document class is loaded.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass lisp-doc-controller (ns:ns-object)\ ((document-class :accessor document-class :initarg :doc-class)\ (menu-class-name :accessor menu-class-name :initarg :menu-class)\ (file-ext :accessor file-ext :initarg :file-ext)\ (doc-ctrlr :accessor doc-ctrlr)\ (open-panel :accessor open-panel)\ (type-ns-str :accessor type-ns-str)\ (ext-ns-str :accessor ext-ns-str)\ (type-array :accessor type-array)\ (documents :accessor documents :initform nil))\ (:default-initargs \ :doc-class nil\ :menu-class nil\ :file-ext nil)\ (:metaclass ns:+ns-object)) \f0 \ \ The document-class slot will contain a reference to the class of document that will be controlled by this controller. This will be set by the caller when the instance is created. In the same manner the caller will set the name to be used in custom menu items that we will add to do those things that cannot be done by the standard menu items. The initializing call must also provide the file extension to be used when saving or loading this type of document.\ \ The doc-ctrlr slot contains a reference to the shared document controller instance that is used by the CCL IDE. We will register new loan documents with this shared document controller so that other menu items will work properly. As mentioned previously, one of the things that our document controller will have to manage is opening up loan documents. To do that we use an NSOpenPanel object. We only need to create one once, so that is put into the open-panel slot. At various times we need to pass instances of NSString to Objective-C functions, so we cache the strings needed in the type-ns-str, ext-ns-str, and type-array slots. The documents slot will keep track of all instances of our loan documents. This is not currently used in any way and does not delete objects when loan documents are closed. It was primarily used for early debugging. It is entirely possible that the mac-ptrs there could become invalid when a document is closed, so if you decide to modify the code to use this slot in any way, be careful.\ \ We start to do more interesting things in an "initialize-instance :after" method:\ \ \f1 (defmethod initialize-instance :after ((self lisp-doc-controller) \ &key menu-class file-ext &allow-other-keys)\ (ccl:terminate-when-unreachable self)\ (when menu-class\ (setf (type-ns-str self) (ccl::%make-nsstring menu-class))\ (setf (ext-ns-str self) (ccl::%make-nsstring file-ext))\ (setf (doc-ctrlr self) (#/sharedDocumentController ns:ns-document-controller))\ (setf (open-panel self) (make-instance ns:ns-open-panel))\ (#/retain (open-panel self))\ (setf (type-array self)\ (#/arrayByAddingObject: (make-instance ns:ns-array) (ext-ns-str self)))\ (make-and-install-menuitems-after "File" "New"\ (list (concatenate 'string "New " menu-class) \ "newDoc"\ nil\ self))\ (make-and-install-menuitems-after "File" "Open..."\ (list (concatenate 'string "Open " menu-class "...") \ "openDoc"\ nil\ self))\ (make-and-install-menuitems-after "File" "Print..."\ (list (concatenate 'string "Print " menu-class "...") \ (concatenate 'string "print" menu-class ":")\ nil\ nil)))) \f0 \ \ First we create those NSString objects described earlier. The type-ns-str for this project will contain "Loan", the ext-ns-str will contain "loan", and the type-array slot will just contain the "loan" NSString.\ \ Next we create three new menu items. You can look at the menu-utils.lisp file to see how the make-and-install-menuitems-after function works. Basically it locates a menu item specified by the name of the top-level menu and the name of the menu item and then adds a new menu item following it. In this case we create three new menu items that will be named "New Loan", "Open Loan" and "Print Loan...". Other types of document that you might create could similarly add menu items for other types of document. In a stand-alone application we would use the standard New, Open, and Print... menu items instead. This is one of the compromises that must be made to operate within the IDE without modifying it in any way. \ \ Note that both the "New ..." and "Open ..." send action messages directly to this instance of lisp-doc-controller. The new "Print..." menu item, however, specifies nil as the target. What this means is that the target will be the "First Responder". In this way the action message will be sent to whichever window is the top window at the time. In a document-based application, the document object is also in the first responder chain, so we can write an appropriate Objective-C method for our loan-document class that will handle the message. The action message for the print object is specialized to each type of class. In that way, if multiple document types were being supported by multiple instances of lisp-doc-controller, the print action message would be appropriate for each type document. Also recall that menu items are automatically enabled and disabled according to whether there is any object in the responder chain that will accept that action message. That means that our custom loan print menu item will only be enabled when a loan window is the active window. \ \ Why did we not just use the action "printDocument:" as standard document applications do? The reason is that there is a default implementation of this method for all NSDocument objects, including those that represent Hemlock documents within the IDE. But the default method relies on the user overriding other print methods and the NSDocument classes defined for Hemlock windows do not do so. So if we used the "printDocument:" action our print menu-item would be enabled for all windows, but would do nothing for any but our own.\ \ If the user should ever cause this instance of lisp-doc-controller to be garbage collected we want to deallocate those NSString objects that we created, so we define an appropriate method to do this.\ \ \f1 (defmethod ccl:terminate ((self lisp-doc-controller))\ (#/release (type-ns-str self))\ (#/release (ext-ns-str self))\ (#/release (type-array self))\ (#/release (open-panel self))) \f0 \ \ Next we must create the newDoc and openDoc methods that will be called when corresponding menu items are selected.\ \ \f1 (objc:defmethod (#/newDoc :void)\ ((self lisp-doc-controller))\ (let ((new-doc (make-instance (document-class self))))\ (push new-doc (documents self))\ ;; register the document with the shared controller so that things like\ ;; "save" and "close" will work properly\ (#/addDocument: (doc-ctrlr self) new-doc)\ (#/makeWindowControllers new-doc)\ (#/showWindows new-doc))) \f0 \ \ This method just mimics what a real NSDocumentController would do when a New menu item is selected. It makes an instance of the new document and registers it with the "real" NSDocumentController. Then it calls the same methods that an NSDocumentController would to cause the document to make its window controllers and then show those windows. \ \ \f1 (objc:defmethod (#/openDoc :void)\ ((self lisp-doc-controller))\ (let ((result (#/runModalForTypes: (open-panel self) (type-array self))))\ (when (eql result 1)\ (let ((urls (#/URLs (open-panel self))))\ (dotimes (i (#/count urls))\ (let ((doc (make-instance (document-class self))))\ (setf doc (#/initWithContentsOfURL:ofType:error: \ doc\ (#/objectAtIndex: urls i)\ (type-ns-str self)\ (%null-ptr)))\ (if doc\ (progn\ (pushnew doc (documents self))\ (#/addDocument: (doc-ctrlr self) doc)\ (#/makeWindowControllers doc)\ (#/showWindows doc))\ (#_NSRunAlertPanel #@"ALERT" \ #@"Could not open specified file ... ignoring it."\ #@"OK" \ (%null-ptr)\ (%null-ptr))))))))) \f0 \ \ The openDoc method also mimics what a real NSDocumentController object would do. It runs the openPanel in such a way that it will only permit the selection of files with a .loan extension (or whatever extension was specified when the lisp-doc-controller was created). The returned value will be 1 if the user selected a file or 0 if the selection was cancelled. In theory a user can select multiple files to open, although the default values for the open panel permit only single files. But to be on the safe side we will iterate through the set of files selected and create a new document for each one. Note that we first create an instance of our class and then call the standard init method initWithContentsOfURL:ofType:error:. Since init methods need not return the same object that was allocated, we need to do the setf to make sure we use whatever object is returned by the init method. We do not need to worry about what it does. When we look at loan-document methods we will see what must be done to make sure that this is done correctly. \ \ If the init should fail we run an alert panel with a single OK button that simply indicates that the file could not be open. If we wanted to be more precise about why it couldn't be open, we could modify the init call to provide a ptr to an error object that would be created by the init function. Then we could use that error object to alert the user about what happened.\ \ That's all the functionality required for our lisp-doc-controller class. Since the changes to the window controller class defined in the previous project are fairly minimal, we will look at that next. First note that to avoid confusion I changed the name of the class from "loan-controller" in the previous project to "loan-win-controller" for this one. It would not have been necessary to do this since they are safely interned within separate packages, but I wanted to avoid any confusion. \ \ The new loan-win-controller class definition has only one small modification from the loan-controller class in the previous project. We add a single slot:\ (orig-date-text :foreign-type :id :accessor orig-date-text)\ This will keep a reference to the origination date text field. You will recall that we set this link up when we modified the nib file.\ \ The only other changes are in the #/wakeFromNib method which now looks like:\ \ \f1 (objc:defmethod (#/awakeFromNib :void) \ ((self loan-win-controller))\ (#/setEnabled: (loan-text self) #$NO)\ ;; set the sliders to update continuously so that the text boxes reflect the current value\ ;; Note that we can set this in IB for text boxes, but not, apparently, for sliders\ (#/setContinuous: (int-slider self) #$YES)\ (#/setContinuous: (dur-slider self) #$YES)\ (#/setContinuous: (pay-slider self) #$YES)\ ;; tell the text cells not to handle undo\ (#/setAllowsUndo: (#/cell (loan-text self)) #$NO)\ (#/setAllowsUndo: (#/cell (int-text self)) #$NO)\ (#/setAllowsUndo: (#/cell (dur-text self)) #$NO)\ (#/setAllowsUndo: (#/cell (pay-text self)) #$NO)\ (#/setAllowsUndo: (#/cell (orig-date-text self)) #$NO)) \f0 \ \ We called #/setAllowsUndo: with an argument of #$NO for the cell sub-object of each of the editable text fields. When we discuss the undo functionality provided by our loan-document class we will see how undo is handled. What we are doing here is disabling the default undo methods that come with all NSTextFieldCell objects. Basically, these handle the undo and redo for text editing operations. So you would see something like "Undo Typing" in the Edit menu. But the undo/redo semantics for our Loan objects are a bit more subtle than that and we don't want these default undo operations to interfere with what we want to do. This is another one of those attributes of objects that arguably should be settable within IB, but isn't. \ \ Those are all of the changes needed for our loan-win-controller, so we will next discuss the loan-document class. The first thing needed in the loan-doc.lisp file are the appropriate "require" statements.\ \ \f1 (eval-when (:compile-toplevel :load-toplevel :execute)\ (require :menu-utils)\ (require :decimal)\ (require :date)\ (require :lisp-doc-controller)\ (require :loan-win-cntrl)\ (require :loan-pr-view)\ (require :ns-string-utils)) \f0 \ \ You have seen most of these previously. The loan-print-view functionality will be discussed later. The ns-string-utils.lisp file contains a number of useful routines that we will examine quickly here.\ \ \f1 (defun ns-to-lisp-string (ns-str)\ (if (plusp (#/length ns-str))\ (%get-cstring (#/cStringUsingEncoding: ns-str #$NSUTF8StringEncoding))\ "")) \f0 \ \ This converts an NSString object to a lisp string. This is occasionally useful when the Objective-C bridge functions do not automatically do the conversion for you. \ \ \f1 (defun lisp-str-to-ns-data (lisp-str)\ (with-cstrs ((str lisp-str))\ (#/dataWithBytes:length: ns:ns-data str (1+ (length lisp-str))))) \f0 \ \ This function converts a lisp string to an NSData object. This will be useful when we need to pass an object that represents the state of our loan objects to an Objective-C method. We will basically put a lisp form into a string and then encode the string into an NSData object that gets passed. We will do that both when we save a file and when we specify how to undo an operation.\ \ \f1 (defun ns-data-to-lisp-str (nsdata)\ (%get-cstring (#/bytes nsdata))) \f0 \ \ This function reverses the process and creates a lisp string from an NSData object.\ \ \f1 (defun lisp-object-to-ns-data (obj)\ (lisp-str-to-ns-data (format nil "~s" obj))) \f0 \ \ Completing the encoding process, this function creates a string that represents a lisp object. For this project that will be a simple list of values. \ \ \f1 (defun ns-data-to-lisp-object (nsdata)\ (read-from-string (ns-data-to-lisp-str nsdata))) \f0 \ \ And completing the decoding process, this function reads the lisp object from the string retrieved from the NSData object.\ \ ( \f1 defun lisp-to-temp-nsstring (string)\ ;; creates a string that is not owned by caller\ ;; so no release is necessary\ (with-encoded-cstrs :utf-8 ((s string))\ (#/stringWithUTF8String: ns:ns-string s))) \f0 \ \ This is a function that I should have created for previous projects. In several of those projects we created NSStrings that were used as arguments to Objective-C functions and then #/released them before exiting the function. Here we use one of the class convenience functions described earlier which create an object of the desired type (here an NSString) that we do not own and therefore do not need to release later.\ \ The loan-doc class definition is identical to the definition of the loan class in the previous project with the exception of a single new slot:\ \f1 (last-set-param :accessor last-set-param :initform nil) \f0 \ This slot will be used in the implementation of the "undo" functionality and will be discussed later.\ \ Once the loan-doc class is defined we create an instance of the lisp-doc-controller class that will be used to manage all loan-doc instances.\ \ \f1 (defvar *loan-doc-controller* (make-instance 'lisp-doc-controller\ :doc-class (find-class 'loan-doc)\ :menu-class "Loan"\ :file-ext "loan")) \f0 \ \ We set the class to be our loan-doc class, the menu text to be "Loan" and the file extension of be "loan" (note that the "." is not needed as part of the extension).\ \ All of the functionality used to compute the values of loan parameters is exactly the same as the previous project and will not be further discussed here. The remainder of this discussion will focus on new functionality added to the loan-doc class for window-controller management, undo/redo, open/save, and printing operations.\ \ The first method we will discuss is #/makeWindowControllers.\ \ \f1 (objc:defmethod (#/makeWindowControllers :void) \ ((self loan-doc))\ (let ((lwc (make-instance 'loan-win-controller\ :with-loan self)))\ (#/setShouldCloseDocument: lwc #$YES)\ (#/addWindowController: self lwc))) \f0 \ \ This method will be invoked by the lisp-doc-controller object that we just created whenever a user elects to create a new loan document. This is a standard NSDocument method that is typically defined by a subclass in order to create multiple windows for a single document and is normally called by the shared document controller. When just a single window is needed, a standard document subclass would normally override #windowNibName to specify which nib file from within the application's bundle should be used to create that window. In our case, the nib file that we want to use is outside of the CCL IDE's bundle, so we need to do something a bit different. As we did in the previous project, we invoke the #'initWithLoan method that we defined for the loan-win-controller class. In addition we tell the window controller that when the window closes it should also close the document. Finally we add the window controller to ourself. I'm not entirely sure what all this accomplishes, but the Apple documentation is quite clear that this is something that should be done if you override this method. \ \ Note that the window controller will now have two slots that point to our loan-doc object: document and loan. We do not necessarily need both. The document slot is necessary because it is used for standard NSWindowController functionality, so we could change all of the KVC paths used to bind user interface objects to use the document slot rather than the loan slot and do away with the latter altogether. Part of the reason I didn't do this is just laziness. I didn't want to go back into IB and change all of the binding paths. But as I considered doing this I also realized that the document slot in the lisp-window-controller would not be set until sometime after the nib file was loaded whereas the loan slot was set prior to this event. IB provides a way to specify default values to use if a key-path returns a null value and presumably I would have needed to set all of these as well to prevent problems when the newly created interface objects began to ask for values via a slot that was not yet defined. Rather than walk down that path I chose to take the easy route and have two different slots.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Undo/Redo Functionality \i0 \ \ Before examining the next loan-doc methods we need to discuss how "undo" and "redo" normally work and how we want to implement them for our loan documents. Apples' functionality is really quite clever. Each document has its own NSUndoManager instance that coordinates undo and redo operations whenever a window owned by that document has the current focus. The way this works is that whenever an operation that we want to undo is done, it creates the invocation that would be needed to reverse the action being taken and registers that action with the undo manager. That action should itself be "undoable". Assuming that the reverse action is in fact undoable, then when it is invoked it will create and register its own reverse action. The undo manager knows that it is in the process of doing an undo when this happens and puts the reverse action on the "redo" stack instead of on the "undo" stack. All of this seems fairly straight-forward, but we need to think about how it applies to our loan application.\ \ The first problem that we have applying undo functionality to this application is that setting a new value will always result in new values for other parameters as well as the one changed. At the very least the dependent variable being computed is changed and frequently others are as well. So just changing the value back to what it was will not always result in an overall state that was the same as it was previously. What we really need to do is to capture the entire state of the loan just before we change something and restore that state to undo the effects of the change. \ \ The second problem is how to manage the size of the undo stack. Recall that we made all the slider and text-box updates continuous to provide instantaneous feedback to the user about the effects that are generated by the change. If we regarded each of these individual changes as an action that could be undone, then there would be a huge number of actions on the undo stack and that would make the undo functionality virtually unusable. To address this problem we will treat a sequence of changes to any single parameter as if it were a single un-doable action. So we can move a slider back and forth as much as we want and then undo to set the state of the loan back to what it was before we started to move it.\ \ Note that changing a slider and then subsequently changing the text field that sets the same parameter will be treated as a single action. Arguably you might want to treat these as separate actions. That could be done by creating a different bind target for each control and then in the corresponding Objective-C methods each could update the same slot and notify the other that the slot had changed. Feel free to make this change if you want.\ \ We will encapsulate the state of a loan as a simple list that contains the relevant parameter slot-values. The reverse action for every change we make will be to set the state of the loan back to what it was, as indicated by the list of slot-values. When a reverse action is first created it will capture the current state of the loan as a list of parameters and give it to the undo manager as an NSData object that should be passed back as a parameter to the undo method that we will name "setLoanState:". This method will, of course, register its own reverse action with the undo manager; that being simply to set the state back to its current setting. Let's now look at the methods that are used to do all this.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defmethod create-undo ((self loan-doc) undo-name &optional (always-undo nil))\ (when (or always-undo (not (eq (last-set-param self) undo-name)))\ (let ((undo (#/undoManager self))\ (st (lisp-object-to-ns-data (get-loan-state self))))\ (#/setLoanState: (#/prepareWithInvocationTarget: undo self)\ st)\ (unless (#/isUndoing undo)\ (#/setActionName: undo undo-name))\ (setf (last-set-param self) undo-name)))) \f0 \ \ The create-undo method is called as the first thing from every action that changes a loan parameter value. It is here that we note whether the action being done is the same as the previous action or something new. The last-set-param slot is used to track the las action. If it is the same, then we do not create and register a new reverse action and simply return. The last one that we registered will suffice to return the state to what is was previously. Otherwise we prepare the undo manager by calling #/prepareWithInvocationTarget: . This alerts it to accept the next message that is sent to it and package it up to be sent later if the user elects to undo. The second argument to the #/prepareWithInvocationTarget: method tells the undo manager where to send the message to cause the undo. In this case, it is simply sent back to this instance. We register the #setLoanState method as our undo method by calling it with the undo manager as its first argument. The undo manager will package up the message and any parameters sent along (the st NSData object in this case) and save them for use when an undo is invoked. At that time it will send the message to the target object (which we set as self when we prepared the undo manager) along with the st parameter. After we register the undo message we set up the string that will be shown in the undo menu item to make it easier for the user to know exactly what is being undone. We only need to do that if we are not in the process of undoing something. In that case the undo manager will take the title from the current undo, which is exactly what we want to happen.\ \ Note that the reverse of a #/setLoanState action is another #/setLoanState action and in this case we always want to create an appropriate reverse. So create-undo provides an optional parameter that lets us specify that an undo should be registered even if the previous action was the same.\ \ \f1 (objc:defmethod (#/setLoanState: :void)\ ((self loan-doc) (st :id))\ (create-undo self nil t)\ ;; called when user does an "undo" \ (set-loan-state self (ns-data-to-lisp-object st))) \f0 \ \ This is the method that is actually called by the undo manager whenever the user elects to undo or redo something. It first creates its own undo and makes sure that it is always created by calling create-undo with the always-undo parameter set to t. Then it calls the lisp set-loan-state method to do the real work using the parameter list recovered from the NSData object.\ \ \f1 (defmethod get-loan-state ((self loan-doc))\ ;; returns a list of loan state values suitable for use by set-loan-state\ (list (loan-amount self)\ (interest-rate self)\ (loan-duration self)\ (monthly-payment self)\ (origination-date self)\ (first-payment self)\ (max-dur self)\ (min-dur self)\ (min-pay self))) \f0 \ \ The get-loan-state method simply gathers up the slots that we need to reproduce a loan's state and puts them into a list.\ \ \f1 (defmethod set-loan-state ((self loan-doc) state-list)\ (setf (last-set-param self) nil)\ (#/willChangeValueForKey: self #@"loanAmt")\ (setf (loan-amount self) (pop state-list))\ (#/didChangeValueForKey: self #@"loanAmt")\ (#/willChangeValueForKey: self #@"interestRate")\ (setf (interest-rate self) (pop state-list))\ (#/didChangeValueForKey: self #@"interestRate")\ (#/willChangeValueForKey: self #@"loanDuration")\ (setf (loan-duration self) (pop state-list))\ (#/didChangeValueForKey: self #@"loanDuration")\ (#/willChangeValueForKey: self #@"monthlyPayment")\ (setf (monthly-payment self) (pop state-list))\ (#/didChangeValueForKey: self #@"monthlyPayment")\ (#/willChangeValueForKey: self #@"originationDate")\ (setf (origination-date self) (pop state-list))\ (#/didChangeValueForKey: self #@"originationDate")\ (#/willChangeValueForKey: self #@"firstPayment")\ (setf (first-payment self) (pop state-list))\ (#/didChangeValueForKey: self #@"firstPayment")\ (#/willChangeValueForKey: self #@"maxDur")\ (setf (max-dur self) (pop state-list))\ (#/didChangeValueForKey: self #@"maxDur")\ (#/willChangeValueForKey: self #@"minDur")\ (setf (min-dur self) (pop state-list))\ (#/didChangeValueForKey: self #@"minDur")\ (#/willChangeValueForKey: self #@"minPay")\ (setf (min-pay self) (pop state-list))\ (#/didChangeValueForKey: self #@"minPay")\ (#/willChangeValueForKey: self #@"totInterest")\ (setf (pay-schedule self) nil)\ (#/didChangeValueForKey: self #@"totInterest")\ (compute-new-loan-values self)) \f0 \ \ The set-loan-state method first sets the last-set-param slot to nil, thus assuring that a subsequent change to any parameter value will initiate the generation of a new undo entry. Then it takes a list of loan parameters and uses them to set the corresponding loan values. Finally it calls the compute-new-loan-value method to compute the new loan-schedule slot. Note that it makes sure to notify KVC that the corresponding key paths have been changed so that any user interface objects that are bound to them will be changed as well. Also note that any argument given to set-loan-state is used exactly once (for a couple of reasons, not the least of which is that it is newly created from an NSData object). So there is no problem with destroying it using the pop calls.\ \ To complete the undo functionality we must assure that every method that initiates a change to the loan state calls the create-undo method as the first thing that it does. For example:\ \ \f1 (objc:defmethod (#/setLoanAmt: :void)\ ((self loan-doc) (amt :id))\ (create-undo self #@"set loan amount")\ (setf (loan-amount self) (lisp-from-ns-decimal amt))\ (compute-new-loan-values self)) \f0 \ \ This method is identical to its counterpart in the previous project except for an additional call to create-undo that occurs at the beginning of the method. The string provided would result in the undo menu showing "Undo set loan amount". Similar changes must be made to all of the #/set... methods.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Open/Save Functionality \i0 \ \ The next functionality that we will implement for our loan-document class is support for opening and saving files. First let's talk about the "save" menu item. Typical save functionality is to enable that menu item when a document has been modified and disable it when it has not. The NSUndoManager tracks this and can manage this for normal documents. It is not entirely clear to me what the CCL IDE does to implement save, but the default seems to do some fairly strange things. For normal lisp documents it appears to always be enabled, even when the document is unmodified (for example right after we save it). And for custom documents such as ours it often seems to be disabled even when it is modified. A little experimentation showed that the default save functionality worked just fine even when the menu item was disabled, so the solution was to simply validate the menu item ourselves. The following function does that.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (objc:defmethod (#/validateMenuItem: #>BOOL) \ ((self loan-doc) (item :id))\ (let* ((action (#/action item)))\ (cond ((eql action (ccl::@selector #/saveDocument:))\ (#/isDocumentEdited self))\ (t (call-next-method item))))) \f0 \ \ This is a standard method that is called on the first responder for every menu item when a menu is opened. Since our loan-doc instance is in the First Responder chain we can implement it for the loan-doc class and simply enable the menu item whenever our document has been edited and disable it otherwise. This works just like a normal application. When some other type of window has the focus the no loan-doc instance will be in the first responder chain and normal validation of the save menu-item will be done.\ \ When an application is saved we want to force it to be saved with a ".loan" extension. To do that we define the method below.\ \ \f1 (objc:defmethod (#/prepareSavePanel: #>BOOL) \ ((self loan-doc) (panel :id))\ (#/setRequiredFileType: panel #@"loan")\ #$YES) \f0 \ \ This method is called just before the save panel is displayed to a user. We simply set the required file type and it will automatically be filled in for the user as part of the file name.\ \ There are basically three ways that documents can participate in saving and loading documents. In each case a pair of methods must be overridden by the document. Which one is selected depends on how involved the document wants to be in the process. The easiest approach is to just worry about what data needs to be loaded or saved and let the NSDocument default methods worry about the rest. This is the approach taken here. To do this we will override the #/readFromData:ofType:error: and #/dataOfType:error: methods. Consult the Document-Based Application Architecture document described previously for other ways to implement document saving and loading functionality.\ \ \f1 (objc:defmethod (#/dataOfType:error: :id) \ ((self loan-doc) (dtype :id) (err (:* :id)))\ (declare (ignore dtype err))\ (lisp-object-to-ns-data (get-loan-state self))) \f0 \ \ This method is called when a user elects to save a file. Having implemented the undo functionality, this is now a pretty easy thing to do. We get the loan state and package it up as an NSData object. Recall that when we discussed the lisp-object-to-ns-data function we explained that it creates a string that represents a lisp object that is then used to create the NSData object. What happens to it after that is not something that we have to concern ourselves with too much. As long as we are given the same object back when that file is opened, we will be just fine. In fact, it turns out that for NSData objects such as this the output file will simply contain the text that we gave it. It is possible to use TextEdit or most any other text editing application (including CCL itself) to open it up and look at it.\ \ If saving and restoring our document required more complex and/or interconnected lisp objects, then we would certainly want to do something other than create a simple list. In this case it might be appropriate to use lisp's make-load-form to generate a set of loadable forms that could be used when the file was opened. Perhaps in a later project I will do something like that.\ \ \f1 (objc:defmethod (#/readFromData:ofType:error: #>BOOL) \ ((self loan-doc) (data :id) (dtype :id) (err (:* :id)))\ (declare (ignore err dtype))\ (set-loan-state self (ns-data-to-lisp-object data))\ #$YES) \f0 \ \ This method is invoked when a loan file is opened. It simply converts the NSData object back to a lisp list and uses it to reassign data values to the document's slots, much as we did for undo actions.\ \ And that's it! You can now easily save and load .loan documents.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Print Functionality \i0 \ \ The last of the new document functions we will discuss is printing. In a typical document-based application the selection of the print menu item results in sending a "printDcoument:" message to the first responder which ultimately reaches the document being displayed in the active window. In non-document-based applications the print menu item will typically send a "print:" message to the first responder. All NSTextFields will respond to a print: message by printing themselves. The print menu item in the CCL IDE uses the "print:" method rather than :printDocument. This works just fine when all the windows have a single NSTextField which can then print itself. But this is clearly not going to work for our loan window. Whichever text field happened to be the first responder at the time would simply print itself. I pondered various ways that I might alter those individual print: methods to change that behavior, but in the end decided that the easiest solution was simply to create a "Print Loan..." menu item that does what we want it to do. I decided not to just make it call #/printDocument: because it would then be applicable to all document objects and those defined for the CCL IDE had no functionality to support printing. So I set up the menu item to create a unique message for the loan class that only a loan-doc would respond to. Therefore, when a window for a document other than a loan-doc was active, the Print Loan... menu item would be disabled. \ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (objc:defmethod (#/printLoan: :void)\ ((self loan-doc) (sender :id))\ (declare (ignore sender))\ (#/printDocument: self self)) \f0 \ \ The method above is invoked when the Print Loan... menu item is selected. You will recall that we set up that menu-item when the lisp-doc-controller was initialized. This method, in turn, calls the standard #/printDocument: method on itself. That will make life easier when we create a stand-alone loan application in the next project. The #/printDocument: method will in turn invoke the \f1 #/printOperationWithSettings:error: method shown below.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (objc:defmethod (#/printOperationWithSettings:error: :id)\ ((self loan-doc) (settings :id) (err (:* :id)))\ (declare (ignore err settings))\ (let ((pr-view (make-instance 'loan-print-view\ :loan self)))\ (#/printOperationWithView:printInfo: ns:ns-print-operation \ pr-view \ (#/printInfo self)))) \f0 \ \ This method returns an NSPrintOperation object which controls printing operations. We create it by specifying the view that will get drawn and providing a NSPrintInfo object. The latter is an object that contains information about things like paper size, number of copies, print margins, etc. Although it is possible to create a custom version of this object, we simply use the default version that is shared by all documents. As far as the loan-doc object is concerned, this is all that it needs to do to provide printing. The NSView object that we provide will be an instance of our custom loan-print-view class. That is defined within the loan-print-view.lisp source file that we will examine right after we talk generally about printing.\ \ The printing support within Cocoa makes it quite easy to print an existing view. Basically you can hand the view to the printing subsystem and it will arrange for the view to draw itself on a page rather than on a screen. If what is in your screen view exactly matches what you want to print and fits onto a single page (or can be automatically divided into pages), then life is pretty easy. However if your view is much larger than a single page and programmatic control is needed to decide how pages should be composed or you want to print data with a different format than is used on the screen, then some code is needed to make that happen. To demonstrate how to do this in lisp we will print a loan document with all the basic parameter data at the top of the first page and with details about the payout schedule printed one line per month at the bottom of the first page and onto subsequent pages.\ \ Cocoa printing functions are designed to handle views that are larger than a single page. By default, Cocoa's printing facility will try to map the view that you give to it onto multiple pages. For things like text documents that default works quite well. For many other applications the developer will want to specify how that mapping occurs. Cocoa provides a method for the application to specify which rectangle within the view corresponds to each page number. We will see a bit later how we take advantage of that mechanism to do something a bit different. Once you specify which rectangle within the view is to be printed for a given page, the printing functionality will try to map that rectangle onto the available space on a page. By default it positions it at the upper left corner of the page. For our purposes this will work just fine, but be aware that there are ways that this can be modified by the programmer. A view to be drawn can also be clipped or scaled in either the horizontal or vertical direction or both. For more information about how to implement printing see "Printing Programming Topics for Cocoa":\ http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Printing/Printing.html\ \ Our loan-print-view class is entirely new, so we will examine it and its methods in some detail. The first lines you will see in the file loan-pr-view.lisp specify some required files.\ \ \f1 (eval-when (:compile-toplevel :load-toplevel :execute)\ (require :date)\ (require :nslog-utils)) \f0 \ \ You probably looked at the date.lisp utility file when we used it earlier. We will use another function from that file here to generate an appropriate string to print.\ \ The nslog-utils.lisp utility file is worth a look because of its use in debugging. In fact, these functions are not actually used in the final code. I left calls to them in the code as comments so that you can see how I went about seeing what was going on. For a normal lisp program, the programmer might find it useful to insert format statements to print out significant values while a function is running. Unfortunately, you can get some pretty strange results if you try to do that within an Objective-C function that is called in response to some event. I crashed CCL more than once before I got it through my head that this just wasn't a very smart thing to do. There is, however, a way to do something equivalent and that is to invoke the same #_NSLog function that Objective-C programmers do in their code. This will create an entry into the console log that can be easily viewed using Apple's Console application. I added some utility functions that make calling this a bit easier. These are largely self-explanatory, so I show them here without further explanation. I encourage you to create your own versions of these for other sorts of objects.\ \ \f1 (defun log-rect (r &optional (log-string ""))\ (#_NSLog (lisp-to-temp-nsstring (concatenate 'string \ log-string \ "x = %F y = %F width = %F height = %F"))\ #>CGFloat (ns:ns-rect-x r)\ #>CGFloat (ns:ns-rect-y r)\ #>CGFloat (ns:ns-rect-width r)\ #>CGFloat (ns:ns-rect-height r))) \f0 \ \ \f1 (defun log-size (s &optional (log-string ""))\ (#_NSLog (lisp-to-temp-nsstring (concatenate 'string \ log-string \ "width = %F height = %F"))\ #>CGFloat (ns:ns-size-width s)\ #>CGFloat (ns:ns-size-height s))) \f0 \ \ \f1 (defun log-float (f &optional (log-string ""))\ (#_NSLog (lisp-to-temp-nsstring (concatenate 'string \ log-string \ "%F"))\ #>CGFloat f)) \f0 \ \ \f1 (defun interleave (l1 l2)\ (let ((lst1 (if (listp l1) l1 (list l1)))\ (lst2 (if (listp l2) l2 (list l2))))\ (if (atom l1)\ (setf (cdr lst1) lst1)\ (if (atom l2)\ (setf (cdr lst2) lst2)))\ (mapcan #'(lambda (el1 el2)\ (list el1 el2))\ lst1\ lst2))) \f0 \ \ \f1 (defun log-4floats (f1 f2 f3 f4 &optional (log-strings '("" "" "" "")))\ (#_NSLog (lisp-to-temp-nsstring (apply #'concatenate 'string \ (interleave log-strings "%F ")))\ #>CGFloat f1\ #>CGFloat f2\ #>CGFloat f3\ #>CGFloat f4)) \f0 \ \ Now we'll continue with the loan-print-view.lisp file.\ \ \f1 (defclass loan-print-view (ns:ns-view)\ ((loan :accessor loan \ :initarg :loan)\ (attributes :accessor attributes \ :initform (make-instance ns:ns-mutable-dictionary))\ (page-line-count :accessor page-line-count\ :initform 0)\ (line-height :accessor line-height)\ (page-num :accessor page-num\ :initform 0)\ (page-rect :accessor page-rect))\ (:metaclass ns:+ns-object)) \f0 \ \ Since we are not going to print anything that looks very much like what is displayed in our window, we will define a brand new view that we will give to the printing functions. We saw previously that an instance of this view was created in the loan-doc object's #/printOperationWithSettings:error: method. This view class contains a slot that will link to the loan object so that we can retrieve the data to be printed. It also has slots that contain various values and objects necessary to printing. Each of these will be discussed as they are used.\ \ \f1 (defmethod initialize-instance :after ((self loan-print-view)\ &key &allow-other-keys)\ ;; assure loan still exists if user closes the window while we are printing\ (#/retain (loan self))\ (ccl:terminate-when-unreachable self)\ (let* ((font (#/fontWithName:size: ns:ns-font #@"Courier" 8.0)))\ (setf (line-height self) (* (+ (#/ascender font) (abs (#/descender font))) 1.5))\ (#/setObject:forKey: (attributes self) font #$NSFontAttributeName))) \f0 \ \ When initializing the loan-print-view we first make sure that the loan-doc object won't go away if the user decides to print a loan and then close the loan-doc window before printing has been completed. This is done using a #/retain call on the loan-doc. When this object is reclaimed by garbage collection we will do the corresponding #/release.\ \ Next the function specifies a font to use. I picked a fixed-width font to make things line up on the page a bit more uniformly and a size that makes lines fit well, but feel free to experiment to find choices that you like better. Next we set the line-height that should be used for each line based on the font. There are possibly more precise ways to do this, but they seemed to involve more futzing around than I wanted to do and the technique shown here seemed to work quite nicely. The only attribute that we will set for the text that we will be printing is the font. If you want to make something bold or italic or otherwise modify the attributes, this might be a good place to do that.\ \ \f1 (defmethod ccl:terminate ((self loan-print-view))\ (#/release (loan self))\ (#/release (attributes self))) \f0 \ \ As previously noted, the ccl:terminate method will release objects that it owns.\ \ \f1 (objc:defmethod (#/knowsPageRange: :) \ ((self loan-print-view) (range (:* #>NSRange)))\ ;; compute printing parameters and set the range\ (let* ((pr-op (#/currentOperation ns:ns-print-operation))\ (pr-info (#/printInfo pr-op))\ ;; (pg-size (#/paperSize pr-info))\ ;; (left-margin (#/leftMargin pr-info))\ ;; (right-margin (#/rightMargin pr-info))\ ;; (top-margin (#/topMargin pr-info))\ ;; (bottom-margin (#/bottomMargin pr-info))\ (image-rect (#/imageablePageBounds pr-info))\ (pg-rect (ns:make-ns-rect 0 \ 0 \ (ns:ns-rect-width image-rect)\ (ns:ns-rect-height image-rect))))\ ;; (log-size pg-size "pg-size: ")\ ;; (log-4floats left-margin right-margin top-margin bottom-margin \ ;; (list "Margins: left = " " right = " " top = " " bottom = "))\ ;; (log-rect image-rect "imageable rect: ")\ (setf (page-rect self) pg-rect)\ ;; (log-rect pg-rect "my page rect: ")\ (#/setFrame: self pg-rect)\ ;; (log-float (line-height self) "Line Height: ")\ (setf (page-line-count self) (floor (ns:ns-rect-height pg-rect) \ (line-height self)))\ ;; start on page 1\ (setf (ns:ns-range-location range) 1)\ ;; compute the number of pages for 9 header lines on page 1 and 2 header\ ;; lines on subsequet pages plus a line per payment\ (let* ((pay-lines-on-p-1 (- (page-line-count self) 7))\ (other-pages-needed (ceiling (max 0 (- (list-length (pay-schedule (loan self)))\ pay-lines-on-p-1))\ (page-line-count self))))\ (setf (ns:ns-range-length range)\ (1+ other-pages-needed))))\ #$YES) \f0 \ \ The #/knowsPageRange: method is called to let the user do completely custom pagination if desired. If this method returns #$YES, then the view will subsequently be asked for the rectangle to use for each page via calls to the #/rectForPage method. As a side-effect, this method must set the page range in the range parameter that is passed in.\ \ I did quite a bit of experimenting to figure out the relationship between various rectangles and things like margins. Unfortunately that was a (rather rare) necessity because I was not able to get a very clear picture from any of Apple's documentation. You will see vestiges of my search included as comments in the source code. I decided to leave all of them there so that I could discuss what I found out and perhaps help others to understand a bit better. #/paperSize simply returns the exact paper size available in points in the user space. So for my 8.5" x 11" paper it returned a size structure with the width = 612.000000 height = 792.000000. This is exactly what would be expected at 72 points per inch, which is what all of Apple's drawing assumes. Note that always having 72 points per inch doesn't pose a drawing limitation of any kind because coordinates can be specified as fractions of points. \ \ The left and right margins both returned 72 points (i.e. 1") and both top and bottom margins returned 90 points (i.e. 1.25"). As near as I can tell, the default print functions do nothing with these values. I believe that they are there only to provide information for developers who want to lay out their own pages. The way that you would use these is described in the next paragraph.\ \ The #/imageablePageBounds call resulted in \ imageable rect: x = 18.000000 y = 40.000000 width = 576.000000 height = 734.000000 \ This turned out to be the most useful value to have as it precisely specifies the maximum width and height of a view rectangle that will fit onto a page. The x and y values specify where the user view will be placed on the page. You don't need to worry about that unless you want your drawing routine to honor the specified margins. If so, then the drawing routine would need to know both the requested margin, as returned by the #/...Margin calls and the minimal margin that you will get regardless, as specified by the x and y values returned by the #/imageablePageBounds call. It would have to subtract the minimal margin values from the requested margin values to derive an additional amount of margin that the drawing routine should provide in order to see the requested margins on the printed page. Then rectangles used within the drawing routine would have to be positioned accordingly. I did not do that for this project, which results in printing that is rather close to the edges of the pages. Feel free to adjust the printing routines to respect requested margins.\ \ For this project both the page-rect slot and the frame rect of the view are set to a rectangle positioned at 0,0 with the same bounds as the imageable page. This is the maximum space that can be printed by the printer. We will see how this is used below. The page-line-count slot is set to a computed value that is the number of lines that can be fitted into the space available on the imageable page. Finally, to determine the number of pages that will be needed we first determine the total number of loan schedule lines that can be printed on the first page (i.e. after allowing for the fixed information at the top of the page) and then divide the number of additional lines needed by the number of lines per page. We set the range parameter to be the total number of pages required.\ \ \ \f1 (objc:defmethod (#/rectForPage: #>NSRect)\ ((self loan-print-view) (pg #>NSInteger))\ (setf (page-num self) (1- pg))\ (page-rect self)) \f0 \ \ The #/rectForPage: method is called to get a rectangle for a particular page number. Page numbers start with 1, so we subtract 1 to make it 0-relative and set the page-num slot, which will be used by the drawing routine to decide what to print. The rectangle returned is always the same, namely the one that we previously computed and saved which is positioned at 0,0 and has the size of the imageable space on a printer page.\ \ \f1 (objc:defmethod (#/isFlipped #>BOOL)\ ((self loan-print-view))\ ;; we compute coords from upper left\ #$YES) \f0 \ \ Normally coordinates in views have the origin at the lower left corner. For our purposes computation is a little easier if we have the origin in the upper left, so the #/isFlipped method is overridden to return #$YES. The Cocoa drawing environment makes several changes to accommodate this, including changes to how fonts are drawn so that they have the correct orientation. Note that this also affects how rectangles are specified, namely the position specified now refers to the upper left corner rather than the lower left corner.\ \ \f1 (objc:defmethod (#/drawRect: :void)\ ((self loan-print-view) (r #>NSRect))\ (with-slots (loan attributes page-line-count line-height page-num page-rect) self\ (ns:with-ns-rect (line-rect (ns:ns-rect-x r) \ (- (ns:ns-rect-y r) line-height)\ (ns:ns-rect-width r) \ line-height)\ \ ;; (log-rect r "draw rect: ")\ (labels ((draw-next-line (str)\ (incf (ns:ns-rect-y line-rect) line-height)\ (#/drawInRect:withAttributes: \ (lisp-to-temp-nsstring str)\ line-rect\ attributes))\ (draw-next-payment (sched-line)\ (draw-next-line \ (format nil\ "~1\{On ~a balance = $~$ + interest of $~$ - payment of $~$ = ~a balance of $~$~\}"\ sched-line))))\ (when (zerop page-num)\ ;; print all the basic loan info\ (draw-next-line (format nil \ "Loan ID: ~a" \ (ns-to-lisp-string (#/displayName loan))))\ (draw-next-line (format nil \ "Amount: $~$"\ (/ (loan-amount loan) 100)))\ (draw-next-line (format nil \ "Origination Date: ~a"\ (date-string (origination-date loan))))\ (draw-next-line (format nil \ "Annual Interest Rate: ~7,4F%"\ (* 100 (interest-rate loan))))\ (draw-next-line (format nil \ "Loan Duration: ~D month~:P"\ (loan-duration loan)))\ (draw-next-line (format nil \ "Monthly Payment: $~$"\ (/ (monthly-payment loan) 100)))\ ;; draw spacer line\ (incf (ns:ns-rect-y line-rect) line-height))\ ;; print the appropriate schedule lines for this page\ (let* ((lines-per-page (- page-line-count (if (zerop page-num) 7 0)))\ (start-indx (if (zerop page-num) 0 (+ (- page-line-count 7) \ (* lines-per-page (1- page-num)))))\ (end-indx (min (length (pay-schedule loan)) \ (+ start-indx lines-per-page 1))))\ (dolist (sched-line (subseq (pay-schedule loan) start-indx end-indx))\ (draw-next-payment sched-line))))))) \f0 \ \ The #/drawRect: method is where things really happen. The rectangle passed into the routine tells the view where to draw. In our case that rectangle will always be the same as the one that we provided in the #/rectForPage method, but that isn't necessarily always the case. If, for example, we had provided a rectangle that was larger than the imageable area, then the Cocoa printing functions would have decided whether to clip or scale the page according to parameters set in the NSPrintInfo object that we provided. If the rectangle was to be clipped, then the rectangle passed to this method could have been smaller than the one we provided in the #/rectForPage method. So its probably always a good idea to make use of the rectangle parameter rather than the one that you expect to get.\ \ This method makes use of the ability of an NSString to draw itself within a specified rectangle using specified attributes. Basically what we do here is create such a rectangle at the top of the page that spans the width available and as each line is printed we move it down the page. We move the rectangle down prior to printing, so it is initialized above the actual position desired for the first line.\ \ Two utility functions are defined within our #/drawRect: method using the labels construct: draw-next-line and draw-next-payment. The draw-next-line function increments the line rectangle and then converts a lisp string parameter into an NSString object and tells it to draw itself in that rectangle. The draw-next-payment function formats a single line from the payment schedule and then calls draw-next-line to print it. A single list of values is passed as a parameter to this function so the format function uses the "~1\{...\}" construct to iterate over that list. The iteration count specifier, 1, is needed because there are more values in the list than we actually want to use and we don't want the format statement to begin using them for a second iteration. If that happened it would run out of parameters and trigger an error.\ \ The rest of the #/drawRect: method is straight-forward. Header lines are first formatted and printed if we are printing page zero. Then we extract the appropriate sub-sequence of loan payment lines from the loan-doc's pay-schedule and print each of them on a subsequent line. Each payment-line is a list of values, so it is easy to just pass that list to the draw-next-payment function as discussed above. Arguably we should grab the pay-schedule and copy it once at the beginning of printing so that if the user changes some parameter in the window while printing is being done it won't affect what is printed. In practice it all seemed to happen pretty quickly, but if you are concerned about this, go ahead and make this change as well.\ \ And that is all of the code needed to turn our loan application into a full-fledged document application that will run under the CCL IDE. Just by virtue of doing a "(require :loan-doc)" in the listener you will see three new menu-items in the File menu. You can use them to make a new loan document that you can then save and subsequently open and of course you can print any loan document. Just remember that if you accidentally choose the "Print ..." method that CCL provides while a loan window is active, the result will be that only one text field will be printed.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 Challenges: \i0 \ \ Modify the lisp-document-controller's ccl:terminate method so that if it should ever by garbage-collected it would remove the menu items that were added; thus removing all trace of our document class. It should also close any open documents at that time. To do that appropriately it will need to know which documents are still open. Modify the loan-document object to update the appropriate lisp-document-controller object so that it can remove the document from its documents slot when it is closed.\ \ Modify the print routines to respect the requested margins.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 Projects 8-11: Lisp Controller Projects \b0 \ \ In Project 5 where we built the Package viewing window, it was necessary to implement Objective-C methods for supplying the proper elements from our lisp arrays as demanded by the NSTextView interface objects. As you might expect, doing this sort of thing is fairly common, both in the Objective-C and Lisp worlds. To help developers, Cocoa provides a number of "data controller" classes. The idea is that you can, for example, hand an NSArray to an NSArrayController and it will provide all of the relevant interface methods that make it act as a data source for an NSTableView. These controllers also have "add" and "remove" methods that can be invoked by interface buttons in order to create and add or remove an Objective-C object from the collection. This is all well and good if you happen to have an NSArray or other Objective-C container class and if every sort of thing that you want inside your collection is an Objective-C object, but that's not so useful for Lisp programmers.\ \ We could, of course, just use those Objective-C container classes directly if wanted to go to a lot of trouble. But in my opinion you might as well just use Objective-C directly if you want to go that route. Instead, in this project we'll create an analog to an Objective-C controller that knows how to use Lisp arrays, lists and hash-tables as container objects and which can instantiate any sort of Lisp object and add it to the collection in response to a user-interface control action. In other words, it will act very similarly to a standard Objective-C controller, but be much more useful to Lisp programmers.\ \ In addition, we would like our Lisp controller to be easily accessible from within IB, so I have provided an Xcode project that you can build. It will create a framework that you will need to install and an IB Plugin module that you will need to load into Interface Builder. That will allow you to directly select and configure a lisp-controller object as part of your interface design.\ \ For purposes of this document I assume that developers prefer to just use my lisp-controller object and do not care about understanding what goes on behind the scenes. To accommodate those who want a deeper understanding or who might want to create variations of their own I have created a reference and tutorial for the lisp-controller:\ ...ccl/contrib/krueger/Interface Projects/Documentation/LispController Reference\ that also describes its implementation in some detail.\ \ Before describing projects that use the the lisp-controller object it is necessary to build and install the Lisp Controller plugin so that the lisp-controller class is accessible within IB. The instructions for doing this are included in the LispController Reference manual. Do that now, I'll wait.\ \ OK. Now that you have the plugin installed in IB we can consider some ways that it can be used.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 Introduction \i0\b0 \ \ All example code for the lisp-controller will be found in the ...ccl/contrib/krueger/InterfaceProjects directory.\ \ The instructions that follow assume some familiarity with IB procedures. If you're not already familiar with how to use IB, you may want to look at early projects described in InterfaceBuilderWithCCLTutorial2.0.pdf which describes the use of Interface Builder (in conjunction with Lisp of course) in much more detail. Each example assumes that you have mastered the techniques described in previous examples.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 8: Auto generated list displayed in an NSTableView \b0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \fs24 \cf0 \ This example shows how to configure a lisp-controller to generate new objects that are displayed in an NSTableView. It demonstrates the use of indices, function names, and forms as column accessors. It shows how to configure interface buttons to add and remove objects from the table. It shows how to use number formatters for table columns both to control how data is displayed and to provide hints that tell the lisp-controller how data conversion should be done. Finally, it contains example notification functions for all types of notification. This example doesn't do anything too useful, but it nicely illustrates a number of different lisp-controller features.\ \ The Lisp code and NIB file for this example are in the directory "ip:Controller Test 1". To follow along you can double-click lc-test1.nib so that it opens up in IB. Or you can start with a new Cocoa window nib and follow along with the instructions below to see how everything gets set up. If you start up IB without giving it a file or if you have IB started and select "New" from the file menu, IB will ask you to select a template to be used as a basis for your new nib. From the Cocoa category select "window".\ \ Select the File's Owner object and in the Object Identity browser pane set the class field to "LispControllerTest". This will be the name of the class that we will create that will load this nib at runtime. Add an outlet field for this class call "lispCtrl". When you get done with that, the Identity pane should look much like Figure 8.1 below.\ \ {{\NeXTGraphic Pasted Graphic 8.tiff \width5740 \height10580 }¬}\ Figure 8.1: LispControllerTest Identity\ \ Next locate and drag a Table View object from the Library to your new window. Configure it to have four columns. Label the columns and window as you see fit. Add a couple of buttons below the table and label them "+" and "-" or whatever you like to indicate that they result in adding and deleting a row, respectively. When completed, this window may look something like Figure 8.2 below.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 9.tiff \width9740 \height5780 }¬}\ Figure 8.2: Lisp Controller Test 1 window\ \ Now we will add a lisp-controller to the mix. From the "Lisp Controller Plugin" folder in the Library window, drag a Lisp Controller object to the document window (where the File's Owner and other objects are already shown). Control-click on the lisp-controller and a pop-up window similar to the one in Figure 8.3 will be shown.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 10.tiff \width6340 \height3760 }¬}\ Figure 8.3: Lisp Controller bindings\ \ Click in the circle to the right of the "owner" outlet and drag that to the File's Owner object. Similarly connect the view outlet to the Table View in your display window. Control-click on the Table View and connect both the delegate and dataSource outlets to the Lisp Controller object. This will cause the Table View to ask the Lisp Controller for data when it needs it and to tell the LIsp Controller about events that occur that might be of interest (such as changing what is selected). \ \ Control-click on the "+" button (or your equivalent) and drag to the Lisp Controller object. Link it to the "insert:" action. Similarly control-click and drag from the "-" button to the Lisp Controller and link it to the "remove:" action. As you might expect, this will cause the buttons to send the action specified to the Lisp Controller when they are pressed. We want to make sure that those buttons will only be enabled when it is appropriate. For example, we would want to disable the "-" button if nothing was currently selected or there wasn't anything in the table. To do that we'll bind the enabled state of the button to a slot value in our Lisp Controller. If you've worked through the tutorial you're already familiar with bindings and how they work. If not, well, trust me and follow the directions and everything will work out. Click on the "+" button and select the bindings pane in the inspector window. Click on the small arrow by "Enabled" to show the fields for that section. \i Before \i0 you click in the checkbox before "Bind to:", select "Lisp Controller" from the pull-down. Then click in the check-box. If you don't do it in this order, then IB may add whatever object is the default in the pull-down to your document window. If that happens to you, just delete the object. In the "Model Key Path" field put "canInsert". This is the name of a foreign slot in the Lisp Controller that at runtime will be contain a logical value that says whether it is currently appropriate to enable insertion into the table. The bindings pane for the "+" button should look like Figure 8.4 below.\ \ {{\NeXTGraphic Pasted Graphic 12.tiff \width5740 \height10580 }¬}\ Figure 8.4: "+" button bindings\ \ \ Similarly bind the enable state of the "-" button to the Lisp Controller Model Key Path "canRemove".\ \ After you've done all that, control-click on the Lisp Controller object and the pop-up should look much like Figure 8.5 below.\ \ {{\NeXTGraphic Pasted Graphic 11.tiff \width6340 \height6140 }¬}\ Figure 8.5: Lisp Controller after linking\ \ \ Next we'll configure the Lisp Controller for our application. Click on the Lisp Controller and select the attributes pane in the inspector window. Initially this will look like Figure 4 above. For this example we'll display a list of lists. Each sublist will represent one row in the table and will have four elements, one for each column. We'll make the first column be a date and we'll sort the rows in the table by the numeric value in the second column. The third and fourth elements in the sublist will be arbitrary lisp objects. Simple!\ \ The first thing to do is to set the Root Type field to "list" and check the "Generate Root" box. We'll see in a bit how this happens, but effectively what we've told the Lisp Controller is that to create a new root object it needs to create a "list". Next, in the Type Information table specify that the child type of the list type is "cons". This takes a little advantage of the Lisp type hierarchy to avoid having to define our own types in order to tell the Lisp Controller what to do. We've now told the Lisp Controller that in order to insert a new object into the root (of type List) that it needs to create a new object of type "cons".\ \ The initform table tells the Lisp Controller exactly how to create new objects of specified types. For the "cons" type enter the initform: (ctl::make-dated-list). When the Lisp Controller needs a new object of type "cons" it will evaluate this form. For the type "list" enter the initform: nil. This will be evaluated to create a new object of the type "list". This will be done to generate a new root, just as we requested when we checked the "Generate Root" box earlier.\ \ We decided that we wanted to sort the rows based on the descending value of the second element in each row. To make that happen we make an entry into the sort table. For the cons type we enter a Sort Key of #'second and a Sort Predicate of #'>. \ \ Next we configure the Lisp Controller to call specified functions to notify us when various events occur. We'll look more closely at those functions when we get to the Lisp code, but for now enter #'ct1::selected-cell as the "When Selected" function, #"ct1::edited-cell as the "When Edited" function, #'ct1::added-row as the "When Added" function, and #'ct1::removed-row as the "When Removed" function.\ \ Once we're done with this configuration, the Lisp Controller attributes pane should look like Figure 8.6.\ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic 14.tiff \width5740 \height15180 }¬}\ Figure 8.6: Completed Lisp Controller attributes\ \ The final thing to do in IB is to configure each column of the table so that it displays the values that we want in an appropriate manner. We'll attach a date formatter for the first column and a number formatter for the second. Let's start with the date formatter for column 1. Drag a Date Formatter object from the Library window into your document window. Next we need to connect that formatter to the column. Attaching a formatter to a column can be a bit tricky because it must actually be connected to the Text Field Cell object for the column. Recall that in IB you get to more deeply embedded objects within a complex arrangement of cooperating objects like a Table View by clicking repeatedly. Each click selecting a more deeply embedded object. The trick to getting to the Text Field Cell is (assuming nothing in the Table is already selected) to click 4 consecutive times over the "Text Cell" displayed in the window. Each click selects a more deeply embedded object until you get to the Text Cell. Now control-click and drag from the Text Cell in the first column to the Date Formatter object. You can then configure the Date Formatter to display dates in whatever form you wish: long, short, or something in between.\ \ Similarly, add a Number Formatter to your document window and connect it to the Text Cell for the second column. Why do we do this? Recall that we decided to sort on this value and we have also left all columns editable. If a user decided to enter a non-numeric value into a value for the second column in some row, then our sorting would try to compare it using the #'< predicate that we specified, resulting in a run-time error. We could write our own predicate that did type checking and did something appropriate with anything that might be there, but in this case it is trivially easy to just enforce the number constraint so that we don't have to worry about it.\ \ Ok, so we have now constrained how things look a bit, but we still need to configure each column so that the Lisp Controller knows how to retrieve the value that will be displayed in each column from the list that represents each row. For this example we will do something fairly simple, namely just use the corresponding list element for each column. But we could just as well use any arbitrary function on that list for each column. Select the Table Column object for column 1 (click three times over the column if nothing is already selected). If you have not already done so, you can change the column's title here. In the "Identifier" field type the number 0. This is an indexical accessor that tells the Lisp Controller that if the row is represented by a sequence it should retrieve the value for this column from element 0 of that sequence. Similarly, set the Identifier for the second Table Column to the number 1. Just to be different and to demonstrate that other forms of accessor work, set the Identifier for the third Table Column to #'third. And demonstrating that a form also works, set the Identifier field for the fourth Table Column to (nth 3 :row). At runtime the keyword :row is effectively replaced by the actual row object (in this case a list). Arbitrary forms and functions are permitted. And the object that represents a row could be an arbitrary object as well.\ \ We are now done with IB. If you have created your own NIB rather than use mine, save it in some convenient location and make sure to save it as a NIB rather than as an XIB. See the tutorial for more information about this difference.\ \ The lisp code for this example is "ip;Controller Test 1;controller-test1.lisp" and is also shown immediately below:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 ;; controller-test1.lisp\ \ ;; Test window that displays lisp lists using an NSTableView\ \ (eval-when (:compile-toplevel :load-toplevel :execute)\ (require :lisp-controller)\ (require :ns-string-utils)\ (require :nslog-utils)\ (require :date))\ \ (defpackage :controller-test1\ (:nicknames :ct1)\ (:use :ccl :common-lisp :iu :lc)\ (:export test-controller get-root))\ \ (in-package :ct1)\ \ (defclass lisp-controller-test (ns:ns-window-controller)\ ((lisp-ctrl :foreign-type :id :accessor lisp-ctrl))\ (:metaclass ns:+ns-object))\ \ (defmethod get-root ((self lisp-controller-test))\ (when (lisp-ctrl self)\ (root (lisp-ctrl self))))\ \ (objc:defmethod (#/initWithNibPath: :id)\ ((self lisp-controller-test) (nib-path :id))\ (let* ((init-self (#/initWithWindowNibPath:owner: self nib-path self)))\ init-self))\ \ (defun make-dated-list ()\ (list (now) 0 (random 20) (random 30)))\ \ (defun selected-cell (window controller root row-num col-num obj)\ (declare (ignore window controller root))\ (cond ((and (minusp row-num) (minusp col-num))\ (ns-log "Nothing selected"))\ ((minusp row-num)\ (ns-log (format nil "Selected column ~s with title ~s" col-num obj)))\ ((minusp col-num)\ (ns-log (format nil "Selected row ~s: ~s" row-num obj)))\ (t\ (ns-log (format nil "Selected ~s in row ~s, col ~s" obj row-num col-num)))))\ \ (defun edited-cell (window controller root row-num col-num obj old-val new-val)\ (declare (ignore window controller root))\ (ns-log (format nil "Changed ~s in row ~s, col ~s: ~s to ~s"\ old-val row-num col-num obj new-val)))\ \ (defun added-row (window controller root parent new-row)\ (declare (ignore window controller root))\ (ns-log (format nil "Added ~s to ~s" new-row parent)))\ \ (defun removed-row (window controller root parent old-row)\ (declare (ignore window controller root))\ (ns-log (format nil "Removed row ~s from ~s " old-row parent)))\ \ (defun test-controller ()\ (let* ((nib-name (lisp-to-temp-nsstring\ (namestring (truename "ip:Controller Test 1;lc-test1.nib"))))\ (wc (make-instance 'lisp-controller-test\ :with-nib-path nib-name)))\ (#/window wc)\ wc))\ \ (provide :controller-test1) \f0 \ \ As with all examples, this one is put into its own package so you can safely experiment with it without worrying about interfering with your own code. First we define the lisp-controller-test class, which you will recall was specified as the File's Owner object for our NIB. If you're now thinking that the names are different, then go back to the longer tutorial to learn about name conversions between Objective-C and Lisp. A lisp-controller-test object is an NSWindowController subclass so it knows how to manage windows and load NIB files. It has a single slot that is linked to the lisp-controller object that is created when the NIB is loaded. \ \ The get-root method retrieves the root object from that attached lisp-controller. Since we let the lisp-controller generate the root, we may want to find out what was created at some later point. \ \ The #/initWithNibPath: method will look familiar to anyone who has worked through other tutorials. It is there simply to facilitate loading NIB files from arbitrary locations without having to include the NIB file in the application bundle where NSWindowControllers like to find them.\ \ The make-dated-list function was specified in the initform for the cons type when we configured the Lisp Controller in IB. It creates a list of four elements. The function "now" is defined in "ip:Utilities;date.lisp" and just returns the current time/date in the normal Lisp internal format. Note that we don't have to worry about doing any sort of conversion for display by Objective-C view objects because that is done by the lisp-controller for us.\ \ The next four functions are the notification functions that we specified for the Lisp Controller in IB. These functions only log an appropriate message to the console log. Use the Console application to see these when the application is running. Perhaps what is significant here is understanding the arguments provided by the lisp-controller. For a complete discussion of those parameters see the \i Notification Functions \i0 section in part 2 of this document.\ \ Finally there is a test-controller function that can be called from the REPL to get things started.\ \ And that's it for the configuration and code needed. If everything is installed properly and your initialization functions have been set up as specified in the longer tutorial, then in the listener you can do the following:\ \ \f1 Welcome to Clozure Common Lisp Version 1.5-dev-r13523M-trunk (DarwinX8664)!\ ? (require :controller-test1)\ :CONTROLLER-TEST1\ ("NS-STRING-UTILS" "DATE" "DECIMAL" "NS-OBJECT-UTILS" "NSLOG-UTILS" "ASSOC-ARRAY" "LIST-UTILS" "LISP-CONTROLLER" "CONTROLLER-TEST1")\ ? (ct1:test-controller)\ # (#x129D5E60)>\ ? \f0 \ \ That will open up a window and you can add, edit, and remove to your heart's content.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 9: Class Browser \b0\fs24 \ \ In the second example we will create a somewhat more practical application. It illustrates the interaction between a lisp-controller and an NSOutlineView. It also illustrates how both rows and their children can be objects; in this case they are instances of Lisp Class objects. It's actually a very simple application that illustrates just how easy it is to display hierarchical arrangements of objects. The general idea is that the top-level rows will be all the immediate sub-classes of the class T. Each row will display one of these classes and when that row is expanded the indented rows immediately beneath it will display its immediate sub-classes. Any class with subclasses can be expanded in subsequent rows that are even more indented. The second column will provide the name of the package where the class name is interned. We'll sort all classes at any level alphabetically. In operation the window will look like Figure 9.1 below.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 15.tiff \width11380 \height12620 }¬}\ Figure 9.1: The Class Browser Window\ \ As for previous projects, you can start with the example NIB file "ip:Controller Test 2;lc-test2.nib" or start with your own and build it as described below.\ \ Open up IB and start with a new Cocoa window template. Change the title for the window to "Class Browser" or something else that you prefer. Find and drag an Outline View from the Library window to the new window. Configure it to have two columns labeled "Class" and "Package". Click once on the new Outline View to select the Scroll View object and then select the size pane of the inspector window. Click on both of the red arrows inside the box shown to cause the object to be resized when the window is resized. By default all View objects will automatically resize subviews, which is what we want here, so we'll leave that alone. The resizing behavior that we want is for the last column to remain the same size, even when the window is resized and the first column to get any additional width that comes with being in a bigger window. To get that, select the Outline View object and in the attributes pane of the inspector window select the "First Column Only" option for the "Col. Sizing" value using the pull-down menu.\ \ Next we'll configure the File's Owner object. Click on it and in the Identity Inspector window change the name of its class to LispBrowser. As in previous projects, we'll define a corresponding class in Lisp and create an instance of it at runtime. Add an outlet to this class called lispCtrl of type id. This should look like Figure 9.2 below.\ \ {{\NeXTGraphic 3__#$!@%!#__Pasted Graphic.tiff \width5740 \height10040 }¬}\ Figure 9.2: LispBrowser Identity Inspector\ \ Next we'll add and configure a Lisp Controller for our application. Find the Lisp Controller Plugin folder in the Library window and drag a Lisp Controller object to your document window. First we will link the lisp-controller to other objects in our interface design. Start by ctrl-clicking on the Lisp Controller and drag to the Outline view. Make the Outline View the value for the view outlet. Then ctrl-click and drag from the Lisp Controller to the File's Owner and make it the value of the owner outlet. Next ctrl-click on the outline view and drag from both the dataSource and delegate outlets to the Lisp Controller object. Finally ctrl-click and drag from the File's Owner object to the Lisp Controller and set the lispCtrl outlet. When you have completed all of those actions you can ctrl-click on the Lisp Controller and the window that pops up should be as shown in Figure 9.3 below.\ \ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic 3.tiff \width6340 \height4660 }¬}\ Figure 9.3: Lisp Controller links\ \ \ Next we will configure the lisp-controller attributes. Click on the Lisp Controller object and select the attributes pane in the inspector window. Initially this will look like Figure 4 above. For this example we'll display the names and packages for standard Lisp class objects (i.e. objects of class #). Set the Root Type field to: class. Select the Generate Root checkbox. In the Type Information table we will tell the lisp-controller how to find the "children" of a class and what type to expect for each child. This will require a single entry in the Type Information table. That entry will specify\ Type: class\ Child Type: class\ Children Key: #'ccl::class-direct-subclasses\ So what we have told the lisp-controller is that to find the children of a class object it should funcall #'ccl::class-direct-subclasses on the parent class.\ \ We selected the Generate Root checkbox, so it is necessary to tell the lisp-controller how to construct the root object. Since we told it that the root object was a class, we want to now specify an initform for the type "class". Since we do not allow the addition of any children for this application, constructing the root object is the only time that this initform will be used. So create an entry in the initform table:\ Type: class\ Initform: (find-class t) \ This initform will return the root class which is the super-type of all other classes.\ \ Just for fun, we will order the subclasses shown for any class alphabetically. To do that we will put a single entry in the sort table. That entry will be:\ Type: class\ Sort Key: #'ct2::cl-name\ Sort Predicate: @'string<\ As we'll see when we look at the lisp code for this example, cl-name is a particularly simple function and string< is of course a standard Lisp function.\ \ When the attribute configuration is complete, the inspector window should look like Figure 9.4 below.\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 2.tiff \width5740 \height15340 }¬}\ Figure 9.4: Lisp Controller Attribute Inspector\ \ The last thing to do within IB is to specify accessors for the two table columns. Start by clicking over the first column of the outline view until just the column has been selected. If you have done this correctly, the attribute inspector will show you that you have selected a Table Column. If you have not already set the title for this table column, do so now in the inspector window. I called the first column "Class Name", but you can choose whatever you would like for this. In the Identifier field type in #'ct2::cl-name to indicate that this function should be applied to the row-object (i.e. a class instance) to retrieve the value that should be put into this column. Make sure that this column is not editable by deselecting the Editable check box. If this has been done correctly, the attribute inspector window should look pretty similar to Figure 9.5 below:\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 4.tiff \width5740 \height6640 }¬}\ Figure 9.5: Attribute Inspector for the first column \ \ Modify the second column in much the same way. Title it something like "Package" and set the identifier to be \ #'ct2::cl-package Shortly we'll examine those two Lisp accessor functions.\ \ This completes the interface design using IB. Save the NIB file (not XIB) as lc-test2.nib within the "Controller Test 2" subdirectory of the "Interface Projects" directory.\ \ Now we'll take a look at the Lisp code needed to support this. Open the file "ip:Controller Test 2;controller-test2.lisp" within the CCL IDE.\ \ Everything within this file is done within the package :ct2.\ \ Recall that we specified that the File's Owner is of the class LispBrowser. First we define this class:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass lisp-browser (ns:ns-window-controller)\ ((lisp-ctrl :foreign-type :id :accessor lisp-ctrl))\ (:metaclass ns:+ns-object)) \f0 \ \ This is similar to other NIB-managing classes that were used in projects defined in the InterfaceBuilderWithCCLTutorial. We make it a subclass of NSWindowController in order to make loading and managing NIB files a bit easier.\ \ \f1 (objc:defmethod (#/initWithNibPath: :id)\ ((self lisp-browser) (nib-path :id))\ (let* ((init-self (#/initWithWindowNibPath:owner: self nib-path self)))\ init-self)) \f0 \ \ Similarly, this init method is very similar to other initialization functions done for previous projects. \ \ \f1 (defun cl-name (cls)\ (symbol-name (class-name cls))) \f0 \ \ The cl-name function was specified in IB as both the accessor for the first column and the sort key for ordering the children of any object. This simply returns the class-name as a string.\ \ \f1 (defun cl-package (cls)\ (package-name (symbol-package (class-name cls)))) \f0 \ \ The cl-package function was specified as the accessor for the second column of the table. It simply returns the package where the class name is interned as a Lisp string.\ \ \f1 (defun test-browser ()\ (let* ((nib-name (lisp-to-temp-nsstring\ (namestring (truename "ip:Controller Test 2;lc-test2.nib"))))\ (wc (make-instance 'lisp-browser\ :with-nib-path nib-name)))\ (#/window wc)\ wc)) \f0 \ \ This test function makes an instance of lisp-browser and returns it. All of the rest of the work necessary to support the display is done by the lisp-controller instance in coordination with the NSOutlineView display object. You can expand or contract classes to see their subclasses (if any). Note that they are ordered alphabetically, just as we specified in IB. \ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Project 10: Card Dealer \b0\fs24 \ \ This example is intended to show how the LispController works in conjunction with hash-tables. In fact, not only is the root object a type of hash-table, so are its children and grandchildren. The application deals out four hands of cards (North, South, East, and West) as in a bridge game from a conventional 52-card deck. The display shows each hand and each hand can be expanded to show four suits. A button is defined to deal out a new hand. This isn't a particularly useful application, but does nicely demonstrate the use of hash-tables. And I admit that I have spent some time dealing out hands, showing just the North/South cards, and imagine how I would bid and play them in a bridge match. Then I reveal the East/West cards to figure out what I should have done instead. If you are a bridge player, you might find this amusing.\ \ At runtime after complete expansion of displayed rows the window will look something like Figure 10.1 below.\ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic 5.tiff \width9640 \height10500 }¬}\ Figure 10.1: The Card Dealer window\ \ The positions North, East, South, and West are always shown in this order. And the suits are always displayed in the order shown (which will be familiar to bridge players). For this project it may be easier to look at the Lisp data side of the design first and then design the interface. I am a strong proponent of a data-first design methodology, but my experience is that a sort of iterative approach is probably best: first design the core data structures and data manipulation functionality, next design the interface, and finally design any control/integration functionality needed to bring the two together. So let's see if we can follow that path for this project. \ \ Start by opening up the file "ip:Controller Test 3;controller-test3.lisp" within the CCL IDE. For purposes of this project we want to represent a complete deal of the cards; i.e. four hands, each with four suits, and some number of cards in each suit. There are, of course, many possible ways that we might choose to implement this and I'll admit that my choice was perhaps a bit artificial and made primarily because it demonstrates how hash-tables are handled by the lisp-controller. I chose to represent a whole deal as an instance of an assoc-array. What, you may well ask, is that?\ \ An assoc-array is a class that I created that lets me make a multi-dimensional sparse array that can be indexed by arbitrary lisp objects (anything that could be used as a key in a hash table). I have found this to be a useful way of representing lots of miscellaneous data and in fact these are used in several places in the implementation of the lisp-controller class itself. \ \ Assoc-arrays are defined in the file "ip:Utilities;assoc-array.lisp. An assoc-array is basically a hierarchical set of hash-tables. An assoc-array object contains a root hash-table. Each key in that table is some Lisp value that has been used as the first index when storing into the assoc-array. The value corresponding to that key will either be another hash-table if this is not the last dimension of the assoc-array or the value being stored. In an N-dimensional assoc-array then, there would be N-1 levels of embedded hash-tables and the last index would be the key into the most deeply embedded table. Hash tables are only created as needed to store new values. Perhaps it is just easier to see what the code does ...\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass assoc-array ()\ ((arr-rank :accessor arr-rank :initarg :rank)\ (index1-ht :accessor index1-ht)\ (index-tests :accessor index-tests :initarg :tests)\ (default-value :accessor default-value :initarg :default))\ (:default-initargs\ :rank 2\ :tests nil\ :default nil)) \f0 \ \ The assoc-array instance keeps track of the rank of the array, the first level hash-table, the tests that are used for each dimension to define a matching index and a default value for the table that is returned if no value is found. \ \ \f1 (defmethod initialize-instance :after ((self assoc-array) &key tests &allow-other-keys)\ (setf (index1-ht self) \ (make-hash-table :test (or (first tests) #'eql)))) \f0 \ \ When created, the first level hash-table is initialized.\ \ \f1 (defmethod assoc-aref ((self assoc-array) &rest indices)\ (unless (eql (list-length indices) (arr-rank self))\ (error "Access to ~s requires ~s indices" self (arr-rank self)))\ (do* ((res (index1-ht self))\ (index-list indices (rest index-list))\ (indx (first index-list) (first index-list))\ (found-next t))\ ((null index-list) (if found-next\ (values res t)\ (values (default-value self) nil)))\ (if found-next\ (multiple-value-setq (res found-next) (gethash indx res))\ (return-from assoc-aref (values (default-value self) nil))))) \f0 \ \ Like #'aref for normal arrays, this method retrieves an indexed value. And like gethash, if the indices reference an existing entry, then that value is returned and otherwise the default value is returned. A second returned value indicates whether what was returned was found (t) or the default (nil).\ \ \f1 (defmethod (setf assoc-aref) (new-val (self assoc-array) &rest indices)\ (unless (eql (list-length indices) (arr-rank self))\ (error "Access to ~s requires ~s indices" self (arr-rank self)))\ (let* ((ht (index1-ht self))\ (last-indx (do* ((dim 1 (1+ dim))\ (index-list indices (rest index-list))\ (indx (first index-list) (first index-list))\ (tests (rest (index-tests self)) (rest tests))\ (test (first tests) (first tests)))\ ((>= dim (arr-rank self)) indx)\ (multiple-value-bind (next-ht found-next) (gethash indx ht)\ (unless found-next\ (setf next-ht (make-hash-table :test (or test #'eql)))\ (setf (gethash indx ht) next-ht))\ (setf ht next-ht)))))\ (setf (gethash last-indx ht) new-val))) \f0 \ \ The setf function for assoc-aref will create new hash-tables as necessary to represent each dimension of the path defined by the indices provided. At the end of the path the value is put into the final hash-table.\ \ Following are several utility functions that can be used to inspect what is in an assoc-array. They are provided here without additional explanation. Hopefully the code is relatively self-explanatory.\ \ \f1 (defmethod mapcar-assoc-array ((func function) (self assoc-array) &rest indices)\ ;; collects list of results of applying func to each bound index at\ ;; the next level after the indices provided.\ (unless (<= (list-length indices) (arr-rank self))\ (error "Access to ~s requires ~s or fewer indices" self (arr-rank self)))\ (do* ((res (index1-ht self))\ (index-list indices (rest index-list))\ (indx (first index-list) (first index-list))\ (found-next t))\ ((null index-list) (when found-next\ ;; apply map function to res\ (typecase res\ (hash-table (mapcar-hash-keys func res))\ (cons (mapcar func res))\ (sequence (map 'list func res)))))\ (if found-next\ (multiple-value-setq (res found-next) (gethash indx res))\ (return-from mapcar-assoc-array nil)))) \f0 \ \ \f1 (defmethod map-assoc-array ((func function) (self assoc-array) &rest indices)\ ;; collects list of results of applying func of two arguments to \ ;; a bound index at the next level after the indices provided and to\ ;; the value resulting from indexing the array by appending that index\ ;; to those provided as initial arguments. This would typically be used\ ;; to get a list of all keys and values at the lowest level of an\ ;; assoc-array.\ (unless (<= (list-length indices) (arr-rank self))\ (error "Access to ~s requires ~s or fewer indices" self (arr-rank self)))\ (do* ((res (index1-ht self))\ (index-list indices (rest index-list))\ (indx (first index-list) (first index-list))\ (found-next t))\ ((null index-list) (when found-next\ ;; apply map function to res\ (typecase res\ (hash-table (map-hash-keys func res))\ (cons (mapcar func res nil))\ (sequence (map 'list func res nil)))))\ (if found-next\ (multiple-value-setq (res found-next) (gethash indx res))\ (return-from map-assoc-array nil)))) \f0 \ \ \f1 (defun print-last-level (key val)\ (format t "~%Key = ~s Value = ~s" key val)) \f0 \ \ \f1 (defmethod last-level ((self assoc-array) &rest indices)\ (apply #'map-assoc-array #'print-last-level self indices)) \f0 \ \ \f1 (defmethod map-hash-keys ((func function) (self hash-table))\ (let ((res nil))\ (maphash #'(lambda (key val)\ (push (funcall func key val) res))\ self)\ (nreverse res))) \f0 \ \ \f1 (defmethod mapcar-hash-keys ((func function) (self hash-table))\ (let ((res nil))\ (maphash #'(lambda (key val)\ (declare (ignore val))\ (push (funcall func key) res))\ self)\ (nreverse res))) \f0 \ \ OK, so now that you know a little bit more about what an assoc-array is, we'll show how to use one to represent four bridge hands. Later we will display it in an NSOutlineView using our lisp-controller. All the following code is defined in package :ct3 as shown in controller-test3.lisp. At this point we will only look at the parts of that file that define the data structures (i.e. the "model" from Apple's Model/View/Controller paradigm). After that we will define the interface (i.e. the View) and finally we will come back to Lisp to add to the Controller part of the process.\ \ We will represent any combination of cards from a deck as a bit vector that is 52 bits long. Cards included have a corresponding bit value of 1. Cards not included have a corresponding bit of 0. This representation can be used to represent a single hand, a complete deck, a single suit, or any other card combination. For example, we define some useful constants as follows:\ \ \f1 (defconstant *aces* #*1000000000000100000000000010000000000001000000000000)\ (defconstant *kings* #*0100000000000010000000000001000000000000100000000000)\ (defconstant *queens* #*0010000000000001000000000000100000000000010000000000)\ (defconstant *jacks* #*0001000000000000100000000000010000000000001000000000)\ (defconstant *spades* #*1111111111111000000000000000000000000000000000000000)\ (defconstant *hearts* #*0000000000000111111111111100000000000000000000000000)\ (defconstant *diamonds* #*0000000000000000000000000011111111111110000000000000)\ (defconstant *clubs* #*0000000000000000000000000000000000000001111111111111) \f0 \ \ In addition, we want to define some constants to represent card ranks and suits:\ \f1 \ (defconstant *card-ranks* '("A" "K" "Q" "J" "10" "9" "8" "7" "6" "5" "4" "3" "2"))\ (defconstant *card-suits* '("Spades" "Hearts" "Diamonds" "Clubs"))\ (defconstant *hand-suits* '("Spades" "Hearts" "Diamonds" "Clubs" "North" "East" "South" "West")) \f0 \ \ Using all of these constants we can easily create two utility functions that tell us about any particular card.\ \ \f1 (defun card-rank (card)\ ;; card is a bit index\ (nth (mod card 13) *card-ranks*)) \f0 \ \ \f1 (defun card-suit (card)\ ;; card is a bit index\ (nth (floor card 13) *card-suits*)) \f0 \ \ To deal cards we will start with a full deck and randomly remove cards from it one by one. \ \ \f1 (defun deal-cards ()\ ;; randomizes and returns four unique hands\ (let ((deck (full-deck))\ (deal (make-instance 'assoc-array :rank 2))\ (card nil))\ (dotimes (i 13)\ (setf card (pick-random-card deck))\ (add-card deal "West" card)\ (remove-card card deck)\ (setf card (pick-random-card deck))\ (add-card deal "North" card)\ (remove-card card deck)\ (setf card (pick-random-card deck))\ (add-card deal "East" card)\ (remove-card card deck)\ (setf card (pick-random-card deck))\ (add-card deal "South" card)\ (remove-card card deck))\ deal)) \f0 \ \ The deal parameter defined within the let form just creates a two-dimensional assoc-array. The indices of this assoc-array will be the position (i.e. North, East, South, or West) and the suit of the card. The value indexed for any position and suit combination will be a list of the ranks of the cards of the specified suit dealt to the specified position. The function was deliberately designed to represent the way that a dealer would hand out cards; viz. one at a time in rotation. In the following we'll look at the functions called in a little more detail.\ \ \f1 (defun full-deck ()\ (make-array '(52) :element-type 'bit :initial-element 1)) \f0 \ \ The full-deck function simply makes a bit-vector will all bits set.\ \ \f1 (defun pick-random-card (deck)\ ;; returns a card index\ (let* ((cnt (count 1 deck))\ (card (1+ (random cnt))))\ (position-if #'(lambda (bit)\ (when (plusp bit)\ (decf card))\ (zerop card))\ deck))) \f0 \ \ There are almost certainly more efficient mechanisms for selecting a random card from the set, but in practice the previous function seems to work fast enough. It counts the number of cards left and then picks one of them at random, returning the bit index that refers to that card.\ \ \f1 (defun add-card (deal hand card)\ (setf (assoc-aref deal hand (card-suit card))\ (cons (card-rank card) (assoc-aref deal hand (card-suit card))))) \f0 \ \ The add-card function adds the rank of the card to the list of ranks that is the value of the assoc-array for the specified position (as given by the hand parameter) and suit of the card.\ \ \f1 (defun remove-card (card deck)\ ;; card is a bit index\ (setf (aref deck card) 0)) \f0 \ \ This function removes the card from the deck by setting the corresponding bit to 0.\ \ This completes the definition of the basic data structures. Next we will define an interface to display deals. After that we will come back to Lisp to add the necessary glue functionality to pull everything together. As for all projects you can either open up the nib that I have already created: ."ip:Controller Test 3;lc-test2.nib" or start your own window nib in Interface Builder.\ \ If you are starting from scratch, open up IB and start with a new Cocoa window template. Change the title for the window to "Deal It!" as I did, or to something else that you prefer. Find and drag an Outline View from the Library window to the new window. Configure it to have two columns labeled "Position and Suit" and "Cards" or pick your own titles. Click once on the new Outline View to select the Scroll View object and then select the size pane of the inspector window. Click on both of the red arrows inside the box shown to cause the object to be resized when the window is resized. By default all View objects will automatically resize subviews, which is what we want here, so we'll leave that alone. The resizing behavior that we want is for the last column to remain the same size, even when the window is resized and the first column to get any additional width that comes with being in a bigger window. To get that, select the Outline View object and in the attributes pane of the inspector window select the "First Column Only" option for the "Col. Sizing" value using the pull-down menu.\ \ Next add a button to the interface window and label it "New Deal". This will be used to trigger the generation and display of a new deal. Position and size it as you choose.\ \ Next we'll configure the File's Owner object. Click on it and in the Identity Inspector window change the name of its class to HandOfCards. As in previous projects, we'll define a corresponding subclass of NSWindowController in Lisp and create an instance of it at runtime. Add an outlet to this class called lispCtrl of type id. Also add an Action called "deal:" with a type of id. This should look like Figure 10.2 below.\ \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural \cf0 {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 6.tiff \width5740 \height10500 }¬}\ Figure 10.2: Identity of File's Owner\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \cf0 \ Now ctrl-click and drag from the New Deal button the the File's Owner Object. Link it to the "deal:" action that you defined for the File's Owner. Later we'll create a function in Lisp that will handle this message.\ \ Next we'll add and configure a Lisp Controller for our application. Find the Lisp Controller Plugin folder in the Library window and drag a Lisp Controller object to your document window. First we will link the lisp-controller to other objects in our interface design. Start by ctrl-clicking on the Lisp Controller and drag to the Outline view. Make the Outline View the value for the view outlet. Then ctrl-click and drag from the Lisp Controller to the File's Owner and make it the value of the owner outlet. Next ctrl-click on the outline view and drag from both the dataSource and delegate outlets to the Lisp Controller object. Finally ctrl-click and drag from the File's Owner object to the Lisp Controller and set the lispCtrl outlet. These are exactly the same actions that we took for the Class Browser project, so at this point if you ctrl-click on the Lisp Controller object you should see something that looks like Figure 14 from the previous project.\ \ Next we will configure the lisp-controller attributes. Click on the Lisp Controller object and select the attributes pane in the inspector window. Initially this will look like Figure 4 above. For this example we'll display the assoc-array that we used to represent a complete deal, so set the Root Type field to: iu::assoc-array. Make sure that the Generate Root checkbox is NOT checked since we will use the New Deal button to trigger the generation of a new root element to be displayed. In the Type Information table we will tell the lisp-controller how to find the "children" of our assoc-array. Not that we do not need to specify the child-type since we will never be creating new instances of them. For our purposes we want there to be two levels displayed under the root object. The first level contain each of the hands and for each hand we will want to display each of the four suits. This will require two entries in the Type Information table. The first tells the lisp-controller how to find the first-level hash-table within the assoc-array:\ Type: iu::assoc-array\ Children Key: #'ct3::hand-children\ What this will do is take advantage of our knowledge about how assoc-arrays are implemented to return the hash-table that is used internally. This is not generally a good practice, but please remember that I wanted to create an example that used hash-tables and this seemed like an easy way to go. \ \ The second entry in the Type information table is needed to \i block \i0 the default behavior of the lisp-controller. By default the children of a hash-table entry are found by treating the value of that entry as a parent and then finding its children. So the type of the value determines what function is applied to find its children. In our case, when we get to the point where the value of a hash-table entry is a list of card ranks, if we did nothing else its type would be found to be LIST. The default way of finding the children of a list is just to use all elements of the list as the children. So in our case, each card in the list would be treated as a child and the suit would be shown as being expandable. Expanding it would show as many children as there are cards in the suit. So what we really want to do here is define a Children Key entry that guarantees a null return when applied to the list of card ranks. So to do that we define the following entry:\ Type: cons\ Children Key: #'null\ Since CONS is a subtype of LIST, the lisp-controller will identify a list of card-ranks as being of type CONS and look for a valid children-key for that type. We have defined #'null as being that children-key. Applying #'null to any CONS type will return nil, so we have guaranteed that no children will be found. If you would like to see what happens without this table entry, just eliminate it from the NIB file and run the test function.\ \ Note that in our example we took advantage of the fact that we didn't want to use lists as parent objects anywhere else in the table. It is possible that you may not be so lucky and might want some types of lists to have children and some not or have different sorts of lists have different children-keys. In those cases it would be necessary to create an appropriate type definition and use it to distinguish different types of lists. Generally this should be quite easy to do.\ \ No object is being created for us by the lisp-controller, so no entries into the Initform table are required.\ \ We want to order the position and suit hash-table pairs that are displayed so that we see the same order every time. That is, we effectively want to order the children of hash-tables which are, as discussed above, ht-entry objects. There are a few different ways that we could do this. For example, we could define special types that correspond to the different sub-types of ht-entry and then define a sort-key and predicate for each type. For this project we can rather easily define a single predicate for a list of ht-entry children, so that is what we will do. Create a single Sort Table entry:\ Type: lc::ht-entry\ Sort Key: lc::ht-key\ Sort Predicate: ct3::hand-suit-order\ Note that we intentionally specified the sort key and predicate without using the #' notation. It is entirely optional and we left it off here as a means of testing that both forms work correctly. This entry specifies that we should sort a list of ht-entry children using #'ct3::hand-suit-order as a predicate applied to the keys of the ht-entry key-value pairs. I should add parenthetically that I am not entirely happy with the requirement that the developer understand the internals of ht-entry objects in order to make this work and may consider alternative syntax at some future time. \ \ When the attribute configuration is complete, the inspector window should look like Figure 10.3 below.\ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic 9.tiff \width5740 \height15360 }¬}\ Figure 10.3: Lisp Controller Attribute Inspector\ \ The last thing to do within IB is to specify accessors for the two table columns. Start by clicking over the first column of the outline view until just the column has been selected. If you have done this correctly, the attribute inspector will show you that you have selected a Table Column. If you have not already set the title for this table column, do so now in the inspector window. I called the first column "Position and Suit", but you can choose whatever you would like for this. In the Identifier field type in the keyword :key to indicate that the key of the key-value pair should be put into this column. Make sure that this column is not editable by deselecting the Editable check box. If this has been done correctly, the attribute inspector window should look pretty similar to Figure 10.4 below:\ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic 10.tiff \width5740 \height6360 }¬}\ Figure 10.4: Attribute Inspector for the first column \ \ Modify the second column in much the same way. Title it something like "Cards" and set the identifier to be \ #'ct3::sorted-by-rank. By default, this function will be applied to the \i value \i0 of the ht-entry key-value pair. We could have equivalently specified this identifier as the form:\ (ct3::sorted-by-rank :value)\ Shortly we'll examine this Lisp accessor function.\ \ This completes the interface design using IB. Save the NIB file (not XIB) as lc-test3.nib within the "Controller Test 3" subdirectory of the "Interface Projects" directory.\ \ Next we will go back to Lisp and define the remaining functionality required for the Controller part of the Model/View/Controller paradigm.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass hand-of-cards (ns:ns-window-controller)\ ((lisp-ctrl :foreign-type :id :accessor lisp-ctrl))\ (:metaclass ns:+ns-object)) \f0 \ \ This is our NSWindowController subclass.\ \ \f1 (objc:defmethod (#/initWithNibPath: :id)\ ((self hand-of-cards) (nib-path :id))\ (let* ((init-self (#/initWithWindowNibPath:owner: self nib-path self)))\ init-self))\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 This is the same init method that we have used previously. \f1 \ \ (objc:defmethod (#/deal: :void)\ ((self hand-of-cards) (sender :id))\ (declare (ignore sender))\ (unless (eql (lisp-ctrl self) (%null-ptr))\ (setf (root (lisp-ctrl self)) (deal-cards)))) \f0 \ \ The deal method will be invoked when the "New Deal" button is pushed by the user. It sets the root of the associated lisp-controller to the assoc-array that results from calling deal-cards. The lisp-controller then manages all further interaction with the NSOutlineView object.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defun hand-children (hand)\ ;; hand will be an assoc-array\ (iu::index1-ht hand)) \f0 \ \ This function was specified as the children-key for the root assoc-array. This just returns the top-level hash-table from that object. So the root will be a hash-table and its displayed children will be key-value pairs that represent entries within that hash-table. In this case, there will be one key for each of the four positions and the value of each key will be another hash-table.\ \ \f1 (defun hand-suit-order (a b)\ (< (position a *hand-suits* :test #'string=) \ (position b *hand-suits* :test #'string=)))\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0 \cf0 \ The hand-suit-order function was specified as the sort-predicate for ht-entry objects. Since those objects might have either positions as keys or suits as keys (depending on how deeply nested we are), the sort predicate must be able to handle either case. So we made a single ordering that works for either that we called *hand-suits*. This is a bit of a kludge and it might have been somewhat clearer to define a separate type for each subtype of ht-entry and a corresponding sort predicate. If you're ambitious, feel free to modify the code to make this happen.\ \ For the second column we specified an accessor called #'sorted-by-rank. We want this to apply only to values that are a list of card ranks. To accomplish that we define a type called rank-list that can be used to identify such lists and our accessor uses this type to determine whether or not to return a value to be displayed. Note that this is a general consideration needed when displaying objects of different types at different levels of an NSOutlineView. The column accessors will be applied whenever it is valid to do so. So you must assure that specified accessor functions can handle any object type that might be given to it by your application. In this case we assure that the value of the key-value pair is in fact a list of card ranks.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (deftype rank-list ()\ '(satisfies all-ranks)) \f0 \ \ \f1 (defun all-ranks (rank-list)\ (and (listp rank-list)\ (null (set-difference rank-list *card-ranks* :test #'string=)))) \f0 \ \ \f1 (defun higher-rank (r1 r2)\ (< (position r1 *card-ranks* :test #'string=) (position r2 *card-ranks* :test #'string=))) \f0 \ \ \f1 (defun sorted-by-rank (rlist)\ (when (typep rlist 'rank-list)\ (format nil "~\{~a~^, ~\}" (sort-list-in-place rlist #'higher-rank)))) \f0 \ \ The final bit of Lisp code needed is a test function that we can use in the listener to start things off:\ \ \f1 (defun test-deal ()\ (let* ((nib-name (lisp-to-temp-nsstring\ (namestring (truename "ip:Controller Test 3;lc-test3.nib"))))\ (wc (make-instance 'hand-of-cards\ :with-nib-path nib-name)))\ (#/window wc)\ wc)) \f0 \ \ In the listener type (ct3:test-deal) to open up the window.\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 \ Project 11: Binding Test \b0\fs24 \ \ This example is intended to show how the LispController can be used to bind IB interface elements to normal Lisp slots in normal (non-Objective-C) Lisp class instances. The window is simple and will be familiar to those who have worked through the tutorial. We will re-implement the Simple Sum example, but will bind the interface elements to Lisp slots. So the window will look as in Figure 11.1 below:\ \ {{\NeXTGraphic 1__#$!@%!#__Pasted Graphic 1.tiff \width9600 \height5840 }¬}\ Figure 11.1: Simple Sum Window\ \ As usual you can follow along with the interface design by opening the NIB file: "ip:BindingTest;binding.nib" in IB or starting from scratch with your own Window. If you are starting from scratch, open up IB and start with a new Cocoa window template. Change the title for the window to "Sum Window" as I did, or to something else that you prefer. Now let's design the interface. As we did before, select your window and give it a title in the inspector window. From the library window select and drag three "Text Fields" and one Button of your choice to your window and arrange them any way that pleases you. Double-click on your button to rename it to "Sum" If you like, select a text field and use the inspector to explore various options you can select for its appearance.\ \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural \cf0 Note that my third text field is barely visible (only visible at all because I clicked on it) because I used the inspector to make it's border invisible and deselected the "Draws Background" box. I also deselected the "Selectable" and "Editable" boxes so that the user cannot click within it to do anything. When you change an attribute of a view object, IB tries to make it look exactly the way that it will at runtime so that you can easily visualize it. That's true even if it makes your object invisible. Sometimes that can make it hard to find when you need it, so you may want to wait until the design is mostly complete before setting attributes that make something invisible.\ \ {{\NeXTGraphic 4__#$!@%!#__Pasted Graphic.tiff \width5740 \height12320 }¬}\ Figure 11.2: Setting attributes for the output text field\ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \cf0 \ Next we need to add a lisp-controller to the project. Find the Lisp Controller Plugin folder in the Library window and drag a Lisp Controller object to your document window. In the attribute inspector for the Lisp Controller set the root type to bnd::sum-owner. This will be the name of the Lisp class that we will define. As for all Lisp Controller values, this is the Lisp name of the class. Then check the Generate Root checkbox. Since we are only going to use the Lisp Controller as an intermediate target on the way to our Lisp Object it isn't necessary to put any information about its "children" into the type table. But since we configured the Lisp Controller to generate the root object we must tell it how to do so. So make an entry into the Initform table:\ Type: bnd::sum-owner\ Initform: (bnd::random-sum-owner)\ This initform will call a function that we will define in Lisp. It will simply create an instance of the sum-owner class that has the two input values initially set to random values. \ \ This is all of the configuration needed for the Lisp Controller. The only field absolutely required when simply binding through the root object would be the Root Type. When the root object is set, the Lisp Controller will validate that its type is valid. When configuration is complete, the attribute pane for the Lisp Controller should look much like Figure 11.3 below.\ \ \ {{\NeXTGraphic 5__#$!@%!#__Pasted Graphic.tiff \width5740 \height15660 }¬}\ Figure 11.3: Lisp Controller attribute inspector\ \ Finally we need to set up the binding paths for our three TextFields. Click on the first input TextField and then open up the binding pane of the inspector window. Click on the arrow next to "Value" to reveal the binding configuration fields needed to create a binding between the TextField's value and some other object. Before clicking in the "Bind to:" checkbox, first select "Lisp Controller" from the pull-down menu. Then click in the "Bind to:" checkbox. Doing these two things in reverse order will result in IB adding a SharedObjectsController to your document window. You can just select and delete it if this happens to you. Additionally click in the "Continuously Updates Value" checkbox. This makes sure that the value is updated in the bound slot even if you don't leave the Text Field after editing it. Make sure the Controller Key field is blank and enter "root._bnd_input1" into the Model Key Path field. This tells the Text Field to get its value from the bnd::input-1 slot of the Lisp object that is the value of the root of the Lisp Controller. When you get done this should look much like Figure 11.4 below.\ \ \ {{\NeXTGraphic 2__#$!@%!#__Pasted Graphic 1.tiff \width5740 \height8280 }¬}\ Figure 11.5: Binding Pane for first TextField\ \ Now do the same for the second Text Field, but bind its value to root._bnd_input2. Finally bind the sum Text Edit field to "root._bnd_sum".\ \ And that's all of the IB work needed to make this application work. Let's take a look at the Lisp code needed to support it.\ \ The code is contained in the file "ip:BndingTest;binding.lisp". All code is defined within a package named :binding (nickname :bnd). The first thing done is to define an object class to hold the two values and their sum. We will set the root of the lisp-controller to be one of these objects. Then the slots of this object will be referenced from the TextFields by virtue of the bindings that we created in IB. The class is defined as follows:\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f1 \cf0 (defclass sum-owner ()\ ((input-1 :reader input-1 :initarg :input-1)\ (input-2 :reader input-2 :initarg :input-2)\ (sum :reader sum :initform 0))\ (:default-initargs\ :input-1 0\ :input-2 0)) \f0 \ \ The slots were defined to have automatically generated reader methods, but not writers. We will define these explicitly so that we can add code to make them KVC-compliant as follows.\ \ \f1 (defmethod (setf input-1) (new-value (self sum-owner))\ ;; we set up the slot writer to note a change in value to make it KVC compliant\ (will-change-value-for-key self 'input-1)\ (setf (slot-value self 'input-1) new-value)\ (did-change-value-for-key self 'input-1)) \f0 \ \ \f1 (defmethod (setf input-2) (new-value (self sum-owner))\ ;; we set up the slot writer to note a change in value to make it KVC compliant\ (will-change-value-for-key self 'input-2)\ (setf (slot-value self 'input-2) new-value)\ (did-change-value-for-key self 'input-2)) \f0 \ \ \f1 (defmethod (setf sum) (new-value (self sum-owner))\ ;; we set up the slot writer to note a change in value to make it KVC compliant\ (will-change-value-for-key self 'sum)\ (setf (slot-value self 'sum) new-value)\ (did-change-value-for-key self 'sum)) \f0 \ \ Then we add an initialize-instance method just to assure that the sum value initially equals the real sum of the two input value slots:\ \ \f1 (defmethod initialize-instance :after ((self sum-owner) \ &key &allow-other-keys)\ (setf (sum self) (+ (input-1 self) (input-2 self)))) \f0 \ \ Next we create the functions that we specified in IB for creating the lisp-controller's root:\ \ \f1 (defun random-sum-owner ()\ ;; return a sum-owner instance with random initial values\ (make-instance 'sum-owner\ :input-1 (random 100)\ :input-2 (random 100))) \f0 \ \ Next we create a class and methods for the File's Owner class, just as we have done for many other projects. Nothing too special here except to make it the target of the "Sum" button and provide a method that will be invoked when the button is pushed.\ \ \f1 (defclass binding-window-owner (ns:ns-object)\ ((controller :foreign-type :id\ :accessor controller)\ (nib-objects :accessor nib-objects :initform nil))\ (:metaclass ns:+ns-object)) \f0 \ \ \f1 (defmethod initialize-instance :after ((self binding-window-owner) \ &key &allow-other-keys)\ (setf (nib-objects self)\ (load-nibfile \ (truename "ip:BindingTest;binding.nib") \ :nib-owner self\ :retain-top-objs t))\ ;; we do the following so that ccl:terminate will be called before we are garbage \ ;; collected and we can release the top-level objects from the NIB that we retained\ ;; when loaded\ (ccl:terminate-when-unreachable self)) \f0 \ \ \f1 (defmethod ccl:terminate ((self binding-window-owner))\ (dolist (top-obj (nib-objects self))\ (unless (eql top-obj (%null-ptr))\ (#/release top-obj)))) \f0 \ \ Below is the #/doSum method that we specified for the button in IB. It simply sets the sum. I suppose the one thing to note here is that although we put most of the method within a "with-slots" form, we don't include the sum slot itself, but rather access it via the more complete (sum (root (controller self))) form. Why is that? The answer is that when the with-slots form is used, the accessor methods defined for the slot are bypasses and the method "slot-value" (or some compiler-generated equivalent) is used. If we were to include the sum slot in the variables declared for the with-slots form then the extra methods that we added to call will-change-value-for and did-change-value-for would never get called. That's just a little something to be aware of if you implement the writer for a slot that you want to bind to in the way that I did.\ \ \f1 (objc:defmethod (#/doSum: :void) \ ((self binding-window-owner) (s :id))\ (declare (ignore s))\ (with-slots (input-1 input-2) (root (controller self))\ (setf (sum (root (controller self)))\ (+ input-1 input-2)))) \f0 \ \ Then we create a simple test function that returns this window.\ \ \f1 (defun test-binding ()\ (make-instance 'binding-window-owner)) \f0 \ \ In addition, you can run additional tests from the listener by changing the root object of the lisp-controller to see what happens. For example you could do:\ \ \f1 ? (setf bwo (bnd:test-binding))\ # (#x12C5A690)>\ ? (setf lc (bnd:controller bwo))\ # (#x12C52930)>\ ? (setf (lc:root lc) (bnd::random-sum-owner))\ ;Compiler warnings :\ ; In an anonymous lambda form at position 15: Undeclared free variable LC\ #\ ? (setf (lc:root lc) (bnd::random-sum-owner))\ ;Compiler warnings :\ ; In an anonymous lambda form at position 15: Undeclared free variable LC\ #\ ? \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \f0\b\fs26 \cf0 \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b0\fs24 \cf0 and watch what happens in your window. You could also set the individual slots input-1 and input-2 in the current root object. Note that these changes will be immediately reflected in the TextFields that are bound to these slots. But of course the sum field will not be updated until you click on the sum button.\ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \i\b \cf0 That's it for the projects so far. Others topics that need corresponding projects include ... \i0\b0 \ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b \cf0 Stand-Alone Applications, Quartz Graphics \b0 , \b OpenGL Graphics \b0 \ \ \ \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\ql\qnatural\pardirnatural \b\fs26 \cf0 Debugging Hints \b0\fs24 \ \ 1. Although you can redefine Objective-C methods at runtime, there are occasions when the presence or absence of such a function is cached. If, for example, you add a new delegate method for a class D, don't expect that method to be called for any already instantiated example of class D. That's because any object that has a D-class object as its delegate will, at the time the delegate outlet is set, determine which delegate methods D-class objects support and never call any others. If you create a new instance of D, and it is made a delegate, then your new method will be called.\ \ 2. If you run into Objective-C runtime exceptions that make it necessary to "Force Quit" CCL, it's likely that what you have done is prematurely release some Objective-C object. If you unnecessarily retain one you may get a memory leak, but that typically does not cause a runtime problem. Premature releasing, on the other hand, will almost surely cause you grief. If you prematurely release an object that has slots which are bound somehow, KVC will log a message indicating that this happened. Open up the console application to see these messages.\ \ 3. If you add a format statement in your code somewhere to help with debugging, the output may or may not go to the listener window. If the function is called in an Objective-C method it may display in the AltConsole window associated with this Lisp session. But as discussed above, using format statements within Objective-C functions should probably be avoided altogether. Replace them with calls to #_NSLog instead\ \ 4. Memory management of Objective-C objects can be a fairly tricky proposition at times. I discovered that objects are sometimes retained in places where you might not expect. For example, any direct or indirect use of the #/window function for window controller objects (or any object derived from it) results in an increase in the reference count of the window. I suspect this is actually a bug in Apple's code, but who knows, there may be some undocumented reason for this. One technique that can help resolve issues like this is to use the #/retainCount function which will tell you what the current count is for any Objective-C object. Apples discourages its use because it doesn't say anything about what objects did those retains, but I found it useful at times.\ \ 5. If some runtime event results in calling a lisp function which errors in some way you will typically see some sort of error in the AltConsole window. Sometimes just doing a simple :pop command will get you out of the situation, but it is easy to get into a situation where you just re-trigger the error. To get out of this situation type :R to find available restarts and pick the one that just goes on to the next event as in the following dialog.\ \ > Error: value #1=((2 3 2 2) . #1#) is not of the expected type (SATISFIES CCL::PROPER-LIST-P).\ > While executing: (:INTERNAL LISP-CONTROLLER::|-[LispController numberOfRowsInTableView:]|), in process Initial(0).\ > Type :POP to abort, :R for a list of available restarts.\ > Type :? for other options.\ 1 > :r\ > Type (:C ) to invoke one of the following restarts:\ 0. Return to break level 1.\ 1. #\ 2. Process the next event\ 1 > (:C 2)\ Invoking restart: Process the next event\ \ \ \ }