How to detect QTableWidget scroll source (code itself/user (wheel)/user (scrollbar))? - qt

I'm writing a program using Qt 4.8 that displays a table (QTableWidget) filled with filenames and file's params. First an user adds files to the list and then clicks process. The code itself updates the contents of the table with simple progress description. I want the table by default to be scrolled automatically to show the last processed file and that code is ready.
If I want to scroll it by hand the widget is being scrolled automatically as soon as something changes moving the viewport to the last element. I want to be able to override the automated scroll if I detect that it was the user who wanted to change view.
This behavior can be seen in many terminal emulator programs. When there's a new line added the view is scrolled but when user forces the terminal to see some previous lines the terminal does not try to scroll down.
How could I do that?
Solution:
I created an object which filters event processed by my QTableWidget and QScrollBar embedded inside. If I spot the event that should turn off automatic scrolling I just set a flag and stop scrolling view if that flag is set.
Everything is implemented inside tableController class. Here are parts of three crucial methods.
bool tableController::eventFilter(QObject* object, QEvent* event)
{
switch (event->type())
{
case QEvent::KeyPress:
case QEvent::KeyRelease:
case QEvent::Wheel:
case QEvent::MouseButtonDblClick:
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
_autoScrollEnabled = false;
default:
break;
}
return QObject::eventFilter(object, event);
}
void tableController::changeFile(int idx)
{
[...]
if (_autoScrollEnabled)
{
QTableWidgetItem* s = _table.item(_engine.getLastProcessed(), 1);
_table.scrollToItem(s);
}
[...]
}
void tableController::tableController()
{
[...]
_autoScrollEnabled = true;
_table.installEventFilter(this);
_table.verticalTableScrollbar()->installEventFilter(this);
[...]
}
Thanks for all the help. I hope somebody will find it useful :)

Subclass QTableWidget and overload its wheelEvent. You can use the parameters of the supplied QWheelEvent object in order to determine if the user scrolled up or down.
Then use a simple boolean flag which is set (or reset) in your wheelEvent override. The method which is responsible for calling scrollToBottom() should then consider this boolean flag.
You will have to find a way to figure out when to set or reset that flag, e.g. always set it when the user scrolls up and reset it when the user scrolls down and the currently displayed area is at the bottom.

connect(_table->view()->verticalScrollBar(), &QAbstractSlider::actionTriggered, this, [this](int) {
_autoScrollEnabled = false;
});

Related

DispatchKeyEvent stops firing after Xamarin Forms Entry control IsFocused

