Qt QML ComboBox override wheel events - qt

Is there any way to override ComboBox MouseArea to ignore wheel event instead of changing current index? ComboBox itself has no option to change wheel focus behaviour. So far I've tried to override onWheel from CB MouseArea with code like this:
ComboBox {
Component.onCompleted: {
for (var i = 0; i < combobox_ctrl.children.length; ++i) {
console.log(combobox_ctrl.children[i])
console.log(combobox_ctrl.children[i].hasOwnProperty('onWheel'))
if (combobox_ctrl.children[i].hasOwnProperty('onWheel')) {
console.log(combobox_ctrl.children[i]['onWheel'])
combobox_ctrl.children[i]['onWheel'] = function() { console.log("CB on wheel!") }
//combobox_ctrl.children[i]onWheel = function() { console.log("CB on wheel!")
//combobox_ctrl.children[i].destroy()
}
}
}
}
But I get
TypeError: Cannot assign to read-only property "wheel"
Did anyone was able to disable wheel events on ComboBox in Qml?
// EDIT
for example in Slider control I was able to remove wheel event handling like this:
Slider {
Component.onCompleted: {
for (var i = 0; i < slider.children.length; ++i) {
console.log(slider.children[i])
if (slider.children[i].hasOwnProperty("onVerticalWheelMoved") && slider.children[i].hasOwnProperty("onHorizontalWheelMoved")) {
console.log("Found wheel area!")
slider.children[i].destroy()
}
}
}
}
But in slider WheelArea is not responsible for handling "click" events.

You can place MouseArea over ComboBox and steel wheel event.
ComboBox {
anchors.centerIn: parent
model: [ "Banana", "Apple", "Coconut" ]
MouseArea {
anchors.fill: parent
onWheel: {
// do nothing
}
onPressed: {
// propogate to ComboBox
mouse.accepted = false;
}
onReleased: {
// propogate to ComboBox
mouse.accepted = false;
}
}
}

It's not currently possible, as ComboBox is not derived from MouseArea, but FocusScope, which has no support for these kinds of events.
A similar problem was mentioned in a suggestion recently:
Disable mouse wheel scroll event on QtQuick.Controls
If you're after a hacky way of doing it, it seems like the only option you have left is to apply a patch to ComboBox.qml that removes the onWheel handler:
diff --git a/src/controls/ComboBox.qml b/src/controls/ComboBox.qml
index 4e29dfe..3413cac 100644
--- a/src/controls/ComboBox.qml
+++ b/src/controls/ComboBox.qml
## -407,13 +407,6 ## Control {
popup.toggleShow()
overridePressed = false
}
- onWheel: {
- if (wheel.angleDelta.y > 0) {
- __selectPrevItem();
- } else if (wheel.angleDelta.y < 0){
- __selectNextItem();
- }
- }
}
Another alternative that doesn't involve modifying Qt code would be to add an intermediate MouseArea above ComboBox's, and then somehow only forward specific events through to ComboBox's MouseArea. Or, create a custom C++ item that does the equivalent. You may have more control that way.

