class TogaStepper(NSStepper):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def onChange_(self, stepper) -> None:
        self.interface.value = Decimal(stepper.floatValue).quantize(
            self.interface.step)
        if self.interface.on_change:
            self.interface.on_change(self.interface)

    @objc_method
    def controlTextDidChange_(self, notification) -> None:
        try:
            value = str(self._impl.input.stringValue)
            # Try to convert to a decimal. If the value isn't a number,
            # this will raise InvalidOperation
            Decimal(value)
            # We set the input widget's value to the literal text input
            # This preserves the display of "123.", which Decimal will
            # convert to "123"
            self.interface.value = value
            if self.interface.on_change:
                self.interface.on_change(self.interface)
        except InvalidOperation:
            # If the string value isn't valid, reset the widget
            # to the widget's stored value. This will update the
            # display, removing any invalid values from view.
            self.impl.set_value(self.interface.value)
Exemple #2
0
class TogaButton(NSButton):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def onPress_(self, obj) -> None:
        if self.interface.on_press:
            self.interface.on_press(self.interface)
class TogaDocument(NSDocument):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def autosavesInPlace(self) -> bool:
        return True

    @objc_method
    def readFromFileWrapper_ofType_error_(self, fileWrapper, typeName,
                                          outError) -> bool:
        self.interface.read()
        return True
Exemple #4
0
class TogaTextField(NSTextField):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def textDidChange_(self, notification) -> None:
        if self.interface.on_change:
            self.interface.on_change(self.interface)

    @objc_method
    def textShouldEndEditing_(self, textObject) -> bool:
        return self.interface.validate()

    @objc_method
    def becomeFirstResponder(self) -> bool:
        # Cocoa gives and then immediately revokes focus when the widget
        # is first displayed. Set a local attribute on the first *loss*
        # of focus, and only trigger Toga events when that attribute exists.
        if hasattr(self, '_configured'):
            if self.interface.on_gain_focus:
                self.interface.on_gain_focus(self.interface)
        return send_super(__class__, self, 'becomeFirstResponder')

    @objc_method
    def textDidEndEditing_(self, textObject) -> None:
        # Cocoa gives and then immediately revokes focus when the widget
        # is first displayed. Set a local attribute on the first *loss*
        # of focus, and only trigger Toga events when that attribute exists.
        if hasattr(self, '_configured'):
            if self.interface.on_lose_focus:
                self.interface.on_lose_focus(self.interface)
        else:
            self._configured = True

        send_super(__class__,
                   self,
                   'textDidEndEditing:',
                   textObject,
                   argtypes=[c_void_p])
Exemple #5
0
class RefreshableClipView(NSClipView):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def constrainScrollPoint_(self, proposedNewOrigin: NSPoint) -> NSPoint:
        constrained = send_super(__class__,
                                 self,
                                 'constrainScrollPoint:',
                                 proposedNewOrigin,
                                 restype=NSPoint,
                                 argtypes=[NSPoint])

        if self.superview and self.superview.refreshTriggered:
            return NSMakePoint(
                constrained.x,
                max(proposedNewOrigin.y,
                    -self.superview.refreshView.frame.size.height))

        return constrained

    @objc_method
    def documentRect(self) -> NSRect:
        rect = send_super(__class__,
                          self,
                          'documentRect',
                          restype=NSRect,
                          argtypes=[])

        if self.superview and self.superview.refreshTriggered:
            return NSMakeRect(
                rect.origin.x,
                rect.origin.y - self.superview.refreshView.frame.size.height,
                rect.size.width, rect.size.height +
                self.superview.refreshView.frame.size.height)
        return rect
