Determine the start of a selection in a TableView? - qt

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

Related

swiftUI picker connects to button and the button calls a function

currently I’m creating an app that displays visualizations of different sorting algorithms. Right now I’m trying to get a SwiftUi picker to connect to different sorting algorithms, and connect the algorithms to a “SORT” button, so that when someone chooses a algorithm and presses “SORT”, the function will be called. Is there a way to set a ID (if that’s what it’s called) to a button and picker, so that the picker sorting algorithms functions connects to the SORT button? So far I have the picker change the ".navigaionTitle" to which algorithm I chose. Any help would be deeply appreciated, thanks.
Hopefully this is enough code so you folks can help
Button {
Task {
}
} label: {
Text("SORT")
}
Picker(
selection: $selectAlgorithm,
label: Text(myTitle),
content: {
Text("bubble Sort").tag("Bubble sort")
Text("insertion sort").tag("Insertion sort")
})
func bubbleSort() async throws {
var isSorted = false
var counter = 0
while !isSorted {
isSorted = true
for i in 0 ..< data.count - 1 - counter {
if data[i] > data[i + 1] {
swapHelper(i, i + 1)
try await Task.sleep(until: .now.advanced(by: .milliseconds(10)), clock: .continuous)
isSorted = false
}
}
counter = counter + 1
}
func swapHelper(_ firstIndex: Int, _ secondIndex: Int) {
let temp = data[secondIndex]
data[secondIndex] = data[firstIndex]
data[firstIndex] = temp
}
}

how do I run a Jetpack Compose test in realtime?

