This directory contains the source code for the TextEdit application. TextEdit is a simple text editor based on the NSText and NSDocument subsystems of Cocoa.
This is a copy from the source code provided by Apple at https://developer.apple.com/library/archive/samplecode/TextEdit/Introduction/Intro.html, https://developer.apple.com/library/archive/samplecode/TextEdit/TextEdit.zip.
This version is hosted at: https://github.com/tempelmann/TextEdit
It has been modified in the following ways:
- Edited and tested with Xcode 10.1 on macOS 10.13.6 (High Sierra).
- Converted to use ARC (Automatic Reference Counting).
- Converted the README.rtf into README.md and removed README.txt
For more details, see the commit history of the git repository.
What follows is the original text of version 1.9, 2013-08-26.
Subclass of NSDocument
. One instance of this is created for every document (new or saved) in TextEdit.
The text contents (characters, attachments, attributes) of the document are kept in an instance of NSTextStorage
.
Document
overrides readFromURL:ofType:error:
since it wants to specify the user options for opening files, such as encoding and whether to open rich text documents as plain. These come from DocumentController
, who hangs on to the values chosen by the user when the open panel was displayed. The main reading workhorse is readFromURL:ofType:encoding:ignoreRTF:ignoreHTML:error:
, which does a bunch of text-specific stuff to load the document with specified options.
Document
also overrides writeToURL:ofType:error:
since it wants more control over the writing process. However it will sometimes invoke the NSDocument
version of this method by calling super
, and to support that there is an override of fileWrapperOfType:error:
as well.
Note that even with these customizations, TextEdit is able to benefit from many of NSDocument
's built-in saving features, such as its ability to track documents on disk after renaming, safe-saving, hidden extension handling, autosaving for recovery purposes, etc...
Document
hangs on to a number of properties, such as the scaleFactor
, viewSize
, readOnly
, backgroundColor
, hyphenationFactor
, etc. These are kept up to date with user's choices by various methods, and written out to rich text documents when saving.
There are also document properties that are settable by the user, such as author
, company
, keywords
, ... These are stored in instance variables in Document
and updated via bindings from the document properties panel. Like other properties, these are also written out with rich text documents.
In order to enable these properties to be undoable, Document
implements setValue:forDocumentProperty:
. This is called from the standard KVC method setValue:forKey:
for properties we want to be undoable. By registering setValue:forDocumentProperty:
as the callback in NSUndoManager
, we workaround the NSUndoManager bug where prepareWithInvocationTarget:
fails to freeze-dry invocations with "known" methods such as setValue:forKey:
.
Note the "trick" of providing string localizations in comments as a way to get genstrings to pick up the localizations when there are actually no explicit corresponding calls to NSLocalizedString
and variants in the code. This allows us to provide menu titles for changes to document properties. (Search for "For genstrings".)
Override of printOperationWithSettings:error:
enables TextEdit to customize its printing by first making sure the text for the whole document is laid out before printing, and by adding an print accessory view to control whether pages should be numbered or not.
In the printInfo for the document, TextEdit sets horizontal pagination to NSFitPagination
. This allows the text to be printed with the same wrapping as on the screen. In "wrap-to-window" mode this means the text might need to be scaled smaller when printed. This behavior should really be controlled by a check box in the TextEdit accessory on print panel, but this hasn't happened yet.
As you will note, there is a good deal of code to deal with encoding of the characters in the document when the document contains plain text. The instance variable documentEncoding stores the encoding of the document; this is either deduced from the file or specified by the user when the document is opened. Keeping this encoding around allows the document to be saved with the same encoding as it was read. (When in memory the character encoding of the document is somewhat meaningless, because the characters in the document are stored in an NSString
, whose backing stores are always expressed in terms of Unicode characters. The encoding determines how to save the document when saved as plain text.)
Subclass of NSWindowController
for managing the Document class's interaction with its document window and views.
DocumentWindowController
is responsible for creating the NSLayoutManager
and one or more NSTextView
s (depending on whether "wrap to page" mode is selected). These provide the UI layer for the text backing, which is stored in the Document
class.
DocumentWindowController
establishes a number of connections:
It observes the printInfo
, richText
, viewSize
, and hasMultiplePages
properties of the document. When it gets told these are changed (in observeValueForKeyPath:ofObject:change:context:
, it updates the window or view as appropriate.
It observes the backgroundColor
of the text view and scaleFactor
of the scrollView
, communicating any changes in these to the document. (These properties are changed directly in the view by the user.)
DocumentWindowController
binds its layout manager's hyphenationFactor
to document.hyphenationFactor
, and text view's editable
to document.readOnly
(through a negating value transformer). These allow reflecting changes from these in UI without any intermediate glue code.
setHasMultiplePages:
determines whether the document is in wrap-to-page mode or not; study this method, addPage, and removePage to see how to create and manipulate NSTextView
s programmatically.
The method textEditDoForegroundLayoutToCharacterIndex:
shows how to get the text system to lay text out in the foreground up to a certain character location. By default the text system does its layout in the background, which allows bringing up the window fairly quickly. The user can even edit, print, or save the document while the background layout is going on. This method enables having the first portion of the document already laid out. Note that this was useful from a user point of view in Tiger, where the scrollbar for the document raced down the page as background layout happened, but it's less interesting in Leopard when non-contiguous background layout is enabled. However, we still do it, and this method is also ueful for when printing the document (since TextEdit doesn't rewrap or relayout when printing and just uses its existing layout into).
This class implements a number of NSTextView
, NSLayoutManager
, and NSWindow
delegate methods.
textView:clickedOnLink:atIndex:
is overridden to allow opening links that represent text files directly in TextEdit, revealing other files in Finder, and opening non-local file URLs in the user's browser. textView:doubleClickedOnCell:inRect:
, textView:writablePasteboardTypesForCell:atIndex:
, and textView:writeCell:atIndex:
allow opening and copying attached documents.
layoutManager:didCompleteLayoutForTextContainer:atEnd:
controls adding/removing of pages as the document is edited or laid out.
windowWillUseStandardFrame:defaultFrame:
method provides "standard" and "user" sizes for resizing (or right-sizing) the document window via the green button.
This class takes care of setting the document as non-transient after moving or resizing the window.
Subclass of NSDocumentController
. Most NSDocument
-based applications don't need to subclass NSDocumentController
. TextEdit does this to provide "transient document" behavior, as well as customizing the open panel.
When the document controller is initialized, it binds its autosaving delay field to user defaults, in order to be automatically notified when the user changes this preference.
Transient document is the untitled document put up when TextEdit is first launched, or activated with no other documents open. If the user makes no changes to this untitled document before opening a new document, we get rid of the untitled document and visually replace it with the opened one. NSDocument
doesn't yet have support for this, hence the code in TextEdit.
The open panel customizations include an accessory view which lets the user choose whether to load rich text documents as plain, and the text encoding to be used for plain documents. These settings are then made available (in the context of the open operation) to the Document
class via the methods lastSelectedEncoding
, lastSelectedIgnoreHTML
, and lastSelectedIgnoreRich
.
In wrap-to-page mode there is one NSTextView
per page. MultiplePageView
is the top level view which groups all of these views. It is inserted as the document view of the scroll view in the document window. MultiplePageView
is fairly simple, providing support for conversions between page numbers and rects, and drawing the background for the pages.
A possible enhancement to this class would be to have it allow the user to manipulate the page margins by dragging guides around. An advanced exercise would be to add custom markers to the ruler to allow changing the page margins via the ruler as well.
Contains ScalingScrollView
, a subclass of NSView
to implement a scroll view with a popup to allow setting the zoom factor. This class is fairly generic and can easily be used in a variety of cases. The scaleFactor
property can be observed.
In TextEdit, this class is always in use in every document window. However, in wrap-to-window mode, the horizontal scroller and the scale popup are disabled, and the scale is set to 100%.
An NSWindowController
subclass that controls the preferences window. Since the switch to a bindings-based preferences window, this class has become greatly simplified. However, some preferences, such as HTML saving options and font settings, require special action and are still handled here.
The preferences controller also makes sure that the window size settings are valid; if the user enters an invalid dimension, the field is reset to its previous value.
This file contains the central controller object for TextEdit. With TextEdit's move to NSDocument
, and creation of several other controllers, this class has shed some weight in Leopard and has fewer responsibilities.
In its +initialize method this class registers default values for all preferences (besides fonts, which are handled directly by NSFont
's facilities for "user fonts").
Controller object also provides the little support necessary for allowing TextEdit to provide the "Open File" and "Open Selection" services to other applications. All that needs to happen to support this powerful feature is an entry in the Info.plist
file listing the services provided, and methods in Controller to respond to the services requests: openFile:userData:error:
and openSelection:userData:error:
.
Since openFile:userData:error:
may be given arbitrary text as file names, it does some clean-up and extra checks. For instance, it trims whitespace and does "" expansion (where "" is used as a shortcut to mean "home folder").
This file provides the class EncodingManager
, which does most of the sophisticed text encoding related stuff. This class also manages a panel which lets the user customize the list of encodings available in the application.
In addition, EncodingManager
provides the ability to load the accessory view used in open and save panels.
The EncodingPopUpButtonCell
class implements a popup which lets the user choose from a list of encodings. The list can be customized via an entry in the popup which brings up a customization panel. The class's instances assure that they are updated (via notifications) after any change in the customization panel.
Manages the "Select Line" panel. Subclass of NSWindowController
. Uses NSScanner
to parse user input.
The beefiest code in this class is getRange:inTextView:fromLineSpec:toLineSpec:relative:
, which figures out the range of characters to be selected based on user's input. Note that this input can be an absolute line (1..number of lines), an absolute range (two line numbers separated by a dash), or a relative line or range (number prefixed by plus or minus).
Manages the “Document Properties” panel. Yet another subclass of NSWindowController
.
This class is an example of implementing an inspector-style panel with bindings. It goes out of its way to take care of one area not covered automatically by the kit, namely committing of editing (that is, the user has typed a value in a field but not hit return or tab to leave the field) when the user deactivates the document window or closes the properties panel. Since there is no validation necessary for any of the fields in this panel, always committing is a straightforward solution.
DocumentPropertiesPanelController
keeps track of the current active document by observing mainWindow.windowController.document
on NSApp, and updating the KVO-compliant property inspectedDocument
when these are received.
All the fields in this panel are hooked up to the inspectedDocument
through an NSObjectController
, using the corresponding properties in Document
— author
, company
, keywords
, etc. Any new fields can be added to the panel with no changes to this class.
DocumentPropertiesPanelController also implements the NSEditorRegistration protocol, objectDidBeginEditing:
and objectDidEndEditing:
. As the user starts/stops editing in the fields in the panel, these methods are invoked. DocumentPropertiesPanelController
passes these straight through to the inspectedDocument
. In turn NSDocument
commits/discards editing as necessary when saving or closing documents. In addition, whenever a document is deactivated, any editing in the currently edited field is committed; this is done in stopInspectingDocumentOfWindow:
.
In order to commit editing when the properties panel itself is closed or deactivated, DocumentPropertiesPanelController
listens to the NSWindowDidResignKeyNotification
notification on the panel itself.
Although the single instance of this class is created at launch time, none of the notifications are signed up for until the panel is brought up the first time, in windowDidLoad
. So this panel has minimal performance impact when not used.
Finally, this class implements a method to toggle the panel (if active, order it out; otherwise, activate, ordering in if necessary), and a validateMenuItem:
method to update the title of the "Show Properties" menu item.
This class is mostly reusable; just the name of the class, the nib, validateMenuItem:
, and reference to [Document class]
need changing. It can be made into an semi-abstract NSWindowController
for inspectors by removing these pieces into a subclass.
A subclass of the new NSViewController
class. Used for adding an accessory view to the print panel. In the case of TextEdit, the accessory view contains just a checkbox, controlling whether or not to include headers and footers on printouts.
PrintPanelAccessoryController
has a BOOL pageNumbering
property to determine whether headers and footers should be included. The property is actually not stored in this class, but set directly in the printInfo, which is the representedObject
.
The method keyPathsForValuesAffectingPreview
is overridden to return "pageNumbering" as a way to declare that any changes to this property should update the print preview displayed in the print panel.
An override of localizedSummaryItems
provides the info to be displayed in the print panel's "Summary" pane.
These two methods above are part of the NSPrintPanelAccessorizing
protocol.