Dynamic Table Editing

I want users to be able to edit table cells in response to selecting a menu item, since I’m using double-clicking on a cell for something else (and have therefore disabled editing on the table’s columns)

So, when the menu item is selected, I’m calling the method “editColumn:row:withEvent:select:” of my table view on the selected row and column. The problem is, editing of that row and column continues until the user clicks on another cell. Even if a new value is entered for the cell and return is pressed, and that entry is processed via the ‘on change cell value’ handler, and even if I explicitly set the selected row to something else during that handler, the cell remains selected and editable until the user specifically clicks outside of it.

I can’t be the first one to have chosen to go this way with table editing, so I’m wondering how others have handled ending editing once the user has entered a value?

-Joe

So far, I’ve found out that the following don’t work:

  1. ending editing by telling the main window to do so via:
    call method “endEditingFor:” of (window “mainWindow”) with parameter {(window “mainWindow”)}

  2. Variations of the above, ending editing for the table view and table column. As with the above, no
    errors are reported, but nothing changes either.

I can’t believe there’s no way to get it to end editing on hitting return.

From what I’ve been able to gather, doing something like was done in iTunes is pretty difficult with Cocoa. Getting it so that selecting a row, then single-clicking on the text in a cell causes the cell to go into editing mode, while selecting a row then single-clicking in a cell area that has no text causes it to do nothing, while double-clicking in the area causes the song to play – getting it to do all that is pretty tough from what I can see, even in Objective-C.

Sure, you can implement the textDidEndEditing: method in the field editor’s delegate to get the “finish entry on pressing return” behavior, but you still have the double-click behavior problem. I suppose you’d have to subclass NSTableView to get it all right, even though Objective-C and Cocoa should require subclassing almost never.

Sorry, I’m just venting my frustration a bit. Like others who have beaten their heads against this problem, I’m disappointed that Apple hasn’t included the behavior of what is their best known interface in their core UI package, despite how long iTunes has been out there.

-Joe

Addendum:

call method “makeFirstResponder:” of (window “mainWindow”) with parameter {(window “mainWindow”)}

Doesn’t work either. It results in an NSInternalScriptError(8) for what to me seems to be no good reason. This works in Cocoa with Objective-C: [[self window] makeFirstResponder:self]. It’s even recommended by Apple. Go figure.

-Joe

It appears as though there is no way to end editing on a table cell in Applescript. All the methods recommended by Apple for straight Objective-C either have no effect or cause an NSInternalScriptError. It’s too bad Apple crippled their product in this way.

Hi Ransom,

I don’t know if this is the best way, but you can end editing by toggling the enabled property of the table. I’m using a data source so this might not work for you.

About the returns. They work when I use a data source, but the edit moves to the next row.

About the single click edit you can detect single ‘click’ or ‘double click’ with these handlers, but you would need to know if the user clicked on the selected row. You can do this by using one of the selection changed handlers. I used the ‘should select row’ handler. Then if the user selects a different row, I set a global flag like row_changed and set it to true. Then in the ‘click’ handler, you look at the flag. If it’s true do nothing and false somehow set the selected row to edit. Then reset the flag to false. I haven’t done the changing to edit part yet. I think I’d rather have the user hit a button or something to play the song.

It seems to me that the iTunes browser uses a browser type view on the double click and play song. While the one click on selection and edit part is for changing names in the playlist section. I don’t see where in iTunes both options are used at the same time.

gl,

I don’t know if this is the best way, but you can end editing by toggling the enabled property of the table. I’m using a data source so this might not work for you.

Unfortunately, that method yields the NSInternalScriptError in my case.

When are you toggling it?

About the returns. They work when I use a data source, but the edit moves to the next row.

That’s right. The standard way to exit the editing mode seems to be to have the user click on a different row. If I could put up with that, then my method of calling editColumn: to begin editing would be fine. I could just name the menu item “go into editing mode” instead of “rename item”, I suppose. But that seems too much like an early-80’s word processor (“Oh, you have to go into editing mode to edit your document. In data entry mode, you can only append.” Those were the days.)

I guess it’s not that bad to have a dialog box pop up when you want to change an entry in the table column. It’s just a shame that something so conceptually simple and natural is something one must jump through hoops for, and even then the result isn’t what users have been led to expect based on Apple’s own software.