Using Jetpack Compose on Android.
I have a test, that simulates a selection of several Text composable in a Column.
The selection starts by a long-press on the first item and then moves down over more Text composables and stop well inside the Column.
Usually the test should run unattended and fast.
But I want to be able to show the selection process in real time (for demonstration purposes and also to see, if it works like it's designed, e.g. at the beginning I forgot that I have to wait some time after the down()).
The first Text composable in the column is also used to find the element (->anchor), and the parent is the Column which is used to perform the move.
This is the function that performs the selection:
val duration = 3000L
val durationLongPress = 1000L
fun selectVertical(anchor: SemanticsNodeInteraction, parent: SemanticsNodeInteraction) {
anchor.performTouchInput {
down(center)
}
clock.advanceTimeBy(durationLongPress)
// the time jumps here, but the selection of the first word is visible
val nSteps = 100
val timeStep = (duration-durationLongPress)/nSteps
parent.performTouchInput {
moveTo(topCenter)
val step = (bottomCenter-topCenter)*0.8f/ nSteps.toFloat()
repeat(nSteps) {
moveBy(step, timeStep)
}
up()
}
}
this is the composable:
#Composable
fun SelectableText() {
val text = """
|Line
|Line start selecting here and swipe over the empty lines
|Line or select a word and extend it over the empty lines
|Line
|
|
|
|Line
|Line
|Line
|Line
""".trimMargin()
Column {
SelectionContainer {
Column {
Text("simple")
Text(text = text) // works
}
}
SelectionContainer {
Column {
Text("crash")
text.lines().forEach {
Text(text = it)
}
}
}
SelectionContainer {
Column {
Text("space")
text.lines().forEach {
// empty lines replaced by a space works
Text(text = if (it == "") " " else it)
}
}
}
}
}
a test goes like this:
#Test
fun works_simple() {
val anchor = test.onNodeWithText("simple")
val textNode = anchor.onParent()
textNode.printToLog("simple")
controlTime(duration) {
selectVertical(anchor, textNode)
}
}
controlTime is the part that does not work. I don't add it's code here to keep the solution open.
I tried to disable the autoAdvance on the virtual test clock and stepping the time in a loop in a coroutine.
When I step the time in 1ms steps and add a delay(1) each, the wait is correct, but I don't see the selection expanding (I want at least see the steps). Instead I see the selection of the first word, then nothing until the end of the move and then the end result.
I also divided the move into smaller steps e.g. 10 or 100, but it's still not showing the result.
ok, I found the solution myself when sleeping... the "sleeping" brain is obviously working on unsolved problems (well, I know this already).
The key is, to do each move that should be visible in it's own performXXX. I think, the result is only propagated to the UI, when the code block is finished, which makes sense for a test.
parent.performTouchInput {
inRealTime("moveBy($step, $timeStep)", timeStep) {
moveBy(step)
}
}
I couldn't find a way to determine the duration of a so called "frame", so I advance either the virtual or the real clock, depending on which is lagging until both reach the target time. This can probably be optimized to jump both clocks in one step. I'll investigate that later.
It's interesting, that even 100 steps don't show a smooth selection move.
Instead, there are still only a few steps, even when the step time is increased.
Btw. this purpose of this code is to show a crash in SelectionContainer, when it encounters an empty Text("") composable for a bug report I created. I will provide it on the issue tracker, but I also want to have the test in our app development, to see, when it's solved and to avoid a library that doesn't work. Sometimes we encounter regressions in libs, e.g. if the fix has a bug.
This is the complete test code:
package com.example.myapplication
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.*
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Before
import org.junit.Rule
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
#RunWith(AndroidJUnit4::class)
class CrashTest {
val duration = 3000L
val durationLongPress = 1000L
#Composable
fun SelectableText() {
val text = """
|Line
|Line start selecting here and swipe over the empty lines
|Line or select a word and extend it over the empty lines
|Line
|
|
|
|Line
|Line
|Line
|Line
""".trimMargin()
Column {
SelectionContainer {
Column {
Text("simple")
Text(text = text) // works
}
}
SelectionContainer {
Column {
Text("crash")
text.lines().forEach {
Text(text = it)
}
}
}
SelectionContainer {
Column {
Text("space")
text.lines().forEach {
// empty lines replaced by a space works
Text(text = if (it == "") " " else it)
}
}
}
}
}
#Rule
#JvmField
var test: ComposeContentTestRule = createComposeRule()
#Before
fun setUp() {
test.setContent { SelectableText() }
test.onRoot().printToLog("root")
}
val clock get() = test.mainClock
fun inRealTime(what: String? = null, duration: Long = 0, todo: () -> Unit) {
clock.autoAdvance = false
what?.let { Log.d("%%%%%%%%%%", it) }
val startVirt = clock.currentTime
val startReal = System.currentTimeMillis()
todo()
while (true) {
val virt = clock.currentTime - startVirt
val real = System.currentTimeMillis() - startReal
Log.d("..........", "virt: $virt real: $real")
if (virt > real)
Thread.sleep(1)
else
clock.advanceTimeByFrame()
if ((virt > duration) and (real > duration))
break
}
clock.autoAdvance = true
}
fun selectVertical(anchor: SemanticsNodeInteraction, parent: SemanticsNodeInteraction) {
inRealTime("down(center)", durationLongPress) {
anchor.performTouchInput {
down(center)
}
}
val nSteps = 100
val timeStep = (duration-durationLongPress)/nSteps
Log.d("----------", "timeStep = $timeStep")
var step = Offset(1f,1f)
parent.performTouchInput {
step = (bottomCenter-topCenter)*0.8f/ nSteps.toFloat()
}
repeat(nSteps) {
parent.performTouchInput {
inRealTime("moveBy($step, $timeStep)", timeStep) {
moveBy(step)
}
}
}
parent.performTouchInput {
inRealTime("up()") {
up()
}
}
}
#Test
fun works_simple() {
val anchor = test.onNodeWithText("simple")
val textNode = anchor.onParent()
textNode.printToLog("simple")
selectVertical(anchor, textNode)
}
#Test
fun crash() {
val anchor = test.onNodeWithText("crash")
val textNode = anchor.onParent()
textNode.printToLog("crash")
selectVertical(anchor, textNode)
}
#Test
fun works_space() {
val anchor = test.onNodeWithText("space")
val textNode = anchor.onParent()
textNode.printToLog("space")
selectVertical(anchor, textNode)
}
}

How to implement NSTableViewRowlView like Xcode IB object selector

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()
}
}
}

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
}
}
}

finding a specific element of a ListModel based on its properties in QtQuick 2.0

I have a ListModel with an objectId role (integer and unique). I want to query the model to find other properties of an element with a specific objectId. How can I do that?
You can loop other your model to retrieve each element one by one searching for your objectId
for(var i = 0; i < myModel.count; i++) {
var elemCur = myModel.get(i);
if(searchedId == elemCur.objectId) {
console.log("Found it at index : ", i);
}
}

Resources