Ok. After hacking around I've managed to come with solution that is acceptable for me but may introduce some regressions in some situations. pressed and hovered properties are no longer usable
import QtQuick.Controls.Private 1.0
ComboBox {
Component.onCompleted: {
for (var i = 0; i < combobox_ctrl.children.length; ++i) {
if (combobox_ctrl.children[i].hasOwnProperty('onWheel') && combobox_ctrl.children[i] !== mouseArea) {
combobox_ctrl.children[i].destroy()
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: {
if (combobox_ctrl.activeFocusOnPress)
forceActiveFocus()
if (!Settings.hasTouchScreen)
combobox_ctrl.__popup.toggleShow()
}
onClicked: {
if (Settings.hasTouchScreen)
combobox_ctrl.__popup.toggleShow()
}
}
}
This way we can mimic mouse area that was originaly inside the ComboBox. Popup is shown as it was (at least I didn't see any regresion in it yet). However two properties are inaccesible right now

I created a separate file called NonScrollingComboBox.qml with the following code following this post: https://stackoverflow.com/a/33080217/969016
Now I can just use NonScrollingComboBox as a component instead of ComboBox on places where I don't want the mouse scroll to change the value
import QtQuick 2.0
import QtQuick.Controls 1.4
ComboBox {
id: combobox_ctrl
Component.onCompleted: {
for (var i = 0; i < combobox_ctrl.children.length; ++i) {
if (combobox_ctrl.children[i].hasOwnProperty('onWheel')
&& combobox_ctrl.children[i] !== mouseArea) {
combobox_ctrl.children[i].destroy()
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: {
if (combobox_ctrl.activeFocusOnPress)
forceActiveFocus()
combobox_ctrl.__popup.toggleShow()
}
onClicked: {
combobox_ctrl.__popup.toggleShow()
}
}
}
usage:
NonScrollingComboBox {
anchors.verticalCenter: parent.verticalCenter
model: ["item one", "item 2"]
}

This seems to apply only to Qt Quick Controls 1 ComboBox. On Qt Quick Controls 2 ComboBox the wheel mouse event is not enabled by default and can be enabled manually by setting to true the property wheelEnabled (documented in the base class Control). Also the combobox won't keep a "focus" on mouse events so you can freely use the wheel on other mouse areas by just entering them.

Related

QML ListView - change all but current item

I'm following this tutorial (without the flickable content in each entry) for Qt 4.8 while using Qt 5.7 with QtQuick 2.0. The way the ListView there works is as follows:
User clicks on item in list
Alternative (detailed) view of item is displayed
User has to click on Close button in detailed view to reset the state of entry to its default compact view.
This leads to a clutter where at some point if the user clicks on all items in which case all will be shown in their full view. Having the user click on the Close button every time he/she opens a detailed view also is (omho) not that handy.
I've altered the entry to close when the user clicks on the view. I'm also trying to prevent this clutter and achieve a more (omho) flowing behaviour:
User clicks on item in list
Alternative view of item is displayed
User clicks on detailed view to reset state of entry to its default compact view OR
User clicks on another entry and all currently in detailed view entries are reset to their compact view
Currently I'm looping through my ListView's contentItem.children[loop_index] and setting the state to "" ("Details" = show detailed view | "" = show compact view). Due to the way ListView works (loading/unloading delegates on demand) this is quite unreliable and I often get an undefined reference when I try to access the state of other delegates. The following MouseArea, which I'm using to do all that, is part of every delegate:
// state is a QML `State` that is bound to the delegate (see below for the details on it)
MouseArea {
anchors.fill: background
onClicked: {
// Iterate through all other entries and close them
for (var entry = 0; entry < listView.count; ++entry) {
if(listView.contentItem.children[entry] !== gestureEntry) {
console.log("Hide other element");
listView.contentItem.children[entry].state = ""; // IT FAILS HERE (SOMETIMES)
}
}
// Change view of current entry
if(gestureEntry.state === "Details") {
gestureEntry.state = "";
console.log("Hiding details")
}
else {
gestureEntry.state = "Details";
console.log("Showing details");
}
}
}
with state being a delegate's state:
states: State {
name: "Details"
PropertyChanges { target: background; color: "white" }
PropertyChanges { target: gestureImage; width: 130; height: 130 } // Make picture bigger
PropertyChanges { target: gestureEntry; detailsOpacity: 1; x: 0; y: 0 } // Make details visible
PropertyChanges { target: gestureEntry; height: listView.height } // Fill the entire list area with the detailed view
}
I'm thinking that the state information can be stored inside the ListModel itself making it possible to iterate through the model's contents (which are always there unlike the contents of the delegates) however I don't know how to automatically update my list (and the currently visible/invisible delegates) when an entry changes in the model. From what I've found so far it seems not possible to do that since the ListView doesn't actively monitor its ListModel.
Is this indeed the case? If yes, then is it possible to go around this problem in a different way?
Why don't you use the currentIndex property of your ListView?
Just modify your delegate like this:
Item {
id: gestureEntry
...
state: ListView.isCurrentItem?"Details":""
...
MouseArea {
anchors.fill: background
onClicked: {
if(listView.currentIndex == index)
listView.currentIndex = -1
else
listView.currentIndex = index
}
}
}
EDIT:
The only issue with the solution above is that - upon loading - an entry in the ListView is preselected which automatically triggers the detailed view of that entry. In order to avoid that the following needs to be added to listView:
Component.onCompleted: {
listView.currentIndex = -1;
}
This ensures that no entry will be preselected.
guess it is an issue because you stored a state in your delegate. You should not do this as described in the delegate-property (Link), because the delegates get reused when they get out of view.
At least you should use a when: ListView.isCurrentItem in the State and depend on a value of the ListView. So only your current delegate is maximized. Then in the MouseArea only set `ListView.view.currentIndex = index'. Don't change the state manually in the function!
I ran in the same trouble, removed the states completely and just used the attached property ListView.isCurrentItem. But binding the state to a Value from the ListView should also work, because it's not stored in the delegate.
Minimal example:
import QtQuick 2.0
Item {
width: 800
height: 600
ListView {
id: view
anchors.fill: parent
model: 3
spacing: 5
currentIndex: -1
delegate: Rectangle {
id: delegate
color: ListView.isCurrentItem ? "lightblue" : "green" // directly change properties depending on isCurrentItem
height: 100
width: 100
states: State {
name: "maximized"
when: delegate.ListView.isCurrentItem // bind to isCurrentItem to set the state
PropertyChanges {
target: delegate
height: 200
}
}
MouseArea {
anchors.fill: parent
//onClicked: delegate.ListView.view.currentIndex = model.index // if only selection is wanted
onClicked: {
//console.debug("click");
if (delegate.ListView.isCurrentItem)
{
delegate.ListView.view.currentIndex = -1;
}
else
{
delegate.ListView.view.currentIndex = model.index;
}
}
}
Text {
anchors.centerIn: parent
text: index
}
}
Text {
text: "CurrentIndex: " + parent.currentIndex
}
}
}

Unable to get highlighting working on TextEdit

TL;DR: TextEdit paints highlighted text only when I click on it. Nothing helps
I have a ListView with a QAbstractListModel model with string properties.
Those string properties are being spellchecked and QSyntaxHighlighter is used to show spell errors. I create QSyntaxHighlighter descendant in Component.onCompleted of TextEdit. I double-checked highlighting get's executed with correct spell errors and setFormat() of Highlighter is executed with correct positions. The problem is that it draws text in red (invalidates) only when I click on the TextEdit itself.
TextEdit lives in a Flickable (to track cursor) and Flickable lives in a Rectangle (to have nice background and border). Binding to some signals and calling update() of TextEdit does not help.
After spellcheck finishes, I emit rehighlight() signal of created SyntaxHighlighter.
Rectangle {
id: descriptionRect
height: 30
border.width: descriptionTextInput.activeFocus ? 1 : 0
clip: true
Flickable {
id: descriptionFlick
contentWidth: descriptionTextInput.paintedWidth
contentHeight: descriptionTextInput.paintedHeight
anchors.fill: parent
interactive: false
flickableDirection: Flickable.HorizontalFlick
height: 30
clip: true
focus: false
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX+width <= r.x+r.width)
contentX = r.x+r.width-width;
}
TextEdit {
id: descriptionTextInput
width: descriptionFlick.width
height: descriptionFlick.height
text: description
onTextChanged: model.editdescription = text
Component.onCompleted: {
globalModel.initDescriptionHighlighting(index, descriptionTextInput.textDocument)
}
onCursorRectangleChanged: descriptionFlick.ensureVisible(cursorRectangle)
}
}
}
Here is a small sample of project with demonstration of how it does not work until you click on a text https://bitbucket.org/ribtoks/qt-highlighting-issue
Any ideas how I can solve this?
Just encountered this issue on 5.11.2 and found the following fix which allows updating of individual blocks without having to highlight/deselect the whole text area
rehighlightBlock(newBlock);
Q_EMIT document()->documentLayout()->updateBlock(newBlock);
The issue was probably caused by QTBUG-44765, fixed in Qt 5.5.
Given the low level of the bug, I don't think it is practically to work around it.
You can work around that by appending an empty string to the TextEdit when you're done with the syntax highlighting
TextEdit {
id: captionTextEdit
width: wrapperFlick.width
height: wrapperFlick.height
text: display
readOnly: true
Component.onCompleted: {
itemsModel.initHighlighter(index, captionTextEdit.textDocument)
}
Connections {
target: itemsModel
onUpdateTextEdit: {
console.log("Update element at index: " + indexToUpdate)
if (indexToUpdate == index)
{
console.log("Update me!")
captionTextEdit.append("")
}
}
}
onCursorRectangleChanged: wrapperFlick.ensureVisible(cursorRectangle)
}
where updateTextEdit(indexToUpdate) is a new signal your itemsModel has to emit.
itemsmodel.h
signals:
void updateTextEdit(int indexToUpdate);
itemsmodel.cpp
void ItemsModel::initHighlighter(int index, QQuickTextDocument *document) {
// Signal mapper could be avoided if lamda slot are available (Qt5 and C++11)
QSignalMapper* signalMapper = new QSignalMapper(this);
if (0 <= index && index < m_ItemsList.length()) {
SingleItem *item = m_ItemsList.at(index);
SpellCheckHighlighter *highlighter = new SpellCheckHighlighter(document->textDocument(), item);
QObject::connect(item, SIGNAL(spellCheckResultsReady()),
highlighter, SLOT(rehighlight()));
// TODO: Don't connect this slot for Qt 5.5+ to avoid performance overhead
QObject::connect(item, SIGNAL(spellCheckResultsReady()),
signalMapper, SLOT(map()));
signalMapper->setMapping(item, index);
}
connect(signalMapper, SIGNAL(mapped(int)),
this, SIGNAL(updateTextEdit(int)));
}
Full code is available here: https://bitbucket.org/swarta/rehighlighdemo/branch/workaround#diff

How to vertical scroll to a specific item

I have a ListView including a lot of radio buttons. The list is bigger than the visible area. One of the radio buttons is checked. Sometimes, if the selected radio button is outside the visible area I want to scroll to it:
ScrollView {
anchors.fill:parent
ListView {
anchors.fill: parent
model: valuesList
delegate: RadioButton {
id: radioBtn
//check of value is index type and do the corresponding checked? test
checked: valueIsIndex ? (parseInt(valueFromParent) == index ? true : false) : (valueFromParent == valueString ? true : false)
onClicked: {
root.selected(valueString, index)
}
Component.onCompleted: {
if(checked)
//Here i want to scroll the list to display this radiobutton
}
}
}
}
Any ideas howto to get the list scrolled? I've played around a lot of with hightlights and contentY but nothing worked.
I've use the ScrollView around the ListView to automatically get the systems scrollbars on the desktop. On mobile devices i have just the flickable ListView.
EDIT
I get it on the way with the help of BaCaRoZzo. Here is my current working example:
ScrollView {
id: scrollView
anchors.fill:parent
property int yOfCheckedRadioButton: 0
ListView {
id:listView
anchors.fill: parent
spacing: Math.round(appWindow.height*0.05)
model: internalValuesList
delegate: RadioButton {
id: radioBtn
//check of value is index type and do the corresponding checked? test
checked: checktest()
style: MyRadioButtonStyle {
myRadioBtn: radioBtn
labelString: value
}
Component.onCompleted: {
//set the position of the checked RadioButton to scroll to it later onContentHeightChange
if(checked) {
var checkedRadioBtnPositionY = Math.round((radioBtn.height + listView.spacing) * index - radioBtn.height * 1.5)
if( checkedRadioBtnPositionY > 0)
scrollView.yOfCheckedRadioButton = checkedRadioBtnPositionY
else
scrollView.yOfCheckedRadioButton = 0
}
}
}
onContentHeightChanged: {
//scroll to the checked RadioButton
contentY = scrollView.yOfCheckedRadioButton
}
}
}
I recall problems with scroll before Qt 5.4 when I found a workaround like:
ScrollView {
anchors.fill: parent // mind how you stretch it
contentItem:
Flow {
id: flow
spacing: 10 // mind gaps
width: parent.parent.width - 20 // select proper width
// Put anything you would like to scroll in here
// Mind that Flow positions items one after another
// left to right, top to bottom
// You can also try containers other than Flow
// but whether it works or not may depend on Qt version
ExclusiveGroup { id: tabPositionGroup }
RadioButton {
text: "RB1"
checked: true
exclusiveGroup: tabPositionGroup
}
RadioButton {
text: "RB2"
exclusiveGroup: tabPositionGroup
}
}
}
Whether ScrollView needs an explicit contentItem or not is another matter and it certainly may not need it but that does not hurt if the SrollView needs to resolve what it actually scrolls.

Items inside TabView/Tab are not accessible

Suppose we have a QML file like the following:
Window {
Component.onCompleted: rect.color ="green"
TabView {
Tab {
Rectangle {
id: rect
color: "white"
}
}
}
}
When I run this code, for some reason, it gives me this error:
ReferenceError: rect is not defined
Somebody can say it's a scope problem but the following code works fine:
Window {
Component.onCompleted: rect.color ="green"
Item {
Item {
Rectangle {
id: rect
color: "white"
}
}
}
}
In my case I have a big form with tabs and controls inside it and I pass all the controls to several functions to validate the form, i.e. some code like this:
function onClose() {
validate(control1);
validate(control2);
// etc
}
but while accessing controls by id I get the above error.
How about just binding the rectangle to a color parameter instead of a hardcoded value?
This wil separate your Model and View code further to make it more readable elsewhere in your project as well...try:
Window {
property string myColor: "White"
Component.onCompleted: myColor = "Green"
TabView {
Tab {
Rectangle {
id: rect
color: myColor
}
}
}
}
To view the QML object tree, you need to start your project in debug mode. Then split your code window so that the "Locals and Expressions" view is showing (checkbox ticked on the right side). You will see your root item and all the other controls nested in a tree and now you can directly edit the values of their properties, and the changes will render immediately. There is a youtube video tutorial on debugging: https://youtu.be/mPXn6L2Wftc?t=19m55s
I'm not sure if the tree would give you access to the controls at runtime, but it might help you with debugging.
Ok, since Tab item cannot be accessed from outside I think it can be done in this way:
TabView {
id: tabView
Tab {
title: "tab1"
function validate() { /* validate all the controls related to tab1 only */ }
Item { id: item1 }
Item { id: item2 }
}
Tab {
title: "tab2"
function validate() { /* validate all the controls related to tab2 only */ }
Item { id: item3 }
Item { id: item4 }
}
function validateTabs() {
for(var i = 0; i < tabView.count;i ++) {
var tab = tabView.getTab(i);
if(tab && tab.active && tab.item.validate) {
if(!tab.item.validate())
return false;
}
}
return true;
}
}
The good point that if some Tab wasn't opened and so not changed it will not be validated.

Reject external files dragged in a DropArea without breaking the DropArea

In my application I'm displaying a list of audio files and the user can drag an external file to add it to the list. I want to be able to refuse the drag if no file in the list is supported by my application.
The issue is that when I call drag.accepted = false; in onEntered of my DropArea then it becomes completely unresponsive to any other event.
Here is some sample code showing the issue. If you drag an MP3 in the window you see that it works. Then if you drag any other file it won't work, as expected. But then dragging an MP3 file back will not work either.
import QtQuick 2.1
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 640
height: 480
DropArea {
anchors.fill: parent
onEntered: {
console.log("[Droparea] entered");
// Ensure at least one file is supported before accepted the drag
var validFile = false;
for(var i = 0; i < drag.urls.length; i++) {
if(validateFileExtension(drag.urls[i])) {
validFile = true;
break;
}
}
if(!validFile) {
console.log("No valid files, refusing drag event");
drag.accepted = false;
return false;
}
}
onExited: {
console.log("[Droparea] entered");
}
onDropped: {
console.log("[Droparea] dropped");
}
// Only MP3s
function validateFileExtension(filePath) {
var extension = filePath.split('.').pop();
var valid = false;
if(extension == "mp3") {
valid = true;
}
return valid;
}
}
Text {
id: textDrop
anchors.centerIn: parent
text: "Please drag element"
}
}
Is there a bug in the DropArea or did I misunderstood something? I know I can filter the files in the onDropped but then you loose the visual feedback you get on OSX when dragging file on an area that does not accept them.
It has been a known bug for a long time. A patch has been submitted and after been stalled for several months is now merged into 5.6 branch.
Anyone who wants to use this functionality MUST upgrade to Qt 5.6 or MANULLY integrate the available patch into his/her Qt version.
QQuickDropAreaPrivate, contained in DropArea, updates the containsDrag flag to true when a dragEnterEvent occurs, emitting the entered signal. It updates containsDrag to false when adragLeaveEvent occurs, emitting an exited signal. However, when the drag event is not accepted dragLeaveEvent is never called, leaving the private object in a incosistent state. Each subsequent dragEnterEvent is discarded since containsDrag is still true, i.e. the previous drag event is still considered active and the entered is no more emitted.
Since the issue is related to an interaction between private APIs and usage of the public APIs, the problem does not affect filtering using keys. Unfortunately, this approach does not seem to fit for the presented use case.
A quite partial workaround is to use a MouseArea along with the DropArea. The latter disables itself when a rejection occurs while the former enables back the DropArea for the next drop. This workaround covers the common case in which a wrong item is dropped inside the DropArea, which is the most common and intuitive for an end user. Releasing the wrong item outside the DropArea invalidate the mechanism (until the next drop).
Here's the code:
import QtQuick 2.1
import QtQuick.Controls 1.0
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 640
height: 480
visible: true
MouseArea {
anchors.fill: parent
hoverEnabled: true
enabled: !drop.enabled
onContainsMouseChanged: drop.enabled = true
}
DropArea {
id: drop
anchors.fill: parent
onEntered: {
console.log("[Droparea] entered");
// Ensure at least one file is supported before accepted the drag
for(var i = 0; i < drag.urls.length; i++)
if(validateFileExtension(drag.urls[i]))
return
console.log("No valid files, refusing drag event")
drag.accept()
drop.enabled = false
}
onExited: console.log("[Droparea] exited")
onDropped: console.log("[Droparea] dropped")
// Only MP3s
function validateFileExtension(filePath) {
return filePath.split('.').pop() == "mp3"
}
}
Text {
id: textDrop
anchors.centerIn: parent
text: "Please drag element"
}
}
you never put accepteed = true
just add drag.accepted = true after you set the valid as valid
for(var i = 0; i < drag.urls.length; i++) {
if(validateFileExtension(drag.urls[i])) {
validFile = true;
drag.accepted = true;
break;
}
}

Resources