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)
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
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])
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
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)
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)
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)
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)
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)