Реагировать на события мыши в текстовом поле в табличном представлении на основе представления

У меня есть текстовые поля внутри пользовательского представления внутри файла NSOutlineView. Для редактирования одной из этих ячеек требуется один щелчок, пауза и еще один щелчок. Первый одиночный щелчок выбирает строку табличного представления, а второй одиночный щелчок рисует курсор в поле. Двойной щелчок по ячейке, который позволяет редактировать в представлении таблицы на основе ячеек, выбирает только строку.

Поведение, которое я хочу: один щелчок, чтобы изменить выбор и отредактировать.

Что мне нужно переопределить, чтобы получить такое поведение?

Я читал некоторые другие сообщения:

  • Шаблон NSTextField приспособленца, по-видимому, не применим к основанным на представлениях табличные представления, где все представления ячеек создаются из перьев.
  • Я попытался создать подкласс NSTextField, например этот решение описывает, но мой переопределенный метод mouseDown не вызывается. Вызываются переопределенные awakeFromNib и viewWillDraw (упомянутые в этом сообщении) . Конечно, mouseDown вызывается, если я помещаю текстовое поле где-то за пределами табличного представления.

Для сравнения, NSSegmentedControl в представлении моей ячейки изменяет свое значение без предварительного выбора строки.


Вот рабочее решение, адаптированное из принятого ответа:

В подклассе представления схемы:

-(void)mouseDown:(NSEvent *)theEvent {
    [super mouseDown:theEvent];

    // Forward the click to the row's cell view
    NSPoint selfPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
    NSInteger row = [self rowAtPoint:selfPoint];
    if (row>=0) [(CellViewSubclass *)[self viewAtColumn:0 row:row makeIfNecessary:NO]
            mouseDownForTextFields:theEvent];
}

В подклассе представления ячейки таблицы:

// Respond to clicks within text fields only, because other clicks will be duplicates of events passed to mouseDown
- (void)mouseDownForTextFields:(NSEvent *)theEvent {
    // If shift or command are being held, we're selecting rows, so ignore
    if ((NSCommandKeyMask | NSShiftKeyMask) & [theEvent modifierFlags]) return;
    NSPoint selfPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
    for (NSView *subview in [self subviews])
        if ([subview isKindOfClass:[NSTextField class]])
            if (NSPointInRect(selfPoint, [subview frame]))
                [[self window] makeFirstResponder:subview];
}

person paulmelnikow    schedule 18.08.2011    source источник


Ответы (6)


Я попытаюсь вернуть одолжение ... Подкласс NSOutlineView и переопределить -mouseDown: следующим образом:

- (void)mouseDown:(NSEvent *)theEvent {
    [super mouseDown:theEvent];

    // Only take effect for double clicks; remove to allow for single clicks
    if (theEvent.clickCount < 2) {
        return;
    }

    // Get the row on which the user clicked
    NSPoint localPoint = [self convertPoint:theEvent.locationInWindow
                                   fromView:nil];
    NSInteger row = [self rowAtPoint:localPoint];

    // If the user didn't click on a row, we're done
    if (row < 0) {
        return;
    }

    // Get the view clicked on
    NSTableCellView *view = [self viewAtColumn:0 row:row makeIfNecessary:NO];

    // If the field can be edited, pop the editor into edit mode
    if (view.textField.isEditable) {
        [[view window] makeFirstResponder:view.textField];
    }
}
person Dov    schedule 19.08.2011
comment
Спасибо! Я делаю успехи с этим подходом! Однако это не работает полностью. Есть несколько текстовых полей, поэтому мне нужно проверить, какое поле (если есть) нажато, прежде чем действовать. Пересылка события в подпредставление для обработки приводит к дублированию, что имеет смысл, поэтому вместо этого я передаю его методу mouseDownForTextFields. Я мог бы связать все поля получения с коллекцией выходов, что мне пришлось бы сделать для каждого стиля представления (семь из них). Есть ли способ получить их все программно? - person paulmelnikow; 19.08.2011
comment
Я так не думаю, вне размышлений. Поддерживает ли Mac теперь коллекции розеток в Lion? Что касается Snow Leopard, это было только для iOS. - person Dov; 19.08.2011
comment
О, ты прав. Пока только iOS. Как я могу использовать отражение? Я думаю, я мог бы попытаться переопределить setTextField:, чтобы реализовать свою собственную коллекцию, или создать текстовые поля, которые регистрируются в представлении схемы в awakeFromNib. - person paulmelnikow; 19.08.2011
comment
Я бы не советовал переопределять setTextField:, это будет похоже на взлом и может иметь побочные эффекты. Я сам не использовал отражение в Objective-C, но я прочитал эту интересную статью: pilky.me/view/21 Я бы предложил пройтись по массиву -subviews вашего представления и проверить те, которые являются NSTextFields для проверки попадания с помощью NSPointInRect() - person Dov; 20.08.2011

