Skip to content
This repository has been archived by the owner on Oct 17, 2019. It is now read-only.

[Updated] Implements #132: Pasting images into room #180

Merged
merged 8 commits into from
Jan 10, 2018

Conversation

christarazi
Copy link
Contributor

This is an update to PR #138

@mujx
Copy link
Owner

mujx commented Dec 24, 2017

Thanks a lot for your work! The preview modal is really good 👍

The only issue I spotted was with the file naming. On the dialog the default name should be the local filename. Also when I send the image it appears with the temporary filename and not with the one I gave. You can pass around the full local path, I don't think there is a need to create a temporary file.

Copy link
Contributor

@Ralith Ralith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work! There are some issues remaining. In addition to the specific notes on the code, the message for the first commit is confusing; most of the changes it describes no longer apply.

void audioUploaded(const QString &roomid, const QString &filename, const QString &url);
void imageUploaded(const QString &roomid, QSharedPointer<QFile> file, const QString &url);
void fileUploaded(const QString &roomid, QSharedPointer<QFile> file, const QString &url);
void audioUploaded(const QString &roomid, QSharedPointer<QFile> file, const QString &url);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these QFile? The whole point of the change to use QIODevice is that the uploaded data is not necessarily a file. As the filename (and only the filename) is needed for UI purposes, this should remain a QString.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is an oversight on my end. You are correct, they do not need to take a QFile. However, it is not clear to me what the QString should be, if the data does not come from a file (where the string would obviously be the file name).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just invent something descriptive, depending on the actual origin of the data. For example, this PR might use the name "clipboard.png" for UI and upload filename purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, how would I know if I received a file or a buffer containing data? Would the best method be to try to cast to a QFile and use the file name if it exists, and if the cast fails, then use the "clipboard.png" mentioned?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not need to know what specific QIODevice generic code receives; doing so defeats the purpose of having generic code. For example, the approach you outline will break down once there are non-clipboard sources of in-memory data (e.g. imagery captured directly from a camera). Justifiable uses of dynamic casting in C++ are extremely rare.

Instead, whenever a filename will eventually be needed alongside a QIODevice, you should pass a filename along from the very beginning. For example, the signature of FilteredTextEdit::image could be changed to void FilteredTextEdit::image(QSharedPointer<QIODevice> data, const QString &filename)

void uploadAudio(const QString &roomid, const QString &filename);
void uploadImage(const QString &roomid, QSharedPointer<QIODevice> iodev);
void uploadFile(const QString &roomid, QSharedPointer<QIODevice> iodev);
void uploadAudio(const QString &roomid, QSharedPointer<QIODevice> iodev);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these need to be shared pointers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're shared pointers because notice the lambda in the MatrixClient::upload*() member functions for responding to a QNetworkReply::finished signal. By the time that lambda is executed (asynchronously), we have lost the scope of the QIODevice. If I were to pass by const ref, the reference would be bound to a temporary local, which would cause a segfault somewhere during the execution of the lambda. To resolve this, the best method I could think of was to create a pointer so that it's allocated on the heap.

I would love to hear if you have a better method.


emit image(file);

previewDialog_->close();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you close the preview dialog no matter what, wouldn't it be simpler to do so in one place, in the dialog's implementation at the same time you emit confirmImageUpload? That way it's impossible to forget, and much less likely to be broken by future changes.