I am building a Xamarin Forms mobile app that runs in Android on a Zebra scanner. I flip 2 different StackLayouts to IsVisble true/false to display different stuff in the UI. (StackLayout1 and StackLayout2)
The customer wants the user to be able to use the app entirely from the hardware keyboard on the scanner. So I have used the device Settings so that it never displays the virtual keyboard (I don’t think that matters for the issue I am having.)
I am overriding DispatchKeyEvent in a PageRenderer in the Android project and everything is working great … except.
The problem case:
StackLayout1 is displayed
the user taps an Entry control, putting the focus there
the user taps a button in the UI
the app displays StackLayout2
at this point the DispatchKeyEvent never fires no matter what key I press on the device keyboard
If an Entry box does NOT get the focus (step #2 above) the DispatchKeyEvent always fires in StackLayout2 and the StackLayouts display as expected.
If I programatically put the focus in an Entry box in StackLayout2 at step #3 above the DispatchKeyEvent fires fine.
That is not an OK solution. I have tried to progamatically put the focus on StackLayout2, and that code seems to do what is expected but DispatchKeyEvent does not fire.
Maybe I need to do something in the Android-project PageRenderer so that it is aware of StackLayout2 when it is made IsVisible = true.
Update 2: I found that I did NOT need custom StackLayouts. The solution which I posted below does not include any of this stuff I am describing in Update 1 (sorry, if that's confusing).
Update 1:
I added a ViewRenderer for both StackLayouts, and the code is hitting the OnElementChanged event when StackLayout2's IsVisible property flips to true, just great. Although the problem case is the same: DispatchKeyEvent does not fire once StackLayout2 is displayed, if an EntryBox had the focus in StackLayout1
Here is the OnElementChanged part of the new StackLayout ViewRenders
async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "IsVisible":
if (Element.IsVisible)
{
if (sender is StackLayout)
{
this.FocusableViewAvailable(this); // if I comment these 2 lines out I get the same bad result
this.Focusable = true; // if I comment these 2 lines out I get the same bad result
this.FocusableInTouchMode = true;
var dd = this.RequestFocus(); // this is always false
var ee = this.IsFocused; // this is always false
}
}
break;
}
}
Also, as I am pointing out in the comments ^ there, IsFocused is always false.
Ideas?
My hunch, "Maybe I need to do something in the Android project PageRenderer" was correct. In the DispatchKeyEvent I had to make the MainPage have the focus when the keypress was handled.
Here is what the DispatchKeyEvent looks like now (notice the comments):
public override bool DispatchKeyEvent(KeyEvent ke)
{
// MainPage.ReceiveKeyPress(e); is the method that this method returns to
bool KeyPressWasHandled = false;
KeyPressWasHandled = (Element as MainPage).ReceiveKeyPress(ke);
if (KeyPressWasHandled)
{
// this next block seems to be needed so that this class
// continues to receive the keypress event after an Entry box has had the focus
this.Focusable = true;
this.FocusableInTouchMode = true;
this.RequestFocus();
return true; // returning true tells the parent class that the keypress has been handled
} else
{
try
{
return base.DispatchKeyEvent(ke);
}
Now the "problem case" in my initial post is no longer a problem.
NOTE: I found that I did NOT need the custom ViewRenderers that I had made for the StackLayouts.

"please wait" during long operation in Qt?

I need to do an operation after a user clicking a button, and that may take some time. How can I display a "busy" or "please wait" icon/message (like a spinning circle etc) and prevent a user from clicking the button again (or some other buttons) during the time? Thanks.
Use your QApplication object's static function setOverrideCursor(const QCursor &cursor)
Set the wait cursor, to make the application "busy", as shown below.
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
And restore using below function, when you feel the work is done
QApplication::restoreOverrideCursor();
some documentation help:
http://doc.qt.io/qt-5/qguiapplication.html#setOverrideCursor
Now to disable the window, override the event filter function of your window class.
declare a global variable, that says "busy" or "notbusy". ex: _isBusy.
In the event filter do something as shown below.
eventfilter is a virtual function of QObject.
So you can override in your window class as shown below.
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (_isBusy) //your global variable to check "busy or "notbusy"
{
//Just bypass key and mouse events
switch(event->type())
{
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::KeyPress:
case QEvent::KeyRelease:
return true;
}
}
}
Note: "MainWindow" in the above function declaration is your class name. Also in the switch statement, I limited the count to only 4 events, add as many events you want.

How to make QComboBox popup upwards?

my QComboBox-derived class lives in a QGraphicsScene at the bottom end of the (visible) screen - but it pops up downwards, thus out of view.
(How) is it possible to force the popup to open above the widget?
I've tried re-implementing showPopup like this:
void MyComboBox::showPopup()
{
QAbstractItemView *popupView = view();
popupView->move(0,-100);
//popupView->window->move(0,-100);
QComboBox::showPopup();
}
The result is, that the content seems to be shifted, but not the underlying popup object.
I think it might be possible to find a solution with styles as indicated in
this article, but I can't find any Styles control that might be helpful here. I am rather new to C++ as well as Qt, so I might be missing something obvious.
I'd appreciate any help on this matter!
Best regards,
Sebastian
With the information found here, I was able to get it done this way:
void SteuerQComboBox::showPopup() {
QComboBox::showPopup();
QWidget *popup = this->findChild<QFrame*>();
popup->move(popup->x(),popup->y()-this->height()-popup->height());
}
Note that it's crucially important to call the base classes "showPopup" first.
Thanks to everybody who was reading my question and thinking about it!
user1319422's solution isn't bad, but it has two problems.
If your platform has GUI animation, the listbox will animate opening downwards, then is moved above the text box.
If you disable combobox animation (or you don't have it), the call to QComboBox::showPopup() still makes the GUI element start to appear on the screen already. So, moving it there would cause it to flicker as it appears in the first place and moves to the next.
So, to address the first problem, I just switched off animation:
void MyComboBox::showPopup()
{
bool oldAnimationEffects = qApp->isEffectEnabled(Qt::UI_AnimateCombo);
qApp->setEffectEnabled(Qt::UI_AnimateCombo, false);
QComboBox::showPopup();
qApp->setEffectEnabled(Qt::UI_AnimateCombo, oldAnimationEffects);
}
Then, for the second problem, I moved the frame in the Show event:
bool MyComboBox::eventFilter(QObject *o, QEvent *e)
{
bool handled = false;
if (e->type() == QEvent::Show)
{
if (o == view())
{
QWidget *frame = findChild<QFrame*>();
//For some reason, the frame's geometry is GLOBAL, not relative to the QComboBox!
frame->move(frame->x(),
mapToGlobal(lineEdit()->geometry().topLeft()).y() - frame->height());
}
}
/*else if other filters here*/
if (!handled)
handled = QComboBox::eventFilter(o, e);
return handled;
}
if you want to force popup to open above only when it is out of view you can do this:
void SteuerQComboBox::showPopup() {
QComboBox::showPopup();
QWidget *popup = this->findChild<QFrame*>();
if((popup->y() + popup->height()) > this->window()->height())
popup->move(popup->x(),popup->y()-this->height()-popup->height());
}

How to keep a list from scrolling on dataProvider refresh/update/change?

I have a simple list and a background refresh protocol.
When the list is scrolled down, the refresh scrolls it back to the top. I want to stop this.
I have tried catching the COLLECTION_CHANGE event and
validateNow(); // try to get the component to reset to the new data
list.ensureIndexIsVisible(previousIndex); // actually, I search for the previous data id in the IList, but that's not important
This fails because the list resets itself after the change (in DataGroup.commitProperties).
I hate to use a Timer, ENTER_FRAME, or callLater(), but I cannot seem to figure out a way.
The only other alternatives I can see is sub-classing the List so it can catch the dataProviderChanged event the DataGroup in the skin is throwing.
Any ideas?
Actually MUCH better solution to this is to extend DataGroup. You need to override this.
All the solutions here create a flicker as the scrollbar gets resetted to 0 and the it's set back to the previous value. That looks wrong. This solution works without any flicker and the best of all, you just change DataGroup to FixedDataGroup in your code and it works, no other changes in code are needed ;).
Enjoy guys.
public class FixedDataGroup extends spark.components.DataGroup
{
private var _dataProviderChanged:Boolean;
private var _lastScrollPosition:Number = 0;
public function FixedDataGroup()
{
super();
}
override public function set dataProvider(value:IList):void
{
if ( this.dataProvider != null && value != this.dataProvider )
{
dataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
}
super.dataProvider = value;
if ( value != null )
{
value.addEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
}
}
override protected function commitProperties():void
{
var lastScrollPosition:Number = _lastScrollPosition;
super.commitProperties();
if ( _dataProviderChanged )
{
verticalScrollPosition = lastScrollPosition;
}
}
private function onDataProviderChanged(e:CollectionEvent):void
{
_dataProviderChanged = true;
invalidateProperties();
}
override public function set verticalScrollPosition(value:Number):void
{
super.verticalScrollPosition = value;
_lastScrollPosition = value;
}
}
I ll try to explain my approach...If you are still unsure let me know and I ll give you the source code as well.
1) Create a variable to store the current scroll position of the viewport.
2) Add Event listener for Event.CHANGE and MouseEvent.MOUSE_WHEEL on the scroller and update the variable created in step 1 with the current scroll position;
3) Add a event listener on your viewport for FlexEvent.UpdateComplete and set the scroll position to the variable stored.
In a nutshell, what we are doing is to have the scroll position stored in variable every time user interacts with it and when our viewport is updated (due to dataprovider change) we just set the scroll position we have stored previously in the variable.
I have faced this problem before and solved it by using a data proxy pattern with a matcher. Write a matcher for your collection that supports your list by updating only changed objects and by updating only attributes for existing objects. The goal is to avoid creation of new objects when your data source refreshes.
When you have new data for the list (after a refresh), loop through your list of new data objects, copying attributes from these objects into the objects in the collection supporting your list. Typically you will match the objects based on id. Any objects in the new list that did not exist in the old one get added. Your scroll position will normally not change and any selections are usually kept.
Here is an example.
for each(newObject:Object in newArrayValues){
var found:Boolean = false;
for each(oldObject:Object in oldArrayValues){
if(oldObject.id == newObject.id){
found = true;
oldObject.myAttribute = newObject.myAttribute;
oldObject.myAttribute2 = newObject.myAttribute2;
}
}
if(!found){
oldArrayValues.addItem(newObject);
}
}
My solution for this problem was targeting a specific situation, but it has the advantage of being very simple so perhaps you can draw something that fits your needs from it. Since I don't know exactly what issue you're trying to solve I'll give you a description of mine:
I had a List that was progressively loading data from the server. When the user scrolled down and the next batch of items would be added to the dataprovider, the scrollposition would jump back to the start.
The solution for this was as simple as stopping the propagation of the COLLECTION_CHANGE event so that the List wouldn't catch it.
myDataProvider.addEventListener(
CollectionEvent.COLLECTION_CHANGE, preventRefresh
);
private function preventRefresh(event:CollectionEvent):void {
event.stopImmediatePropagation();
}
You have to know that this effectively prevents a redraw of the List component, hence any added items would not be shown. This was not an issue for me since the items would be added at the end of the List (outside the viewport) and when the user would scroll, the List would automatically be redrawn and the new items would be displayed. Perhaps in your situation you can force the redraw if need be.
When all items had been loaded I could then remove the event listener and return to the normal behavior of the List component.

FLEX: Programmatically remove Alert?

I need to programmatically remove an alert.
This is why:
My application uses BrowserManager to enable deep linking based off of the content in the #hash part of the url. If an alert is currently up, and the user hits the back button, the application will revert back to its previous state. But the Alert will still be up, and in many cases irrelevant at that point.
So is there a way to programmatically remove the Alert? so when the hash fragment changes I can remove it.
Thanks!
It turns out the Alert.show function returns an Alert reference and then just uses PopUpManager to add it to the display list. so if you capture the return reference when you call Alert.show you can tell PopUpManager to remove it. :)
You can do this by keeping the Alert object as member data, and then setting its visible property to false when you're done with it. Next time you need to show an Alert, don't create a new one - grab the one you've already created and set its properties, then set visible to true again.
private var myAlert : Alert;
public void showAlert( message: String, title : String ) : void
{
hideAlert();
myAlert = Alert.show( message, title, Alert.OK | Alert.NONMODAL );
}
public void hideAlert() : void
{
if( myAlert != null && myAlert.visible ) {
myAlert.visible = false;
}
}
I don't think that is possible.
You can create your own alert component subclassing TitleWindow and then use PopupManager to show/hide them.

Resources