Была такая же проблема. После долгих усилий это волшебным образом сработало, когда я выбрал None вместо Regular по умолчанию (другой вариант - Source List) для параметра Highlight табличного представления в IB!

Другим вариантом является решение на https://stackoverflow.com/a/13579469/804616, которое кажется более конкретным. но немного хакерский по сравнению с этим.

person trss    schedule 26.03.2013
comment
Вы имеете в виду выбор «Нет» вместо «Обычный» и отсутствие изменения кода? Это решение, которое вы связали, выглядит великолепно. - person paulmelnikow; 26.03.2013
comment
Ага! Я предполагаю, что реализация по умолчанию validateProposedFirstResponder:forEvent: NSTableView проверяет тип Highlight и соответственно возвращает YES или NO. - person trss; 27.03.2013
comment
Я проголосовал за него, потому что это звучит полезно, но у меня еще не было возможности попробовать его вместо другого кода. - person paulmelnikow; 01.04.2013
comment
Ах хорошо. Поскольку вы отредактировали ответ, я подумал, что вы, должно быть, пробовали :) - person trss; 01.04.2013
comment
Я попробовал это решение, которое вы упомянули. Это работает, но тогда событие нажатия мыши в подклассе табличного представления не вызывается. - person Miek; 21.06.2014
comment
У меня тоже была эта проблема, и я подтверждаю, что простое изменение режима выделения на «Нет» решило ее. - person Dejal; 11.11.2014
comment
Это связано с тем, что реализация переходит к super, если стиль выделения равен None. - person corbin dunn; 08.05.2017

Вы действительно хотите переопределить validateProposedFirstResponder и разрешить создание конкретного первого ответчика (или нет) в зависимости от вашей логики. Реализация в NSTableView (вроде) такова (я переписываю ее как псевдокод):

