Drag n drop (#223)

Added drop interface for image files, images, and text, dragged from other applications.  (#153,#223)

- Lock aspect ration on new image objects by default
- Communicate context menu click location to paste actions
This commit is contained in:
Jaye Evins
2025-08-12 16:06:43 -04:00
committed by GitHub
parent 524e9cc9e9
commit f147407a46
9 changed files with 241 additions and 56 deletions
+84 -2
View File
@@ -38,6 +38,7 @@
#include "model/Markup.h" #include "model/Markup.h"
#include "model/Settings.h" #include "model/Settings.h"
#include <QMimeData>
#include <QMouseEvent> #include <QMouseEvent>
#include <QtMath> #include <QtMath>
#include <QtDebug> #include <QtDebug>
@@ -105,6 +106,7 @@ namespace glabels
setMouseTracking( true ); setMouseTracking( true );
setFocusPolicy(Qt::StrongFocus); setFocusPolicy(Qt::StrongFocus);
setAcceptDrops( true );
connect( model::Settings::instance(), SIGNAL(changed()), this, SLOT(onSettingsChanged()) ); connect( model::Settings::instance(), SIGNAL(changed()), this, SLOT(onSettingsChanged()) );
onSettingsChanged(); onSettingsChanged();
@@ -590,7 +592,7 @@ namespace glabels
// //
if ( mState == IdleState ) if ( mState == IdleState )
{ {
emit contextMenuActivate(); emit contextMenuActivate( model::Point( xWorld, yWorld ) );
} }
} }
} }
@@ -621,7 +623,7 @@ namespace glabels
/* /*
* Emit signal regardless of mode * Emit signal regardless of mode
*/ */
emit pointerMoved( xWorld, yWorld ); emit pointerMoved( model::Point( xWorld, yWorld ) );
/* /*
@@ -1027,6 +1029,85 @@ namespace glabels
} }
//
// Handle drag enter event
//
void LabelEditor::dragEnterEvent( QDragEnterEvent *event )
{
if ( event->mimeData()->hasUrls() ||
event->mimeData()->hasImage() ||
event->mimeData()->hasText() )
{
event->acceptProposedAction();
}
else
{
event->ignore();
}
}
//
// Handle drag move event
//
void LabelEditor::dragMoveEvent( QDragMoveEvent *event )
{
if ( event->mimeData()->hasUrls() ||
event->mimeData()->hasImage() ||
event->mimeData()->hasText() )
{
event->acceptProposedAction();
}
else
{
event->ignore();
}
}
//
// Handle drop event
//
void LabelEditor::dropEvent( QDropEvent *event )
{
/*
* Transform to label coordinates
*/
QTransform transform;
transform.scale( mScale, mScale );
transform.translate( mX0.pt(), mY0.pt() );
QPointF pWorld = transform.inverted().map( event->position() );
auto xWorld = model::Distance::pt( pWorld.x() );
auto yWorld = model::Distance::pt( pWorld.y() );
auto p = model::Point( xWorld, yWorld );
if ( event->mimeData()->hasUrls() )
{
mUndoRedoModel->checkpoint( tr("Drop") );
mModel->pasteAsUrls( event->mimeData(), p );
event->acceptProposedAction();
}
else if ( event->mimeData()->hasImage() )
{
mUndoRedoModel->checkpoint( tr("Drop") );
mModel->pasteAsImage( event->mimeData(), p );
event->acceptProposedAction();
}
else if ( event->mimeData()->hasText() )
{
mUndoRedoModel->checkpoint( tr("Drop") );
mModel->pasteAsText( event->mimeData(), p );
event->acceptProposedAction();
}
else
{
event->ignore();
}
}
/// ///
/// Draw Background Layer /// Draw Background Layer
/// ///
@@ -1283,4 +1364,5 @@ namespace glabels
emit zoomChanged(); emit zoomChanged();
} }
} // namespace glabels } // namespace glabels
+5 -2
View File
@@ -57,9 +57,9 @@ namespace glabels
// Signals // Signals
///////////////////////////////////// /////////////////////////////////////
signals: signals:
void contextMenuActivate(); void contextMenuActivate( model::Point p );
void zoomChanged(); void zoomChanged();
void pointerMoved( const model::Distance& x, const model::Distance& y ); void pointerMoved( model::Point p );
void pointerExited(); void pointerExited();
void modeChanged(); void modeChanged();
@@ -126,6 +126,9 @@ namespace glabels
void leaveEvent( QEvent* event ) override; void leaveEvent( QEvent* event ) override;
void keyPressEvent( QKeyEvent* event ) override; void keyPressEvent( QKeyEvent* event ) override;
void paintEvent( QPaintEvent* event ) override; void paintEvent( QPaintEvent* event ) override;
void dragEnterEvent( QDragEnterEvent *event ) override;
void dragMoveEvent( QDragMoveEvent *event ) override;
void dropEvent( QDropEvent *event ) override;
///////////////////////////////////// /////////////////////////////////////
+30 -9
View File
@@ -196,8 +196,8 @@ namespace glabels
connect( model::Settings::instance(), SIGNAL(changed()), this, SLOT(onSettingsChanged()) ); connect( model::Settings::instance(), SIGNAL(changed()), this, SLOT(onSettingsChanged()) );
connect( QApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(clipboardChanged()) ); connect( QApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(clipboardChanged()) );
#if 0 #if 0
connect( mLabelEditor, SIGNAL(pointerMoved(double, double)), connect( mLabelEditor, SIGNAL(pointerMoved(model::Point)),
this, SLOT(onPointerMoved(double, double)) ); this, SLOT(onPointerMoved(modelPoint)) );
connect( mLabelEditor, SIGNAL(pointerExited()), this, SLOT(onPointerExit()) ); connect( mLabelEditor, SIGNAL(pointerExited()), this, SLOT(onPointerExit()) );
#endif #endif
@@ -253,7 +253,7 @@ namespace glabels
manageActions(); manageActions();
setTitle(); setTitle();
connect( mLabelEditor, SIGNAL(contextMenuActivate()), this, SLOT(onContextMenuActivate()) ); connect( mLabelEditor, SIGNAL(contextMenuActivate(model::Point)), this, SLOT(onContextMenuActivate(model::Point)) );
connect( mModel, SIGNAL(nameChanged()), this, SLOT(onNameChanged()) ); connect( mModel, SIGNAL(nameChanged()), this, SLOT(onNameChanged()) );
connect( mModel, SIGNAL(modifiedChanged()), this, SLOT(onModifiedChanged()) ); connect( mModel, SIGNAL(modifiedChanged()), this, SLOT(onModifiedChanged()) );
connect( mModel, SIGNAL(selectionChanged()), this, SLOT(onSelectionChanged()) ); connect( mModel, SIGNAL(selectionChanged()), this, SLOT(onSelectionChanged()) );
@@ -611,7 +611,7 @@ namespace glabels
contextPasteAction = new QAction( tr("&Paste"), this ); contextPasteAction = new QAction( tr("&Paste"), this );
contextPasteAction->setIcon( Icons::EditPaste() ); contextPasteAction->setIcon( Icons::EditPaste() );
contextPasteAction->setStatusTip( tr("Paste the clipboard") ); contextPasteAction->setStatusTip( tr("Paste the clipboard") );
connect( contextPasteAction, SIGNAL(triggered()), this, SLOT(editPaste()) ); connect( contextPasteAction, SIGNAL(triggered()), this, SLOT(editContextPaste()) );
contextDeleteAction = new QAction( tr("&Delete"), this ); contextDeleteAction = new QAction( tr("&Delete"), this );
contextDeleteAction->setIcon( QIcon::fromTheme( "edit-delete" ) ); contextDeleteAction->setIcon( QIcon::fromTheme( "edit-delete" ) );
@@ -1345,7 +1345,21 @@ namespace glabels
void MainWindow::editPaste() void MainWindow::editPaste()
{ {
mUndoRedoModel->checkpoint( tr("Paste") ); mUndoRedoModel->checkpoint( tr("Paste") );
mModel->paste(); mModel->paste( model::Point() );
}
///
/// Edit->Paste Action (from context menu)
///
void MainWindow::editContextPaste()
{
// Extract original context menu click location
auto *action = qobject_cast<QAction *>(sender());
auto p = action->data().value<model::Point>();
mUndoRedoModel->checkpoint( tr("Paste") );
mModel->paste( p );
} }
@@ -1708,8 +1722,13 @@ namespace glabels
/// ///
/// Context Menu Activation /// Context Menu Activation
/// ///
void MainWindow::onContextMenuActivate() void MainWindow::onContextMenuActivate( model::Point p )
{ {
// Save click location for potential paste action
QVariant variant;
variant.setValue( p );
contextPasteAction->setData( variant );
if ( mModel->isSelectionEmpty() ) if ( mModel->isSelectionEmpty() )
{ {
noSelectionContextMenu->popup( QCursor::pos() ); noSelectionContextMenu->popup( QCursor::pos() );
@@ -1736,10 +1755,12 @@ namespace glabels
/// ///
/// Pointer moved: update Cursor Information in Status Bar /// Pointer moved: update Cursor Information in Status Bar
/// ///
void MainWindow::onPointerMoved( double x, double y ) void MainWindow::onPointerMoved( model::Point p )
{ {
/* TODO: convert x,y to locale units and set precision accordingly. */ /* TODO: set precision accordingly. */
cursorInfoLabel->setText( QString( "%1, %2" ).arg(x).arg(y) ); auto units = model::Settings::units();
cursorInfoLabel->setText( QString( "%1, %2" ).arg( p.x().toString(units) )
.arg( p.y().toString(units) ) );
} }
+3 -2
View File
@@ -109,6 +109,7 @@ namespace glabels
void editCut(); void editCut();
void editCopy(); void editCopy();
void editPaste(); void editPaste();
void editContextPaste();
void editDelete(); void editDelete();
void editSelectAll(); void editSelectAll();
void editUnSelectAll(); void editUnSelectAll();
@@ -150,10 +151,10 @@ namespace glabels
void helpReportBug(); void helpReportBug();
void helpAbout(); void helpAbout();
void onContextMenuActivate(); void onContextMenuActivate( model::Point );
void onZoomChanged(); void onZoomChanged();
void onPointerMoved( double, double ); void onPointerMoved( model::Point );
void onPointerExit(); void onPointerExit();
void onNameChanged(); void onNameChanged();
+101 -39
View File
@@ -33,7 +33,6 @@
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QFileInfo> #include <QFileInfo>
#include <QMimeData>
#include <QtDebug> #include <QtDebug>
@@ -1480,68 +1479,131 @@ namespace glabels
const QClipboard *clipboard = QApplication::clipboard(); const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData(); const QMimeData *mimeData = clipboard->mimeData();
if ( mimeData->hasFormat( MIME_TYPE ) ) return mimeData->hasFormat( MIME_TYPE ) ||
{ mimeData->hasUrls() ||
return true; mimeData->hasImage() ||
} mimeData->hasText();
else if ( mimeData->hasImage() )
{
return true;
}
else if ( mimeData->hasText() )
{
return true;
}
return false;
} }
/// ///
/// Paste from clipboard /// Paste from clipboard
/// ///
void Model::paste() void Model::paste( Point p )
{ {
const QClipboard *clipboard = QApplication::clipboard(); const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData(); const QMimeData *mimeData = clipboard->mimeData();
if ( mimeData->hasFormat( MIME_TYPE ) ) if ( mimeData->hasFormat( MIME_TYPE ) )
{ {
// Native objects pasteAsNativeObjects( mimeData, p );
QByteArray buffer = mimeData->data( MIME_TYPE ); }
QList <ModelObject*> objects = XmlLabelParser::deserializeObjects( buffer, this ); else if ( mimeData->hasUrls() )
{
unselectAll(); pasteAsUrls( mimeData, p );
foreach ( ModelObject* object, objects )
{
addObject( object );
selectObject( object );
}
} }
else if ( mimeData->hasImage() ) else if ( mimeData->hasImage() )
{ {
// Create object from clipboard image pasteAsImage( mimeData, p );
auto* object = new ModelImageObject();
object->setImage( qvariant_cast<QImage>(mimeData->imageData()) );
object->setSize( object->naturalSize() );
object->setPosition( (w()-object->w())/2.0, (h()-object->h())/2.0 );
addObject( object );
unselectAll();
selectObject( object );
} }
else if ( mimeData->hasText() ) else if ( mimeData->hasText() )
{ {
// Create object from clipboard text pasteAsText( mimeData, p );
auto* object = new ModelTextObject(); }
object->setText( mimeData->text() ); }
object->setSize( object->naturalSize() );
object->setPosition( (w()-object->w())/2.0, (h()-object->h())/2.0 );
///
/// Paste as native objects
///
void Model::pasteAsNativeObjects( const QMimeData* mimeData, Point p )
{
QByteArray buffer = mimeData->data( MIME_TYPE );
QList <ModelObject*> objects = XmlLabelParser::deserializeObjects( buffer, this );
unselectAll();
foreach ( ModelObject* object, objects )
{
object->setPositionRelative( p.x(), p.y() );
addObject( object ); addObject( object );
unselectAll();
selectObject( object ); selectObject( object );
} }
} }
///
/// Paste as URLs ( currently only supports local image files )
///
void Model::pasteAsUrls( const QMimeData* mimeData, Point p )
{
auto x = p.x();
auto y = p.y();
auto xOffset = Distance::pt( 10 );
auto yOffset = Distance::pt( 10 );
unselectAll();
for ( auto url : mimeData->urls() )
{
if ( url.isLocalFile() )
{
auto name = url.toLocalFile();
QImage image( name );
if ( !image.isNull() )
{
auto* object = new ModelImageObject();
object->setImage( name, image );
object->setSize( object->naturalSize() );
object->setPosition( x, y );
addObject( object );
selectObject( object );
x = fmod( x + xOffset, w() );
y = fmod( y + yOffset, h() );
}
else
{
qWarning() << "Cannot paste" << name
<< ": does not exist or currently unsupported file type.";
}
}
else
{
qWarning() << "Cannot paste" << url.toString()
<< ": currently unsupported file location.";
}
}
}
///
/// Paste as image
///
void Model::pasteAsImage( const QMimeData* mimeData, Point p )
{
auto* object = new ModelImageObject();
object->setImage( qvariant_cast<QImage>(mimeData->imageData()) );
object->setSize( object->naturalSize() );
object->setPosition( p.x(), p.y() );
addObject( object );
unselectAll();
selectObject( object );
}
///
/// Paste as text
void Model::pasteAsText( const QMimeData* mimeData, Point p )
{
auto* object = new ModelTextObject();
object->setText( mimeData->text() );
object->setSize( object->naturalSize() );
object->setPosition( p.x(), p.y() );
addObject( object );
unselectAll();
selectObject( object );
}
/// ///
/// Draw label objects /// Draw label objects
/// ///
+6 -1
View File
@@ -31,6 +31,7 @@
#include <QDir> #include <QDir>
#include <QList> #include <QList>
#include <QMimeData>
#include <QObject> #include <QObject>
#include <QPainter> #include <QPainter>
@@ -207,7 +208,11 @@ namespace glabels
void copySelection(); void copySelection();
void cutSelection(); void cutSelection();
bool canPaste(); bool canPaste();
void paste(); void paste( Point p );
void pasteAsNativeObjects( const QMimeData* mimeData, Point p );
void pasteAsUrls( const QMimeData* mimeData, Point p );
void pasteAsImage( const QMimeData* mimeData, Point p );
void pasteAsText( const QMimeData* mimeData, Point p );
///////////////////////////////// /////////////////////////////////
// Drawing operations // Drawing operations
+2
View File
@@ -73,6 +73,8 @@ namespace glabels
{ {
smDefaultImage = new QImage( ":images/checkerboard.png" ); smDefaultImage = new QImage( ":images/checkerboard.png" );
} }
mLockAspectRatio = true;
} }
+5
View File
@@ -24,6 +24,8 @@
#include "Distance.h" #include "Distance.h"
#include <QMetaType>
namespace glabels namespace glabels
{ {
@@ -52,4 +54,7 @@ namespace glabels
} }
Q_DECLARE_METATYPE( glabels::model::Point )
#endif // model_Point_h #endif // model_Point_h
+4
View File
@@ -1328,6 +1328,10 @@
<source>Resize</source> <source>Resize</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Drop</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>glabels::MainWindow</name> <name>glabels::MainWindow</name>