About the single click edit you can detect single ‘click’ or ‘double click’ with these handlers, but you would need to know if the user clicked on the selected row. You can do this by using one of the selection changed handlers. I used the ‘should select row’ handler. Then if the user selects a different row, I set a global flag like row_changed and set it to true. Then in the ‘click’ handler, you look at the flag. If it’s true do nothing and false somehow set the selected row to edit. Then reset the flag to false.

That’s exactly what I was thinking of doing. But first I needed to make sure I could go into and out of editing mode smoothly and at will. It seems to have turned out that I can’t. The field editor seems to latch on to first responder status and refuse to give it up, even using Apple’s ‘guaranteed’ methods for making it do so. The only way to make it give up first responder status seems to be to click on a different cell in the table. Every other method seems to result in an error.

I haven’t done the changing to edit part yet.

That’s where I used the call to the editColumn: method. It works great. You go right into editing mode on the selected cell. But then you’re in editing mode on that cell until the user clicks out of the cell. Hitting enter does cause the “on change cell value” handler to be called, but then it just goes right back into editing mode. What I need is some way to take it out of editing mode, preferably one that works during the “on change cell value” handler. But everything I’ve tried there has either had no effect at all, such as setting the main window’s first responder to itself (as Apple recomended as a first try), or raised an error, such as explicitly calling endEditingFor: (as Apple recommends as a final, guaranteed-to-work solution).

I think I’d rather have the user hit a button or something to play the song.

That’s always an option, and it’s how I started out when I was using a browser object. But double-clicking just felt more right, so I switched to the table object. I don’t recall how browser objects handle editing, though.

It seems to me that the iTunes browser uses a browser type view on the double click and play song. While the one click on selection and edit part is for changing names in the playlist section. I don’t see where in iTunes both options are used at the same time.

Hmm. Maybe we’re talking about two different things. In my iTunes, if I single-click in the “Name” column of the list of songs in the current play list, the first time I single-click there the selection changes to that song. The second time I single-click there, if I click on the text of the song name, then it goes into editing mode. If I single-click where the text of the song name is not, then nothing happens. If I double-click anywhere in that cell, the song plays.

Doing that with anything that uses Cocoa seems very difficult. I started out using a browser object, but there’s no double-action for the browser. I switched to table views, and I see no way to make it go into editing mode only if I single-click on the /text/ in a previously selected row.

Am I missing something?

-Joe

Hi Ransom,

I found a better way to exit the editting. This one might work for the method you’re using also.

tell (table view “table” of scroll view “scroll” of window “main”)
set s to selected row
set selected rows to {}
set selected row to s
end tell

I tried this earlier and it didn’t work, but I think I was using a bad reference and just skipped it.

Toggling the enabled property was done from a button when in edit mode. The bad part about this was that you loose the blue highlighting of the selection. It turns grey. The above method yields the blue highlighted selection.

I didn’t know that you could edit the track names like that, maybe because I always double click by habit.

This is very difficult and I’m learning a lot from your question. It’s like a puzzle to me. I still have to try going into edit mode.

gl,

Hi Kel,

I put your code in the ‘on change value’ handler, which is called when I press return while in editing mode and seems to be my only option for where to place code to exit editing mode.

After making the change, I launched the program then went into editing mode on row two of the table. I made an edit, then pressed return. Row two went from the blue editing outline to a solid grey bar, then back to the blue editing outline - I was back in editing mode. The field editor’s tenacity strikes again!

I also tried calling makeFirstResponder: with a parameter of mainWindow right after the line that changes the selected rows to the empty set, and that didn’t make any change in the result. I then tried calling endEditing, and as usual that cause the NSInternalScriptError crash.

If you get a chance, try going into edit mode via the “editColumn:” call. If your code works for you, then at least we’ll know it’s something else about my code that’s causing the problem.

Here’s my call for reference:


set editingRow to selected row of table view "firstTableView" of scroll view "firstScrollView" of window "mainWindow"
call method "editColumn:row:withEvent:select:" of (table view "firstTableView" of scroll view "firstScrollView" of window "mainWindow") with parameters {0, editingRow - 1, 0, true}

(Note that, as per Apple’s docs, the withEvent: parameter is 0/nil, which simply means that a user event didn’t call it, but rather it’s being called programmatically.)

-Joe

Hi Ransom,

I think it’s that method of not using a data source that’s causing your errors and will try the cocoa method again. I couldn’t figure out what to put in the event parameter (0 here).

Later,

Hi Joe,

Guess what. It works! Almost. I think the problem in your script has something to do with the winthout data source method. It does a lot of things behind the scene.

I’ll post the tester (with data source) that I’m working on after I get out the kinks. The remaining hurdle is getting the selection to not change when there is more than one row when the return is hit and sometimes the flag is set wrong. I was thinking that it might be a good idea to let the selection change when Return is pressed and not change when Enter is pressed. Maybe use the ‘should selection change’ handler to stop the selection from changing.

Later,

Great! I’m glad you got it to work.

The reason I’m not using a data source is that everything my program does operates on files. The edit function edits file names, for example. It didn’t make sense to me to use a data source if I’d always have to pull the data out of there after every change, and sync up the file system with it. Basically, the file system is my data source.

So, too bad if it can’t work without a data source. :frowning:

-Joe

Hi Ransom,

How did you find out how to use the ‘change cell value’ handler? I can’t find any examples. I always get errors. Maybe there’s a bug with it.

gl,

I just check marked it and put in some code, after reading that it existed in some part of the docs.

When you were having success coming out of editing, how were you doing it?

-Joe

Hi Joe,

Here’s the tester. Still need to fix the bug with the flag and do something about the selection changing to a different row. I stopped working on this when trying to get the darn without data source to edit something.

property table_data : {}
property no_change : true

on awake from nib theObject
set theDataSource to make new data source at end of data sources with properties {name:“names”}
make new data column at end of data columns of theDataSource with properties {name:“names”, sort order:ascending, sort type:alphabetical, sort case sensitivity:case insensitive}
–set sorted of theDataSource to true
–set sort column of theDataSource to data column “names” of theDataSource
set data source of theObject to theDataSource
–append theDataSource with tableData
end awake from nib

on clicked theObject
set n to name of theObject
if n is “add” then
– add new empty row
append data source “names” with {{}}
else if n is “test” then
– for test button
else if n is “table” then
if no_change then
– check if there is a selected row
– user could have clicked outside data area
– and there was no selection
set s to selected row of theObject
if s > 0 then
– go into edit mode for current selected row
call method “editColumn:row:withEvent:select:” of theObject with parameters {0, (s - 1), 0, true}
log “entering edit mode”
end if
else
set no_change to true
log “not entering edit mode”
end if
end if
end clicked

on selection changed theObject
– check if no chnge is true
– ExitEditMode subroutine makes two changes
– if no_change is true then
set no_change to false
log “changing”
– end if
end selection changed

on keyboard up theObject event theEvent
set r to edited row of theObject
– check for Return or Enter only if table is in edit mode
if r > 0 then
set c to characters of theEvent
if c is return or c is (ASCII character 3) then
ExitEditMode(theObject)
end if
end if
end keyboard up

on ExitEditMode(theTable)
tell (table view “table” of scroll view “scroll” of window “main”)
set s to selected row
set selected rows to {}
set selected row to s
end tell
return
end ExitEditMode

Two buttons “add” and “test” and the table “table” are connected to the ‘clicked’ handler. The table is also connected to the other handlers. The table is set to ‘allows empty election’ with IB.

I still have more ideas about how to make the ‘change cell value’ work right. It seems like the handler is not being called, but shows a notification error. I don’t understand that yet.

Editted: in the ExitEditMode subroutine, I should have used the passed parameter theTable. I think I was thinking that you might not be able to pass references at the time.

gl,

Hi Kel,

I found a way to do it without a data source! It should work when using a data source, too.

You just subclass NSTableView and override - textDidEndEditing: to handle the return key in the same way as clicking on a different row is handled. I found the Obj-C code here: http://www.borkware.com/quickies/one?topic=NSTableView