Exemple #6
0
class WindowDelegate(NSObject):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def windowShouldClose_(self, notification) -> bool:
        return self.impl.cocoa_windowShouldClose()

    @objc_method
    def windowDidResize_(self, notification) -> None:
        if self.interface.content:
            # print()
            # print("Window resize", (
            #   notification.object.contentView.frame.size.width,
            #   notification.object.contentView.frame.size.height
            # ))
            if (notification.object.contentView.frame.size.width > 0.0 and
                    notification.object.contentView.frame.size.height > 0.0):
                # Set the window to the new size
                self.interface.content.refresh()

    ######################################################################
    # Toolbar delegate methods
    ######################################################################

    @objc_method
    def toolbarAllowedItemIdentifiers_(self, toolbar):
        "Determine the list of available toolbar items"
        # This method is required by the Cocoa API, but isn't actually invoked,
        # because customizable toolbars are no longer a thing.
        allowed = NSMutableArray.alloc().init()
        for item in self.interface.toolbar:
            allowed.addObject_(toolbar_identifier(item))
        return allowed

    @objc_method
    def toolbarDefaultItemIdentifiers_(self, toolbar):
        "Determine the list of toolbar items that will display by default"
        default = NSMutableArray.alloc().init()
        for item in self.interface.toolbar:
            default.addObject_(toolbar_identifier(item))
        return default

    @objc_method
    def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(
            self, toolbar, identifier, insert: bool):
        "Create the requested toolbar button"
        native = NSToolbarItem.alloc().initWithItemIdentifier_(identifier)
        try:
            item = self.impl._toolbar_items[str(identifier)]
            if item.label:
                native.setLabel(item.label)
                native.setPaletteLabel(item.label)
            if item.tooltip:
                native.setToolTip(item.tooltip)
            if item.icon:
                native.setImage(item.icon._impl.native)

            item._impl.native.append(native)

            native.setTarget_(self)
            native.setAction_(SEL('onToolbarButtonPress:'))
        except KeyError:
            pass

        # Prevent the toolbar item from being deallocated when
        # no Python references remain
        native.retain()
        native.autorelease()
        return native

    @objc_method
    def validateToolbarItem_(self, item) -> bool:
        "Confirm if the toolbar item should be enabled"
        return self.impl._toolbar_items[str(item.itemIdentifier)].enabled

    ######################################################################
    # Toolbar button press delegate methods
    ######################################################################

    @objc_method
    def onToolbarButtonPress_(self, obj) -> None:
        "Invoke the action tied to the toolbar button"
        item = self.impl._toolbar_items[str(obj.itemIdentifier)]
        item.action(obj)
Exemple #7
0
class RefreshableScrollView(NSScrollView):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    # Create Header View
    @objc_method
    def viewDidMoveToWindow(self) -> None:
        self.refreshTriggered = False
        self.isRefreshing = False
        self.refreshView = None
        self.refreshIndicator = None
        self.createRefreshView()

    @objc_method
    def createContentView(self):
        superClipView = ObjCInstance(send_super(__class__, self,
                                                'contentView'))
        if not isinstance(superClipView, RefreshableClipView):
            # create new clipview
            documentView = superClipView.documentView
            clipView = RefreshableClipView.alloc().initWithFrame(
                superClipView.frame)

            clipView.documentView = documentView
            clipView.copiesOnScroll = False
            clipView.drawsBackground = False

            self.setContentView(clipView)
            superClipView = ObjCInstance(
                send_super(__class__, self, 'contentView'))

        return superClipView

    @objc_method
    def createRefreshView(self) -> None:
        # delete old stuff if any
        if self.refreshView:
            self.refreshView.removeFromSuperview()
            self.refreshView.release()
            self.refreshView = None

        self.verticalScrollElasticity = NSScrollElasticityAllowed

        # create new content view
        self.createContentView()

        self.contentView.postsFrameChangedNotifications = True
        self.contentView.postsBoundsChangedNotifications = True

        NSNotificationCenter.defaultCenter.addObserver(
            self,
            selector=SEL('viewBoundsChanged:'),
            name=NSViewBoundsDidChangeNotification,
            object=self.contentView,
        )

        # Create view to hold the refresh widgets refreshview
        contentRect = self.contentView.documentView.frame
        self.refreshView = NSView.alloc().init()
        self.refreshView.translatesAutoresizingMaskIntoConstraints = False

        # Create spinner
        self.refreshIndicator = NSProgressIndicator.alloc().init()
        self.refreshIndicator.style = NSProgressIndicatorSpinningStyle
        self.refreshIndicator.translatesAutoresizingMaskIntoConstraints = False
        self.refreshIndicator.displayedWhenStopped = True
        self.refreshIndicator.usesThreadedAnimation = True
        self.refreshIndicator.indeterminate = True
        self.refreshIndicator.bezeled = False
        self.refreshIndicator.sizeToFit()

        # Center the spinner in the header
        self.refreshIndicator.setFrame(
            NSMakeRect(
                self.refreshView.bounds.size.width / 2 -
                self.refreshIndicator.frame.size.width / 2,
                self.refreshView.bounds.size.height / 2 -
                self.refreshIndicator.frame.size.height / 2,
                self.refreshIndicator.frame.size.width,
                self.refreshIndicator.frame.size.height))

        # Put everything in place
        self.refreshView.addSubview(self.refreshIndicator)
        # self.refreshView.addSubview(self.refreshArrow)
        self.contentView.addSubview(self.refreshView)

        # set layout constraints
        indicatorHCenter = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.refreshIndicator,
            NSLayoutAttributeCenterX,
            NSLayoutRelationEqual,
            self.refreshView,
            NSLayoutAttributeCenterX,
            1.0,
            0,
        )
        self.refreshView.addConstraint(indicatorHCenter)

        indicatorVCenter = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.refreshIndicator,
            NSLayoutAttributeCenterY,
            NSLayoutRelationEqual,
            self.refreshView,
            NSLayoutAttributeCenterY,
            1.0,
            0,
        )
        self.refreshView.addConstraint(indicatorVCenter)

        refreshWidth = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.refreshView,
            NSLayoutAttributeWidth,
            NSLayoutRelationEqual,
            self.contentView,
            NSLayoutAttributeWidth,
            1.0,
            0,
        )
        self.contentView.addConstraint(refreshWidth)

        refreshHeight = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.refreshView,
            NSLayoutAttributeHeight,
            NSLayoutRelationEqual,
            None,
            NSLayoutAttributeNotAnAttribute,
            1.0,
            HEADER_HEIGHT,
        )
        self.contentView.addConstraint(refreshHeight)

        refreshHeight = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(  # noqa: E501
            self.refreshView,
            NSLayoutAttributeTop,
            NSLayoutRelationEqual,
            self.contentView,
            NSLayoutAttributeTop,
            1.0,
            -HEADER_HEIGHT,
        )
        self.contentView.addConstraint(refreshHeight)

        # Scroll to top
        self.contentView.scrollToPoint(NSMakePoint(contentRect.origin.x, 0))
        self.reflectScrolledClipView(self.contentView)

    # Detecting scroll
    @objc_method
    def scrollWheel_(self, event) -> None:
        if event.phase == NSEventPhaseEnded:
            if self.refreshTriggered and not self.isRefreshing:
                self.reload()

        send_super(__class__, self, 'scrollWheel:', event, argtypes=[c_void_p])

    @objc_method
    def viewBoundsChanged_(self, note) -> None:
        if self.isRefreshing:
            return

        if self.contentView.bounds.origin.y <= -self.refreshView.frame.size.height:
            self.refreshTriggered = True

    # Reload
    @objc_method
    def reload(self) -> None:
        """Start a reload, starting the reload spinner"""
        self.isRefreshing = True
        self.refreshIndicator.startAnimation(self)
        self.interface.on_refresh(self.interface)

    @objc_method
    def finishedLoading(self):
        """Invoke to mark the end of a reload, stopping and hiding the reload spinner"""
        self.isRefreshing = False
        self.refreshTriggered = False
        self.refreshIndicator.stopAnimation(self)
        self.detailedlist.reloadData()

        # Force a scroll event to make the scroll hide the reload
        cgEvent = core_graphics.CGEventCreateScrollWheelEvent(
            None, kCGScrollEventUnitLine, 2, 1, 0)
        scrollEvent = NSEvent.eventWithCGEvent(cgEvent)
        self.scrollWheel(scrollEvent)
Exemple #8
0
class TogaTree(NSOutlineView):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    # OutlineViewDataSource methods
    @objc_method
    def outlineView_child_ofItem_(self, tree, child: int, item):
        # Get the object representing the row
        if item is None:
            node = self.interface.data[child]
        else:
            node = item.attrs['node'][child]

        # Get the Cocoa implementation for the row. If an _impl
        # doesn't exist, create a data object for it, and
        # populate it with initial values for each column.
        try:
            node_impl = node._impl
        except AttributeError:
            node_impl = TogaData.alloc().init()
            node_impl.attrs = {'node': node}
            node._impl = node_impl

        return node_impl

    @objc_method
    def outlineView_isItemExpandable_(self, tree, item) -> bool:
        try:
            return item.attrs['node'].can_have_children()
        except AttributeError:
            return False

    @objc_method
    def outlineView_numberOfChildrenOfItem_(self, tree, item) -> int:
        if item is None:
            # How many root elements are there?
            # If we're starting up, the source may not exist yet.
            if self.interface.data is not None:
                return len(self.interface.data)
            else:
                return 0
        else:
            # How many children does this node have?
            return len(item.attrs['node'])

    @objc_method
    def outlineView_viewForTableColumn_item_(self, tree, column, item):

        col_identifier = str(column.identifier)

        try:
            value = getattr(item.attrs['node'], col_identifier)

            # if the value is a widget itself, just draw the widget!
            if isinstance(value, toga.Widget):
                return value._impl.native

            # Allow for an (icon, value) tuple as the simple case
            # for encoding an icon in a table cell. Otherwise, look
            # for an icon attribute.
            elif isinstance(value, tuple):
                icon_iface, value = value
            else:
                try:
                    icon_iface = value.icon
                except AttributeError:
                    icon_iface = None
        except AttributeError:
            # If the node doesn't have a property with the
            # accessor name, assume an empty string value.
            value = ''
            icon_iface = None

        # If the value has an icon, get the _impl.
        # Icons are deferred resources, so we provide the factory.
        if icon_iface:
            icon = icon_iface.bind(self.interface.factory)
        else:
            icon = None

        # creates a NSTableCellView from interface-builder template (does not exist)
        # or reuses an existing view which is currently not needed for painting
        # returns None (nil) if both fails
        identifier = at('CellView_{}'.format(self.interface.id))
        tcv = self.makeViewWithIdentifier(identifier, owner=self)

        if not tcv:  # there is no existing view to reuse so create a new one
            tcv = TogaIconView.alloc().initWithFrame_(
                CGRectMake(0, 0, column.width, 16))
            tcv.identifier = identifier

            # Prevent tcv from being deallocated prematurely when no Python references
            # are left
            tcv.retain()
            tcv.autorelease()

        tcv.setText(str(value))
        if icon:
            tcv.setImage(icon.native)
        else:
            tcv.setImage(None)

        return tcv

    @objc_method
    def outlineView_heightOfRowByItem_(self, tree, item) -> float:

        default_row_height = self.rowHeight

        if item is self:
            return default_row_height

        heights = [default_row_height]

        for column in self.tableColumns:
            value = getattr(item.attrs['node'], str(column.identifier))

            if isinstance(value, toga.Widget):
                # if the cell value is a widget, use its height
                heights.append(
                    value._impl.native.intrinsicContentSize().height)

        return max(heights)

    @objc_method
    def outlineView_pasteboardWriterForItem_(self, tree, item) -> None:
        # this seems to be required to prevent issue 21562075 in AppKit
        return None

    # @objc_method
    # def outlineView_sortDescriptorsDidChange_(self, tableView, oldDescriptors) -> None:
    #
    #     for descriptor in self.sortDescriptors[::-1]:
    #         accessor = descriptor.key
    #         reverse = descriptor.ascending == 1
    #         key = self.interface._sort_keys[str(accessor)]
    #         try:
    #             self.interface.data.sort(str(accessor), reverse=reverse, key=key)
    #         except AttributeError:
    #             pass
    #         else:
    #             self.reloadData()

    @objc_method
    def keyDown_(self, event) -> None:
        # any time this table is in focus and a key is pressed, this method will be called
        if toga_key(event) == {'key': Key.A, 'modifiers': {Key.MOD_1}}:
            if self.interface.multiple_select:
                self.selectAll(self)
        else:
            # forward call to super
            send_super(__class__, self, 'keyDown:', event, argtypes=[c_void_p])

    # OutlineViewDelegate methods
    @objc_method
    def outlineViewSelectionDidChange_(self, notification) -> None:
        if notification.object.selectedRow == -1:
            selected = None
        else:
            selected = self.itemAtRow(
                notification.object.selectedRow).attrs['node']

        if self.interface.on_select:
            self.interface.on_select(self.interface, node=selected)

    # target methods
    @objc_method
    def onDoubleClick_(self, sender) -> None:
        if self.clickedRow == -1:
            node = None
        else:
            node = self.itemAtRow(self.clickedRow).attrs['node']

        if self.interface.on_select:
            self.interface.on_double_click(self.interface, node=node)