&FilteredTextEdit::receiveImage);
previewDialog_->show();
} else if (source->hasFormat("x-special/gnome-copied-files") &&
QImageReader{source->text()}.canRead()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a user pastes in non-image files?

Copy link
Contributor Author

@christarazi christarazi Dec 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of now, pasting non-image files will trigger the else clause and therefore, Qt's default behaviour will take over. I think adding support for pasting non-image files (generic files, aka not audio, video, etc) is fairly simple, but probably out of the scope of this PR. It is definitely something I can look into in the future.

previewDialog_->close();
return;
}
if (!img.save(file->fileName(), "PNG")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saving the file to disk remains an unnecessary pessimization, and it's further unclear how you are guaranteeing that the temporary files are deleted when no longer needed.

You can encode images without touching the disk using a QImageWriter (for example).

{
if (source->hasImage()) {
QPixmap const img = qvariant_cast<QPixmap>(source->imageData());
previewDialog_ = new dialogs::PreviewImageOverlay{img, this};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does the PreviewImageOverlay you create here get deleted? Why does it need to be dynamically allocated at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason it is dynamically allocated is because the dialog is created on a paste event, which is dynamic behaviour by nature. The same approach is used in src/SideBarActions.cc and src/UserInfoWidget.cc, to name a few examples.

How it is deleted is an oversight. I have locally changed previewDialog_ to be a QSharedPointer so that it can be de-allocated automatically. I also pass this as a parameter to the constructor as to create a parent-child relationship with Qt's object model.

Please let me know if you have a better approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI semantics you describe are orthogonal to the actual construction/destruction of the object; you could let PreviewImageOverlay be a regular class member, and just show/hide/reset it as necessary. Dynamic allocation can be simpler, but requires care to avoid introducing leaks. Here, using Qt's parent-child relationship does not avoid a leak because many pastes may occur in the lifetime of a TextInputWidget.

Switching to a QSharedPointer fixes the problem, but may be confusing, as it's presumably not intended that ownership of the value be shared. Instead, I'd suggest using std::unique_ptr, and furthermore replacing the call to new with std::make_unique; calling new explicitly should generally be avoided, as it's prone to this type of mistake.

QImageReader{source->text()}.canRead()) {
// Special case for X11 users. See "Notes for X11 Users" in source.
// Source: http://doc.qt.io/qt-5/qclipboard.html
previewDialog_ = new dialogs::PreviewImageOverlay{source->text(), this};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above re: deletion.

FilteredTextEdit::insertFromMimeData(const QMimeData *source)
{
if (source->hasImage()) {
QPixmap const img = qvariant_cast<QPixmap>(source->imageData());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QMimeData::imageData is documented as returning a QVariant containing a QImage. Why is a QPixmap used here instead?

@@ -711,16 +711,16 @@ MatrixClient::uploadImage(const QString &roomid, const QString &filename)
return;
}

emit imageUploaded(roomid, filename, object.value("content_uri").toString());
emit imageUploaded(roomid, iodev.dynamicCast<QFile>(), object.value("content_uri").toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear whether there's any handling for the case where this cast fails and returns an empty QSharedPointer. Regardless, as discussed elsewhere, QFile should not be used here.

@@ -61,11 +61,11 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client,

ImageItem::ImageItem(QSharedPointer<MatrixClient> client,
const QString &url,
const QString &filename,
QSharedPointer<QFile> file,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will this handle images that aren't written to disk?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure how to approach this yet as I did not foresee this as a problem.

To properly load the image, we also need access to the image data. It's not a difficult change, but requires numerous functions to be modified to be able to pass the image data along with the filename. I would like to hear your thoughts on this one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give a few examples of functions that would need to be modified? An image in the process of being uploaded shouldn't need to be accessed from very many places, and I don't imagine there are many places other than the timeline or the upload procedure itself where a filename is pertinent.

Copy link
Contributor Author

@christarazi christarazi Dec 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following is the calling order which would need to be modified to pass the image data:

  1. MatrixClient::imageUploaded
  2. TimelineViewManager::queueImageMessage
  3. TimelineView::addUserMessage
  4. ImageItem constructor

Edit: The reason is because in item 4, a call to setImage is used with the image file name. If that file doesn't exist on the filesystem, then the issue occurs. The obvious solution is to pass the image data to setImage instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem particularly egregious to me. If the increase in number of parameters bothers you, you could always create a struct Image { QImage data; QString name; }; or similar to bundle them up for convenience.

@christarazi
Copy link
Contributor Author

christarazi commented Dec 31, 2017

I've added a new commit which hopefully addresses all the issues pointed out.

Copy link
Contributor

@Ralith Ralith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This is shaping up nicely, but I think we can still make it even better.


const auto formats = source->formats();
const auto idx = formats.indexOf(
QRegularExpression{"image/\\w+", QRegularExpression::CaseInsensitiveOption});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to be more selective than image/.*?

Copy link
Contributor Author

@christarazi christarazi Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily. I just find it more reassuring than one that gives image/ as a potential match (even though that should never happen). I do not mind changing it if you prefer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly I'm leery of inadvertently filtering out future valid image types. It's not immediately obvious to me that Qt's interpretation of \w is a superset of valid media type characters. I wouldn't object to image/.+ either, but if something's generating clipboard contents with garbage mime-types anything we do is going to be best-effort anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops I forgot to address this one. I will make the change locally, and add it to the next set of changes if there is one.

FilteredTextEdit::insertFromMimeData(const QMimeData *source)
{
if (source->hasImage()) {
const QImage image = qvariant_cast<QImage>(source->imageData());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On further investigation, it's apparent to me that we can and therefore must use QMimeData::data to avoid re-encoding the image at all, rather than decoding the image here and re-encoding it below. This will avoid silent quality loss, improve robustness, and reduce CPU and memory use. The mime type obtained from QMimeData should also be propagated alongside the data so it can be included in the matrix event verbatim, removing any need to guess it.

I apologize for not identifying this option earlier; I'm not super familiar with how rich copy/paste works.

Copy link
Contributor Author

@christarazi christarazi Jan 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgive me if I'm missing something here, but using QMimeData::data won't prevent re-encoding, because we must pass a QString representing the MIME type, which is then used to create (encode the raw pixels) the QByteArray. I also am not too familiar with copy/pasting media, but I've noticed that when copying an image (not from a file manager), it will copy the raw pixel bytes, understandably, as there's no notion of a file. I only came to this realization when creating this PR.

Therefore, re-encoding cannot be avoided. However, your suggestion has reduced the amount of code necessary and made it much cleaner.

Copy link
Contributor

@Ralith Ralith Jan 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we must pass a QString representing the MIME type, which is then used to create (encode the raw pixels) the QByteArray

My understanding is that QMimeData::data performs no transcoding. The supplied mimeType is used to select which of multiple pieces of attached data you want (from the alternatives exposed by formats). This can be observed in the QMimeDataPrivate::retrieveTypedData implementation's handling of the type == QVariant::ByteArray case, which for image data falls through ultimately to QMimeDataPrivate::getData, which is a simple linear search through dataList.

I also am not too familiar with copy/pasting media, but I've noticed that when copying an image (not from a file manager), it will copy the raw pixel bytes, understandably, as there's no notion of a file.

What was your methodology here? If we're genuinely receiving image/* mime types, as the current PR suggests, I would expect the data to be in the advertised format. In fact, reviewing the implementation, it appears to me that QMimeData::imageData returns nonempty results only when the mime type is application/x-qt-image. PNG data certainly doesn't have to be physically stored in a file to be valid.

Right clicking on a jpg image in Chrome, selecting "Copy image," and inspecting the clipboard state with xclip -o -selection clipboard -t TARGETS reveals that the clipboard contains data in formats image/png and text/html. Dumping the former yields a valid png image, and the latter yields a brief HTML document containing an image tag. I'm a bit perplexed by Chrome's apparent decision to transcode the image, but it's apparent that we do not need to.

re-encoding cannot be avoided

If uncompressed data is actually the norm, we should take care to encode it as png (or perhaps something more modern, like webp?) for sanity's sake, but per the above testing this does not appear to be the case.

Copy link
Contributor Author

@christarazi christarazi Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, seems I have drawn an incorrect conclusion because of a misinterpretation of the QImage::bits method. These are the steps that I took which led me to make my initial claim:

QImage img = qvariant_cast<QImage>(source->imageData());
QByteArray arr = QByteArray{img.bits(), img.byteCount()};

For some reason, I thought that this would essentially convert the QImage into a byte array. Instead, it gives the actual raw pixel bytes, which led me to incorrectly believe that "Copy image" copies raw bytes.

Alright well, I apologize for that, my knowledge of the clipboard is clearly lacking. I was not even aware of that particular capability of xclip, that's pretty nifty. Thanks for your informative reply.

Copy link
Contributor

@Ralith Ralith Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think QImage probably is uncompressed data, since it supports editing. I'm still a bit confused about whether imageData is ever nonempty when pasting e.g. image/png; it seems like it can't be, but I assume you've the original code and it does something at least some of the time. Perhaps Qt automatically decodes incoming pastes for convenience of display.

One potential subtlety to bear in mind is the case where many image formats available. For example, here's the list firefox supplies:

text/html
text/_moz_htmlinfo
text/_moz_htmlcontext
image/png
image/bmp
image/x-bmp
image/x-MS-bmp
image/jpeg
image/tiff
image/x-icon
image/x-ico
image/x-win-bitmap
image/vnd.microsoft.icon
application/ico
image/ico
image/icon
text/ico

Given that the data supplied by both firefox and chrome seems to be re-encoded (the input was a jpeg image, and the image/jpeg content was 6 times smaller than the input) IMO a lossless format should be preferred. I'm happy to work under the hypothesis that everyone's helpful and orders paste data in descending quality--i.e. that it is correct to pick the first image content if any exists--but if this is determined to often be false, we may in the future want to add some heuristics to try to make a good selection. Might be worth leaving a note to ourselves in a comment.

@@ -51,13 +51,13 @@ struct PendingMessage
PendingMessage(mtx::events::MessageType ty,
int txn_id,
QString body,
QString filename,
const QString &fn,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this argument name changed?

color: #caccd1;
}

QListWidget {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this line deleted?

Copy link
Contributor Author

@christarazi christarazi Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops that was accidental. It probably slipped in when I was doing a rebase onto upstream from my previous PR. It has been fixed. Thanks for catching this.

@@ -42,6 +42,7 @@ class FileItem : public QWidget

FileItem(QSharedPointer<MatrixClient> client,
const QString &url,
const QSharedPointer<QIODevice> data,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Unlike with image/audio/video, I'm not sure we can/should do much in the client with the data of an arbitrary opaque file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed because TimelineView::addUserMessage takes in a generic type Widget, which happens to be one of the four FileItem, ImageItem, AudioItem, and VideoItem. Therefore, we need those types to have the same interface (parameters to the constructor).

Copy link
Contributor

@Ralith Ralith Jan 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not particularly happy about having never-used arguments making things confusing on both ends for the sake of convenience to a template. That said, any refactoring of addUserMessage is probably best done separately from this PR.

request.setHeader(QNetworkRequest::ContentTypeHeader, mime.name());

auto reply = post(request, file.readAll());
auto reply = post(request, iodev->readAll());
Copy link
Contributor

@Ralith Ralith Jan 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's inappropriate to read the QIODevice fully into memory before transmitting. This wastes time and memory, and could hang the UI. Instead, it should be streamed. Note that QNetworkAccessManager::post supports this natively.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not aware of that particular overload. Fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am having trouble with this one actually. It seems that QNetworkAccessManager::post when taking in a pointer of QIODevice, will try to create another QSharedPointer. This causes problems, because in our case, the pointer to QIODevice is already a QSharedPointer. Somewhere this causes a segfault because of a double-free.

I was able to reproduce the issue. The following code is based on the stack trace from gdb.

Here's the code:

#include <QBuffer>
#include <QFile>
#include <QPointer>
#include <iostream>
#include <memory>

static void not_deleter(QObject *obj)
{
}

static void deleter(QObject *obj)
{
    delete obj;
}

auto foo(QIODevice *p)
{
    std::cerr << "Before cast\n";
    QBuffer *buf = qobject_cast<QBuffer*>(p);
    std::cerr << "After cast\n";

    std::cerr << "Before unique_ptr ctor\n";
    QSharedPointer<QIODevice>{p, &deleter};      // segfault
    QSharedPointer<QIODevice>{p, &not_deleter};  // OK
    std::cerr << "After unique_ptr ctor\n";
}

int main()
{
    std::cerr << "Begin main()\n";
    QSharedPointer<QFile> ptr{new QFile{}};

    std::cerr << "Before call to foo()\n";
    foo(ptr.data());
    std::cerr << "After call to foo()\n";

    std::cerr << "End main()\n";
    return 0;
}

Here's the stack trace:

Thread 1 "nheko" received signal SIGSEGV, Segmentation fault.
0x0000000e00000001 in ?? ()
(gdb) bt
#0  0x0000000e00000001 in  ()
#1  0x00007ffff5ea1b29 in QMetaObject::cast(QObject const*) const ()
    at /usr/lib/libQt5Core.so.5
#2  0x00007ffff5db9e7d in QNonContiguousByteDeviceFactory::createShared(QIODevice*) ()
    at /usr/lib/libQt5Core.so.5
#3  0x00007ffff6b39ecc in  () at /usr/lib/libQt5Network.so.5
#4  0x00007ffff6b40330 in  () at /usr/lib/libQt5Network.so.5
#5  0x00007ffff6b41c0c in  () at /usr/lib/libQt5Network.so.5
#6  0x00007ffff6b41cf5 in  () at /usr/lib/libQt5Network.so.5
#7  0x00007ffff6bf4fda in  () at /usr/lib/libQt5Network.so.5
#8  0x00007ffff5ec9452 in QObject::event(QEvent*) () at /usr/lib/libQt5Core.so.5
#9  0x00007ffff76d1e3c in QApplicationPrivate::notify_helper(QObject*, QEvent*) ()
    at /usr/lib/libQt5Widgets.so.5
#10 0x00007ffff76d9816 in QApplication::notify(QObject*, QEvent*) ()
    at /usr/lib/libQt5Widgets.so.5
#11 0x00007ffff5e981e0 in QCoreApplication::notifyInternal2(QObject*, QEvent*) ()
    at /usr/lib/libQt5Core.so.5
#12 0x00007ffff5e9ae46 in QCoreApplicationPrivate::sendPostedEvents(QObject*, int, QThreadData*) () at /usr/lib/libQt5Core.so.5
#13 0x00007ffff5ef50a4 in  () at /usr/lib/libQt5Core.so.5
#14 0x00007ffff2670270 in g_main_context_dispatch () at /usr/lib/libglib-2.0.so.0
#15 0x00007ffff2671f69 in  () at /usr/lib/libglib-2.0.so.0
#16 0x00007ffff2671fae in g_main_context_iteration () at /usr/lib/libglib-2.0.so.0
#17 0x00007ffff5ef4691 in QEventDispatcherGlib::processEvents(QFlags<QEventLoop::ProcessEventsFlag>) () at /usr/lib/libQt5Core.so.5
#18 0x00007fffe8f2c152 in  () at /usr/lib/libQt5XcbQpa.so.5
#19 0x00007ffff5e9682b in QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>) ()
    at /usr/lib/libQt5Core.so.5
#20 0x00007ffff5e9fb18 in QCoreApplication::exec() () at /usr/lib/libQt5Core.so.5
#21 0x0000555555a21ee7 in main(int, char**) (argc=1, argv=0x7fffffffe028)
    at /home/chris/dev/nheko/src/main.cc:166

See the link above for the source code of QNonContiguousByteDeviceFactory::createShared.

Copy link
Contributor

@Ralith Ralith Jan 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to the documentation of QSharedPointer::create, you can see that it does not create a new QSharePointer owning exactly the same object, but rather creates a new, unrelated QSharedPointer with its own, new, object, the arguments passed in being forwarded to the new object's constructor. This makes sense, because (although the weird syntax highlighting makes it a bit hard to see) the value being passed in is a pointer to the pointer, not the pointer itself.

The documentation for QNetworkAccessManager::post specifies that you must ensure the QIODevice remains valid until the returned reply emits its finished signal. If you're seeing memory errors, this may be the cause. I believe a convenient way to ensure the QIODevice lives that long would be attaching a closure that captures the QSharedPointer that owns it to the signal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks. Yes that fixed the issue.

void
FilteredTextEdit::receiveImage(const QByteArray &img, const QString &img_name)
{
QSharedPointer<QBuffer> buffer{new QBuffer{const_cast<QByteArray *>(&img), this}};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to being const-unsafe (it is almost never correct to use const_cast), this exposes a pointer to the imageData_ member of PreviewImageOverlay. What happens if the user pastes in another image while the emitted QBuffer is still in use somewhere?

Instead, create a QBuffer that owns its own storage using its default constructor, and initialize it by calling QBuffer::setData.

void setImageAndCreate(const QString &path);

signals:
void confirmImageUpload(const QByteArray &data, const QString &img_name);
Copy link
Contributor

@Ralith Ralith Jan 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid inadvertent sharing and possible UB, this should emit the QByteArray by value. QByteArray is internally ref-counted so doing so is cheap.

public:
PreviewImageOverlay(QWidget *parent = nullptr);

void setImageAndCreate(const QByteArray &data, const QString &type);
Copy link
Contributor

@Ralith Ralith Jan 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it copies it internally anyway, this might as well take the QByteArray by value.

@alphapapa
Copy link
Contributor

@Ralith I'm just an interested user, but thanks for your very thorough reviews of this patch. I'm glad that nheko is getting such high-quality code! (I wish I had a reviewer like you for my own projects!)

@christarazi
Copy link
Contributor Author

@Ralith I would like to echo @alphapapa 's comment. I must admit your attitude and determination for quality code is quite inspiring and motivating, and exhibits one of the many qualities that I love about free and open source software and their respective communities. So thank you for your hard work and I wish everyone a happy new year. :)

@Ralith
Copy link
Contributor

Ralith commented Jan 2, 2018

Thank you both for saying so! I'm glad my comments have been helpful.

@alphapapa
Copy link
Contributor

@christarazi Forgive me for neglecting to thank you for making this patch as well! :)

PreviewImageOverlay::setImageAndCreate(const QByteArray &data, const QString &type)
{
imageData_ = data;
image_.loadFromData(imageData_);
Copy link
Contributor

@Ralith Ralith Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have a fallback (perhaps a QLabel with some explanatory text) for when this fails (and returns false), i.e. when the user pasted in an image of a type that Qt doesn't support decoding, or that QImage can't accurately guess the format of. In those cases we should still allow uploading, but the current behavior might produce a confusing UI.

@@ -83,7 +84,14 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client,
url_ = QString("%1/_matrix/media/r0/download/%2")
.arg(client_.data()->getHomeServer().toString(), media_params);

setImage(QPixmap(filename));
if (!data.isNull()) {
data->reset();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return code of this shouldn't be ignored unless you can prove that it's guaranteed to succeed.

if (!file.open(QIODevice::ReadWrite)) {
qDebug() << "Error while reading" << filename;
if (!iodev->open(QIODevice::ReadWrite)) {
qDebug() << "Error while reading buffer" << iodev.data();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logged errors should contain the specific error message.

const QString &filename,
QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{QFileInfo(filename).fileName()}
, text_{QFileInfo{filename}.fileName()}
Copy link
Contributor

@Ralith Ralith Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filename is no longer a path on disk, so this won't work. You probably want to just copy in filename directly.

@mujx
Copy link
Owner

mujx commented Jan 7, 2018

Could you remove the full path from the default name on the modal and only set the name of the file?

@christarazi
Copy link
Contributor Author

New commit added addressing the second round of issues. Thanks again for your vigilance and patience!

@mujx
Copy link
Owner

mujx commented Jan 8, 2018

Thanks again for your work! Could you rebase on master to resolve the conflicts and let the ci run?

Squashes the following commits:

 - Use QKeySequence::Copy instead of hardcoded ctrl-c
 - Add ability to embed images and text in same message
 - Remove unneeded explicit call to insertFromMimeData()
 - Fix case for X11 users when copying an image
 - Add preview dialog for pasted images
 - Image data no longer written to disk; only in memory
 - Fix incorrect type used in qvariant_cast
 - PreviewImageOverlay is no longer dynamic; created on stack
 - Images are encoded based on MIME data; default is PNG
 - Handle images that are not on disk in ImageItem
 - Close PreviewImageOverlay dialog after upload button click
 - Follow consistent dialog styles in PreviewImageOverlay
 - Grab encoded image data directly from clipboard rather than explicity
   transcoding.
 - Fix inadvertant deletion in styles
 - Pass pointer to QNetworkAccessManager::post instead of reading all
   the data at once
 - Remove unnecessary use of const_cast
 - Use pass-by-value for QByteArray
 - Log reason for QIODevice failing to open
 - Add missing check of return value for QIODevice::reset
 - Do not create QFileInfo on nonexistent file
 - Add note about MIME image data
 - Only show filename inside PreviewImageOverlay rather than path
 - Add error message to PreviewImageOverlay on failed read
 - Make regex for image type more lenient
 - Fix inadvertent renaming of parameter
@christarazi
Copy link
Contributor Author

I have rebased the changes onto master.

Copy link
Contributor

@Ralith Ralith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good progress! Thanks for sticking with this.

const QString &filename,
QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{QFileInfo(filename).fileName()}
, text_{QFileInfo{filename}.fileName()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filename no longer identifies a path on disk.

@@ -66,13 +66,15 @@ VideoItem::VideoItem(QSharedPointer<MatrixClient> client,

VideoItem::VideoItem(QSharedPointer<MatrixClient> client,
const QString &url,
const QSharedPointer<QIODevice> data,
const QString &filename,
QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{QFileInfo(filename).fileName()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filename no longer identifies a path on disk.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For VideoItem, AudioItem, and FileItem, filename does actually identify a path on disk. Only ImageItem's doesn't. This is because they are only created by the user explicitly selecting them to upload.

Once they can be uploaded through other means (copy-paste), then I think it would make sense to modify this (similar to how ImageItem is now).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they need a uniform interface, then the interface should be semantically uniform. That means no inconsistent special meanings. filename should be standardized as purely UI.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not a fan of this but it could be handled in a different PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't like it, but I thought it would be out of the scope of this PR to add changes unrelated to image pasting. I can definitely tackle this after this PR is merged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR changes the pervasive filename argument to file-ish widget constructors from "always a full path on disk" to not. IMO is is therefore in scope; inconsistency here is a landmine.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged it before addressing the filename issue because I didn't want to drag this PR any further with refactoring. Although it is related, I think it would be better to have a more focused PR without the fear of moving too much out of scope.

if (!null && reset) {
p.loadFromData(data->readAll());
} else {
p.load(filename);
Copy link
Contributor

@Ralith Ralith Jan 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because filename no longer identifies a path on disk, this fallback doesn't make sense. An error message (see QIODevice::errorString) should be displayed instead.

const QString &filename,
QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{QFileInfo(filename).fileName()}
, text_{QFileInfo{filename}.fileName()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filename no longer identifies a path on disk.

bool
FilteredTextEdit::canInsertFromMimeData(const QMimeData *source) const
{
return (source->hasImage() || source->hasText() ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't hasText redundant to checks made by QTextEdit::canInsertFromMimeData?


QNetworkRequest request(QString(endpoint.toEncoded()));
request.setHeader(QNetworkRequest::ContentLengthHeader, file.size());
request.setHeader(QNetworkRequest::ContentLengthHeader, iodev->size());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the QIODevice::size documentation, this is incorrect if the device is not a random access device. The header should be omitted entirely if QIODevice::isSequential returns true.

The spec doesn't seem to require the header at all, so I'm not sure why we're providing it in the first place. I believe it works fine without, so maybe it would be better to just remove this line entirely? Or is it of use to middleboxes sometimes?

if (!file.open(QIODevice::ReadWrite)) {
qDebug() << "Error while reading" << filename;
if (!iodev->open(QIODevice::ReadOnly)) {
qDebug() << "Error while reading buffer" << iodev->errorString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A QIODevice is not necessarily a buffer.

 - Fix incorrect fallback to filename
 - Fix incorrect error message
 - Remove unneeded http content length header
 - Remove redundant check for MIME data
 - Run make lint
@@ -59,7 +59,8 @@ struct PendingMessage
, filename(filename)
, event_id(event_id)
, widget(widget)
{}
{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those were fine. There are some issues with the clang-format running on travis (macos) and the one on liinux, so this will fail on travis. Not sure if this behaviour can be changed with a flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I will revert these lines.

@mujx mujx merged commit ddfce13 into mujx:master Jan 10, 2018
@christarazi
Copy link
Contributor Author

I will begin work immediately on addressing the filename issue as discussed. Thanks for merging!

@christarazi christarazi deleted the iodev-pasteimg branch January 10, 2018 21:39
@alphapapa
Copy link
Contributor

@christarazi Thanks for your work on this! I just pulled and built and tested, and it worked great! Using Ubuntu 14.04 with KDE 4, I took a screenshot with KSnapshot, copied it to the clipboard, and pasted it directly into nheko.

@mujx @Ralith Thanks for your work on this as well!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants