How to implement NSTableViewRowlView like Xcode IB object selector - nstablerowview

I am trying to implement a NSTableView that looks similar to the Xcode IB object selector (bottom right panel). As shown below when a row is selected a full width horizontal line is draw above and below the selected row.
I have successfully created a subclass of NSTableRowView and have used the isNextRowSelected property to determine whether to draw a full width separator and this almost works.
The issue is the row above the selected row is not being redrawn unless you happened to select a row and then select the row below it immediately afterwards.
How can I efficiently get the NSTableView to redraw the row above the selected row every time ?
Here is my implementation when a single row is selected
And another if a the row immediately below is now selected - which is what I want.
/// This subclass draws a partial line as the separator for unselected rows and a full width line above and below for selected rows
/// | ROW |
/// | ---------- | unselected separator
/// |------------| selected separator on row above selected row
/// | ROW |
/// |------------| selected separator
///
/// Issue: Row above selected row does not get redrawn when selected row is deselected
class OSTableRowView: NSTableRowView {
let separatorColor = NSColor(calibratedWhite: 0.35, alpha: 1)
let selectedSeparatorColor = NSColor(calibratedWhite: 0.15, alpha: 1)
let selectedFillColor = NSColor(calibratedWhite: 0.82, alpha: 1)
override func drawSeparator(in dirtyRect: NSRect) {
let yBottom = self.bounds.height
let gap: CGFloat = 4.0
let xLeft: CGFloat = 0.0
let xRight = xLeft + self.bounds.width
let lines = NSBezierPath()
/// Draw a full width separator if the item is selected or if the next row is selected
if self.isSelected || self.isNextRowSelected {
selectedSeparatorColor.setStroke()
lines.move(to: NSPoint(x: xLeft, y: yBottom))
lines.line(to: NSPoint(x: xRight, y: yBottom))
lines.lineWidth = 1.0
} else {
separatorColor.setStroke()
lines.move(to: NSPoint(x: xLeft+gap, y: yBottom))
lines.line(to: NSPoint(x: xRight-gap, y: yBottom))
lines.lineWidth = 0.0
}
lines.stroke()
}
override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none {
let selectionRect = self.bounds
selectedSeparatorColor.setStroke()
selectedFillColor.setFill()
selectionRect.fill()
}
}
}
After reading a few other posts I tried adding code to cause the preceding row to be redraw. This appears to have not effect.
func selectionShouldChange(in tableView: NSTableView) -> Bool {
let selection = tableView.selectedRow
if selection > 0 {
tableView.setNeedsDisplay(tableView.rect(ofRow: selection-1))
tableView.displayIfNeeded()
}
return true
}
And nor does this.
func tableViewSelectionDidChange(_ notification: Notification) {
guard let tableView = self.sidebarOutlineView else {
return
}
let row = tableView.selectedRow
if row > 0 {
tableView.setNeedsDisplay(tableView.rect(ofRow: row-1))
print("row-1 update rect: \(tableView.rect(ofRow: row-1))")
}
}
Seems odd that neither of these trigger redrawing of the row - am I missing something here!
EDIT:
OK I found something that seems to work OKish - there is still a visible lag in the redrawing of the row above the deselected row which is not present in the XCode tableView.
var lastSelectedRow = -1 {
didSet {
guard let tableView = self.sidebarOutlineView else {
return
}
if oldValue != lastSelectedRow {
if oldValue > 0 {
if let view = tableView.rowView(atRow: oldValue-1, makeIfNecessary: false) {
view.needsDisplay = true
}
}
if lastSelectedRow > 0 {
if let view = tableView.rowView(atRow: lastSelectedRow-1, makeIfNecessary: false) {
view.needsDisplay = true
}
}
}
}
}
and then simply set the value of the variable lastSelectedRow = tableView.selectedRow in the tableViewSelectionDidChange(:) method.
I think perhaps the tableView needs to be subclassed to make sure that both rows are redrawn in the same update cycle.

This NSTableRowView subclass seems to work fine with no visible lag in redrawing the row above any more.
The solution was to override the isSelected properly and set needsDisplay on the row above each time.
/// This subclass draws a partial line as the separator for unselected rows and a full width line above and below for selected rows
/// | ROW |
/// | ---------- | unselected separator
/// |------------| selected separator on row above selected row
/// | ROW |
/// |------------| selected separator
///
/// Issue: Row above selected row does not get redrawn when selected row is deselected
class OSTableRowView: NSTableRowView {
let separatorColor = NSColor(calibratedWhite: 0.35, alpha: 1)
let selectedSeparatorColor = NSColor(calibratedWhite: 0.15, alpha: 1)
let selectedFillColor = NSColor(calibratedWhite: 0.82, alpha: 1)
/// Override this and whenever it is changed set the previous row to be updated
override var isSelected: Bool {
didSet {
if let tableView = self.superview as? NSTableView {
let row = tableView.row(for: self)
if row > 0 {
tableView.rowView(atRow: row-1, makeIfNecessary: false)?.needsDisplay = true
}
}
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override func drawSeparator(in dirtyRect: NSRect) {
let yBottom = self.bounds.height
let gap: CGFloat = 4.0
let xLeft: CGFloat = 0.0
let xRight = xLeft + self.bounds.width
let lines = NSBezierPath()
/// Draw a full width separator if the item is selected or if the next row is selected
if self.isSelected || self.isNextRowSelected {
selectedSeparatorColor.setStroke()
lines.move(to: NSPoint(x: xLeft, y: yBottom))
lines.line(to: NSPoint(x: xRight, y: yBottom))
lines.lineWidth = 1.0
} else {
separatorColor.setStroke()
lines.move(to: NSPoint(x: xLeft+gap, y: yBottom))
lines.line(to: NSPoint(x: xRight-gap, y: yBottom))
lines.lineWidth = 0.0
}
lines.stroke()
}
override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none {
let selectionRect = self.bounds
selectedSeparatorColor.setStroke()
selectedFillColor.setFill()
selectionRect.fill()
}
}
}

Related

SwiftUI - share dictionary among views, unclear what arguments to use at #Main / WindowGroup

I'm trying to build an app (macOS, but would be the same for iOS) that creates a number of grids, the outcome of which is to be shown in a second screen. For this, I'm sharing data across these screens, and I'm running into an issue here, I hope someone can help or point me in the right direction. I'll share a simplified version of the code below (working in Xcode 14.0.1)
The code creates a dictionary that can be shown in a grid, on which calculations can be done. The idea is then to add this grid, with some descriptive variables, into another dictionary
The building blocks of the grid are cells
Import Foundation
struct Cell: Comparable, Equatable, Identifiable, Hashable {
static func == (lhs: Cell, rhs: Cell) -> Bool {
lhs.randomVarOne == rhs.randomVarOne
}
var randomVarOne: Double
var randomVarTwo: Bool
// other vars omitted here
var id: Int { randomVarOne }
static func < (lhs: Cell, rhs: Cell) -> Bool {
return lhs.randomVarOne < rhs.randomVarOne
}
}
this is also where there are a bunch of funcs to calculate next neighbor cells in the grid etc
then the grid is defined in a class:
class Info: ObservableObject, Hashable {
static func == (lhs: Info, rhs: Info) -> Bool {
lhs.grid == rhs.grid
}
func hash(into hasher: inout Hasher) {
hasher.combine(grid)
}
#Published var grid = [Cell]()
var arrayTotal = 900
#Published var toBeUsedForTheGridCalculations: Double = 0.0
var toBeUsedToSetTheVarAbove: Double = 0.0
var rowTotalDouble: Double {sqrt(Double(arrayTotal)) }
var rowTotal: Int {
Int(rowTotalDouble) != 0 ? Int(rowTotalDouble) : 10 }
The class includes a func to create and populate the grid with Cells and add these Cells to the grid var. It also includes the formulas to do the calculations on the grid using a user input. The class did not seem to need an initializer.
This is the Scenario struct:
struct Scenario: Comparable, Equatable, Identifiable, Hashable {
static func == (lhs: Scenario, rhs: Scenario) -> Bool {
lhs.scenarioNumber == rhs.scenarioNumber
}
func hash(into hasher: inout Hasher) {
hasher.combine(scenarioNumber)
}
var scenarioNumber: Int
var date: Date
var thisIsOneSnapshot = [Info]()
var id: Int { scenarioNumber }
static func < (lhs: Scenario, rhs: Scenario) -> Bool {
return lhs.scenarioNumber < rhs.scenarioNumber
}
}
added hashable since it uses the Info class as an input.
Then there is the class showing the output overview
class OutputOverview: ObservableObject {
#Published var snapshot = [Scenario]()
// the class includes a formula of how to add the collection of cells (grid) and the additional variables to the snapshot dictionary. Again no initializer was necessary.
Now to go to the ContentView.
struct ContentView: View {
#Environment(\.openURL) var openURL
var scenarioNumberInput: Int = 0
var timeStampAssigned: Date = Date.now
#ObservedObject private var currentGrid: Info = Info()
#ObservedObject private var scenarios: Combinations = Combinations()
var usedForTheCalculations: Double = 0.0
var rows =
[
GridItem(.flexible()),
// whole list of GridItems, I do not know how to calculate these:
// var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal)
//gives error "Cannot use instance member 'currentGrid' within property initializer;
// property iunitializers run before 'self' is available
]
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
LazyHGrid(rows: rows, spacing: 0) {
ForEach(0..<currentGrid.grid.count, id :\.self) { w in
let temp = currentGrid.grid[w].varThatAffectsFontColor
let temp2 = currentGrid.grid[w].varThatAffectsBackground
Text("\(currentGrid.grid[w].randomVarOne, specifier: "%.2f")")
.frame(width: 25, height: 25)
.border(.black)
.font(.system(size: 7))
.foregroundColor(Color(wordName: temp))
.background(Color(wordName: temp2))
}
}
.padding(.top)
}
VStack{
HStack {
Button("Start") {
}
// then some buttons to do the calculations
Button("Add to collection"){
scenarios.addScenario(numbering: scenarioNumberInput, timeStamp:
Date.now, collection: currentGrid.grid)
} // this should add the newly recalculated grid to the dictionary
Button("Go to Results") {
guard let url = URL(string: "myapp://scenario") else { return }
openURL(url)
} // to go to the screen showing the scenarios
Then the second View, the ScenarioView:
struct ScenarioView: View {
#State var selectedScenario = 1
#ObservedObject private var scenarios: OutputOverview
var pickerNumbers = [ 1, 2, 3, 4 , 5]
// this is to be linked to the number of scenarios completed,this code is not done yet.
var rows =
[
GridItem(.flexible()),
GridItem(.flexible()),
// similar list of GridItems here....
var body: some View {
Form {
Section {
Picker("Select a scenario", selection: $selectedScenario) {
ForEach(pickerNumbers, id: \.self) {
Text("\($0)")
}
}
}
Section {
ScrollView {
if let idx = scenarios.snapshot.firstIndex(where:
{$0.scenarioNumber == selectedScenario}) {
LazyHGrid(rows: rows, spacing: 0) {
ForEach(0..<scenarios.snapshot[idx].thisIsOneSnapshot.count,
id :\.self) { w in
let temp =
scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsFontColor
let temp2 =
scenarios.snapshot[idx].thisIsOneSnapshot[w].varThatAffectsBackground
Text("\(scenarios.snapshot[idx].thisIsOneSnapshot[w].randomVarOne, specifier: "%.2f")")
.frame(width: 25, height: 25)
.border(.black)
.font(.system(size: 7))
.foregroundColor(Color(wordName: temp))
.background(Color(wordName: temp2))
}
}
}
}
}
}
}
}
Now while the above does not (for the moment..) give me error messages, I am not able to run the PreviewProvider in the second View. The main problem is in #main:
import SwiftUI
#main
struct ThisIsTheNameOfMyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.handlesExternalEvents(matching: ["main"])
WindowGroup("Scenarios") {
ScenarioView()
// error messages here: 'ScenarioView' initializer is inaccessible due to "private"
// protection level - I don't know what is set to private in ScenarioView that could
// cause this
// second error message: missing argument for parameter 'scenarios' in call
}
.handlesExternalEvents(matching: ["scenario"])
}
}
I am at a loss on how to solve these 2 error messages and would be very grateful for any tips or guidance. Apologies if this question is very long, I scanned many other forum questions and could not find any good answers.
I have tried adding pro forma data in #main as follows
#main
struct FloodModelScenarioViewerApp: App {
#State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
var body: some Scene {
WindowGroup {
ContentView()
}
.handlesExternalEvents(matching: ["main"])
WindowGroup("Scenarios") {
ScenarioView(scenarios: scenarios)
}
.handlesExternalEvents(matching: ["scenario"])
}
}
This still gives 2 error messages:
same issue with regards to ScenarioView initialiser being inaccessible due to being 'private'
Cannot convert value of type 'Scenario' to expected argument type 'OutputOverview'
Just remove the private from
#ObservedObject private var scenarios: OutputOverview
The value is coming from he parent so the parent needs access. So put
#StateObject private var scenarios: OutputOverview = .init()
in FloodModelScenarioViewerApp
#StateObject is for initializing ObservableObjects and #ObservedObject is for passing them around.
I don't know if your code will work after you read this question, and that's because there are many things to correct, but you can start with these:
In Cell, you shouldn't use an id that is a variable, this may cause inconsistent behavior. Use something like:
let id = UUID()
When you initialize ContentView, you can't use currentGrid inside a variable because currentGrid will not be available before all variables are initialized. Meaning, you are trying to initialize rows before currentGrid actually exists. You can try using the .onAppear modifier:
var rows = [GridItem]()
var body: some View {
GeometryReader { geometry in
// ... view code in here
}
.onAppear {
var rows = Array(repeating: GridItem(.flexible()), count: currentGrid.rowTotal)
}
}
This creates the view and, before showing it, the grid is set to its proper value.
The message 'ScenarioView' initializer is inaccessible due to "private" protection level seems clear: you must provide a value to to the variable scenarios (it doesn't have a default value) but it's marked as private. Remove private.
#ObservedObject var scenarios: OutputOverview
Then, remember to pass a value of type OutputOverview for the variable when you call the view:
ScenarioView(scenarios: aVariableOfTypeOutputOverview)
The type mismatch error you get inside the #main code is also clear - you have defined a variable of type Scenario:
#State var scenarios = Scenario(scenarioNumber: 1, date: Date.now)
but ScenarioView requires another type:
#ObservedObject private var scenarios: OutputOverview
One of them needs change for your code to work.

TableView cell request Focus

I'm new in JavaFX and have the following issue:
I have a tableview inside a BorderPane. I want it to focus on the last row/1st column when it's loaded. I have tried the following:
requestfocus()
scrollTo()
focusModel.focus()
selectionModel.select()
What happens is that the cell I want is indeed blue (as if it was selected) but the first cell has a blue border. So, when I try to use the arrow keys, the selected cell moves to the first row.
BTW, I'm using TornadoFX.
Any ideas?
Thanks in advance!
class CashflowTab : View() {
override val root: HBox by fxml()
private val mController : CashflowController by inject()
private val mainView : MainView by inject()
// Get the buttons
private val buttonCashflow : Button by fxid("btnCashflow")
init {
// Setup the buttons
buttonCashflow.action {
setupCashflowTable()
}
}
/** Displays the TableView for the Cashflow */
private fun setupCashflowTable() {
var initialFocus = true
// List of entries for the category ComboBox
val categoryList = mController.getCashFlowCategoryList()
// Create the table
val cashTable = tableview<CashEntry>(mController.getCashEntryList()) {
isEditable = true
column(Constants.COL_COUNT, CashEntry::countProperty)
column(Constants.COL_DATE, CashEntry::dateProperty).makeEditable(LocaleDateConverter())
column(Constants.COL_INCOME, CashEntry::incomeProperty).makeEditable(CurrencyConverter())
column(Constants.COL_EXPENSES, CashEntry::expensesProperty).makeEditable(CurrencyConverter())
column(Constants.COL_PROFIT, CashEntry::profitProperty).converter(CurrencyConverter())
column(Constants.COL_TOTAL_PROFIT, CashEntry::totalProfitProperty).converter(CurrencyConverter())
column(Constants.COL_COMMENTS, CashEntry::commentsProperty).makeEditable()
column(Constants.COL_CATEGORY, CashEntry::categoryProperty).useComboBox(categoryList)
// Scroll to and focus on the last cell on startup
if (initialFocus) {
val lastRow = mController.getCashEntryList().size - 1
requestFocus()
scrollTo(lastRow)
focusModel.focus(lastRow)
selectionModel.select(lastRow)
initialFocus = false
}
onEditCommit {entry ->
// Update the list
mController.updateCashEntryList(entry)
// Move to the next cell
requestFocus()
focusModel.focusRightCell()
#Suppress("UNCHECKED_CAST")
selectionModel.select(focusModel.focusedCell.row, focusModel.focusedCell.tableColumn as TableColumn<CashEntry, *>)
}
enableCellEditing()
// Enable edit on key typed
addEventHandler(KeyEvent.KEY_PRESSED) {keyEvent ->
if (keyEvent.code.isDigitKey || keyEvent.code.isLetterKey) {
if (editingCell == null) {
val currentSelectedCell = selectedCell
if (currentSelectedCell != null && currentSelectedCell.tableColumn.isEditable) {
edit(currentSelectedCell.row, currentSelectedCell.tableColumn)
}
}
}
}
}
// Add the table to the view
mainView.root.center = cashTable
cashTable.tableMenuButtonVisibleProperty()
// Ensure no other node can get focus
cashTable.focusedProperty().onChange {
val focusOwner = currentStage?.scene?.focusOwnerProperty()?.value
// Check if the focus owner is the table or a cell
if (focusOwner !is TableView<*> && focusOwner !is TextField) {
cashTable.requestFocus()
}
}
}
}
You should use
Platform.runLater(() -> {
requestFocus();
scrollTo(lastRow);
...
});
to update the GUI.

Determine the start of a selection in a TableView?

I have a TableView in my QML:
import QtQuick.Controls 1.4
TableView {
id: table
selectionMode: Controls_1.SelectionMode.ContiguousSelection
function onTableSelectionChanged() {
console.log(selection)
}
}
Is it possible to determine the start and the end of the selection?
E.g. whether the user is selecting items from low index to high index or from high index to low index.
You have to use the onSelectionChanged signal from table.selection next to table.selection.forEach to implement an algorithm that calculates the required indexes:
Connections {
target: table.selection
onSelectionChanged:{
console.log("Change Selection")
if(table.selection.count > 0){
var start = table.rowCount;
var end = 0;
table.selection.forEach(function(rowIndex){
if(rowIndex < start)
start = rowIndex;
if(rowIndex > end)
end = rowIndex
})
console.log("start: ", start, "end: ", end)
}
}
}

change text in arkitview via button(swift4)

My code uses arkit. All I want to do is when I change the text of the label. The change is reflected in the arkit text. Currently its not. Whatever the arkit label stays and default and never changes no matter what is on the label on the view controller.
import UIKit;import ARKit
class ViewController: UIViewController {
#IBOutlet var arkitView:ARSCNView!
#IBOutlet var outputImageView:UIImageView!
#IBOutlet var ffox: UILabel!
#IBOutlet var enterText: UITextField!
var textGeometry: SCNText!
//4. Create Our ARWorld Tracking Configuration
let configuration = ARWorldTrackingConfiguration()
//5. Create Our Session
let augmentedRealitySession = ARSession()
override func viewDidAppear(_ animated: Bool) {
arkitView.session.run(configuration)
//1. Create The Text
textGeometry = SCNText(string: ffox.text!, extrusionDepth: 1)
//2. Set It From The UILabel
textGeometry.string = ffox.text!
//3. Create A Material For The Text
let material = SCNMaterial()
material.diffuse.contents = UIColor.green
textGeometry.materials = [material]
//4. Create A Holder Node To Hold The Text
let node = SCNNode()
node.position = SCNVector3(x: 0, y: 0.02, z: -0.01)
node.scale = SCNVector3(x: 0.01, y: 0.01, z: 0.01)
node.geometry = textGeometry
arkitView.scene.rootNode.addChildNode(node)
arkitView.automaticallyUpdatesLighting = true
DispatchQueue.main.async {
self.textGeometry.string = self.ffox.text!
}
}
#IBAction func takeScreenshotAction() {
if let d = enterText.text
{
ffox.text = d
}}}
Looking at your code you may want to set the Font and Font Size e.g:
textGeometry.font = UIFont(name: "Helvatica", size: 3)
Changing The Text:
To change the text of SCNText you need to access it's string property.
Declare you SCNText as a var above your Class Definition e.g.
let textGeometry: SCNText!
And personally I would initialize it in ViewDidLoad.
Then to change it you simply call:
textGeometry.string = "I Have Changed The String"
So in your example assuming as I said that your SCNText is called textGeometry you can do something like this:
textGeometry.string = ffox.text
An Example Of Creating An SCNText Geometry:
class Text: SCNNode{
var textGeometry: SCNText!
/// Creates An SCNText Geometry
///
/// - Parameters:
/// - text: String (The Text To Be Displayed)
/// - depth: Optional CGFloat (Defaults To 1)
/// - font: UIFont
/// - textSize: Optional CGFloat (Defaults To 3)
/// - colour: UIColor
init(text: String, depth: CGFloat = 1, font: String = "Helvatica", textSize: CGFloat = 3, colour: UIColor) {
super.init()
//1. Create The Text Geometry With String & Depth Parameters
textGeometry = SCNText(string: text , extrusionDepth: depth)
//2. Set The Font With Our Set Font & Size
textGeometry.font = UIFont(name: font, size: textSize)
//3. Set The Flatness To Zero (This Makes The Text Look Smoother)
textGeometry.flatness = 0
//4. Set The Colour Of The Text
textGeometry.firstMaterial?.diffuse.contents = colour
//5. Set The Text's Material
self.geometry = textGeometry
//6. Scale The Text So We Can Actually See It!
self.scale = SCNVector3(0.01, 0.01 , 0.01)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Using This Class In Your Scene:
//1. Create An SCNText With A White Colour
let nodeToAdd = Text(text: "Hello AR", colour: .white)
//2. Add The Text To The Scene
augmentedRealityView.scene.rootNode.addChildNode(nodeToAdd)
//3. Set The Text's Position Center On The Screen & 1.5m In Front Of The Camera
nodeToAdd.position = SCNVector3(0, 0, -1.5)
Hope this helps...
Update:
The code above works perfectly however since your are having trouble implementing it, here is how you code should look:
class Example: UIViewController {
//1. Create A Reference To Our ARSCNView In Our Storyboard Which Displays The Camera Feed
#IBOutlet weak var augmentedRealityView: ARSCNView!
//2. Create A Label To Display Text
#IBOutlet weak var ffox: UILabel!
//3. Create The Text Gemoetry
var textGeometry: SCNText!
//4. Create Our ARWorld Tracking Configuration
let configuration = ARWorldTrackingConfiguration()
//5. Create Our Session
let augmentedRealitySession = ARSession()
//--------------------
//MARK: View LifeCycle
//--------------------
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
augmentedRealityView.session.run(configuration)
//1. Create The Text
textGeometry = SCNText(string: ffox.text!, extrusionDepth: 1)
//2. Set It From The UILabel
textGeometry.string = ffox.text!
//3. Create A Material For The Text
let material = SCNMaterial()
material.diffuse.contents = UIColor.green
textGeometry.materials = [material]
//4. Create A Holder Node To Hold The Text
let node = SCNNode()
node.position = SCNVector3(x: 0, y: 0.02, z: -0.01)
node.scale = SCNVector3(x: 0.01, y: 0.01, z: 0.01)
node.geometry = textGeometry
augmentedRealityView.scene.rootNode.addChildNode(node)
augmentedRealityView.automaticallyUpdatesLighting = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I expect the reason your app is crashing is perhaps you have not setup the ffox text in Interface Builder and Wired it up as in the image below:
Also remember the UILabel must contain text!
Final Update:
Since Sam has still not been able to get this to work, I am writing a more in-depth example, which does the following:
(1) The text in a UILabel will be displayed in an SCNTextGeometry on Load.
(2) You can change the string of the SCNTextGeometry by pressing one of three buttons,
(3) You can change the string of the SCNTextGeometry by using a UITextField Input.
All of this has been fully tested and works as does my other example.
You can download the full example here: Sample Project
And of course the `ViewController' Class:
import UIKit
import ARKit
//--------------------
//MARK: TextNode Class
//--------------------
class TextNode: SCNNode{
var textGeometry: SCNText!
/// Creates An SCNText Geometry
///
/// - Parameters:
/// - text: String (The Text To Be Displayed)
/// - depth: Optional CGFloat (Defaults To 1)
/// - font: UIFont
/// - textSize: Optional CGFloat (Defaults To 3)
/// - colour: UIColor
init(text: String, depth: CGFloat = 1, font: String = "Helvatica", textSize: CGFloat = 3, colour: UIColor) {
super.init()
//1. Create The Text Geometry With String & Depth Parameters
textGeometry = SCNText(string: text , extrusionDepth: depth)
//2. Set The Font With Our Set Font & Size
textGeometry.font = UIFont(name: font, size: textSize)
//3. Set The Flatness To Zero (This Makes The Text Look Smoother)
textGeometry.flatness = 0
//4. Set The Colour Of The Text
textGeometry.firstMaterial?.diffuse.contents = colour
//5. Set The Text's Material
self.geometry = textGeometry
//6. Scale The Text So We Can Actually See It!
self.scale = SCNVector3(0.01, 0.01 , 0.01)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//--------------------------
//MARK: UITextFieldDelegate
//--------------------------
extension ViewController: UITextFieldDelegate{
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
print("Current Text = \(textField.text!)")
DispatchQueue.main.async {
self.textNode.textGeometry.string = textField.text!
}
return true
}
}
class ViewController: UIViewController {
//1. Create A Reference To Our ARSCNView In Our Storyboard Which Displays The Camera Feed
#IBOutlet weak var augmentedRealityView: ARSCNView!
//2. Create A Reference To Our SCNNode
var textNode: TextNode!
//2. Create A Label To Display Text
#IBOutlet weak var ffox: UILabel!
//3. Create UITextField So Text Can Be Changed Dynamically
#IBOutlet weak var textChangerField: UITextField!
//3. Create Our ARWorld Tracking Configuration
let configuration = ARWorldTrackingConfiguration()
//3. Create Our Session
let augmentedRealitySession = ARSession()
//--------------------
//MARK: View LifeCycle
//--------------------
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
augmentedRealityView.session.run(configuration)
//1. Create An SCNText With A White Colour & With The Label's Text
textNode = TextNode(text: ffox.text!, colour: .white)
//2. Add The Text To The Scene
augmentedRealityView.scene.rootNode.addChildNode(textNode)
//3. Set The Text's Position Center On The Screen & 1.5m In Front Of The Camera
textNode.position = SCNVector3(0, 0, -1.5)
//4. Set The Lighting
augmentedRealityView.automaticallyUpdatesLighting = true
//5. Assign The Delegate To The TextChangerField
textChangerField.delegate = self
}
/// Changes The Text In The Button At The Bottom Of The Screen
///
/// - Parameter sender: UIButton
#IBAction func changeText(_ sender: UIButton){
guard let textToDisplay = sender.titleLabel?.text else { return }
DispatchQueue.main.async {
self.textNode.textGeometry.string = textToDisplay
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
In Interface Builder you will have the following:

QML TreeView passes previous selection when clicking to collapse or expand

I have a QML TreeView containing some onClicked() logic that calls a Q_INVOKABLE function that takes in the current row number and the parent row number of the TreeView as parameters. The problem is that when I select something, and then I click to expand or collapse something. The previous values are still getting passed which sometimes makes the application crash. I've tried to call treeView.selection.clearCurrentIndex() and treeView.selection.clearSelection() in onCollapsed() and onExpanded() which deselects the item, but for some reason still passes the values from the previously selected item.
//main.qml
TreeView {
id: treeView
anchors.fill: parent
model: treeviewmodel
selection: ItemSelectionModel {
model: treeviewmodel
}
TableViewColumn {
role: "name_role"
title: "Section Name"
}
onCollapsed: {
treeView.selection.clearSelection() // deselects the item, but still passes the previous values
}
onExpanded: {
treeView.selection.clearSelection()
}
onClicked: {
console.log("Current Row: " + treeView.currentIndex.row + "Parent Row: " + treeView.currentIndex.parent.row)
//I need something here that will set treeView.currentIndex.row and treeView.currentIndex.parent.row to -1
//so that when I collapse or expand, -1 gets passed instead of the previous values
}
}
I was able to solve this by setting some additional flags (thanks #Tarod for the help). I had to save the value of the rows so that I could check if they changed. If they did not change, I would not call the function, so no obsolete values would get passed.
TreeView {
id: treeView
anchors.fill: parent
model: treeviewmodel
property int currentRow: -1
property int parentRow: -1
property int lastCurrentRow: -1
property int lastParentRow: -1
selection: ItemSelectionModel {
model: treeviewmodel
}
TableViewColumn {
role: "name_role"
title: "Section Name"
}
onCollapsed: {
currentRow = -1
parentRow = -1
}
onExpanded: {
currentRow = -1
parentRow = -1
}
onClicked: {
console.log("Row: " + treeView.currentIndex.row + " Parent : " + treeView.currentIndex.parent.row)
//logic needed to not reselect last item when collpasing or expanding tree
if (lastCurrentRow === treeView.currentIndex.row && lastParentRow === treeView.currentIndex.parent.row)
{
currentRow = -1
parentRow = -1
}
else
{
lastCurrentRow = treeView.currentIndex.row
lastParentRow = treeView.currentIndex.parent.row
currentRow = treeView.currentIndex.row
parentRow = treeView.currentIndex.parent.row
}
if (currentRow === -1 && parentRow === -1)
{
//nothing selected - do nothing
}
else
{
//omitted some additional logic
}
}
}

Resources