Just in case anyone is reading this and is interested in doing this but isn’t familiar with Objective-C, here’s what needs to be done:

  1. In Interface Builder, go to the doc window (the window usually titled “Main Menu.nib” and having the tabs for Instances, Classes, Images, Sounds, and Nib) and select the Classes tab.

  2. Browse out to NSObject-> NSResponder → NSView → NSControl → NSTableView. Control-click on NSTableView and select “Subclass NSTableView.” You’ll be asked to give your subclass a name - I used the default, MyTableView.

  3. Control-click on your subclass (“MyTableView” in my case) and select “Create Files for MyTableView.” In the window that
    pops up, make sure “MyTableView.h” and “MyTableView.m” are check=marked, and make sure your project’s name is checkmarked. Press the “Choose” button.

  4. Select your table view object in the window you’ve built (or add one to your window if you haven’t done so already, then
    select it – make sure you’re selecting the table view and not the scroll view).

  5. Pull up the Class Inspector (Shift-Command-I) and choose “Custom Class” from the popup button.

  6. You should see your class (“MyTableView”) in the list. Click on it.

  7. Choose “Applescript” from the popup button in the Class Inspector.

  8. Make sure the “on clicked”, “column clicked” and “on selection changed” handlers are check-marked under “Table View”, and make sure your script is check-marked below. Make sure “Allow…Column Ordering” is check-marked on the Attributes page of the table view. Under the application’s handlers, make sure “on will finish launching” and “on idle” are check-marked and the script is check-marked below. Under the window’s handlers, make sure “awake from nib” is check-marked and the script is check-marked below. If not using a data source, also make sure “on change cell value” handler is clicked.

  9. Save your nib.

  10. Switch to XCode. In your “Other Sources” folder, you should see the .h and .m files you asked Interface Builder to add to your project. Put the following code in the .h file (changing the name under @interface as necessary if you didn’t use the “MyTableView” class name):

/* MyTableView */

#import <Cocoa/Cocoa.h>

@interface MyTableView : NSTableView
{
}

  • (void) textDidEndEditing: (NSNotification *) notification;
    @end
  1. Put the following code in the .m file (again, changing the class name if you didn’t use “MyTableView”):

#import “MyTableView.h”

@implementation MyTableView

// make return and tab only end editing, and not cause other cells to edit

  • (void) textDidEndEditing: (NSNotification *) notification
    {
    NSDictionary *userInfo = [notification userInfo];

    int textMovement = [[userInfo valueForKey:@“NSTextMovement”] intValue];

    if (textMovement == NSReturnTextMovement
    || textMovement == NSTabTextMovement
    || textMovement == NSBacktabTextMovement) {

      NSMutableDictionary *newInfo;
      newInfo = [NSMutableDictionary dictionaryWithDictionary: userInfo];
    
      [newInfo setObject: [NSNumber numberWithInt: NSIllegalTextMovement]
               forKey: @"NSTextMovement"];
    
      notification =
          [NSNotification notificationWithName: [notification name]
                                     object: [notification object]
                                     userInfo: newInfo];
    

    }

    [super textDidEndEditing: notification];
    [[self window] makeFirstResponder:self];

} // textDidEndEditing

@end

  1. In your applescript source, set up your “on will finish launching”, “on awake from nib”, “on idle”, and “on clicked”, “on selection changed”, and “on column clicked” handlers and your global variables:

-- If the user has clicked once, we don't know whether he will click a second time yet. If we immediately go into editing mode,
-- then the user clicks a second time, it works, but it gives some funny visual feedback. So we'll track the first click, and set
-- possibleEditingRequest to 'true'.  In the idle handler (which runs every theDelay seconds), we'll see if possibleEditingRequest
-- is true. If so, we'll go into editing mode. If not, we'll just wait another theDelay seconds and try again.
-- In the on clicked handler, if in the mean time we get a second click, we'll set possibleEditingRequest to 'false' and do the
-- double-clicked stuff.
-- In addition, we need global variables for which UI element is the current first responder, and who it was just before then. That
-- will be used to determine whether to interpret a single mouse click as "I want this UI element to be the first responder" or
-- "I want to edit this table item."
global possibleEditingRequest
global theDelay
global currentFirstResponder
global oldFirstResponder
global okToEdit


