From 8ba6622310105eaf6429f1d114629dab666aab8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20A=2E=20A=CC=81lvarez?= Date: Sun, 24 Mar 2024 21:51:25 +0100 Subject: [PATCH] add LocalTalk over TCP --- Mini vMac.xcodeproj/project.pbxproj | 4 + Mini vMac/AppDelegate.m | 16 +- Mini vMac/Base.lproj/Main.storyboard | 46 ++- Mini vMac/LTOVRTCP.h | 403 +++++++++++++++++++++++++++ Mini vMac/MYOSGLUE.m | 21 +- Mini vMac/MacII-512x384/CNFUDALL.h | 3 +- Mini vMac/MacII-640x480/CNFUDALL.h | 3 +- Mini vMac/MacII/CNFUDALL.h | 3 +- Mini vMac/MacPlus4M/CNFUDALL.h | 3 +- Mini vMac/SettingsViewController.m | 42 ++- 10 files changed, 517 insertions(+), 27 deletions(-) create mode 100644 Mini vMac/LTOVRTCP.h diff --git a/Mini vMac.xcodeproj/project.pbxproj b/Mini vMac.xcodeproj/project.pbxproj index bf230a1..afa50e7 100644 --- a/Mini vMac.xcodeproj/project.pbxproj +++ b/Mini vMac.xcodeproj/project.pbxproj @@ -244,6 +244,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2834204B2B96558700D7DB68 /* LTOVRUDP.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LTOVRUDP.h; sourceTree = ""; }; 283422CA1CF8EF8C0088B634 /* MacPlus4M.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MacPlus4M.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 283422D71CF8EFD80088B634 /* EmulatorProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EmulatorProtocol.h; sourceTree = ""; }; 283422E71CF8F1C80088B634 /* CNFUDOSG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CNFUDOSG.h; sourceTree = ""; }; @@ -257,6 +258,7 @@ 283423EE1CFA329C0088B634 /* Icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon@3x.png"; sourceTree = ""; }; 283CA9821DF47AF300B33D5E /* BTCMouse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BTCMouse.h; sourceTree = ""; }; 285A8C901D05AFD3002993DE /* PlugIn-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "PlugIn-Info.plist"; sourceTree = ""; }; + 287006E92BB032E300A5EB51 /* LTOVRTCP.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LTOVRTCP.h; sourceTree = ""; }; 28848B601CDE97D600B86C45 /* InsertDiskViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InsertDiskViewController.h; sourceTree = ""; }; 28848B611CDE97D600B86C45 /* InsertDiskViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InsertDiskViewController.m; sourceTree = ""; }; 28848B631CDE97E900B86C45 /* SettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = ""; }; @@ -556,6 +558,7 @@ isa = PBXGroup; children = ( 28E3B831251D195D007C273F /* PICOMMON.h */, + 2834204B2B96558700D7DB68 /* LTOVRUDP.h */, 28E3B7D2251D0F13007C273F /* ADBEMDEV.c */, 28E3B7DE251D0F13007C273F /* ASCEMDEV.c */, 28E3B7CE251D0F12007C273F /* GLOBGLUE.c */, @@ -638,6 +641,7 @@ 283422EF1CF8F33A0088B634 /* Emulator Bundles */, 28D3C6162B76B8970079E915 /* DefaultSceneDelegate.swift */, 28D3C61A2B7781700079E915 /* KeyboardSceneDelegate.swift */, + 287006E92BB032E300A5EB51 /* LTOVRTCP.h */, ); path = "Mini vMac"; sourceTree = ""; diff --git a/Mini vMac/AppDelegate.m b/Mini vMac/AppDelegate.m index 3e63dc0..ef292cb 100644 --- a/Mini vMac/AppDelegate.m +++ b/Mini vMac/AppDelegate.m @@ -68,7 +68,14 @@ NSString *DocumentsChangedNotification = @"documentsChanged"; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults registerDefaults:defaultValues]; - [defaults addObserver:self forKeyPath:@"speedValue" options:0 context:NULL]; + for (NSString *key in @[@"speedValue", @"autoSlow", @"localTalkServer"]) { + [defaults addObserver:self forKeyPath:key options:0 context:NULL]; + } + + NSString *localTalkServer = [defaults valueForKey:@"localTalkServer"]; + if (localTalkServer.length > 0) { + setenv("LTOVRTCP_SERVER", localTalkServer.UTF8String, 1); + } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { @@ -78,6 +85,13 @@ NSString *DocumentsChangedNotification = @"documentsChanged"; sharedEmulator.speed = [defaults integerForKey:@"speedValue"]; } else if ([keyPath isEqualToString:@"autoSlow"]) { sharedEmulator.autoSlow = [defaults integerForKey:@"autoSlow"]; + } else if ([keyPath isEqualToString:@"localTalkServer"]) { + NSString *localTalkServer = [defaults stringForKey:@"localTalkServer"]; + if (localTalkServer.length > 0) { + setenv("LTOVRTCP_SERVER", localTalkServer.UTF8String, 1); + } else { + unsetenv("LTOVRTCP_SERVER"); + } } } } diff --git a/Mini vMac/Base.lproj/Main.storyboard b/Mini vMac/Base.lproj/Main.storyboard index 8870893..30154ef 100644 --- a/Mini vMac/Base.lproj/Main.storyboard +++ b/Mini vMac/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -21,7 +21,7 @@ - + + + + + + + + + + + + @@ -435,7 +451,7 @@ - + @@ -451,12 +467,12 @@ - + - + @@ -542,7 +558,7 @@ - + diff --git a/Mini vMac/LTOVRTCP.h b/Mini vMac/LTOVRTCP.h new file mode 100644 index 0000000..b893a10 --- /dev/null +++ b/Mini vMac/LTOVRTCP.h @@ -0,0 +1,403 @@ +/* + LTOVRTCP.h + + Copyright (C) 2012 Michael Fort, Paul C. Pratt, Rob Mitchelmore + + You can redistribute this file and/or modify it under the terms + of version 2 of the GNU General Public License as published by + the Free Software Foundation. You should have received a copy + of the license along with this file; see the file COPYING. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + license for more details. + */ + +/* + LocalTalk OVeR Transmission Control Protocol + */ + +#define TCP_dolog (dbglog_HAVE && 0) + +#ifndef use_winsock +#define use_winsock 0 +#endif + +#if use_winsock +#define my_INVALID_SOCKET INVALID_SOCKET +#define my_SOCKET SOCKET +#define my_closesocket closesocket +#define socklen_t int +#else +#define my_INVALID_SOCKET (-1) +#define my_SOCKET int +#define my_closesocket close +#endif + +#if TCP_dolog +LOCALPROC dbglog_writeSockErr(char *s) +{ + dbglog_writeCStr(s); + dbglog_writeCStr(": err "); +#if use_winsock + dbglog_writeNum(WSAGetLastError()); +#else + dbglog_writeNum(errno); + dbglog_writeCStr(" ("); + dbglog_writeCStr(strerror(errno)); + dbglog_writeCStr(")"); +#endif + dbglog_writeReturn(); +} +#endif + +/* + Transmit buffer for localtalk data and its metadata + */ +LOCALVAR ui3b tx_buffer[6 + LT_TxBfMxSz] = +"LLpppp"; + + +/* + Receive buffer for LocalTalk data and its metadata + */ +LOCALVAR unsigned int rx_buffer_allocation = 1800; + +LOCALVAR my_SOCKET sock_fd = my_INVALID_SOCKET; +LOCALVAR blnr tcp_ok = falseblnr; + +#if use_winsock +LOCALVAR blnr have_winsock = falseblnr; +#endif + +LOCALPROC start_tcp(void) +{ +#if use_winsock + WSADATA wsaData; +#endif + struct sockaddr_in addr; + +#if use_winsock + if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData)) { +#if TCP_dolog + dbglog_writeln("WSAStartup fails"); +#endif + return; + } + have_winsock = trueblnr; +#endif + + if (my_INVALID_SOCKET == (sock_fd = + socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))) + { +#if TCP_dolog + dbglog_writeSockErr("socket"); +#endif + return; + } + + /* find server from LTOVRTCP_SERVER env, should be in the form 1.2.3.4:12345 */ + char *server = NULL; + char buf[32]; + short port = 0; + if ((server = getenv("LTOVRTCP_SERVER")) && strlen(server) < sizeof(buf)) { + strcpy(buf, server); + char *separator = strchr(buf, ':'); + if (separator == NULL) { + return; + } + *separator = 0; + separator++; + if (strlen(separator) > 1) { + port = (short)atoi(separator); + } + } + + if (port == 0) { + return; + } + + /* connect to server */ + memset((char*)&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr(buf); + addr.sin_port = htons(port); +#if ! use_winsock + errno = 0; +#endif + if (0 != connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr))) { +#if TCP_dolog + dbglog_writeSockErr("connect"); +#endif + MacMsg("Could not connect to LocalTalk server", strerror(errno), falseblnr); + return; + } +#if TCP_dolog + dbglog_writeln("tcp connected"); +#endif + + /* non-blocking I/O is good for the soul */ +#if use_winsock + { + int iResult; + u_long iMode = 1; + + iResult = ioctlsocket(sock_fd, FIONBIO, &iMode); + if (iResult != NO_ERROR) { + /* + printf("ioctlsocket failed with error: %ld\n", iResult); + */ + } + } +#else + fcntl(sock_fd, F_SETFL, O_NONBLOCK); +#endif + + tcp_ok = trueblnr; +} + +LOCALVAR unsigned char *MyRxBuffer = NULL; + +/* + External function needed at startup to initialize the LocalTalk + functionality. + */ +LOCALFUNC blnr InitLocalTalk(void) +{ + LT_PickStampNodeHint(); + + LT_TxBuffer = &tx_buffer[6]; + + MyRxBuffer = malloc(rx_buffer_allocation); + if (NULL == MyRxBuffer) { + return falseblnr; + } + + /* Set up TCP socket */ + start_tcp(); + + /* Initialized properly */ + return trueblnr; +} + +LOCALPROC UnInitLocalTalk(void) +{ + if (my_INVALID_SOCKET != sock_fd) { + if (0 != my_closesocket(sock_fd)) { +#if TCP_dolog + dbglog_writeSockErr("my_closesocket sock_fd"); +#endif + } + } + +#if use_winsock + if (have_winsock) { + if (0 != WSACleanup()) { +#if TCP_dolog + dbglog_writeSockErr("WSACleanup"); +#endif + } + } +#endif + + if (NULL != MyRxBuffer) { + free(MyRxBuffer); + } +} + +LOCALPROC embedPacketLength(ui4r length) +{ + /* + embeds the length of the packet in the packet as a 16-bit big endian + */ + tx_buffer[0] = (length >> 8) & 0xff; + tx_buffer[1] = length & 0xff; +} + +LOCALPROC embedMyPID(void) +{ + /* + embeds my process ID in network byte order in the start of the + Tx buffer we assume a pid is at most 32 bits. As far as I know + there's no actual implementation of POSIX with 64-bit PIDs so we + should be ok. + */ + int i; + +#if LT_MayHaveEcho + ui5r v = LT_MyStamp; +#else + ui5r v = (ui5r)getpid(); +#endif + + for (i = 0; i < 4; i++) { + tx_buffer[2+i] = (v >> (3 - i)*8) & 0xff; + } +} + +GLOBALOSGLUPROC LT_TransmitPacket(void) +{ + size_t bytes; + /* Write the packet to TCP */ +#if TCP_dolog + dbglog_writeln("writing to tcp"); +#endif + embedPacketLength(LT_TxBuffSz + 4); + embedMyPID(); + if (tcp_ok) { + + bytes = send(sock_fd, + (const void *)tx_buffer, LT_TxBuffSz + 6, 0); +#if TCP_dolog + dbglog_writeCStr("sent "); + dbglog_writeNum(bytes); + dbglog_writeCStr(" bytes"); + dbglog_writeReturn(); +#endif + (void) bytes; /* avoid warning about unused */ + } +} + +/* + pidInPacketIsMine returns 1 if the process ID embedded in the packet + is the same as the process ID of the current process + */ +LOCALFUNC int pidInPacketIsMine(void) +{ + /* is the PID in the packet my own PID? */ + int i; + ui5r v; + +#if LT_MayHaveEcho + v = LT_MyStamp; +#else + v = (ui5r)getpid(); +#endif + + for (i = 0; i < 4; i++) { + if (MyRxBuffer[i] != ((v >> (3 - i)*8) & 0xff)) { + return 0; + } + } + + return 1; +} + +/* + packetIsOneISent returns 1 if this looks like a packet that this + process sent and 0 if it looks like a packet that a different + process sent. This provides loopback protection so that we do not + try to consume packets that we sent ourselves. We do this by + checking the process ID embedded in the packet and the IP address + the packet was sent from. It would be neater to just look at the + LocalTalk node ID embedded in the LLAP packet, but this doesn't + actually work, because during address acquisition it is entirely + legitimate (and, in the case of collision, *required*) for another + node to send a packet from what we think is our own node ID. + */ +#if ! LT_MayHaveEcho +LOCALFUNC int packetIsOneISent(void) +{ + return pidInPacketIsMine(); +} +#endif + +LOCALFUNC int GetNextPacket(void) +{ + unsigned char* device_buffer = MyRxBuffer; + if (tcp_ok == falseblnr) + { + return 0; + } + +#if ! use_winsock + errno = 0; +#endif + /* peek length */ + ssize_t bytes = recv(sock_fd, (void *)device_buffer, 2, MSG_PEEK); + if (bytes == 2) + { + int incoming_length = (device_buffer[0] << 8) + device_buffer[1]; + bytes = recv(sock_fd, (void*)device_buffer, 2 + incoming_length, MSG_PEEK); + if (bytes == 2 + incoming_length) + { + /* read the packet */ + bytes = recv(sock_fd, (void*)device_buffer, 2 + incoming_length, 0); + } + } + + if (bytes < 0) { +#if use_winsock + if (WSAEWOULDBLOCK != WSAGetLastError()) +#else + if (ECONNRESET == errno || ETIMEDOUT == errno) + { + MacMsg("Lost connection to LocalTalk server", strerror(errno), falseblnr); +#if TCP_dolog + dbglog_writeCStr("tcp error "); + dbglog_writeCStr(strerror(errno)); + dbglog_writeReturn(); +#endif + tcp_ok = falseblnr; + my_closesocket(sock_fd); + sock_fd = my_INVALID_SOCKET; + } + else if (EAGAIN != errno) +#endif + { +#if TCP_dolog + dbglog_writeCStr("ret"); + dbglog_writeNum(bytes); + dbglog_writeCStr(", bufsize "); + dbglog_writeNum(rx_buffer_allocation); +#if ! use_winsock + dbglog_writeCStr(", errno = "); + dbglog_writeCStr(strerror(errno)); +#endif + dbglog_writeReturn(); +#endif + } + } else { +#if TCP_dolog + dbglog_writeCStr("got "); + dbglog_writeNum(bytes); + dbglog_writeCStr(", bufsize "); + dbglog_writeNum(rx_buffer_allocation); + dbglog_writeReturn(); +#endif + } + return bytes; +} + +GLOBALOSGLUPROC LT_ReceivePacket(void) +{ + int bytes; + +#if ! LT_MayHaveEcho +label_retry: +#endif + bytes = GetNextPacket(); + if (bytes > 0) { +#if LT_MayHaveEcho + CertainlyNotMyPacket = ! pidInPacketIsMine(); +#endif + +#if ! LT_MayHaveEcho + if (packetIsOneISent()) { + goto label_retry; + } +#endif + + { +#if TCP_dolog + dbglog_writeCStr("passing "); + dbglog_writeNum(bytes - 6); + dbglog_writeCStr(" bytes to receiver"); + dbglog_writeReturn(); +#endif + LT_RxBuffer = MyRxBuffer + 6; + LT_RxBuffSz = bytes - 6; + } + } +} diff --git a/Mini vMac/MYOSGLUE.m b/Mini vMac/MYOSGLUE.m index 1324241..6bdb06c 100644 --- a/Mini vMac/MYOSGLUE.m +++ b/Mini vMac/MYOSGLUE.m @@ -107,6 +107,12 @@ LOCALPROC dbglog_close0(void) { #include "COMOSGLU.h" #include "PBUFSTDC.h" +#if EmLocalTalk +#include +#include +#include "LTOVRTCP.h" +#endif + #pragma mark - Cocoa Stuff LOCALFUNC blnr FindNamedChildFilePath(NSString *parentPath, char *ChildName, NSString **childPath) { @@ -1492,10 +1498,14 @@ LOCALFUNC blnr InitOSGLU(void) { #endif if (LoadInitialImages()) if (LoadMacRom()) - if (InitLocationDat()) { - InitKeyCodes(); - IsOk = trueblnr; - } + if (InitLocationDat()) +#if EmLocalTalk + if (InitLocalTalk()) +#endif + if (true) { + InitKeyCodes(); + IsOk = trueblnr; + } } return IsOk; @@ -1508,6 +1518,9 @@ LOCALPROC CheckSavedMacMsg(void) { } LOCALPROC UnInitOSGLU(void) { +#if EmLocalTalk + UnInitLocalTalk(); +#endif #if MySoundEnabled MySound_Stop(); #endif diff --git a/Mini vMac/MacII-512x384/CNFUDALL.h b/Mini vMac/MacII-512x384/CNFUDALL.h index f35bef4..4b0fda9 100644 --- a/Mini vMac/MacII-512x384/CNFUDALL.h +++ b/Mini vMac/MacII-512x384/CNFUDALL.h @@ -35,7 +35,8 @@ #define IncludeHostTextClipExchange 1 #define EnableAutoSlow 1 -#define EmLocalTalk 0 +#define EmLocalTalk 1 +#define LT_MayHaveEcho 0 #define AutoLocation 1 #define AutoTimeZone 1 diff --git a/Mini vMac/MacII-640x480/CNFUDALL.h b/Mini vMac/MacII-640x480/CNFUDALL.h index a652bd0..2f9901e 100644 --- a/Mini vMac/MacII-640x480/CNFUDALL.h +++ b/Mini vMac/MacII-640x480/CNFUDALL.h @@ -35,7 +35,8 @@ #define IncludeHostTextClipExchange 1 #define EnableAutoSlow 1 -#define EmLocalTalk 0 +#define EmLocalTalk 1 +#define LT_MayHaveEcho 0 #define AutoLocation 1 #define AutoTimeZone 1 diff --git a/Mini vMac/MacII/CNFUDALL.h b/Mini vMac/MacII/CNFUDALL.h index 659b0a7..4c1ec2c 100644 --- a/Mini vMac/MacII/CNFUDALL.h +++ b/Mini vMac/MacII/CNFUDALL.h @@ -35,7 +35,8 @@ #define IncludeHostTextClipExchange 1 #define EnableAutoSlow 1 -#define EmLocalTalk 0 +#define EmLocalTalk 1 +#define LT_MayHaveEcho 0 #define AutoLocation 1 #define AutoTimeZone 1 diff --git a/Mini vMac/MacPlus4M/CNFUDALL.h b/Mini vMac/MacPlus4M/CNFUDALL.h index 63a0147..1669853 100644 --- a/Mini vMac/MacPlus4M/CNFUDALL.h +++ b/Mini vMac/MacPlus4M/CNFUDALL.h @@ -36,7 +36,8 @@ #define IncludeHostTextClipExchange 1 #define EnableAutoSlow 1 -#define EmLocalTalk 0 +#define EmLocalTalk 1 +#define LT_MayHaveEcho 0 #define AutoLocation 1 #define AutoTimeZone 1 diff --git a/Mini vMac/SettingsViewController.m b/Mini vMac/SettingsViewController.m index fbcaa3f..077c0ca 100644 --- a/Mini vMac/SettingsViewController.m +++ b/Mini vMac/SettingsViewController.m @@ -19,7 +19,9 @@ typedef enum : NSInteger { SettingsSectionKeyboard, SettingsSectionDisplay, SettingsSectionMachine, - SettingsSectionAbout + SettingsSectionNetwork, + SettingsSectionAbout, + SettingsSectionCount } SettingsSection; @implementation SettingsViewController @@ -154,10 +156,21 @@ typedef enum : NSInteger { } } +- (void)changeLocaltalkServer:(UITextField*)sender { + if ([sender isKindOfClass:[UITextField class]]) { + NSLog(@"Changed server to %@", sender.text); + if (sender.text.length == 0) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"localTalkServer"]; + } else { + [[NSUserDefaults standardUserDefaults] setValue:sender.text forKey:@"localTalkServer"]; + } + } +} + #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return 6; + return SettingsSectionCount; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { @@ -196,6 +209,8 @@ typedef enum : NSInteger { return NSLocalizedString(@"Display Scaling", nil); case SettingsSectionAbout: return aboutTitle; + case SettingsSectionNetwork: + return NSLocalizedString(@"Network", nil); default:return nil; } } @@ -269,7 +284,7 @@ typedef enum : NSInteger { cell.textLabel.text = [bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; cell.detailTextLabel.text = [bundle objectForInfoDictionaryKey:@"CFBundleGetInfoString"]; } - + if (rowHasHeader) { cell.imageView.image = nil; cell.indentationLevel = 1; @@ -297,6 +312,9 @@ typedef enum : NSInteger { } else { filterControl.selectedSegmentIndex = 1; } + } else if (section == SettingsSectionNetwork) { + cell = [self fieldCellForTableView:tableView indexPath:indexPath action:@selector(changeLocaltalkServer:) placeholder:@"address:port" text:[defaults valueForKey:@"localTalkServer"]]; + cell.textLabel.text = @"LocalTalk Server"; } return cell; } @@ -391,4 +409,22 @@ typedef enum : NSInteger { return cell; } +- (UITableViewCell*)fieldCellForTableView:(UITableView*)tableView indexPath:(NSIndexPath*)indexPath action:(SEL)action placeholder:(NSString*)placeholder text:(NSString*)text { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"field" forIndexPath:indexPath]; + UITextField *cellField = (UITextField*)cell.accessoryView; + if (cellField == nil) { + CGRect bounds = cell.bounds; + bounds.size.width /= 2; + cellField = [[UITextField alloc] initWithFrame:bounds]; + cell.accessoryView = cellField; + } else { + [cellField removeTarget:nil action:nil forControlEvents:UIControlEventAllEvents]; + } + cellField.text = text; + cellField.placeholder = placeholder; + cellField.textAlignment = NSTextAlignmentRight; + [cellField addTarget:self action:action forControlEvents:UIControlEventEditingDidEnd|UIControlEventEditingDidEndOnExit]; + return cell; +} + @end