Exemple #9
0
class TogaCanvas(NSView):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def drawRect_(self, rect: NSRect) -> None:
        context = NSGraphicsContext.currentContext.CGContext
        # Save the "clean" state of the graphics context.
        core_graphics.CGContextSaveGState(context)
        if self.interface.redraw:
            self.interface._draw(self.impl, draw_context=context)

    @objc_method
    def isFlipped(self) -> bool:
        # Default Cocoa coordinate frame is around the wrong way.
        return True

    @objc_method
    def mouseDown_(self, event) -> None:
        """Invoke the on_press handler if configured."""
        if self.interface.on_press:
            position = self.convertPoint(event.locationInWindow, fromView=None)
            self.interface.on_press(self.interface, position.x, position.y,
                                    event.clickCount)

    @objc_method
    def rightMouseDown_(self, event) -> None:
        """Invoke the on_alt_press handler if configured."""
        if self.interface.on_alt_press:
            position = self.convertPoint(event.locationInWindow, fromView=None)
            self.interface.on_alt_press(self.interface, position.x, position.y,
                                        event.clickCount)

    @objc_method
    def mouseUp_(self, event) -> None:
        """Invoke the on_release handler if configured."""
        if self.interface.on_release:
            position = self.convertPoint(event.locationInWindow, fromView=None)
            self.interface.on_release(self.interface, position.x, position.y,
                                      event.clickCount)

    @objc_method
    def rightMouseUp_(self, event) -> None:
        """Invoke the on_alt_release handler if configured."""
        if self.interface.on_alt_release:
            position = self.convertPoint(event.locationInWindow, fromView=None)
            self.interface.on_alt_release(self.interface, position.x,
                                          position.y, event.clickCount)

    @objc_method
    def mouseDragged_(self, event) -> None:
        """Invoke the on_drag handler if configured."""
        if self.interface.on_drag:
            position = self.convertPoint(event.locationInWindow, fromView=None)
            self.interface.on_drag(self.interface, position.x, position.y,
                                   event.clickCount)

    @objc_method
    def rightMouseDragged_(self, event) -> None:
        """Invoke the on_alt_drag handler if configured."""
        if self.interface.on_alt_drag:
            position = self.convertPoint(event.locationInWindow, fromView=None)
            self.interface.on_alt_drag(self.interface, position.x, position.y,
                                       event.clickCount)