-- This handler is used to set up the global varaibles
on will finish launching theObject	
	-- Set up the double-click delay based on the user's preference panel setting.
	-- Possible values are 0.15 through 5, but we can only use integers for the idle handler's return value, and we need it
	-- to always be at least one, so mutate the value accordingly.
	set theDelay to contents of default entry "com.apple.mouse.doubleClickThreshold" of user defaults
	set theDelay to theDelay as integer
	if theDelay < 1 then
		set theDelay to 1
	end if

	-- Of course, there's no way we should be going into editing mode just as the app starts up, so set the flag to false.
	set possibleEditingRequest to false

	-- Similarly, there's no way we should be responding to a table column being clicked, so set that flag to false.
	set okToEdit to true

end will finish launching

-- We need to set the initial values of the currentFirstResponder and oldFirstResponder variables, and this seems to be
-- a good place to do it.
on awake from nib theObject
	if the name of theObject is "mainWindow" then
		set currentFirstResponder to first responder of window "mainWindow"
		set oldFirstResponder to currentFirstResponder
	end if
end on awake from nib

-- This handler is used to count the clicks, to call the double-click action and clear the single-click flag on two clicks, 
-- and to set the flag for the single-click action on one click.
on clicked theObject
	if name of theObject is equal to "theTableView" then
		set theEvent to call method "currentEvent" of (window "mainWindow")
		try
			if ((click count of theEvent) as string) = "2" then --> User double-clicked
				-- We got a second click, so clear the flag (see below)
				set possibleEditingRequest to false
				-- do double-click stuff
			else --> User single-clicked
				if okToEdit then
					-- We've only gotten one click so far, so let's note that fact and then wait for another to come
					-- (or not) before the idle handler is called.
					set possibleEditingRequest to true
					return
				else
					set okToEdit to true
					return
				end if
			end if
		on error
			-- The call to "click count of theEvent" errored out. Treat as single-click event.
			if okToEdit then
				-- We've only gotten one click so far, so let's note that fact and then wait for another to come
				-- (or not) before the idle handler is called.
				set possibleEditingRequest to true
				return
			else
				set okToEdit to true
				return
			end if
		end try
	end if
end if

-- If the user single-clicks on an already-selected row, then clicks on another row before the first row goes into editing mode, then 
-- editing mode should not be entered. This would be more important as the double-click threshold gets toward the max of
-- 5 seconds.
on selection changed theObject
	if the name of theObject is "theTableView" then
		set possibleEditingRequest to false
		set okToEdit to false
	end if
end on selection changed

-- This handler is called when the column header is clicked.
-- Here, we're using it to set a flag variable to indicate this is what happened, so when the "on clicked" handler is fired by
-- the click that hit the column header, it will know to ignore this click.
on column clicked theObject table column tableColumn
	if the name of tableColumn is "theTableColumn" then
		set okToEdit to false
	end if
end column clicked

-- This handler is used to actually run the single-click action, if the flag is set.
-- The delay period is set in the 'on will finish launching' handler and is roughly equal to the double-click speed the user
-- set in his System Preferences setting.
on idle theObject
	-- Only go into editing mode if the user has single-clicked within the last "theDelay" period, and the first
	-- responder hasn't changed.
	set currentFirstResponder to first responder of window "mainWindow"
	if currentFirstResponder = oldFirstResponder then
		if possibleEditingRequest then
			set theTable to (table view "theTableView" of scroll view "theScrollView" of window "mainWindow")
			set s to selected row of theTable
			if s > 0 then
				-- go into edit mode for current selected row
				call method "editColumn:row:withEvent:select:" of theTable with parameters {0, (s - 1), 0, true}
			end if
			set possibleGameEditingRequest to false
		end if
	else
		set oldFirstResponder to currentFirstResponder
		set possibleEditingRequest to false
	end if
	-- Wait an amount of time based on the user's double-click threshold from the System Preferences before calling
	-- this handler again.
	return theDelay
end idle


  1. Add code to the changed value hadler, if you’re not using a data source:
on change cell value theObject row theRow table column tableColumn value theValue
-- Your code here to handle theValue of theRow of theColumn of theObject.
end change cell value
  1. Save, build, and run. It should now enter editing mode on single-click (with a bit of a delay, but entirely appropriate based on your double-click settings, and very much like the slight delay in iTunes on trying to edit), exit it cleanly on pressing Return, and still have the double-click action available for something else.

At least, that’s what has worked for me. Your mileage may vary. If I find any quirks as I go through the final implementation, I’ll post updates here.