- (BOOL)validateProposedFirstResponder:(NSResponder *)responder forEvent:(NSEvent *)event {

// We want to not do anything for the following conditions:
// 1. We aren't view based (sometimes people have subviews in tables when they aren't view based)
// 2. The responder to valididate is ourselves (we send this up the chain, in case we are in another tableview)
// 3. We don't have a selection highlight style; in that case, we just let things go through, since the user can't appear to select anything anyways.
if (!isViewBased || responder == self || [self selectionHighlightStyle] == NSTableViewSelectionHighlightStyleNone) {
    return [super validateProposedFirstResponder:responder forEvent:event];
}

if (![responder isKindOfClass:[NSControl class]]) {
    // Let any non-control become first responder whenever it wants
    result = YES;
    // Exclude NSTableCellView. 
    if ([responder isKindOfClass:[NSTableCellView class]]) {
        result = NO;
    }

} else if ([responder isKindOfClass:[NSButton class]]) {
    // Let all buttons go through; this would be caught later on in our hit testing, but we also do it here to make it cleaner and easier to read what we want. We want buttons to track at anytime without any restrictions. They are always valid to become the first responder. Text editing isn't.
    result = YES;
} else if (event == nil) {
    // If we don't have any event, then we will consider it valid only if it is already the first responder
    NSResponder *currentResponder = self.window.firstResponder;
    if (currentResponder != nil && [currentResponder isKindOfClass:[NSView class]] && [(NSView *)currentResponder isDescendantOf:(NSView *)responder]) {
        result = YES;
    }
} else {
    if ([event type] == NSEventTypeLeftMouseDown || [event type] == NSEventTypeRightMouseDown) {
        // If it was a double click, and we have a double action, then send that to the table
        if ([self doubleAction] != NULL && [event clickCount] > 1) {
           [cancel the first responder delay];
        }
   ...
          The code here checks to see if the text field 
        cell had text hit. If it did, it attempts to edit it on a delay. 
        Editing is simply making that NSTextField the first responder.
        ...

     }
person corbin dunn    schedule 08.05.2017

Я написал следующее, чтобы поддержать случай, когда у вас есть более сложный NSTableViewCell с несколькими текстовыми полями или когда текстовое поле не занимает всю ячейку. Здесь есть трюк для переворачивания значений y, потому что, когда вы переключаетесь между NSOutlineView или NSTableView и его NSTableCellViews, система координат переворачивается.

- (void)mouseDown:(NSEvent *)theEvent
{
    [super mouseDown: theEvent];

    NSPoint thePoint = [self.window.contentView convertPoint: theEvent.locationInWindow
                                                      toView: self];
    NSInteger row = [self rowAtPoint: thePoint];
    if (row != -1) {
        NSView *view = [self viewAtColumn: 0
                                      row: row
                          makeIfNecessary: NO];

        thePoint = [view convertPoint: thePoint
                             fromView: self];
        if ([view isFlipped] != [self isFlipped])
            thePoint.y = RectGetHeight(view.bounds) - thePoint.y;

        view = [view hitTest: thePoint];
        if ([view isKindOfClass: [NSTextField class]]) {
            NSTextField *textField = (NSTextField *)view;
            if (textField.isEnabled && textField.window.firstResponder != textField)

                dispatch_async(dispatch_get_main_queue(), ^{
                    [textField selectText: nil];
                });
        }
    }
}
person greg    schedule 01.01.2015
comment
Хороший! Почему dispatch_async? - person paulmelnikow; 01.01.2015
comment
@noa Я делаю это, когда хочу, чтобы текущий поток выполнения продолжался / завершался перед выполнением некоторого вспомогательного фрагмента кода. Например, иногда функции пользовательского интерфейса ожидают возврата до того, как будет сделан другой вызов, изменяющий пользовательский интерфейс (или, по крайней мере, опыт показывает это). - person greg; 02.01.2015
comment
Понял. Зачем это нужно в данном случае? - person paulmelnikow; 02.01.2015
comment
@noa В этом случае я добавил его, потому что хочу, чтобы выбор строки в табличном представлении был завершен до выбора текста. Это может быть не строго необходимо. Но если mouseDown вызывает активацию представления таблицы/схемы, а затем выбор строки, я думаю, что эти действия должны быть завершены до активации текстового поля. Например, mouseDown инициирует изменение первого ответчика на табличное представление, если это еще не было. Затем это может переопределить текстовое поле, установленное в качестве первого ответчика. Опять же, пробег может варьироваться. Я осторожен в этом случае. - person greg; 02.01.2015
comment
Использование dispatch_async для всего, что обновляет интерфейс, не является хорошей практикой. Используйте dispatch_sync в основной очереди, но ничего асинхронного. См., например, stackoverflow.com/questions/17490286/ - person Elise van Looij; 29.05.2017
comment
@ElisevanLooij Это действительно зависит от того, нужно ли вам синхронное поведение или нет, и от того, что вы пытаетесь сделать. В этом случае синхронный вызов заблокирует поток и не позволит завершить реализацию mouseDown. Цель здесь состоит в том, чтобы разрешить выполнение всех действий UIKit, а затем выполнить selectText. Все, что происходит до обновления необходимого пользовательского интерфейса. Я думаю, вы можете путать асинхронность с фоном. Вы можете запустить асинхронную отправку, но все еще в основном потоке, поэтому все действия пользовательского интерфейса в порядке. - person greg; 02.06.2017
comment
Я имел в виду Cocoa, а не UIKit. - person greg; 02.06.2017

Просто хочу отметить, что если все, что вам нужно, это только редактирование (т.е. в таблице без выбора), переопределение -hitTest: кажется более простым и более похожим на Cocoa:

- (NSView *)hitTest:(NSPoint)aPoint
{
    NSInteger column = [self columnAtPoint: aPoint];
    NSInteger row = [self rowAtPoint: aPoint];

    // Give cell view a chance to override table hit testing
    if (row != -1 && column != -1) {
        NSView *cell = [self viewAtColumn:column row:row makeIfNecessary:NO];

        // Use cell frame, since convertPoint: doesn't always seem to work.
        NSRect frame = [self frameOfCellAtColumn:column row:row];
        NSView *hit = [cell hitTest: NSMakePoint(aPoint.x + frame.origin.x, aPoint.y + frame.origin.y)];

        if (hit)
            return hit;
    }

    // Default implementation
    return [super hitTest: aPoint];
}
person Max Seelemann    schedule 17.11.2012
comment
Хороший вопрос, спасибо за публикацию! Могу попробовать для сравнения. - person paulmelnikow; 17.11.2012
comment
Я не уверен, что вы имеете в виду под редактированием в таблице без выбора, но в любом случае проблема с переопределением hitTest: вместо validateProposedFirstResponder:forEvent: заключается в том, что вы пропускаете всю информацию NSEvent: двойной или одиночный щелчок, mouseEvent или иначе и т.д. - person Elise van Looij; 29.05.2017

Вот быстрая версия 4.2 ответа @Dov:

 override func mouseDown(with event: NSEvent) {
    super.mouseDown(with: event)

    if (event.clickCount < 2) {
        return;
    }
    // Get the row on which the user clicked
    let localPoint = self.convert(event.locationInWindow, from: nil)


    let row = self.row(at: localPoint)

    // If the user didn't click on a row, we're done
    if (row < 0) {
        return
    }

    DispatchQueue.main.async {[weak self] in
        guard let self = self else {return}
        // Get the view clicked on
        if let clickedCell = self.view(atColumn: 0, row: row, makeIfNecessary: false) as? YourOutlineViewCellClass{

            let pointInCell = clickedCell.convert(localPoint, from: self)

            if (clickedCell.txtField.isEditable && clickedCell.txtField.hitTest(pointInCell) != nil){

                clickedCell.window?.makeFirstResponder(clickedCell.txtField)

            }

        }
    }

}
person M Afham    schedule 24.03.2020