I've started to extend the qGet DownloadManager to emit the progress of a TransferItem, so that i can connect to it. I'm inserting the progress data into a cell of a TableView model for display with an Delegate, finally the delegate paints the progress bar. That works in theory, but i'm running into the following
Problem: when there are multiple downloads in parallel, then i get progress updates from both signals into both cells!
Both progress bars show progress data, but the signal is kind of mixed and not unique to the current index (QModelIndex index / index.row()).
(Please ignore the small transitioning problem between UserRoles (after clicking the download button "ActionCell" is displayed and then "Install", before the "ProgressBar" shows up.). That is not the main problem here. My question is about the index problem.) The text "112" and "113" is the int index.row.
Questions:
How to update a TableView with progress data for multiple ProgressBars?
What must i change to render a progress bar for each download?
Source
Emit progress of a download
I've added the following things to re-emit the signal through the classes, until it bubbles up to the top, where it becomes connectable from the GUI.
a connection from QNetworkReply - downloadProgress(qint64,qint64) to TransferItem - updateDownloadProgress(qint64,qint64)
void TransferItem::startRequest()
{
reply = nam.get(request);
connect(reply, SIGNAL(readyRead()), this, SLOT(readyRead()));
connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
this, SLOT(updateDownloadProgress(qint64,qint64)));
connect(reply, SIGNAL(finished()), this, SLOT(finished()));
timer.start();
}
the SLOT function TransferItem - updateDownloadProgress(qint64,qint64) as receiver calculates the progress and stores it in progress (QMap<QString, QVariant>).
After the calculation the downloadProgress(this) signal is emitted.
// SLOT
void TransferItem::updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
progress["bytesReceived"] = QString::number(bytesReceived);
progress["bytesTotal"] = QString::number(bytesTotal);
progress["size"] = getSizeHumanReadable(outputFile->size());
progress["speed"] = QString::number((double)outputFile->size()/timer.elapsed(),'f',0).append(" KB/s");
progress["time"] = QString::number((double)timer.elapsed()/1000,'f',2).append("s");
progress["percentage"] = (bytesTotal > 0) ? QString::number(bytesReceived*100/bytesTotal).append("%") : "0 %";
emit downloadProgress(this);
}
QString TransferItem::getSizeHumanReadable(qint64 bytes)
{
float num = bytes; QStringList list;
list << "KB" << "MB" << "GB" << "TB";
QStringListIterator i(list); QString unit("bytes");
while(num >= 1024.0 && i.hasNext()) {
unit = i.next(); num /= 1024.0;
}
return QString::fromLatin1("%1 %2").arg(num, 3, 'f', 1).arg(unit);
}
When a new download is enqueued, i'm connecting the emitted downloadProgress(this) to the Slot DownloadManager - downloadProgress(TransferItem*). (dl is DownloadItem which extends TransferItem).
void DownloadManager::get(const QNetworkRequest &request)
{
DownloadItem *dl = new DownloadItem(request, nam);
transfers.append(dl);
FilesToDownloadCounter = transfers.count();
connect(dl, SIGNAL(downloadProgress(TransferItem*)),
SLOT(downloadProgress(TransferItem*)));
connect(dl, SIGNAL(downloadFinished(TransferItem*)),
SLOT(downloadFinished(TransferItem*)));
}
Finally, i'm re-emitting the download progress one more time:
void DownloadManager::downloadProgress(TransferItem *item)
{
emit signalProgress(item->progress);
}
Now the TableView with Delegate, doDownload(index) and ProgressBarUpdater
QTableView
with added QSortFilterProxyModel (for case-insensitivity)
with added ColumnDelegate, which renders DownloadButton and ProgressBar based on custom UserRoles. The delegate handles the button click: the SIGNAL downloadButtonClicked(index) is emited from the editorEvent(event, model, option, index) method.
actionDelegate = new Updater::ActionColumnItemDelegate;
ui->tableView->setItemDelegateForColumn(Columns::Action, actionDelegate);
connect(actionDelegate, SIGNAL(downloadButtonClicked(QModelIndex)), this, SLOT(doDownload(QModelIndex)));
The doDownload method receives the index and fetches the download URL from the model. Then the URL is added to the DownloadManager
and i'm setting up a ProgressBarUpdater object to set the progress data to the model at the given index. Finally i'm, connecting downloadManager::signalProgress to progressBar::updateProgress and invoke the downloadManager::checkForAllDone to start the download processing.
void UpdaterDialog::doDownload(const QModelIndex &index)
{
QUrl downloadURL = getDownloadUrl(index);
if (!validateURL(downloadURL)) return;
QNetworkRequest request(downloadURL);
downloadManager.get(request); // QueueMode is Parallel by default
ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}
The model update part: the ProgressBarUpdater takes the index and the progress and should update the model at the given index.
ProgressBarUpdater::ProgressBarUpdater(UpdaterDialog *parent, int currentIndexRow) :
QObject(parent), currentIndexRow(currentIndexRow)
{
model = parent->ui->tableView_1->model();
}
void ProgressBarUpdater::updateProgress(QMap<QString, QVariant> progress)
{
QModelIndex actionIndex = model->index(currentIndexRow, UpdaterDialog::Columns::Action);
// set progress to model
model->setData(actionIndex, progress, ActionColumnItemDelegate::DownloadProgressBarRole);
model->dataChanged(actionIndex, actionIndex);
}
The rendering part: i'm rendering the fake ProgressBar from the delegate; fetching the progress data with index.model()->data(index, DownloadProgressBarRole).
void ActionColumnItemDelegate::drawDownloadProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionProgressBarV2 opt;
opt.initFrom(bar);
opt.rect = option.rect;
opt.rect.adjust(3,3,-3,-3);
opt.textVisible = true;
opt.textAlignment = Qt::AlignCenter;
opt.state = QStyle::State_Enabled | QStyle::State_Active;
// get progress from model
QMap<QString, QVariant> progress =
index.model()->data(index, DownloadProgressBarRole).toMap();
QString text = QString::fromLatin1(" %1 %2 %3 %4 %5 ")
.arg(QString::number(index.row()))
.arg(progress["percentage"].toString())
.arg(progress["size"].toString())
.arg(progress["speed"].toString())
.arg(progress["time"].toString());
opt.minimum = 0;
opt.maximum = progress["bytesTotal"].toFloat();
opt.progress = progress["bytesReceived"].toFloat();
opt.text = text;
bar->style()->drawControl(QStyle::CE_ProgressBar,&opt,painter,bar);
}
I've added QString::number(index.row() to the progress bar text, so that each ProgressBar gets its row number rendered. In other words: the rendering is unique to the row, but the incoming progress data is somehow mixed.
I'm stuck on the index problem for a while now. Thank you in advance for your help.
Update: The issue is resolved!
Thank you very much ddriver!! I followed your suggestions and fixed it:
The DownloadManager tracks the progress for all transfers, and you keep each transfer item's data in the respective TransferItem.
The logical thing IMO would be to have a connection from each TransferItem to the corresponding ProgressBarUpdater, and emit from the transfer item.
However, in your case, you are reporting progress not from each individual transfer item, but from the download manager. So each time you are emitting a progress, you are emitting the progress for a particular transfer item to all progress bars.
connect(&downloadManager, SIGNAL(signalProgress(QMap<QString, QVariant>)),
progressBar, SLOT(updateProgress(QMap<QString, QVariant>)));
So instead of a
TransferItem --progress--> CorrespondingUI
you have a:
TransferItem --transferItem--> DownloadManager --progress--> AllUIs
This leads to having one single and varying progress for all progress bars, which corresponds to the last download that happen to report progress before the UI updated. Which is why you get no more variation after the first download is completed, as the manager only updates the progress for the second.
Finally, i'm re-emitting the download progress one more time:
void DownloadManager::downloadProgress(TransferItem *item)
{
emit signalProgress(item->progress);
}
And who exactly needs an anonymous progress, containing no information whatsoever to which transfer it applies? Aside from the bug of course.
Would you be so nice to explain, how to simplify it?
I was at the end of my mental rope yesterday when I commented, on a clear head it doesn't look all that overdone, but still I'd probably go for something more streamlined, involving 3 key components only:
DownloadsManager -> DownloadController -> UI
-> DownloadController -> UI
It just seems redundant to have a DownloadItem and then also a TransferItem, considering that a download is a transfer.
The model and view are totally unnecessary as well, as is storing the progress in the model rather than just having it as a member of the progress bar. You could go with just a regular widget for each download, and place them in a vertical layout.
Update:
The excessive, unnecessary compartmentalization has led to a level of fragmentation which makes it hard to get to the data, needed to make it work once you put everything together. The main issue is you have no way to tie a transfer item to the right progress bar updater, and since you still haven't posted all of the relevant code, the simplest possible solution I can offer involves the following minor changes:
// in DownloadManager
void signalProgress(QMap<QString, QVariant>); // this signal is unnecessary, remove
void DownloadManager::downloadProgress(TransferItem *item) // change this
{
registry[item->request.url()]->updateProgress(item->progress);
}
QMap<QUrl, ProgressBarUpdater *> registry; // add this
// in UpdaterDialog
void UpdaterDialog::doDownload(const QModelIndex &index)
{
QUrl downloadURL = getDownloadUrl(index);
if (!validateURL(downloadURL)) return;
QNetworkRequest request(downloadURL);
downloadManager.get(request); // QueueMode is Parallel by default
ProgressBarUpdater *progressBar = new ProgressBarUpdater(this, index.row());
progressBar->setObjectName("ProgressBar_in_Row_" + QString::number(index.row()) );
// remove the connection - source of the bug, instead register the updater
downloadManager.registry[downloadURL] = progressBar;
QMetaObject::invokeMethod(&downloadManager, "checkForAllDone", Qt::QueuedConnection);
}
That's pretty much it, the progress updater is associated with the URL, and in DownloadManager::downloadProgress instead of emitting the progress to all progress updaters, you simply lookup the one which actually corresponds to the particular download, and only update its progress. It is somewhat clumsy, but as I said, if your design is proper, it would not be needed and you wouldn't have the problem in the first place.
There are other solutions as well:
change the DownloadManager's signal to void signalProgress(TransferItem *), and the body of downloadProgress toemit signalProgress(item); , change to void ProgressBarUpdater::updateProgress(TransferItem *), and in the body compare the transfer item's request's url to the one in the model at the currentIndexRow, and only model-setData() if it is the same. This solution is not very efficient, since it will emit to all progress updaters just to modify one.
cut out the middleman, what I've been suggesting from the start, make DownloadManager ::get() return a pointer to the DownloadItem/TransferItem created in its body, then in UpdaterDialog::doDownload() you can connect the transfer item directly to the appropriate progress updater, so you will no longer need DownloadManager::downloadProgress() and the signalProgress signal, you only need to change the signature of the signal in TransferItem to void downloadProgress(QMap<QString, QVariant>); and emit the progress rather than the item. This is actually the most efficient solution, as it involves nothing extra, jsut the removal of unnecessary stuff.
What I'd like to accomplish is for the button to be called once after I press it with my fingers. Sometimes it works but there are also times it doesn't. Let's say I need to select from a menu. There are times that when I press down or up button, it moves perfectly but sometimes, it will move twice with a single press. I'd like to get that issue fixed.
Somewhere in global:
int debounceDelay = 50;
The code inside the loop
a3StateDownButton = digitalRead(A3);
if (a3StateDownButton != a3DownButtonLastState) {
a3DownButtonLastDebounceTime = millis();
}
if ((millis() - a3DownButtonLastDebounceTime) > debounceDelay) {
if (a3StateDownButton != currenta3ButtonState) {
currenta3ButtonState = a3StateDownButton;
if (currenta3ButtonState == HIGH) {
isDownButtonPressed = true;
// do what ever you need to do when button is high
} else if (currenta3ButtonState == LOW) {
isDownButtonPressed = false;
}
}
}
a3DownButtonLastState = a3StateDownButton;
The button I am using is very similar to this, almost exactly the same.
I only have a resistor connected to one of the pins but I forgot the value I put, most likely 2.2k.
So again, sometimes it's good but not constantly perfect. I'm also thinking that playing with the value of debounceDelay might affect my menu, which I remember it did. The response became slower when the value was increased. I think this is called software debouncing. Maybe there is something I can add to make it a hardware debouncing.
I'm modifying the Qt 5 Terminal example and use a QTextEdit window as a terminal console. I've encountered several problems.
Qt does a strange interpretation of carriage return ('\r') in incoming strings. Ocassionally, efter 3-7 sends, it interprets ('\r') as new line ('\n'), most annoying. When I finally found out I choose to filter out all '\r' from the incoming data.
Is this behaviour due to some setting?
Getting the cursor interaction to work properly is a bit problematic. I want the console to have autoscroll selectable via a checkbox. I also want it to be possible to select text whenever the console is running, without losing the selection when new data is coming.
Here is my current prinout function, that is a slot connected to a signal emitted as soon as any data has arrived:
void MainWindow::printSerialString(QString& toPrint)
{
static int cursPos=0;
//Set the cursorpos to the position from last printout
QTextCursor c = ui->textEdit_console->textCursor();
c.setPosition(cursPos);
ui->textEdit_console->setTextCursor( c );
ui->textEdit_console->insertPlainText(toPrint);
qDebug()<<"Cursor: " << ui->textEdit_console->textCursor().position();
//Save the old cursorposition, so the user doesn't change it
cursPos= ui->textEdit_console->textCursor().position();
toPrint.clear();
}
I had the problem that if the user clicked around in the console, the cursor would change position and the following incoming data would end up in the wrong place. Issues:
If a section is marked by the user, the marking would get lost when new data is coming.
When "forcing" the pointer like this, it gets a rather ugly autoscroll behaviour that isn't possible to disable.
If the cursor is changed by another part of the program between to printouts, I also have to record that somehow.
The append function which sound like a more logical solution, works fine for appending a whole complete string but displays an erratic behaviour when printing just parts of an incoming string, putting characters and new lines everywhere.
I haven't found a single setting regarding this but there should be one? Setting QTextEdit to "readOnly" doesn't disable the cursor interaction.
3.An idea is to have two cursors in the console. One invisible that is used for printouts and that is not possible at all to manipulate for the user, and one visible which enables the user to select text. But how to do that beats me :) Any related example, FAQ or guide are very appreciated.
I've done a QTextEdit based terminal for SWI-Prolog, pqConsole, with some features, like ANSI coloring sequences (subset) decoding, command history management, multiple insertion points, completion, hinting...
It runs a nonblocking user interface while serving a modal REPL (Read/Eval/Print/Loop), the most common interface for interpreted languages, like Prolog is.
The code it's complicated by the threading issues (on user request, it's possible to have multiple consoles, or multiple threads interacting on the main), but the core it's rather simple. I just keep track of the insertion point(s), and allow the cursor moving around, disabling editing when in output area.
pqConsole it's a shared object (I like such kind of code reuse), but for deployment, a stand-alone program swipl-win is more handy.
Here some selected snippets, the status variables used to control output are promptPosition and fixedPosition.
/** display different cursor where editing available
*/
void ConsoleEdit::onCursorPositionChanged() {
QTextCursor c = textCursor();
set_cursor_tip(c);
if (fixedPosition > c.position()) {
viewport()->setCursor(Qt::OpenHandCursor);
set_editable(false);
clickable_message_line(c, true);
} else {
set_editable(true);
viewport()->setCursor(Qt::IBeamCursor);
}
if (pmatched.size()) {
pmatched.format_both(c);
pmatched = ParenMatching::range();
}
ParenMatching pm(c);
if (pm)
(pmatched = pm.positions).format_both(c, pmatched.bold());
}
/** strict control on keyboard events required
*/
void ConsoleEdit::keyPressEvent(QKeyEvent *event) {
using namespace Qt;
...
bool accept = true, ret = false, down = true, editable = (cp >= fixedPosition);
QString cmd;
switch (k) {
case Key_Space:
if (!on_completion && ctrl && editable) {
compinit2(c);
return;
}
accept = editable;
break;
case Key_Tab:
if (ctrl) {
event->ignore(); // otherwise tab control get lost !
return;
}
if (!on_completion && !ctrl && editable) {
compinit(c);
return;
}
break;
case Key_Backtab:
// otherwise tab control get lost !
event->ignore();
return;
case Key_Home:
if (!ctrl && cp > fixedPosition) {
c.setPosition(fixedPosition, (event->modifiers() & SHIFT) ? c.KeepAnchor : c.MoveAnchor);
setTextCursor(c);
return;
}
case Key_End:
case Key_Left:
case Key_Right:
case Key_PageUp:
case Key_PageDown:
break;
}
you can see that most complexity goes in keyboard management...
/** \brief send text to output
*
* Decode ANSI terminal sequences, to output coloured text.
* Colours encoding are (approx) derived from swipl console.
*/
void ConsoleEdit::user_output(QString text) {
#if defined(Q_OS_WIN)
text.replace("\r\n", "\n");
#endif
QTextCursor c = textCursor();
if (status == wait_input)
c.setPosition(promptPosition);
else {
promptPosition = c.position(); // save for later
c.movePosition(QTextCursor::End);
}
auto instext = [&](QString text) {
c.insertText(text, output_text_fmt);
// Jan requested extension: put messages *above* the prompt location
if (status == wait_input) {
int ltext = text.length();
promptPosition += ltext;
fixedPosition += ltext;
ensureCursorVisible();
}
};
// filter and apply (some) ANSI sequence
int pos = text.indexOf('\x1B');
if (pos >= 0) {
int left = 0;
...
instext(text.mid(pos));
}
else
instext(text);
linkto_message_source();
}
I think you should not use a static variable (like that appearing in your code), but rely instead on QTextCursor interface and some status variable, like I do.
Generally, using a QTextEdit for a feature-rich terminal widget seems to be a bad idea. You'll need to properly handle escape sequences such as cursor movements and color mode settings, somehow stick the edit to the top-left corner of current terminal "page", etc. A better solution could be to inherit QScrollArea and implement all the needed painting–selection-scrolling features yourself.
As a temporary workaround for some of your problems I can suggest using ui->textEdit_console->append(toPrint) instead of insertPlainText(toPrint).
To automatically scroll the edit you can move the cursor to the end with QTextEdit::moveCursor() and call QTextEdit::ensureCursorVisible().
I have discovered that geometry->setLineWidth(3); in the code below extends to other QML elements and can distort them, even if those other QML elements are "normal" QML elements (with no QQuickItem subclass beneath them). This seems odd to me that you could affect other elements and I wonder if it is a bug? The documentation says that this function should only affect the current element, but that is not my experience. Can anyone weigh in on why geometry->setLineWidth(3); has such unwieldy power?
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data){
QSGGeometry *geometry =
new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2);
geometry->setDrawingMode(GL_LINES);
geometry->setLineWidth(3);
geometry->vertexDataAsPoint2D()[0].set(0, 0);
geometry->vertexDataAsPoint2D()[1].set(width(), height());
QSGFlatColorMaterial *material = new QSGFlatColorMaterial;
material->setColor(QColor(255, 0, 0));
QSGGeometryNode *node = new QSGGeometryNode;
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
delete oldNode;
return node;
}
If I omit the line geometry->setLineWidth(3); then the problem goes away.
I think it's a bug, sort of. The implementation in gsgbatchrenderer.cpp looks like this:
if (g->drawingMode() == GL_LINE_STRIP || g->drawingMode() == GL_LINE_LOOP || g->drawingMode() == GL_LINES)
glLineWidth(g->lineWidth());
The gl functions are global on the current OpenGL context so they apply to all drawing commands, and there's nothing that sets glLineWidth back to its previous value. However, any other nodes that also are in GL_LINES mode will also set the line width when it's their turn to render. So I'm guessing the other QML elements must be rendering lines without going through this code and setting the glLineWidth themselves. If I'm right then that's the bug and those other elements need to explicitly set the width.
Can you tell us what type those other 'normal' QML elements are?
I implemented the removeRows() method according to the documentation. My data is stored in a QList. I can remove items just fine using:
bool MeasurementManager::removeRows(int row, int count, const QModelIndex &m) {
if(count > 1) {
qDebug() << "MeasurementManager: cannot remove more than one measurement";
return false;
}
beginRemoveRows(QModelIndex(), row, row+count-1);
list.removeAt(row);
endRemoveRows();
return true
}
However, when removing the last item I get the following error message, when executing beginRemoveRows():
ASSERT failure in QList<T>::at: "index out of range"
When removing the last item (leading to the crash) it obviously has to be in row 0, but as long as there are other items in the model I can remove the item in row 0 without any problems.
If I comment out the actual removal of my data like this
beginRemoveRows(QModelIndex(), row, row+count-1);
//list.removeAt(row);
endRemoveRows();
no crash occurs, so my assumption it, that something tries to access one of list's elements after the removal. However when stepping through the function the beginRemoveRows() method clearly is the culprit.
Any help where to start debugging would be appreciated!
I found the solution, my bad. I had connected to the ´selectionChanged()´ signal to a custom slot. This tried to access the recently deleted item in the table model.
I overlooked, that deselection a table item emits an selectionChanged() signal, too.
What you see is probably an artifact of code reordering introduced by optimizing the code.
Compile the code again with all optimizations disabled to avoid confusing the debugger.
Just had the same problem. Simply call reset() and remove the rows afterwards.
void QItemSelectionModel::reset() [virtual slot]
Clears the selection model. Does not emit any signals.