Thanks a ton for all the help, Kel!

-Joe

[ Edited 1:51 PM central time to reflect better code for single/double clicks ]
[ Edited 2:36 PM central time to handle selection changing after single-clicking ]
[ Edited 7:00 PM central time to handle switching from another UI element to the table ]
[ Edited 8:00 PM central time to handle clicks on the column header ]
[ Edited 9:30 AM central time for clarity ]

In my case, I had to add code based on the following code by Jobu and John Den Heyer (available here: http://bbs.applescript.net/viewtopic.php?id=10590) to handle the fact that Applescript Studio doesn’t respond to double-clicks of table views if the table view also has an on clicked handler:

on clicked theObject
   if name of theObject = "names" then
      try
         set theEvent to call method "currentEvent" of (window of theObject)
         if ((click count of theEvent) as string) = "2" then --> User double-clicked
            DOUBLE_CLICKED()
            return
         end if
      end try
      if ((control key down of theEvent) as boolean) then --> User wants more info
         SHOW_WINDOW()
      else --> Regular single-click
         log "single Click"
      end if
      return
   end if
end clicked 

I found a better solution to handling single- and double-clicks smoothly, and the result is very much like iTunes (if you’ve ever noticed there’s sometimes a delay between clicking on a song’s name and the edit box appearing, you’ll know what I mean).

I also figured out that tracking row changes isn’t necessary, becaues the “on clicked” handler isn’t called in that case. All we need to know is whether the user has single- or double-clicked, and that’s tough because when the first click comes in, you have no way of knowing whether another one will come in soon enough to count as a double-click. And what does “soon enough” mean, anyway?

I edited my huge post above to reflect my solution to these issues. If anyone has any further improvements, let me know!

-Joe

A problem arose wherein if you clicked on another UI element (another table, a text field, whatever), then clicked back on the table, it would go into editing mode. My solution was to set up two global variables: the old first responder, and the current first responder. They’re set and checked in the “on idle” handler. If they’re the same, then it’s business as usual. If they’re different, then the user has switched from one UI element to the other, so don’t go into editing mode.

I updated the code above to reflect the new change.

-Joe
(going back to testing and ironing out the bugs)

I have two tables in my application. The main table needs to respond to single clicks and double clicks. The secondary table just needs to respond to single clicks. In both cases, single clicks should lead to editing mode for the selected item, if the selected item didn’t just change because of the single click, if the single click wasn’t the user coming back to this table from some other UI element, and if the user wasn’t just clicking on the table header (whew!).

When I first implemented the solution detailed in the big ol’ post above on the main table, I ripped out Kel’s code for detecting whether we’d just changed selections, because it was requiring an extra single-click to go into editing mode.

Then I implemented the same solution for the secondary table, and suddenly changing rows led to editing mode! D’oh!

So I dutifully restored the “check for selection change” code, and now the secondary table worked right, but the primary table was back to needing an extra single-click to go into editing mode. Double d’oh!

Well, I did the expedient thing and used the “check for selection change” code for the secondary table, but not for the primary table. But I didn’t know why they behaved differently…until just now.

I finally put in a slew of log statements and checked the log. It turns out that the goofy error in the Apple code for “click count of theEvent” is of course causing the rest of the “on clicked” hander to not run. That error only happens when the selection has changed, otherwise everything is peachy. So, it just so happens that this code works (with the try block) in the typical case of not wanting to respond to single-clicks that result in a selection change. In the case that someone did for some reason need to run some code, of course the solution is to put that code in an “on error” segment of the try/end try block.

Just thought I’d log this here so someone else doesn’t have to figure it out.

Oh, and I’ll update the code in the big ol’ post to handle clicks on the table column header without going into editing mode.

-Joe

One other note: if you change the selected row in your tables programmatically - for example, on startup to get back to the selection the user was on when he exited the program - then you’ll want to add a flag variable for that so that the okToEdit varible remains true when that operation is complete, since the ‘on selection changed’ handler will be called.

Otherwise, the code above is working well for me. Of course, my version is fairly well modified from that, as it has to deal with my application’s unique issues. Still, the basics are in the big ol’ post above, and I hope that others will find it useful.

Good luck,

-Joe