Exemple #10
0
class TogaTable(NSTableView):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    # TableDataSource methods
    @objc_method
    def numberOfRowsInTableView_(self, table) -> int:
        return len(self.interface.data) if self.interface.data else 0

    @objc_method
    def tableView_viewForTableColumn_row_(self, table, column, row: int):
        data_row = self.interface.data[row]
        col_identifier = str(column.identifier)

        try:
            value = getattr(data_row, col_identifier)

            # if the value is a widget itself, just draw the widget!
            if isinstance(value, toga.Widget):
                return value._impl.native

            # Allow for an (icon, value) tuple as the simple case
            # for encoding an icon in a table cell. Otherwise, look
            # for an icon attribute.
            elif isinstance(value, tuple):
                icon_iface, value = value
            else:
                try:
                    icon_iface = value.icon
                except AttributeError:
                    icon_iface = None
        except AttributeError:
            # The accessor doesn't exist in the data. Use the missing value.
            try:
                value = self.interface.missing_value
            except ValueError as e:
                # There is no explicit missing value. Warn the user.
                message, value = e.args
                print(message.format(row, col_identifier))
            icon_iface = None

        # If the value has an icon, get the _impl.
        # Icons are deferred resources, so we provide the factory.
        if icon_iface:
            icon = icon_iface.bind(self.interface.factory)
        else:
            icon = None

        # creates a NSTableCellView from interface-builder template (does not exist)
        # or reuses an existing view which is currently not needed for painting
        # returns None (nil) if both fails
        identifier = at('CellView_{}'.format(self.interface.id))
        tcv = self.makeViewWithIdentifier(identifier, owner=self)

        if not tcv:  # there is no existing view to reuse so create a new one
            tcv = TogaIconView.alloc().init()
            tcv.identifier = identifier

            # Prevent tcv from being deallocated prematurely when no Python references
            # are left
            tcv.retain()
            tcv.autorelease()

        tcv.setText(str(value))
        if icon:
            tcv.setImage(icon.native)
        else:
            tcv.setImage(None)

        # Keep track of last visible view for row
        self.impl._view_for_row[data_row] = tcv

        return tcv

    @objc_method
    def tableView_pasteboardWriterForRow_(self, table, row) -> None:
        # this seems to be required to prevent issue 21562075 in AppKit
        return None

    # TableDelegate methods
    @objc_method
    def selectionShouldChangeInTableView_(self, table) -> bool:
        # Explicitly allow selection on the table.
        # TODO: return False to disable selection.
        return True

    @objc_method
    def tableViewSelectionDidChange_(self, notification) -> None:
        if notification.object.selectedRow == -1:
            selected = None
        else:
            selected = self.interface.data[notification.object.selectedRow]

        if self.interface.on_select:
            self.interface.on_select(self.interface, row=selected)

    # 2021-09-04: Commented out this method because it appears to be a
    # source of significant slowdown when the table has a lot of data
    # (10k rows). AFAICT, it's only needed if we want custom row heights
    # for each row. Since we don't currently support custom row heights,
    # we're paying the cost for no benefit.
    # @objc_method
    # def tableView_heightOfRow_(self, table, row: int) -> float:
    #     default_row_height = self.rowHeight
    #     margin = 2
    #
    #     # get all views in column
    #     data_row = self.interface.data[row]
    #
    #     heights = [default_row_height]
    #
    #     for column in self.tableColumns:
    #         col_identifier = str(column.identifier)
    #         value = getattr(data_row, col_identifier, None)
    #         if isinstance(value, toga.Widget):
    #             # if the cell value is a widget, use its height
    #             heights.append(value._impl.native.intrinsicContentSize().height + margin)
    #
    #     return max(heights)

    # target methods
    @objc_method
    def onDoubleClick_(self, sender) -> None:
        if self.clickedRow == -1:
            clicked = None
        else:
            clicked = self.interface.data[self.clickedRow]

        if self.interface.on_double_click:
            self.interface.on_double_click(self.interface, row=clicked)
class TogaList(NSTableView):

    interface = objc_property(object, weak=True)
    impl = objc_property(object, weak=True)

    @objc_method
    def menuForEvent_(self, event):
        if self.interface.on_delete:
            mousePoint = self.convertPoint(event.locationInWindow,
                                           fromView=None)
            row = self.rowAtPoint(mousePoint)

            popup = NSMenu.alloc().initWithTitle("popup")
            delete_item = popup.addItemWithTitle(
                "Delete", action=SEL('actionDeleteRow:'), keyEquivalent="")
            delete_item.tag = row
            # action_item = popup.addItemWithTitle("???", action=SEL('actionRow:'), keyEquivalent="")
            # action_item.tag = row

            return popup

    @objc_method
    def actionDeleteRow_(self, menuitem):
        row = self.interface.data[menuitem.tag]
        self.interface.on_delete(self.interface, row=row)

    # TableDataSource methods
    @objc_method
    def numberOfRowsInTableView_(self, table) -> int:
        return len(self.interface.data) if self.interface.data else 0

    @objc_method
    def tableView_objectValueForTableColumn_row_(self, table, column,
                                                 row: int):
        value = self.interface.data[row]
        try:
            data = value._impl
        except AttributeError:
            data = TogaData.alloc().init()
            data.retain()
            value._impl = data

        data.attrs = {
            attr: attr_impl(value, attr, self.interface.factory)
            for attr in value._attrs
        }

        return data

    # TableDelegate methods
    @objc_method
    def tableView_heightOfRow_(self, table, row: int) -> float:
        return 48.0

    @objc_method
    def selectionShouldChangeInTableView_(self, table) -> bool:
        # Explicitly allow selection on the table.
        # TODO: return False to disable selection.
        return True

    @objc_method
    def tableViewSelectionDidChange_(self, notification) -> None:
        index = notification.object.selectedRow
        if index == -1:
            selection = None
        else:
            selection = self.interface.data[index]

        if self.interface.on_select:
            self.interface.on_select(self.interface, row=selection)