View Issue Details

IDProjectCategoryView StatusLast Update
0017966ScribusUsabilitypublic2026-09-05 12:16
Reporterbuttar Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
PlatformLinuxOSFedora WorkstationOS Version44
Product Version1.7.4.svn 
Summary0017966: [patch] A Docakable Images Palette
DescriptionI don't know if this is the right way to propose a new feature but this is the only one I know. I hope it adds value to Scribus.

A new core, dockable Images palette that consolidates image-management workflows into one modeless interface.

If mature enough, it could eventually replace or absorb much of:

* Manage Images — document image listing, status, relinking and related actions.
* Picture Browser — filesystem browsing, placement and collections, planned for later stages.

Existing image properties, effects and external-editor workflows are reused rather than duplicated. The current patches do not remove either legacy interface.

## IMG1 — `img1-images-palette-foundation-v1.02.patch`

* Adds the core Images dock palette.
* Introduces focused `ImagePalette` and `ImagePaletteDocumentModel` classes.
* Adds Document and Current Page scopes.
* Shows thumbnails, filename/path, page/frame location and basic metadata.
* Follows document lifecycle, page changes and canvas selection.
* Includes applied-master-page images where appropriate.
* Uses guarded pointers, lazy hidden-palette updates and coalesced refresh.
* Establishes the model/view foundation for later image-management scopes.
* Leaves Manage Images and Picture Browser unchanged.

## IMG2 — `img2-images-document-management-v1.30.patch`

* Applies after IMG1 v1.02 and adds document-image management functionality.
* Adds Missing (n) scope and missing-image tracking.
* Adds multi-placement navigation with page/frame identification.
* Adds Show and Export/Print controls.
* Adds actions:
-- Select
-- Replace…
-- Find…
-- Location
-- Edit…
-- Effects…
-- Properties
* Reuses existing Scribus relinking, missing-image search, editor, effects and properties workflows.
* Adds metadata:
-- Format
-- Color Space
-- Pixel Size
-- Native DPI
-- Effective DPI
-- Scale
-- Frame Size
-- Scaled Image Size
* Keeps canvas → palette synchronization automatic; palette → canvas selection remains explicit.
* Preserves palette selection across normal document refreshes.
* Uses a shared inspector label/value rail and natural Qt layout spacing.
* Elides long text safely during resizing.
* Enforces a structural minimum width so the inspector cannot collapse into overlap.
* Moves toward Manage Images parity while leaving both legacy interfaces available for now.

## IMG3 — Images Folder Browsing

* Depends on / applies after: IMG1 `img1-images-palette-foundation-v1.03.patch` and IMG2 `img2-images-document-management-v1.33.patch`.
* Adds a new Browse mode beside the existing Document mode.
* Browses supported image/graphic files directly from a folder, including preview-capable formats such as PDF and SVG.
* Loads folder contents and thumbnails asynchronously via `ImagePaletteFileWorker`.
* Uses generation-based cancellation so stale results are ignored during rapid folder changes.
* Shows file metadata including format, color space, pixel size, native DPI, name, and path.
* Remembers the last browsed folder and falls back to the document/Documents/home directory as needed.
* Keeps Document-mode refresh logic inactive while Browse mode is selected.
* Adds the required Browse/model/worker sources to the build.
* v1.02 adds the direct `cmsettings.h` include required by the thumbnail worker.

### IMG4 — Browse Drag & Drop

* Depends on / applies after: IMG1 `img1-images-palette-foundation-v1.03.patch`, IMG2 `img2-images-document-management-v1.33.patch`, and IMG3 `img3-images-folder-browsing-v1.04.patch`.
* Adds multi-selection to Browse mode with `ExtendedSelection`.
* Enables dragging one or more selected files out as standard `text/uri-list` data using `CopyAction`.
* Reuses Scribus’s existing canvas/file-drop handling; no new placement engine or canvas drop logic is added.
* Adds a multi-selection inspector state:
-- `N files selected`
-- shared folder path
-- per-file metadata cleared to `—`
* Keeps inspector geometry stable when switching between single and multiple selection.
* Adds a stable minimum width for the Browse inspector so the inner splitter cannot be dragged far enough to clip metadata.
* Single PDF/SVG drops continue through Scribus’s existing single-file import path; existing multi-vector behavior is unchanged.
Additional InformationA series of 4 patches, and some screenshots are attached.
Tags#please_test
Attached Files
img1-images-palette-foundation-v1.03.patch (28,018 bytes)   
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt	(revision 27814)
+++ scribus/CMakeLists_Sources.txt	(working copy)
@@ -366,6 +366,8 @@
 	ui/imageclippingpaths.cpp
 	ui/imagelayers.cpp
 	ui/imageinfodialog.cpp
+	ui/imagepalette.cpp
+	ui/imagepalette_documentmodel.cpp
 	ui/inlinepalette.cpp
 	ui/insertTable.cpp
 	ui/insertaframe.cpp
Index: scribus/actionmanager.cpp
===================================================================
--- scribus/actionmanager.cpp	(revision 27814)
+++ scribus/actionmanager.cpp	(working copy)
@@ -848,6 +848,8 @@
 	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
 	name="toolsContent";
 	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
+	name = "toolsImages";
+	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
 	name = "toolsOutline";
 	scrActions->insert(name, new ScrAction("", defaultKey(name), mainWindow));
 	name = "toolsScrapbook";
@@ -1782,6 +1784,7 @@
 	//Tool menu
 	(*scrActions)["toolsProperties"]->setTexts( tr("&Properties"));
 	(*scrActions)["toolsContent"]->setTexts( tr("Content Properties"));
+	(*scrActions)["toolsImages"]->setTexts( tr("&Images"));
 	(*scrActions)["toolsOutline"]->setTexts( tr("&Outline", "Document Outline Palette"));
 	(*scrActions)["toolsScrapbook"]->setTexts( tr("&Scrapbook"));
 	(*scrActions)["toolsLayers"]->setTexts( tr("&Layers"));
@@ -2488,6 +2491,7 @@
 		<< "specialToggleAllPalettes"
 		<< "toolsProperties"
 		<< "toolsContent"
+		<< "toolsImages"
 		<< "toolsOutline"
 		<< "toolsScrapbook"
 		<< "toolsLayers"
Index: scribus/manager/dock_manager.cpp
===================================================================
--- scribus/manager/dock_manager.cpp	(revision 27814)
+++ scribus/manager/dock_manager.cpp	(working copy)
@@ -35,6 +35,7 @@
 #include "ui/aligndistribute.h"
 #include "ui/bookmarkpalette.h"
 #include "ui/contentpalette.h"
+#include "ui/imagepalette.h"
 #include "ui/inlinepalette.h"
 #include "ui/layers.h"
 #include "ui/outlinepalette.h"
@@ -68,6 +69,7 @@
 	alignDistributePalette = new AlignDistributePalette();
 	bookPalette = new BookPalette(this);
 	contentPalette = new ContentPalette(this);
+	imagePalette = new ImagePalette(this);
 	inlinePalette = new InlinePalette(this);
 	layerPalette = new LayerPalette(this);
 	outlinePalette = new OutlinePalette(this);
@@ -81,6 +83,7 @@
 	configureDock(alignDistributePalette);
 	configureDock(bookPalette);
 	configureDock(contentPalette);
+	configureDock(imagePalette);
 	configureDock(inlinePalette);
 	configureDock(layerPalette);
 	configureDock(outlinePalette);
@@ -268,6 +271,7 @@
 	addDockWidgetTabToArea(scrapbookPalette, areaCenterLeft);
 	addDockWidgetTabToArea(bookPalette, areaCenterLeft);
 	addDockWidgetTabToArea(symbolPalette, areaCenterLeft);
+	addDockWidgetTabToArea(imagePalette, areaCenterLeft);
 
 	// Left Bottom
 	auto *areaLeftBottom = addDockWidget(BottomDockWidgetArea, layerPalette, areaLeft);
@@ -295,6 +299,7 @@
 
 	// hide panels that are not visible in default workspace
 	inlinePalette->closeDockWidget();
+	imagePalette->closeDockWidget();
 	scrapbookPalette->closeDockWidget();
 	bookPalette->closeDockWidget();
 	symbolPalette->closeDockWidget();
Index: scribus/manager/dock_manager.h
===================================================================
--- scribus/manager/dock_manager.h	(revision 27814)
+++ scribus/manager/dock_manager.h	(working copy)
@@ -37,6 +37,7 @@
 class OutlinePalette;
 class PropertiesPalette;
 class ContentPalette;
+class ImagePalette;
 class LayerPalette;
 class AlignDistributePalette;
 class InlinePalette;
@@ -88,6 +89,7 @@
 	OutlinePalette *outlinePalette {nullptr};
 	PropertiesPalette *propertiesPalette {nullptr};
 	ContentPalette *contentPalette {nullptr};
+	ImagePalette *imagePalette {nullptr};
 	LayerPalette *layerPalette {nullptr};
 	AlignDistributePalette *alignDistributePalette {nullptr};
 	InlinePalette *inlinePalette {nullptr};
Index: scribus/scribus.cpp
===================================================================
--- scribus/scribus.cpp	(revision 27814)
+++ scribus/scribus.cpp	(working copy)
@@ -167,6 +167,7 @@
 #include "ui/collectforoutput_ui.h"
 #include "ui/colorpicker/colorpicker.h"
 #include "ui/contentpalette.h"
+#include "ui/imagepalette.h"
 #include "ui/contextmenu.h"
 #include "ui/copypagetomasterpagedialog.h"
 #include "ui/customfdialog.h"
@@ -730,6 +731,12 @@
 	contentPalette->setToggleViewAction(scrActions["toolsContent"]);
 	contentPalette->installEventFilter(this);
 
+	// Images
+	imagePalette = dockManager->imagePalette;
+	imagePalette->setMainWindow(this);
+	imagePalette->setToggleViewAction(scrActions["toolsImages"]);
+	imagePalette->installEventFilter(this);
+
 	// Nodes
 	nodePalette = new NodePalette(this);
 	nodePalette->installEventFilter(this);
@@ -1405,6 +1412,7 @@
 	scrMenuMgr->addMenuItemString("SEPARATOR", "Windows");
 	scrMenuMgr->addMenuItemString("toolsProperties", "Windows");
 	scrMenuMgr->addMenuItemString("toolsContent", "Windows");
+	scrMenuMgr->addMenuItemString("toolsImages", "Windows");
 	scrMenuMgr->addMenuItemString("toolsActionHistory", "Windows");
 	scrMenuMgr->addMenuItemString("toolsAlignDistribute", "Windows");
 	scrMenuMgr->addMenuItemString("SEPARATOR", "Windows");
@@ -2609,6 +2617,7 @@
 	updateActiveWindowCaption(doc->documentFileName());
 	propertiesPalette->setDoc(doc);
 	contentPalette->setDoc(doc);
+	imagePalette->setDoc(doc);
 	marksManager->setDoc(doc);
 	nsEditor->setDoc(doc);
 	pagePalette->setView(view);
@@ -2658,6 +2667,7 @@
 	updateActiveWindowCaption(doc->documentFileName());
 	propertiesPalette->setDoc(doc);
 	contentPalette->setDoc(doc);
+	imagePalette->setDoc(doc);
 	nsEditor->setDoc(doc);
 
 	marksManager->setDoc(doc);
@@ -4225,6 +4235,7 @@
 	//<<Palettes
 	propertiesPalette->unsetDoc();
 	contentPalette->unsetDoc();
+	imagePalette->unsetDoc();
 	inlinePalette->unsetDoc();
 	symbolPalette->unsetDoc();
 	pagePalette->setView(nullptr);
@@ -4919,6 +4930,8 @@
 	propertiesPalette->setDoc(doc);
 	contentPalette->unsetDoc();
 	contentPalette->setDoc(doc);
+	imagePalette->unsetDoc();
+	imagePalette->setDoc(doc);
 	marksManager->setDoc(doc);
 	nsEditor->setDoc(doc);
 	symbolPalette->unsetDoc();
@@ -6780,6 +6793,7 @@
 {
 	propertiesPalette->startup();
 	contentPalette->startup();
+	imagePalette->startup();
 	outlinePalette->startup();
 	scrapbookPalette->startup();
 	bookmarkPalette->startup();
@@ -8281,6 +8295,7 @@
 	pageSelector->setMaximum(doc->masterPageMode() ? 1 : doc->Pages->count());
 	if ((!doc->isLoading()) && (!doc->masterPageMode()))
 		pageSelector->setGUIForPage(pageIndex);
+	emit currentPageChanged(pageIndex);
 }
 
 void ScribusMainWindow::setCurrentPage(int p)
Index: scribus/scribus.h
===================================================================
--- scribus/scribus.h	(revision 27814)
+++ scribus/scribus.h	(working copy)
@@ -82,6 +82,7 @@
 class FormatsManager;
 class GuideManager;
 class HelpBrowser;
+class ImagePalette;
 class InlinePalette;
 class LayerPalette;
 class MarksManager;
@@ -258,6 +259,7 @@
 	CharSelect *charPalette {nullptr};
 	PropertiesPalette *propertiesPalette {nullptr};
 	ContentPalette *contentPalette {nullptr};
+	ImagePalette *imagePalette {nullptr};
 	MarksManager *marksManager {nullptr};
 	NotesStylesEditor *nsEditor {nullptr};
 	NodePalette *nodePalette {nullptr};
@@ -621,6 +623,7 @@
 	void TextEffects(int);
 	void UpdateRequest(int updateFlags);
 	void changeLayers(int);
+	void currentPageChanged(int pageIndex);
 
 protected:
 	/*!
Index: scribus/ui/imagepalette.cpp
===================================================================
--- scribus/ui/imagepalette.cpp	(nonexistent)
+++ scribus/ui/imagepalette.cpp	(working copy)
@@ -0,0 +1,396 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#include "imagepalette.h"
+
+#include <QAbstractItemView>
+#include <QDir>
+#include <QEvent>
+#include <QFileInfo>
+#include <QFormLayout>
+#include <QFrame>
+#include <QItemSelectionModel>
+#include <QLabel>
+#include <QListView>
+#include <QSignalBlocker>
+#include <QSplitter>
+#include <QTabBar>
+#include <QTimer>
+#include <QVBoxLayout>
+
+#include "imagepalette_documentmodel.h"
+#include "pageitem.h"
+#include "selection.h"
+#include "scribus.h"
+#include "scribusdoc.h"
+
+ImagePalette::ImagePalette(QWidget* parent)
+	: DockPanelBase("Images", "image-collection", parent),
+	  m_model(new ImagePaletteDocumentModel(this)),
+	  m_refreshTimer(new QTimer(this))
+{
+	setObjectName(QStringLiteral("ImagePalette"));
+
+	auto* panel = new QWidget(this);
+	auto* layout = new QVBoxLayout(panel);
+	layout->setContentsMargins(6, 6, 6, 6);
+	layout->setSpacing(6);
+
+	m_scopeTabs = new QTabBar(panel);
+	m_scopeTabs->setDocumentMode(true);
+	m_scopeTabs->setExpanding(false);
+	m_scopeTabs->addTab(QString());
+	m_scopeTabs->addTab(QString());
+	layout->addWidget(m_scopeTabs);
+
+	auto* splitter = new QSplitter(Qt::Horizontal, panel);
+	splitter->setChildrenCollapsible(false);
+	layout->addWidget(splitter, 1);
+
+	m_imageView = new QListView(splitter);
+	m_imageView->setMinimumSize(220, 180);
+	m_imageView->setModel(m_model);
+	m_imageView->setIconSize(QSize(128, 128));
+	m_imageView->setGridSize(QSize(156, 174));
+	m_imageView->setResizeMode(QListView::Adjust);
+	m_imageView->setViewMode(QListView::IconMode);
+	m_imageView->setMovement(QListView::Static);
+	m_imageView->setFlow(QListView::LeftToRight);
+	m_imageView->setWrapping(true);
+	m_imageView->setWordWrap(true);
+	m_imageView->setEditTriggers(QAbstractItemView::NoEditTriggers);
+	m_imageView->setSelectionMode(QAbstractItemView::SingleSelection);
+
+	auto* inspector = new QWidget(splitter);
+	inspector->setMinimumWidth(210);
+	auto* inspectorLayout = new QVBoxLayout(inspector);
+	inspectorLayout->setContentsMargins(8, 0, 0, 0);
+
+	m_nameValue = new QLabel(inspector);
+	QFont nameFont = m_nameValue->font();
+	nameFont.setBold(true);
+	m_nameValue->setFont(nameFont);
+	m_nameValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	inspectorLayout->addWidget(m_nameValue);
+
+	auto* line = new QFrame(inspector);
+	line->setFrameShape(QFrame::HLine);
+	line->setFrameShadow(QFrame::Sunken);
+	inspectorLayout->addWidget(line);
+
+	auto* form = new QFormLayout();
+	form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+	form->setLabelAlignment(Qt::AlignLeft | Qt::AlignTop);
+	inspectorLayout->addLayout(form);
+
+	m_pathLabel = new QLabel(inspector);
+	m_pathValue = new QLabel(inspector);
+	m_pathValue->setWordWrap(true);
+	m_pathValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	form->addRow(m_pathLabel, m_pathValue);
+
+	m_pageLabel = new QLabel(inspector);
+	m_pageValue = new QLabel(inspector);
+	m_pageValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	form->addRow(m_pageLabel, m_pageValue);
+
+	m_frameLabel = new QLabel(inspector);
+	m_frameValue = new QLabel(inspector);
+	m_frameValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	form->addRow(m_frameLabel, m_frameValue);
+
+	m_dimensionsLabel = new QLabel(inspector);
+	m_dimensionsValue = new QLabel(inspector);
+	m_dimensionsValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	form->addRow(m_dimensionsLabel, m_dimensionsValue);
+
+	m_resolutionLabel = new QLabel(inspector);
+	m_resolutionValue = new QLabel(inspector);
+	m_resolutionValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	form->addRow(m_resolutionLabel, m_resolutionValue);
+
+	m_statusLabel = new QLabel(inspector);
+	m_statusValue = new QLabel(inspector);
+	m_statusValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	form->addRow(m_statusLabel, m_statusValue);
+
+	inspectorLayout->addStretch(1);
+	splitter->setStretchFactor(0, 3);
+	splitter->setStretchFactor(1, 2);
+
+	m_summaryLabel = new QLabel(panel);
+	layout->addWidget(m_summaryLabel);
+
+	setWidget(panel);
+
+	m_refreshTimer->setSingleShot(true);
+	m_refreshTimer->setInterval(75);
+
+	connect(m_scopeTabs, &QTabBar::currentChanged, this, &ImagePalette::scopeChanged);
+	connect(m_imageView->selectionModel(), &QItemSelectionModel::selectionChanged,
+		this, &ImagePalette::handleViewSelectionChanged);
+	connect(m_refreshTimer, &QTimer::timeout, this, &ImagePalette::rebuild);
+	connect(this, &DockPanelBase::visibilityChanged, this, &ImagePalette::handleVisibilityChanged);
+
+	languageChange();
+	clearInspector();
+	updateSummary();
+}
+
+void ImagePalette::setMainWindow(ScribusMainWindow* mw)
+{
+	if (m_ScMW == mw)
+		return;
+
+	if (m_ScMW)
+		disconnect(m_ScMW, &ScribusMainWindow::currentPageChanged, this, &ImagePalette::handleCurrentPageChanged);
+
+	m_ScMW = mw;
+	if (m_ScMW)
+		connect(m_ScMW, &ScribusMainWindow::currentPageChanged, this, &ImagePalette::handleCurrentPageChanged);
+}
+
+void ImagePalette::setDoc(ScribusDoc* doc)
+{
+	if (doc == static_cast<ScribusDoc*>(m_doc))
+	{
+		m_dirty = true;
+		if (isVisible())
+			rebuild();
+		return;
+	}
+
+	if (m_doc)
+	{
+		disconnect(m_doc, &ScribusDoc::docChanged, this, &ImagePalette::handleDocumentChanged);
+		disconnect(m_doc->m_Selection, &Selection::selectionChanged, this, &ImagePalette::handleDocumentSelectionChanged);
+	}
+
+	m_doc = doc;
+	m_model->setDocument(m_doc);
+
+	if (m_doc)
+	{
+		connect(m_doc, &ScribusDoc::docChanged, this, &ImagePalette::handleDocumentChanged);
+		connect(m_doc->m_Selection, &Selection::selectionChanged, this, &ImagePalette::handleDocumentSelectionChanged);
+	}
+
+	m_dirty = true;
+	if (isVisible())
+		rebuild();
+	else
+		updateSummary();
+}
+
+void ImagePalette::unsetDoc()
+{
+	if (m_doc)
+	{
+		disconnect(m_doc, &ScribusDoc::docChanged, this, &ImagePalette::handleDocumentChanged);
+		disconnect(m_doc->m_Selection, &Selection::selectionChanged, this, &ImagePalette::handleDocumentSelectionChanged);
+	}
+
+	m_refreshTimer->stop();
+	m_doc = nullptr;
+	m_model->setDocument(nullptr);
+	m_dirty = false;
+	clearInspector();
+	updateSummary();
+}
+
+void ImagePalette::changeEvent(QEvent* event)
+{
+	DockPanelBase::changeEvent(event);
+	if (event->type() == QEvent::LanguageChange)
+		languageChange();
+}
+
+void ImagePalette::scopeChanged(int index)
+{
+	m_model->setCurrentPageOnly(index == 1);
+	rebuild();
+}
+
+void ImagePalette::handleDocumentChanged()
+{
+	m_dirty = true;
+	scheduleRebuild();
+}
+
+void ImagePalette::handleDocumentSelectionChanged()
+{
+	if (m_scopeTabs->currentIndex() == 1)
+	{
+		m_dirty = true;
+		scheduleRebuild();
+		return;
+	}
+
+	syncSelectionFromDocument();
+	updateSummary();
+}
+
+void ImagePalette::handleCurrentPageChanged()
+{
+	if (m_scopeTabs->currentIndex() != 1)
+		return;
+
+	m_dirty = true;
+	scheduleRebuild();
+}
+
+void ImagePalette::handleVisibilityChanged(bool visible)
+{
+	if (!visible || !m_dirty)
+		return;
+
+	m_refreshTimer->stop();
+	rebuild();
+}
+
+void ImagePalette::handleViewSelectionChanged(const QItemSelection&, const QItemSelection&)
+{
+	showItem(m_model->itemAt(m_imageView->currentIndex()));
+}
+
+void ImagePalette::clearInspector()
+{
+	const QString empty = QString::fromUtf8("—");
+	m_nameValue->setText(empty);
+	m_pathValue->setText(empty);
+	m_pageValue->setText(empty);
+	m_frameValue->setText(empty);
+	m_dimensionsValue->setText(empty);
+	m_resolutionValue->setText(empty);
+	m_statusValue->setText(empty);
+}
+
+void ImagePalette::languageChange()
+{
+	setWindowTitle(tr("Images"));
+	m_scopeTabs->setTabText(0, tr("Document"));
+	m_scopeTabs->setTabText(1, tr("Current Page"));
+	m_pathLabel->setText(tr("Path:"));
+	m_pageLabel->setText(tr("Page:"));
+	m_frameLabel->setText(tr("Frame:"));
+	m_dimensionsLabel->setText(tr("Dimensions:"));
+	m_resolutionLabel->setText(tr("Effective:"));
+	m_statusLabel->setText(tr("Status:"));
+	m_dirty = true;
+	if (isVisible())
+		rebuild();
+	else
+		updateSummary();
+}
+
+void ImagePalette::rebuild()
+{
+	m_refreshTimer->stop();
+	m_model->rebuild();
+	m_dirty = false;
+	syncSelectionFromDocument();
+	updateSummary();
+
+	if (!m_imageView->currentIndex().isValid())
+		clearInspector();
+}
+
+void ImagePalette::scheduleRebuild()
+{
+	if (!isVisible())
+		return;
+	m_refreshTimer->start();
+}
+
+void ImagePalette::showItem(PageItem* item)
+{
+	if (!item)
+	{
+		clearInspector();
+		return;
+	}
+
+	QFileInfo fileInfo(item->Pfile);
+	if (item->isInlineImage)
+	{
+		m_nameValue->setText(tr("Embedded Image"));
+		m_pathValue->setText(tr("Embedded in document"));
+	}
+	else if (item->Pfile.isEmpty())
+	{
+		m_nameValue->setText(tr("Empty Image Frame"));
+		m_pathValue->setText(QString::fromUtf8("—"));
+	}
+	else
+	{
+		m_nameValue->setText(fileInfo.fileName());
+		m_pathValue->setText(QDir::toNativeSeparators(item->Pfile));
+	}
+
+	if (!item->OnMasterPage.isEmpty())
+		m_pageValue->setText(tr("Master: %1").arg(item->OnMasterPage));
+	else if (item->OwnPage < 0)
+		m_pageValue->setText(tr("Not on a Page"));
+	else
+		m_pageValue->setText(QString::number(item->OwnPage + 1));
+
+	m_frameValue->setText(item->itemName());
+
+	if (item->imageIsAvailable)
+	{
+		m_dimensionsValue->setText(tr("%1 × %2 px").arg(item->OrigW).arg(item->OrigH));
+		if (item->imageXScale() != 0.0 && item->imageYScale() != 0.0)
+		{
+			m_resolutionValue->setText(tr("%1 × %2 dpi")
+				.arg(qRound(72.0 / item->imageXScale()))
+				.arg(qRound(72.0 / item->imageYScale())));
+		}
+		else
+			m_resolutionValue->setText(QString::fromUtf8("—"));
+	}
+	else
+	{
+		m_dimensionsValue->setText(QString::fromUtf8("—"));
+		m_resolutionValue->setText(QString::fromUtf8("—"));
+	}
+
+	if (item->isInlineImage)
+		m_statusValue->setText(tr("Embedded"));
+	else if (item->Pfile.isEmpty())
+		m_statusValue->setText(tr("Empty"));
+	else if (item->imageIsAvailable)
+		m_statusValue->setText(tr("Linked"));
+	else
+		m_statusValue->setText(tr("Missing"));
+}
+
+void ImagePalette::syncSelectionFromDocument()
+{
+	if (!m_doc || !m_doc->m_Selection || m_doc->m_Selection->count() != 1)
+		return;
+
+	PageItem* item = m_doc->m_Selection->itemAt(0);
+	QModelIndex index = m_model->indexForItem(item);
+	if (!index.isValid())
+		return;
+
+	const QSignalBlocker blocker(m_imageView->selectionModel());
+	m_imageView->setCurrentIndex(index);
+	showItem(item);
+}
+
+void ImagePalette::updateSummary()
+{
+	if (!m_doc)
+	{
+		m_summaryLabel->setText(tr("No document"));
+		return;
+	}
+
+	const int count = m_model->rowCount();
+	m_summaryLabel->setText(count == 1 ? tr("1 image frame") : tr("%1 image frames").arg(count));
+}
Index: scribus/ui/imagepalette.h
===================================================================
--- scribus/ui/imagepalette.h	(nonexistent)
+++ scribus/ui/imagepalette.h	(working copy)
@@ -0,0 +1,84 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#ifndef IMAGEPALETTE_H
+#define IMAGEPALETTE_H
+
+#include "docks/dock_panelbase.h"
+#include "scguardedptr.h"
+#include "scribusapi.h"
+
+class QLabel;
+class QListView;
+class QTabBar;
+class QTimer;
+class QItemSelection;
+
+class ImagePaletteDocumentModel;
+class PageItem;
+class ScribusDoc;
+class ScribusMainWindow;
+
+class SCRIBUS_API ImagePalette : public DockPanelBase
+{
+	Q_OBJECT
+
+public:
+	explicit ImagePalette(QWidget* parent = nullptr);
+	~ImagePalette() override = default;
+
+	void setMainWindow(ScribusMainWindow* mw);
+
+public slots:
+	void setDoc(ScribusDoc* doc);
+	void unsetDoc();
+
+protected:
+	void changeEvent(QEvent* event) override;
+
+private slots:
+	void scopeChanged(int index);
+	void handleDocumentChanged();
+	void handleDocumentSelectionChanged();
+	void handleCurrentPageChanged();
+	void handleVisibilityChanged(bool visible);
+	void handleViewSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);
+
+private:
+	void clearInspector();
+	void languageChange();
+	void rebuild();
+	void scheduleRebuild();
+	void showItem(PageItem* item);
+	void syncSelectionFromDocument();
+	void updateSummary();
+
+	ImagePaletteDocumentModel* m_model {nullptr};
+	ScribusMainWindow* m_ScMW {nullptr};
+	ScGuardedPtr<ScribusDoc> m_doc {nullptr};
+	QTimer* m_refreshTimer {nullptr};
+	bool m_dirty {false};
+
+	QTabBar* m_scopeTabs {nullptr};
+	QListView* m_imageView {nullptr};
+	QLabel* m_nameValue {nullptr};
+	QLabel* m_pathLabel {nullptr};
+	QLabel* m_pathValue {nullptr};
+	QLabel* m_pageLabel {nullptr};
+	QLabel* m_pageValue {nullptr};
+	QLabel* m_frameLabel {nullptr};
+	QLabel* m_frameValue {nullptr};
+	QLabel* m_dimensionsLabel {nullptr};
+	QLabel* m_dimensionsValue {nullptr};
+	QLabel* m_resolutionLabel {nullptr};
+	QLabel* m_resolutionValue {nullptr};
+	QLabel* m_statusLabel {nullptr};
+	QLabel* m_statusValue {nullptr};
+	QLabel* m_summaryLabel {nullptr};
+};
+
+#endif // IMAGEPALETTE_H
Index: scribus/ui/imagepalette_documentmodel.cpp
===================================================================
--- scribus/ui/imagepalette_documentmodel.cpp	(nonexistent)
+++ scribus/ui/imagepalette_documentmodel.cpp	(working copy)
@@ -0,0 +1,170 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#include "imagepalette_documentmodel.h"
+
+#include <QFileInfo>
+#include <QPainter>
+
+#include "pageitem.h"
+#include "pageitemiterator.h"
+#include "scpage.h"
+#include "scribusdoc.h"
+
+ImagePaletteDocumentModel::ImagePaletteDocumentModel(QObject* parent)
+	: QAbstractListModel(parent)
+{
+}
+
+int ImagePaletteDocumentModel::rowCount(const QModelIndex& parent) const
+{
+	if (parent.isValid())
+		return 0;
+	return m_entries.count();
+}
+
+QVariant ImagePaletteDocumentModel::data(const QModelIndex& index, int role) const
+{
+	if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+		return QVariant();
+
+	const Entry& entry = m_entries.at(index.row());
+	switch (role)
+	{
+	case Qt::DisplayRole:
+		return entry.displayText;
+	case Qt::DecorationRole:
+		return entry.thumbnail;
+	case Qt::ToolTipRole:
+		return entry.filePath.isEmpty()
+			? entry.pageLabel
+			: QStringLiteral("%1\n%2").arg(entry.filePath, entry.pageLabel);
+	default:
+		break;
+	}
+	return QVariant();
+}
+
+void ImagePaletteDocumentModel::setDocument(ScribusDoc* doc)
+{
+	if (m_doc == doc)
+		return;
+
+	beginResetModel();
+	m_doc = doc;
+	m_entries.clear();
+	endResetModel();
+}
+
+void ImagePaletteDocumentModel::setCurrentPageOnly(bool currentPageOnly)
+{
+	m_currentPageOnly = currentPageOnly;
+}
+
+void ImagePaletteDocumentModel::rebuild()
+{
+	beginResetModel();
+	m_entries.clear();
+
+	if (m_doc)
+	{
+		const int options = PageItemIterator::IterateInGroups
+			| PageItemIterator::IterateInDocItems
+			| PageItemIterator::IterateInMasterItems;
+		PageItemIterator iterator(m_doc, options);
+		for (PageItem* item = iterator.current(); item; item = iterator.next())
+		{
+			if (!acceptsItem(item))
+				continue;
+
+			Entry entry;
+			entry.item = item;
+			entry.pageLabel = pageLabel(item);
+			entry.filePath = item->isInlineImage ? QString() : item->Pfile;
+			entry.thumbnail = createThumbnail(item);
+			entry.displayText = QStringLiteral("%1\n%2").arg(imageName(item), entry.pageLabel);
+			m_entries.append(entry);
+		}
+	}
+
+	endResetModel();
+}
+
+PageItem* ImagePaletteDocumentModel::itemAt(const QModelIndex& index) const
+{
+	if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+		return nullptr;
+	return m_entries.at(index.row()).item.data();
+}
+
+QModelIndex ImagePaletteDocumentModel::indexForItem(PageItem* item) const
+{
+	for (int row = 0; row < m_entries.count(); ++row)
+	{
+		if (m_entries.at(row).item.data() == item)
+			return index(row, 0);
+	}
+	return QModelIndex();
+}
+
+bool ImagePaletteDocumentModel::acceptsItem(PageItem* item) const
+{
+	if (!item || item->itemType() != PageItem::ImageFrame || item->isLatexFrame())
+		return false;
+	if (!m_currentPageOnly || !m_doc || !m_doc->currentPage())
+		return true;
+
+	const ScPage* currentPage = m_doc->currentPage();
+	if (m_doc->masterPageMode())
+		return !item->OnMasterPage.isEmpty() && item->OnMasterPage == currentPage->pageName();
+
+	if (item->OnMasterPage.isEmpty())
+		return item->OwnPage == currentPage->pageNr();
+
+	return item->OnMasterPage == currentPage->masterPageName();
+}
+
+QString ImagePaletteDocumentModel::imageName(PageItem* item) const
+{
+	if (item->isInlineImage)
+		return tr("Embedded Image");
+	if (item->Pfile.isEmpty())
+		return tr("Empty Image Frame");
+	return QFileInfo(item->Pfile).fileName();
+}
+
+QString ImagePaletteDocumentModel::pageLabel(PageItem* item) const
+{
+	if (!item->OnMasterPage.isEmpty())
+		return tr("Master: %1").arg(item->OnMasterPage);
+	if (item->OwnPage < 0)
+		return tr("Not on a Page");
+	return tr("Page %1").arg(item->OwnPage + 1);
+}
+
+QIcon ImagePaletteDocumentModel::createThumbnail(PageItem* item) const
+{
+	QPixmap pm(128, 128);
+	pm.fill(Qt::transparent);
+
+	QPainter painter(&pm);
+	const QRect imageRect(6, 6, 116, 116);
+	if (item->imageIsAvailable && item->pixm.width() > 0 && item->pixm.height() > 0)
+	{
+		const QImage preview = item->pixm.scaled(imageRect.width(), imageRect.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
+		const QPoint pos(imageRect.x() + (imageRect.width() - preview.width()) / 2,
+			imageRect.y() + (imageRect.height() - preview.height()) / 2);
+		painter.drawImage(pos, preview);
+	}
+	else
+	{
+		painter.setPen(QPen(Qt::red, 2));
+		painter.drawLine(imageRect.topLeft(), imageRect.bottomRight());
+		painter.drawLine(imageRect.bottomLeft(), imageRect.topRight());
+	}
+	return QIcon(pm);
+}
Index: scribus/ui/imagepalette_documentmodel.h
===================================================================
--- scribus/ui/imagepalette_documentmodel.h	(nonexistent)
+++ scribus/ui/imagepalette_documentmodel.h	(working copy)
@@ -0,0 +1,58 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#ifndef IMAGEPALETTE_DOCUMENTMODEL_H
+#define IMAGEPALETTE_DOCUMENTMODEL_H
+
+#include <QAbstractListModel>
+#include <QIcon>
+#include <QPointer>
+#include <QVector>
+
+#include "scribusapi.h"
+
+class PageItem;
+class ScribusDoc;
+
+class SCRIBUS_API ImagePaletteDocumentModel : public QAbstractListModel
+{
+	Q_OBJECT
+
+public:
+	explicit ImagePaletteDocumentModel(QObject* parent = nullptr);
+
+	int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+	QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+	void setDocument(ScribusDoc* doc);
+	void setCurrentPageOnly(bool currentPageOnly);
+	void rebuild();
+
+	PageItem* itemAt(const QModelIndex& index) const;
+	QModelIndex indexForItem(PageItem* item) const;
+
+private:
+	struct Entry
+	{
+		QPointer<PageItem> item;
+		QString displayText;
+		QString pageLabel;
+		QString filePath;
+		QIcon thumbnail;
+	};
+
+	bool acceptsItem(PageItem* item) const;
+	QString imageName(PageItem* item) const;
+	QString pageLabel(PageItem* item) const;
+	QIcon createThumbnail(PageItem* item) const;
+
+	ScribusDoc* m_doc {nullptr};
+	bool m_currentPageOnly {false};
+	QVector<Entry> m_entries;
+};
+
+#endif // IMAGEPALETTE_DOCUMENTMODEL_H
img2-images-document-management-v1.33.patch (50,790 bytes)   
Index: scribus/ui/imagepalette_documentmodel.h
===================================================================
--- scribus/ui/imagepalette_documentmodel.h	(revision 27814)
+++ scribus/ui/imagepalette_documentmodel.h	(working copy)
@@ -23,17 +23,26 @@
 	Q_OBJECT
 
 public:
+	enum class Scope
+	{
+		Document,
+		CurrentPage,
+		Missing
+	};
+
 	explicit ImagePaletteDocumentModel(QObject* parent = nullptr);
 
 	int rowCount(const QModelIndex& parent = QModelIndex()) const override;
 	QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
 
 	void setDocument(ScribusDoc* doc);
-	void setCurrentPageOnly(bool currentPageOnly);
+	void setScope(Scope scope);
 	void rebuild();
 
 	PageItem* itemAt(const QModelIndex& index) const;
 	QModelIndex indexForItem(PageItem* item) const;
+	int missingCount() const;
+	QVector<PageItem*> placementsForItem(PageItem* item) const;
 
 private:
 	struct Entry
@@ -46,12 +55,16 @@
 	};
 
 	bool acceptsItem(PageItem* item) const;
+	bool isDocumentImage(PageItem* item) const;
+	bool isMissing(PageItem* item) const;
 	QString imageName(PageItem* item) const;
 	QString pageLabel(PageItem* item) const;
+	QString comparablePath(PageItem* item) const;
 	QIcon createThumbnail(PageItem* item) const;
 
 	ScribusDoc* m_doc {nullptr};
-	bool m_currentPageOnly {false};
+	Scope m_scope {Scope::Document};
+	int m_missingCount {0};
 	QVector<Entry> m_entries;
 };
 
Index: scribus/ui/imagepalette_documentmodel.cpp
===================================================================
--- scribus/ui/imagepalette_documentmodel.cpp	(revision 27814)
+++ scribus/ui/imagepalette_documentmodel.cpp	(working copy)
@@ -7,6 +7,7 @@
 
 #include "imagepalette_documentmodel.h"
 
+#include <QDir>
 #include <QFileInfo>
 #include <QPainter>
 
@@ -42,7 +43,7 @@
 	case Qt::ToolTipRole:
 		return entry.filePath.isEmpty()
 			? entry.pageLabel
-			: QStringLiteral("%1\n%2").arg(entry.filePath, entry.pageLabel);
+			: QStringLiteral("%1\n%2").arg(QDir::toNativeSeparators(entry.filePath), entry.pageLabel);
 	default:
 		break;
 	}
@@ -56,19 +57,21 @@
 
 	beginResetModel();
 	m_doc = doc;
+	m_missingCount = 0;
 	m_entries.clear();
 	endResetModel();
 }
 
-void ImagePaletteDocumentModel::setCurrentPageOnly(bool currentPageOnly)
+void ImagePaletteDocumentModel::setScope(Scope scope)
 {
-	m_currentPageOnly = currentPageOnly;
+	m_scope = scope;
 }
 
 void ImagePaletteDocumentModel::rebuild()
 {
 	beginResetModel();
 	m_entries.clear();
+	m_missingCount = 0;
 
 	if (m_doc)
 	{
@@ -78,6 +81,8 @@
 		PageItemIterator iterator(m_doc, options);
 		for (PageItem* item = iterator.current(); item; item = iterator.next())
 		{
+			if (isDocumentImage(item) && isMissing(item))
+				++m_missingCount;
 			if (!acceptsItem(item))
 				continue;
 
@@ -111,11 +116,43 @@
 	return QModelIndex();
 }
 
+int ImagePaletteDocumentModel::missingCount() const
+{
+	return m_missingCount;
+}
+
+QVector<PageItem*> ImagePaletteDocumentModel::placementsForItem(PageItem* item) const
+{
+	QVector<PageItem*> placements;
+	if (!m_doc || !isDocumentImage(item))
+		return placements;
+	if (item->isInlineImage || item->Pfile.isEmpty())
+	{
+		placements.append(item);
+		return placements;
+	}
+
+	const QString path = comparablePath(item);
+	const int options = PageItemIterator::IterateInGroups
+		| PageItemIterator::IterateInDocItems
+		| PageItemIterator::IterateInMasterItems;
+	PageItemIterator iterator(m_doc, options);
+	for (PageItem* candidate = iterator.current(); candidate; candidate = iterator.next())
+	{
+		if (isDocumentImage(candidate) && !candidate->isInlineImage && comparablePath(candidate) == path)
+			placements.append(candidate);
+	}
+	return placements;
+}
+
 bool ImagePaletteDocumentModel::acceptsItem(PageItem* item) const
 {
-	if (!item || item->itemType() != PageItem::ImageFrame || item->isLatexFrame())
+	if (!isDocumentImage(item))
 		return false;
-	if (!m_currentPageOnly || !m_doc || !m_doc->currentPage())
+
+	if (m_scope == Scope::Missing)
+		return isMissing(item);
+	if (m_scope == Scope::Document || !m_doc || !m_doc->currentPage())
 		return true;
 
 	const ScPage* currentPage = m_doc->currentPage();
@@ -128,6 +165,16 @@
 	return item->OnMasterPage == currentPage->masterPageName();
 }
 
+bool ImagePaletteDocumentModel::isDocumentImage(PageItem* item) const
+{
+	return item && item->itemType() == PageItem::ImageFrame && !item->isLatexFrame();
+}
+
+bool ImagePaletteDocumentModel::isMissing(PageItem* item) const
+{
+	return !item->isInlineImage && !item->Pfile.isEmpty() && !item->imageIsAvailable && !QFileInfo::exists(item->Pfile);
+}
+
 QString ImagePaletteDocumentModel::imageName(PageItem* item) const
 {
 	if (item->isInlineImage)
@@ -139,11 +186,21 @@
 
 QString ImagePaletteDocumentModel::pageLabel(PageItem* item) const
 {
+	QString page;
 	if (!item->OnMasterPage.isEmpty())
-		return tr("Master: %1").arg(item->OnMasterPage);
-	if (item->OwnPage < 0)
-		return tr("Not on a Page");
-	return tr("Page %1").arg(item->OwnPage + 1);
+		page = tr("Master: %1").arg(item->OnMasterPage);
+	else if (item->OwnPage < 0)
+		page = tr("Not on a Page");
+	else
+		page = tr("Page %1").arg(item->OwnPage + 1);
+	return tr("%1 · Frame: %2").arg(page, item->itemName());
+}
+
+QString ImagePaletteDocumentModel::comparablePath(PageItem* item) const
+{
+	if (!item || item->Pfile.isEmpty())
+		return QString();
+	return QDir::cleanPath(QFileInfo(item->Pfile).absoluteFilePath());
 }
 
 QIcon ImagePaletteDocumentModel::createThumbnail(PageItem* item) const
Index: scribus/ui/imagepalette.h
===================================================================
--- scribus/ui/imagepalette.h	(revision 27814)
+++ scribus/ui/imagepalette.h	(working copy)
@@ -8,14 +8,24 @@
 #ifndef IMAGEPALETTE_H
 #define IMAGEPALETTE_H
 
+#include <QList>
+#include <QString>
+
 #include "docks/dock_panelbase.h"
 #include "scguardedptr.h"
 #include "scribusapi.h"
 
+class QCheckBox;
 class QLabel;
 class QListView;
+class QPushButton;
+class QGridLayout;
+class QResizeEvent;
+class QSplitter;
 class QTabBar;
 class QTimer;
+class QToolButton;
+class QWidget;
 class QItemSelection;
 
 class ImagePaletteDocumentModel;
@@ -39,6 +49,7 @@
 
 protected:
 	void changeEvent(QEvent* event) override;
+	void resizeEvent(QResizeEvent* event) override;
 
 private slots:
 	void scopeChanged(int index);
@@ -47,14 +58,38 @@
 	void handleCurrentPageChanged();
 	void handleVisibilityChanged(bool visible);
 	void handleViewSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);
+	void previousPlacement();
+	void nextPlacement();
+	void selectFrame();
+	void relinkItem();
+	void searchForItem();
+	void openFileLocation();
+	void editImage();
+	void imageEffects();
+	void imageProperties();
+	void printToggled(bool checked);
+	void visibilityToggled(bool checked);
 
 private:
 	void clearInspector();
+	PageItem* currentItem() const;
+	QString formatText(PageItem* item) const;
 	void languageChange();
+	void navigatePlacement(int offset);
+	void navigateToItem(PageItem* item);
+	bool loadPict(PageItem* item, const QString& filePath);
 	void rebuild();
+	void relinkMatchingImages(const QString& sourcePath, const QString& targetPath, bool brokenLinksOnly);
 	void scheduleRebuild();
+	bool selectPaletteItem(PageItem* item, bool ensureVisible = false);
 	void showItem(PageItem* item);
 	void syncSelectionFromDocument();
+	void updateActionState(PageItem* item);
+	void updateActionLayout(const QList<QPushButton*>& visibleButtons);
+	void updateInspectorLabelWidth();
+	void updateInspectorMinimumWidth();
+	void updateElidedLabels();
+	void updatePlacementState(PageItem* item);
 	void updateSummary();
 
 	ImagePaletteDocumentModel* m_model {nullptr};
@@ -62,22 +97,66 @@
 	ScGuardedPtr<ScribusDoc> m_doc {nullptr};
 	QTimer* m_refreshTimer {nullptr};
 	bool m_dirty {false};
+	bool m_syncSelectionAfterRebuild {false};
+	bool m_syncingSelection {false};
 
 	QTabBar* m_scopeTabs {nullptr};
 	QListView* m_imageView {nullptr};
+	QSplitter* m_splitter {nullptr};
+	QWidget* m_inspector {nullptr};
 	QLabel* m_nameValue {nullptr};
-	QLabel* m_pathLabel {nullptr};
+	QLabel* m_statusLabel {nullptr};
+	QLabel* m_statusValue {nullptr};
 	QLabel* m_pathValue {nullptr};
+	QString m_nameText;
+	QString m_pathText;
+	QString m_pageText;
+	QString m_frameText;
+
+	QWidget* m_placementWidget {nullptr};
+	QGridLayout* m_infoLayout {nullptr};
+	QLabel* m_placementLabel {nullptr};
+	QLabel* m_placementPositionLabel {nullptr};
+	QToolButton* m_previousPlacementButton {nullptr};
+	QToolButton* m_nextPlacementButton {nullptr};
 	QLabel* m_pageLabel {nullptr};
 	QLabel* m_pageValue {nullptr};
 	QLabel* m_frameLabel {nullptr};
 	QLabel* m_frameValue {nullptr};
+
+	QLabel* m_detailsLabel {nullptr};
+	QWidget* m_detailsWidget {nullptr};
+	QGridLayout* m_detailsLayout {nullptr};
+	QLabel* m_formatLabel {nullptr};
+	QLabel* m_formatValue {nullptr};
+	QLabel* m_colorSpaceLabel {nullptr};
+	QLabel* m_colorSpaceValue {nullptr};
 	QLabel* m_dimensionsLabel {nullptr};
 	QLabel* m_dimensionsValue {nullptr};
+	QLabel* m_nativeResolutionLabel {nullptr};
+	QLabel* m_nativeResolutionValue {nullptr};
 	QLabel* m_resolutionLabel {nullptr};
 	QLabel* m_resolutionValue {nullptr};
-	QLabel* m_statusLabel {nullptr};
-	QLabel* m_statusValue {nullptr};
+	QLabel* m_scaleLabel {nullptr};
+	QLabel* m_scaleValue {nullptr};
+	QLabel* m_frameSizeLabel {nullptr};
+	QLabel* m_frameSizeValue {nullptr};
+	QLabel* m_scaledImageSizeLabel {nullptr};
+	QLabel* m_scaledImageSizeValue {nullptr};
+
+	QCheckBox* m_visibleCheck {nullptr};
+	QCheckBox* m_printCheck {nullptr};
+
+	QPushButton* m_searchButton {nullptr};
+	QWidget* m_actionsWidget {nullptr};
+	QGridLayout* m_actionLayout {nullptr};
+	QPushButton* m_selectButton {nullptr};
+	QPushButton* m_relinkButton {nullptr};
+	QPushButton* m_revealButton {nullptr};
+	QPushButton* m_editButton {nullptr};
+	QPushButton* m_effectsButton {nullptr};
+	QPushButton* m_propertiesButton {nullptr};
+
 	QLabel* m_summaryLabel {nullptr};
 };
 
Index: scribus/ui/imagepalette.cpp
===================================================================
--- scribus/ui/imagepalette.cpp	(revision 27814)
+++ scribus/ui/imagepalette.cpp	(working copy)
@@ -8,25 +8,44 @@
 #include "imagepalette.h"
 
 #include <QAbstractItemView>
+#include <QCheckBox>
+#include <QDesktopServices>
 #include <QDir>
 #include <QEvent>
 #include <QFileInfo>
-#include <QFormLayout>
-#include <QFrame>
+#include <QGridLayout>
+#include <QHBoxLayout>
 #include <QItemSelectionModel>
 #include <QLabel>
 #include <QListView>
+#include <QMargins>
+#include <QPushButton>
+#include <QResizeEvent>
+#include <QScopedPointer>
 #include <QSignalBlocker>
+#include <QSizePolicy>
 #include <QSplitter>
+#include <QStyle>
 #include <QTabBar>
 #include <QTimer>
+#include <QToolButton>
+#include <QUrl>
 #include <QVBoxLayout>
 
 #include "imagepalette_documentmodel.h"
 #include "pageitem.h"
+#include "pageitemiterator.h"
 #include "selection.h"
 #include "scribus.h"
 #include "scribusdoc.h"
+#include "units.h"
+#include "util_color.h"
+#include "util_formats.h"
+#include "ui/contentpalette.h"
+#include "ui/customfdialog.h"
+#include "ui/picsearch.h"
+#include "ui/picsearchoptions.h"
+#include "ui/scmessagebox.h"
 
 ImagePalette::ImagePalette(QWidget* parent)
 	: DockPanelBase("Images", "image-collection", parent),
@@ -45,82 +64,184 @@
 	m_scopeTabs->setExpanding(false);
 	m_scopeTabs->addTab(QString());
 	m_scopeTabs->addTab(QString());
+	m_scopeTabs->addTab(QString());
 	layout->addWidget(m_scopeTabs);
 
-	auto* splitter = new QSplitter(Qt::Horizontal, panel);
-	splitter->setChildrenCollapsible(false);
-	layout->addWidget(splitter, 1);
+	m_splitter = new QSplitter(Qt::Horizontal, panel);
+	m_splitter->setChildrenCollapsible(false);
+	layout->addWidget(m_splitter, 1);
 
-	m_imageView = new QListView(splitter);
-	m_imageView->setMinimumSize(220, 180);
+	m_imageView = new QListView(m_splitter);
+	m_imageView->setMinimumSize(180, 180);
 	m_imageView->setModel(m_model);
 	m_imageView->setIconSize(QSize(128, 128));
-	m_imageView->setGridSize(QSize(156, 174));
+	m_imageView->setGridSize(QSize(156, 166));
 	m_imageView->setResizeMode(QListView::Adjust);
 	m_imageView->setViewMode(QListView::IconMode);
 	m_imageView->setMovement(QListView::Static);
 	m_imageView->setFlow(QListView::LeftToRight);
 	m_imageView->setWrapping(true);
-	m_imageView->setWordWrap(true);
+	m_imageView->setWordWrap(false);
+	m_imageView->setTextElideMode(Qt::ElideMiddle);
 	m_imageView->setEditTriggers(QAbstractItemView::NoEditTriggers);
 	m_imageView->setSelectionMode(QAbstractItemView::SingleSelection);
 
-	auto* inspector = new QWidget(splitter);
-	inspector->setMinimumWidth(210);
+	m_inspector = new QWidget(m_splitter);
+	auto* inspector = m_inspector;
 	auto* inspectorLayout = new QVBoxLayout(inspector);
-	inspectorLayout->setContentsMargins(8, 0, 0, 0);
+	inspectorLayout->setContentsMargins(8, 0, 8, 0);
 
 	m_nameValue = new QLabel(inspector);
 	QFont nameFont = m_nameValue->font();
 	nameFont.setBold(true);
 	m_nameValue->setFont(nameFont);
-	m_nameValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_nameValue->setWordWrap(false);
+	m_nameValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
 	inspectorLayout->addWidget(m_nameValue);
 
-	auto* line = new QFrame(inspector);
-	line->setFrameShape(QFrame::HLine);
-	line->setFrameShadow(QFrame::Sunken);
-	inspectorLayout->addWidget(line);
-
-	auto* form = new QFormLayout();
-	form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
-	form->setLabelAlignment(Qt::AlignLeft | Qt::AlignTop);
-	inspectorLayout->addLayout(form);
-
-	m_pathLabel = new QLabel(inspector);
 	m_pathValue = new QLabel(inspector);
-	m_pathValue->setWordWrap(true);
-	m_pathValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
-	form->addRow(m_pathLabel, m_pathValue);
-
-	m_pageLabel = new QLabel(inspector);
-	m_pageValue = new QLabel(inspector);
-	m_pageValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
-	form->addRow(m_pageLabel, m_pageValue);
-
-	m_frameLabel = new QLabel(inspector);
-	m_frameValue = new QLabel(inspector);
-	m_frameValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
-	form->addRow(m_frameLabel, m_frameValue);
+	m_pathValue->setWordWrap(false);
+	m_pathValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+	inspectorLayout->addWidget(m_pathValue);
+	inspectorLayout->addStretch(1);
+
+	m_placementWidget = new QWidget(inspector);
+	m_infoLayout = new QGridLayout(m_placementWidget);
+	m_infoLayout->setContentsMargins(0, 0, 0, 0);
+	m_infoLayout->setHorizontalSpacing(12);
+	m_infoLayout->setColumnStretch(0, 0);
+	m_infoLayout->setColumnStretch(1, 1);
+
+	m_statusLabel = new QLabel(m_placementWidget);
+	m_statusValue = new QLabel(m_placementWidget);
+	m_infoLayout->addWidget(m_statusLabel, 0, 0);
+	m_infoLayout->addWidget(m_statusValue, 0, 1);
+
+	m_placementLabel = new QLabel(m_placementWidget);
+	auto* placementControls = new QWidget(m_placementWidget);
+	auto* placementLayout = new QHBoxLayout(placementControls);
+	placementLayout->setContentsMargins(0, 0, 0, 0);
+	m_previousPlacementButton = new QToolButton(placementControls);
+	m_previousPlacementButton->setArrowType(Qt::LeftArrow);
+	m_previousPlacementButton->setAutoRaise(true);
+	m_placementPositionLabel = new QLabel(placementControls);
+	m_nextPlacementButton = new QToolButton(placementControls);
+	m_nextPlacementButton->setArrowType(Qt::RightArrow);
+	m_nextPlacementButton->setAutoRaise(true);
+	placementLayout->addWidget(m_placementPositionLabel);
+	placementLayout->addWidget(m_previousPlacementButton);
+	placementLayout->addWidget(m_nextPlacementButton);
+	placementLayout->addStretch(1);
+	m_infoLayout->addWidget(m_placementLabel, 1, 0);
+	m_infoLayout->addWidget(placementControls, 1, 1);
+
+	m_pageLabel = new QLabel(m_placementWidget);
+	m_pageValue = new QLabel(m_placementWidget);
+	m_pageValue->setWordWrap(false);
+	m_pageValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+	m_infoLayout->addWidget(m_pageLabel, 2, 0);
+	m_infoLayout->addWidget(m_pageValue, 2, 1);
+
+	m_frameLabel = new QLabel(m_placementWidget);
+	m_frameValue = new QLabel(m_placementWidget);
+	m_frameValue->setWordWrap(false);
+	m_frameValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+	m_infoLayout->addWidget(m_frameLabel, 3, 0);
+	m_infoLayout->addWidget(m_frameValue, 3, 1);
+
+	auto* visibilityControls = new QWidget(m_placementWidget);
+	auto* visibilityLayout = new QVBoxLayout(visibilityControls);
+	const int sectionSpacing = qMax(0, style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing, nullptr, m_placementWidget));
+	visibilityLayout->setContentsMargins(0, sectionSpacing, 0, 0);
+	m_visibleCheck = new QCheckBox(visibilityControls);
+	m_printCheck = new QCheckBox(visibilityControls);
+	visibilityLayout->addWidget(m_visibleCheck);
+	visibilityLayout->addWidget(m_printCheck);
+	m_infoLayout->addWidget(visibilityControls, 4, 1, Qt::AlignLeft);
+	inspectorLayout->addWidget(m_placementWidget);
+	inspectorLayout->addStretch(1);
+
+	m_actionsWidget = new QWidget(inspector);
+	m_actionLayout = new QGridLayout(m_actionsWidget);
+	m_actionLayout->setContentsMargins(0, 0, 0, 0);
+	m_actionLayout->setHorizontalSpacing(12);
+	m_actionLayout->setColumnStretch(0, 0);
+	m_actionLayout->setColumnStretch(1, 1);
+	m_actionLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
+	m_searchButton = new QPushButton(m_actionsWidget);
+	m_selectButton = new QPushButton(m_actionsWidget);
+	m_relinkButton = new QPushButton(m_actionsWidget);
+	m_revealButton = new QPushButton(m_actionsWidget);
+	m_editButton = new QPushButton(m_actionsWidget);
+	m_effectsButton = new QPushButton(m_actionsWidget);
+	m_propertiesButton = new QPushButton(m_actionsWidget);
+	inspectorLayout->addWidget(m_actionsWidget);
+	inspectorLayout->addStretch(1);
 
-	m_dimensionsLabel = new QLabel(inspector);
-	m_dimensionsValue = new QLabel(inspector);
+	m_detailsLabel = new QLabel(inspector);
+	QFont detailsFont = m_detailsLabel->font();
+	detailsFont.setBold(true);
+	m_detailsLabel->setFont(detailsFont);
+	inspectorLayout->addWidget(m_detailsLabel);
+
+	m_detailsWidget = new QWidget(inspector);
+	m_detailsLayout = new QGridLayout(m_detailsWidget);
+	m_detailsLayout->setContentsMargins(0, 0, 0, 0);
+	m_detailsLayout->setHorizontalSpacing(12);
+	m_detailsLayout->setColumnStretch(0, 0);
+	m_detailsLayout->setColumnStretch(1, 1);
+
+	m_formatLabel = new QLabel(m_detailsWidget);
+	m_formatValue = new QLabel(m_detailsWidget);
+	m_formatValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_formatLabel, 0, 0);
+	m_detailsLayout->addWidget(m_formatValue, 0, 1);
+
+	m_colorSpaceLabel = new QLabel(m_detailsWidget);
+	m_colorSpaceValue = new QLabel(m_detailsWidget);
+	m_colorSpaceValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_colorSpaceLabel, 1, 0);
+	m_detailsLayout->addWidget(m_colorSpaceValue, 1, 1);
+
+	m_dimensionsLabel = new QLabel(m_detailsWidget);
+	m_dimensionsValue = new QLabel(m_detailsWidget);
 	m_dimensionsValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
-	form->addRow(m_dimensionsLabel, m_dimensionsValue);
+	m_detailsLayout->addWidget(m_dimensionsLabel, 2, 0);
+	m_detailsLayout->addWidget(m_dimensionsValue, 2, 1);
 
-	m_resolutionLabel = new QLabel(inspector);
-	m_resolutionValue = new QLabel(inspector);
-	m_resolutionValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
-	form->addRow(m_resolutionLabel, m_resolutionValue);
+	m_nativeResolutionLabel = new QLabel(m_detailsWidget);
+	m_nativeResolutionValue = new QLabel(m_detailsWidget);
+	m_nativeResolutionValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_nativeResolutionLabel, 3, 0);
+	m_detailsLayout->addWidget(m_nativeResolutionValue, 3, 1);
 
-	m_statusLabel = new QLabel(inspector);
-	m_statusValue = new QLabel(inspector);
-	m_statusValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
-	form->addRow(m_statusLabel, m_statusValue);
+	m_resolutionLabel = new QLabel(m_detailsWidget);
+	m_resolutionValue = new QLabel(m_detailsWidget);
+	m_resolutionValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_resolutionLabel, 4, 0);
+	m_detailsLayout->addWidget(m_resolutionValue, 4, 1);
 
-	inspectorLayout->addStretch(1);
-	splitter->setStretchFactor(0, 3);
-	splitter->setStretchFactor(1, 2);
+	m_scaleLabel = new QLabel(m_detailsWidget);
+	m_scaleValue = new QLabel(m_detailsWidget);
+	m_scaleValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_scaleLabel, 5, 0);
+	m_detailsLayout->addWidget(m_scaleValue, 5, 1);
+
+	m_frameSizeLabel = new QLabel(m_detailsWidget);
+	m_frameSizeValue = new QLabel(m_detailsWidget);
+	m_frameSizeValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_frameSizeLabel, 6, 0);
+	m_detailsLayout->addWidget(m_frameSizeValue, 6, 1);
+
+	m_scaledImageSizeLabel = new QLabel(m_detailsWidget);
+	m_scaledImageSizeValue = new QLabel(m_detailsWidget);
+	m_scaledImageSizeValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_scaledImageSizeLabel, 7, 0);
+	m_detailsLayout->addWidget(m_scaledImageSizeValue, 7, 1);
+	inspectorLayout->addWidget(m_detailsWidget);
+	inspectorLayout->addStretch(12);
+	m_splitter->setStretchFactor(0, 3);
+	m_splitter->setStretchFactor(1, 2);
 
 	m_summaryLabel = new QLabel(panel);
 	layout->addWidget(m_summaryLabel);
@@ -133,8 +254,20 @@
 	connect(m_scopeTabs, &QTabBar::currentChanged, this, &ImagePalette::scopeChanged);
 	connect(m_imageView->selectionModel(), &QItemSelectionModel::selectionChanged,
 		this, &ImagePalette::handleViewSelectionChanged);
+	connect(m_splitter, &QSplitter::splitterMoved, this, [this](int, int) { updateElidedLabels(); });
 	connect(m_refreshTimer, &QTimer::timeout, this, &ImagePalette::rebuild);
 	connect(this, &DockPanelBase::visibilityChanged, this, &ImagePalette::handleVisibilityChanged);
+	connect(m_previousPlacementButton, &QToolButton::clicked, this, &ImagePalette::previousPlacement);
+	connect(m_nextPlacementButton, &QToolButton::clicked, this, &ImagePalette::nextPlacement);
+	connect(m_selectButton, &QPushButton::clicked, this, &ImagePalette::selectFrame);
+	connect(m_relinkButton, &QPushButton::clicked, this, &ImagePalette::relinkItem);
+	connect(m_searchButton, &QPushButton::clicked, this, &ImagePalette::searchForItem);
+	connect(m_revealButton, &QPushButton::clicked, this, &ImagePalette::openFileLocation);
+	connect(m_editButton, &QPushButton::clicked, this, &ImagePalette::editImage);
+	connect(m_effectsButton, &QPushButton::clicked, this, &ImagePalette::imageEffects);
+	connect(m_propertiesButton, &QPushButton::clicked, this, &ImagePalette::imageProperties);
+	connect(m_visibleCheck, &QCheckBox::toggled, this, &ImagePalette::visibilityToggled);
+	connect(m_printCheck, &QCheckBox::toggled, this, &ImagePalette::printToggled);
 
 	languageChange();
 	clearInspector();
@@ -172,6 +305,7 @@
 
 	m_doc = doc;
 	m_model->setDocument(m_doc);
+	m_syncSelectionAfterRebuild = true;
 
 	if (m_doc)
 	{
@@ -195,6 +329,7 @@
 	}
 
 	m_refreshTimer->stop();
+	m_syncSelectionAfterRebuild = false;
 	m_doc = nullptr;
 	m_model->setDocument(nullptr);
 	m_dirty = false;
@@ -209,9 +344,37 @@
 		languageChange();
 }
 
+void ImagePalette::resizeEvent(QResizeEvent* event)
+{
+	DockPanelBase::resizeEvent(event);
+	updateElidedLabels();
+}
+
 void ImagePalette::scopeChanged(int index)
 {
-	m_model->setCurrentPageOnly(index == 1);
+	ImagePaletteDocumentModel::Scope scope = ImagePaletteDocumentModel::Scope::Document;
+	if (index == 1)
+		scope = ImagePaletteDocumentModel::Scope::CurrentPage;
+	else if (index == 2)
+		scope = ImagePaletteDocumentModel::Scope::Missing;
+	m_model->setScope(scope);
+
+	if (scope == ImagePaletteDocumentModel::Scope::Missing)
+	{
+		m_imageView->setViewMode(QListView::ListMode);
+		m_imageView->setIconSize(QSize(48, 48));
+		m_imageView->setGridSize(QSize());
+		m_imageView->setFlow(QListView::TopToBottom);
+		m_imageView->setWrapping(false);
+	}
+	else
+	{
+		m_imageView->setViewMode(QListView::IconMode);
+		m_imageView->setIconSize(QSize(128, 128));
+		m_imageView->setGridSize(QSize(156, 166));
+		m_imageView->setFlow(QListView::LeftToRight);
+		m_imageView->setWrapping(true);
+	}
 	rebuild();
 }
 
@@ -225,6 +388,7 @@
 {
 	if (m_scopeTabs->currentIndex() == 1)
 	{
+		m_syncSelectionAfterRebuild = true;
 		m_dirty = true;
 		scheduleRebuild();
 		return;
@@ -239,6 +403,7 @@
 	if (m_scopeTabs->currentIndex() != 1)
 		return;
 
+	m_syncSelectionAfterRebuild = true;
 	m_dirty = true;
 	scheduleRebuild();
 }
@@ -254,19 +419,230 @@
 
 void ImagePalette::handleViewSelectionChanged(const QItemSelection&, const QItemSelection&)
 {
-	showItem(m_model->itemAt(m_imageView->currentIndex()));
+	if (m_syncingSelection)
+		return;
+
+	showItem(currentItem());
+	updateSummary();
+}
+
+void ImagePalette::previousPlacement()
+{
+	navigatePlacement(-1);
+}
+
+void ImagePalette::nextPlacement()
+{
+	navigatePlacement(1);
+}
+
+void ImagePalette::selectFrame()
+{
+	PageItem* item = currentItem();
+	if (!item || !m_ScMW || !m_doc)
+		return;
+
+	if (item->OnMasterPage.isEmpty() && m_doc->masterPageMode())
+		m_ScMW->closeActiveWindowMasterPageEditor();
+	else if (!item->OnMasterPage.isEmpty() && !m_doc->masterPageMode())
+		m_ScMW->editMasterPagesStart(item->OnMasterPage);
+
+	m_ScMW->selectItemsFromOutlines(item, true, 1);
+}
+
+void ImagePalette::relinkItem()
+{
+	PageItem* item = currentItem();
+	if (!item || !m_doc || item->isInlineImage)
+		return;
+
+	QString startDir = QFileInfo(item->Pfile).absolutePath();
+	if (startDir.isEmpty() || !QDir(startDir).exists())
+		startDir = QDir::homePath();
+
+	const QString format = FormatsManager::instance()->fileDialogFormatList(FormatsManager::IMAGESIMGFRAME);
+	CustomFDialog dialog(this, startDir, tr("Replace Image"), format,
+		fdShowPreview | fdExistingFiles | fdDisableOk, contextImages);
+	if (dialog.exec() != QDialog::Accepted)
+		return;
+
+	const QString filePath = dialog.selectedFile();
+	if (filePath.isEmpty() || !loadPict(item, filePath))
+		return;
+
+	m_doc->changed();
+	rebuild();
+	selectPaletteItem(item, true);
+}
+
+void ImagePalette::searchForItem()
+{
+	PageItem* item = currentItem();
+	if (!item || !m_doc || item->isInlineImage || item->Pfile.isEmpty())
+		return;
+
+	static QString lastSearchPath;
+	const QFileInfo currentFile(item->Pfile);
+	if (lastSearchPath.isEmpty())
+		lastSearchPath = currentFile.absolutePath();
+
+	QScopedPointer<PicSearchOptions> searchOptions(new PicSearchOptions(this, currentFile.fileName(), lastSearchPath));
+	if (searchOptions->exec() != QDialog::Accepted)
+		return;
+
+	lastSearchPath = searchOptions->getLastDirSearched();
+	if (searchOptions->getMatches().isEmpty())
+	{
+		ScMessageBox::information(this, tr("Scribus - Find Missing Image"),
+			tr("No matching image files were found for \"%1\".").arg(searchOptions->getFileName()),
+			QMessageBox::Ok | QMessageBox::Default | QMessageBox::Escape,
+			QMessageBox::NoButton);
+		return;
+	}
+
+	const bool brokenLink = !item->imageIsAvailable;
+	QScopedPointer<PicSearch> searchResults(new PicSearch(this, searchOptions->getFileName(), searchOptions->getMatches(), brokenLink));
+	if (searchResults->exec() != QDialog::Accepted)
+		return;
+
+	const QString sourcePath = item->Pfile;
+	const QString targetPath = searchResults->getSelectedImage();
+	if (!loadPict(item, targetPath))
+		return;
+
+	if (searchResults->isApplyToMatchingImages())
+		relinkMatchingImages(sourcePath, targetPath, brokenLink);
+
+	m_doc->changed();
+	rebuild();
+	selectPaletteItem(item, true);
+}
+
+void ImagePalette::openFileLocation()
+{
+	PageItem* item = currentItem();
+	if (!item || item->isInlineImage || item->Pfile.isEmpty())
+		return;
+
+	const QString path = QFileInfo(item->Pfile).absolutePath();
+	if (QDir(path).exists())
+		QDesktopServices::openUrl(QUrl::fromLocalFile(path));
+}
+
+void ImagePalette::editImage()
+{
+	PageItem* item = currentItem();
+	if (!item || !m_ScMW || !item->imageIsAvailable || !item->isRaster)
+		return;
+
+	selectFrame();
+	m_ScMW->callImageEditor();
+}
+
+void ImagePalette::imageEffects()
+{
+	PageItem* item = currentItem();
+	if (!item || !m_ScMW || !item->imageIsAvailable || !item->isRaster)
+		return;
+
+	selectFrame();
+	m_ScMW->ImageEffects();
+}
+
+void ImagePalette::imageProperties()
+{
+	PageItem* item = currentItem();
+	if (!item || !m_ScMW || !m_ScMW->contentPalette)
+		return;
+
+	selectFrame();
+	m_ScMW->contentPalette->show();
+	m_ScMW->contentPalette->raise();
+}
+
+void ImagePalette::printToggled(bool checked)
+{
+	PageItem* item = currentItem();
+	if (!item || !m_doc || item->printEnabled() == checked)
+		return;
+
+	item->setPrintEnabled(checked);
+	item->update();
+	m_doc->changed();
+}
+
+void ImagePalette::visibilityToggled(bool checked)
+{
+	PageItem* item = currentItem();
+	if (!item || !m_doc || item->imageVisible() == checked)
+		return;
+
+	item->setImageVisible(checked);
+	item->update();
+	m_doc->changed();
 }
 
 void ImagePalette::clearInspector()
 {
 	const QString empty = QString::fromUtf8("—");
-	m_nameValue->setText(empty);
-	m_pathValue->setText(empty);
-	m_pageValue->setText(empty);
-	m_frameValue->setText(empty);
+	m_nameText = empty;
+	m_pathText = empty;
+	m_pageText = empty;
+	m_frameText = empty;
+	updateElidedLabels();
+	m_formatValue->setText(empty);
+	m_colorSpaceValue->setText(empty);
 	m_dimensionsValue->setText(empty);
+	m_nativeResolutionValue->setText(empty);
 	m_resolutionValue->setText(empty);
+	m_scaleValue->setText(empty);
+	m_frameSizeValue->setText(empty);
+	m_scaledImageSizeValue->setText(empty);
+	m_placementPositionLabel->setText(empty);
+	m_previousPlacementButton->hide();
+	m_nextPlacementButton->hide();
 	m_statusValue->setText(empty);
+	{
+		const QSignalBlocker visibleBlocker(m_visibleCheck);
+		const QSignalBlocker printBlocker(m_printCheck);
+		m_visibleCheck->setChecked(false);
+		m_printCheck->setChecked(false);
+	}
+	updateActionState(nullptr);
+}
+
+PageItem* ImagePalette::currentItem() const
+{
+	return m_model->itemAt(m_imageView->currentIndex());
+}
+
+QString ImagePalette::formatText(PageItem* item) const
+{
+	if (!item || !item->imageIsAvailable)
+		return QString::fromUtf8("—");
+
+	const QString ext = QFileInfo(item->Pfile).suffix().toLower();
+	switch (item->pixm.imgInfo.type)
+	{
+	case 0:
+		return tr("JPEG");
+	case 1:
+		return tr("TIFF");
+	case 2:
+		return tr("PSD");
+	case 3:
+		return tr("EPS/PS");
+	case 4:
+		return tr("PDF");
+	case 5:
+		return tr("JPEG 2000");
+	case 6:
+		return ext.toUpper();
+	case 7:
+		return tr("Embedded PSD");
+	default:
+		return ext.isEmpty() ? tr("Unknown") : ext.toUpper();
+	}
 }
 
 void ImagePalette::languageChange()
@@ -274,12 +650,66 @@
 	setWindowTitle(tr("Images"));
 	m_scopeTabs->setTabText(0, tr("Document"));
 	m_scopeTabs->setTabText(1, tr("Current Page"));
-	m_pathLabel->setText(tr("Path:"));
+	m_scopeTabs->setTabText(2, tr("Missing (%1)").arg(m_model->missingCount()));
+	m_statusLabel->setText(tr("Status:"));
+	m_placementLabel->setText(tr("Placement:"));
 	m_pageLabel->setText(tr("Page:"));
 	m_frameLabel->setText(tr("Frame:"));
-	m_dimensionsLabel->setText(tr("Dimensions:"));
-	m_resolutionLabel->setText(tr("Effective:"));
-	m_statusLabel->setText(tr("Status:"));
+	m_detailsLabel->setText(tr("Details"));
+	m_formatLabel->setText(tr("Format:"));
+	m_colorSpaceLabel->setText(tr("Color Space:"));
+	m_dimensionsLabel->setText(tr("Pixel Size:"));
+	m_nativeResolutionLabel->setText(tr("Native DPI:"));
+	m_resolutionLabel->setText(tr("Effective DPI:"));
+	m_scaleLabel->setText(tr("Scale:"));
+	m_frameSizeLabel->setText(tr("Frame Size:"));
+	m_scaledImageSizeLabel->setText(tr("Scaled Image Size:"));
+	m_visibleCheck->setText(tr("Show"));
+	m_printCheck->setText(tr("Export/Print"));
+	m_selectButton->setText(tr("Select"));
+	m_relinkButton->setText(tr("Replace…"));
+	m_searchButton->setText(tr("Find…"));
+	m_revealButton->setText(tr("Location"));
+	m_editButton->setText(tr("Edit…"));
+	m_effectsButton->setText(tr("Effects…"));
+	m_propertiesButton->setText(tr("Properties"));
+
+	const int leftButtonWidth = qMax(qMax(m_selectButton->sizeHint().width(), m_editButton->sizeHint().width()),
+		qMax(m_effectsButton->sizeHint().width(), m_searchButton->sizeHint().width()));
+	const int rightButtonWidth = qMax(m_relinkButton->sizeHint().width(),
+		qMax(m_revealButton->sizeHint().width(), m_propertiesButton->sizeHint().width()));
+	m_selectButton->setFixedWidth(leftButtonWidth);
+	m_editButton->setFixedWidth(leftButtonWidth);
+	m_effectsButton->setFixedWidth(leftButtonWidth);
+	m_searchButton->setFixedWidth(leftButtonWidth);
+	m_relinkButton->setFixedWidth(rightButtonWidth);
+	m_revealButton->setFixedWidth(rightButtonWidth);
+	m_propertiesButton->setFixedWidth(rightButtonWidth);
+
+	m_previousPlacementButton->setToolTip(tr("Go to the previous placement of this image."));
+	m_nextPlacementButton->setToolTip(tr("Go to the next placement of this image."));
+	m_selectButton->setToolTip(tr("Select this image frame in the document."));
+	m_relinkButton->setToolTip(tr("Choose another image file for this frame."));
+	m_searchButton->setToolTip(tr("Search for the missing image file and relink it."));
+	m_revealButton->setToolTip(tr("Open the folder containing this image."));
+	m_editButton->setToolTip(tr("Open the image in the configured external image editor."));
+	m_effectsButton->setToolTip(tr("Apply image effects to this frame."));
+	m_propertiesButton->setToolTip(tr("Show this frame's image controls in Content Properties."));
+	m_visibleCheck->setToolTip(tr("Show or hide the image inside its frame."));
+	m_printCheck->setToolTip(tr("Include this image frame when exporting or printing."));
+
+	m_nativeResolutionLabel->setToolTip(tr("Original resolution stored for the image."));
+	m_nativeResolutionValue->setToolTip(m_nativeResolutionLabel->toolTip());
+	m_resolutionLabel->setToolTip(tr("Resolution after scaling the image in its frame."));
+	m_resolutionValue->setToolTip(m_resolutionLabel->toolTip());
+	m_scaleLabel->setToolTip(tr("Horizontal and vertical image scaling."));
+	m_scaleValue->setToolTip(m_scaleLabel->toolTip());
+	m_frameSizeLabel->setToolTip(tr("Width and height of the image frame."));
+	m_frameSizeValue->setToolTip(m_frameSizeLabel->toolTip());
+	m_scaledImageSizeLabel->setToolTip(tr("Full image dimensions at the current scale, before frame clipping."));
+	m_scaledImageSizeValue->setToolTip(m_scaledImageSizeLabel->toolTip());
+	updateInspectorLabelWidth();
+	updateInspectorMinimumWidth();
 	m_dirty = true;
 	if (isVisible())
 		rebuild();
@@ -287,18 +717,108 @@
 		updateSummary();
 }
 
+void ImagePalette::navigatePlacement(int offset)
+{
+	PageItem* item = currentItem();
+	if (!item || !m_doc || !m_ScMW)
+		return;
+
+	const QVector<PageItem*> placements = m_model->placementsForItem(item);
+	const int current = placements.indexOf(item);
+	const int targetIndex = current + offset;
+	if (current < 0 || targetIndex < 0 || targetIndex >= placements.count())
+		return;
+
+	PageItem* target = placements.at(targetIndex);
+	navigateToItem(target);
+
+	if (m_scopeTabs->currentIndex() == 1)
+		rebuild();
+
+	selectPaletteItem(target, true);
+	updateSummary();
+}
+
+void ImagePalette::navigateToItem(PageItem* item)
+{
+	if (!item || !m_ScMW || !m_doc)
+		return;
+
+	if (item->OnMasterPage.isEmpty())
+	{
+		if (m_doc->masterPageMode())
+			m_ScMW->closeActiveWindowMasterPageEditor();
+		if (item->OwnPage >= 0)
+			m_ScMW->selectPagesFromOutlines(item->OwnPage);
+	}
+	else
+		m_ScMW->editMasterPagesStart(item->OnMasterPage);
+
+	m_ScMW->selectItemsFromOutlines(item, true, 1);
+}
+
+bool ImagePalette::loadPict(PageItem* item, const QString& filePath)
+{
+	if (!item || !m_doc || filePath.isEmpty())
+		return false;
+
+	item->Pfile = filePath;
+	const bool masterPageMode = !item->OnMasterPage.isEmpty();
+	const bool oldMasterPageMode = m_doc->masterPageMode();
+	if (masterPageMode != oldMasterPageMode)
+		m_doc->setMasterPageMode(masterPageMode);
+	m_doc->loadPict(filePath, item, true, true);
+	if (masterPageMode != oldMasterPageMode)
+		m_doc->setMasterPageMode(oldMasterPageMode);
+	return item->imageIsAvailable;
+}
+
 void ImagePalette::rebuild()
 {
 	m_refreshTimer->stop();
+	PageItem* paletteItem = currentItem();
+	const bool syncFromDocument = m_syncSelectionAfterRebuild;
+	m_syncSelectionAfterRebuild = false;
+
 	m_model->rebuild();
 	m_dirty = false;
-	syncSelectionFromDocument();
+	m_scopeTabs->setTabText(2, tr("Missing (%1)").arg(m_model->missingCount()));
+
+	const bool restored = !syncFromDocument && paletteItem && selectPaletteItem(paletteItem);
+	if (!restored)
+		syncSelectionFromDocument();
 	updateSummary();
 
 	if (!m_imageView->currentIndex().isValid())
 		clearInspector();
 }
 
+void ImagePalette::relinkMatchingImages(const QString& sourcePath, const QString& targetPath, bool brokenLinksOnly)
+{
+	if (!m_doc)
+		return;
+
+	const QFileInfo source(sourcePath);
+	const QFileInfo target(targetPath);
+	const int options = PageItemIterator::IterateInGroups
+		| PageItemIterator::IterateInDocItems
+		| PageItemIterator::IterateInMasterItems;
+	PageItemIterator iterator(m_doc, options);
+	for (PageItem* item = iterator.current(); item; item = iterator.next())
+	{
+		if (!item || item->itemType() != PageItem::ImageFrame || item->isLatexFrame() || item->isInlineImage)
+			continue;
+		if (brokenLinksOnly && item->imageIsAvailable)
+			continue;
+
+		const QFileInfo fileInfo(item->Pfile);
+		if (fileInfo.path() != source.path())
+			continue;
+
+		loadPict(item, QDir(target.path()).filePath(fileInfo.fileName()));
+	}
+}
+
 void ImagePalette::scheduleRebuild()
 {
 	if (!isVisible())
@@ -306,6 +826,29 @@
 	m_refreshTimer->start();
 }
 
+bool ImagePalette::selectPaletteItem(PageItem* item, bool ensureVisible)
+{
+	if (!item || !m_imageView->selectionModel())
+		return false;
+
+	const QModelIndex index = m_model->indexForItem(item);
+	if (!index.isValid())
+		return false;
+
+	QItemSelectionModel* selectionModel = m_imageView->selectionModel();
+	m_syncingSelection = true;
+	selectionModel->clearSelection();
+	selectionModel->select(index, QItemSelectionModel::Select);
+	selectionModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+	m_syncingSelection = false;
+
+	if (ensureVisible)
+		m_imageView->scrollTo(index, QAbstractItemView::EnsureVisible);
+	showItem(currentItem());
+	updateSummary();
+	return true;
+}
+
 void ImagePalette::showItem(PageItem* item)
 {
 	if (!item)
@@ -314,58 +857,102 @@
 		return;
 	}
 
-	QFileInfo fileInfo(item->Pfile);
+	const QString empty = QString::fromUtf8("—");
+	const QFileInfo fileInfo(item->Pfile);
 	if (item->isInlineImage)
 	{
-		m_nameValue->setText(tr("Embedded Image"));
-		m_pathValue->setText(tr("Embedded in document"));
+		m_nameText = tr("Embedded Image");
+		m_pathText = tr("Embedded in document");
 	}
 	else if (item->Pfile.isEmpty())
 	{
-		m_nameValue->setText(tr("Empty Image Frame"));
-		m_pathValue->setText(QString::fromUtf8("—"));
+		m_nameText = tr("Empty Image Frame");
+		m_pathText = empty;
 	}
 	else
 	{
-		m_nameValue->setText(fileInfo.fileName());
-		m_pathValue->setText(QDir::toNativeSeparators(item->Pfile));
+		m_nameText = fileInfo.fileName();
+		m_pathText = QDir::toNativeSeparators(item->Pfile);
 	}
 
 	if (!item->OnMasterPage.isEmpty())
-		m_pageValue->setText(tr("Master: %1").arg(item->OnMasterPage));
+		m_pageText = tr("Master %1").arg(item->OnMasterPage);
 	else if (item->OwnPage < 0)
-		m_pageValue->setText(tr("Not on a Page"));
+		m_pageText = tr("Not on a Page");
 	else
-		m_pageValue->setText(QString::number(item->OwnPage + 1));
-
-	m_frameValue->setText(item->itemName());
+		m_pageText = tr("Page %1").arg(item->OwnPage + 1);
+	m_frameText = item->itemName();
+	updateElidedLabels();
+
+	m_formatValue->setText(formatText(item));
+	m_frameSizeValue->setText(tr("%1 × %2%3")
+		.arg(item->width() * m_doc->unitRatio(), 0, 'f', 2)
+		.arg(item->height() * m_doc->unitRatio(), 0, 'f', 2)
+		.arg(unitGetSuffixFromIndex(m_doc->unitIndex())));
 
 	if (item->imageIsAvailable)
 	{
+		QString colorSpace;
+		const QString ext = fileInfo.suffix().toLower();
+		if ((extensionIndicatesPDF(ext) || extensionIndicatesEPSorPS(ext)) && item->pixm.imgInfo.type != ImageType7)
+			colorSpace = tr("Unknown");
+		else
+			colorSpace = colorSpaceText(item->pixm.imgInfo.colorspace);
+		m_colorSpaceValue->setText(colorSpace);
 		m_dimensionsValue->setText(tr("%1 × %2 px").arg(item->OrigW).arg(item->OrigH));
+		m_nativeResolutionValue->setText(tr("%1 × %2 dpi")
+			.arg(item->pixm.imgInfo.xres).arg(item->pixm.imgInfo.yres));
+
 		if (item->imageXScale() != 0.0 && item->imageYScale() != 0.0)
 		{
 			m_resolutionValue->setText(tr("%1 × %2 dpi")
 				.arg(qRound(72.0 / item->imageXScale()))
 				.arg(qRound(72.0 / item->imageYScale())));
+			m_scaleValue->setText(tr("%1 × %2 %")
+				.arg(item->imageXScale() * 100.0 / 72.0 * item->pixm.imgInfo.xres, 0, 'f', 1)
+				.arg(item->imageYScale() * 100.0 / 72.0 * item->pixm.imgInfo.yres, 0, 'f', 1));
+			m_scaledImageSizeValue->setText(tr("%1 × %2%3")
+				.arg(item->OrigW * item->imageXScale() * m_doc->unitRatio(), 0, 'f', 2)
+				.arg(item->OrigH * item->imageYScale() * m_doc->unitRatio(), 0, 'f', 2)
+				.arg(unitGetSuffixFromIndex(m_doc->unitIndex())));
 		}
 		else
-			m_resolutionValue->setText(QString::fromUtf8("—"));
+		{
+			m_resolutionValue->setText(empty);
+			m_scaleValue->setText(empty);
+			m_scaledImageSizeValue->setText(empty);
+		}
 	}
 	else
 	{
-		m_dimensionsValue->setText(QString::fromUtf8("—"));
-		m_resolutionValue->setText(QString::fromUtf8("—"));
+		m_colorSpaceValue->setText(empty);
+		m_dimensionsValue->setText(empty);
+		m_nativeResolutionValue->setText(empty);
+		m_resolutionValue->setText(empty);
+		m_scaleValue->setText(empty);
+		m_scaledImageSizeValue->setText(empty);
 	}
 
+	updatePlacementState(item);
+
 	if (item->isInlineImage)
 		m_statusValue->setText(tr("Embedded"));
 	else if (item->Pfile.isEmpty())
-		m_statusValue->setText(tr("Empty"));
+		m_statusValue->setText(tr("Empty Frame"));
 	else if (item->imageIsAvailable)
 		m_statusValue->setText(tr("Linked"));
-	else
+	else if (!QFileInfo::exists(item->Pfile))
 		m_statusValue->setText(tr("Missing"));
+	else
+		m_statusValue->setText(tr("Unavailable"));
+
+	{
+		const QSignalBlocker visibleBlocker(m_visibleCheck);
+		const QSignalBlocker printBlocker(m_printCheck);
+		m_visibleCheck->setChecked(item->imageVisible());
+		m_printCheck->setChecked(item->printEnabled());
+	}
+	updateActionState(item);
 }
 
 void ImagePalette::syncSelectionFromDocument()
@@ -374,23 +961,231 @@
 		return;
 
 	PageItem* item = m_doc->m_Selection->itemAt(0);
-	QModelIndex index = m_model->indexForItem(item);
-	if (!index.isValid())
+	selectPaletteItem(item, true);
+}
+
+void ImagePalette::updateActionState(PageItem* item)
+{
+	const bool hasItem = item != nullptr;
+	const bool externalFile = hasItem && !item->isInlineImage && !item->Pfile.isEmpty();
+	const bool available = hasItem && item->imageIsAvailable;
+	const bool raster = available && item->isRaster;
+	const bool missing = externalFile && !available && !QFileInfo::exists(item->Pfile);
+	const bool folderAvailable = externalFile && QDir(QFileInfo(item->Pfile).absolutePath()).exists();
+	const bool canSelect = hasItem && m_ScMW;
+	const bool canReplace = externalFile;
+	const bool canEdit = raster && m_ScMW;
+	const bool canShowProperties = hasItem && m_ScMW;
+
+	m_placementWidget->setVisible(hasItem);
+	m_visibleCheck->setEnabled(hasItem);
+	m_printCheck->setEnabled(hasItem);
+	m_detailsLabel->setVisible(hasItem);
+	m_detailsWidget->setVisible(hasItem);
+
+	m_selectButton->setEnabled(canSelect);
+	m_relinkButton->setEnabled(canReplace);
+	m_searchButton->setEnabled(missing);
+	m_revealButton->setEnabled(folderAvailable);
+	m_editButton->setEnabled(canEdit);
+	m_effectsButton->setEnabled(canEdit);
+	m_propertiesButton->setEnabled(canShowProperties);
+
+	QList<QPushButton*> visibleButtons;
+	if (m_scopeTabs->currentIndex() == 2 && missing)
+	{
+		if (missing)
+			visibleButtons << m_searchButton;
+		if (canReplace)
+			visibleButtons << m_relinkButton;
+		if (canSelect)
+			visibleButtons << m_selectButton;
+		if (folderAvailable)
+			visibleButtons << m_revealButton;
+		if (canShowProperties)
+			visibleButtons << m_propertiesButton;
+	}
+	else
+	{
+		if (canSelect)
+			visibleButtons << m_selectButton;
+		if (canReplace)
+			visibleButtons << m_relinkButton;
+		if (canEdit)
+			visibleButtons << m_editButton;
+		if (folderAvailable)
+			visibleButtons << m_revealButton;
+		if (canEdit)
+			visibleButtons << m_effectsButton;
+		if (canShowProperties)
+			visibleButtons << m_propertiesButton;
+	}
+
+	updateActionLayout(visibleButtons);
+	m_actionsWidget->setVisible(!visibleButtons.isEmpty());
+}
+
+void ImagePalette::updateActionLayout(const QList<QPushButton*>& visibleButtons)
+{
+	if (!m_actionLayout)
+		return;
+
+	while (QLayoutItem* item = m_actionLayout->takeAt(0))
+		delete item;
+
+	const QList<QPushButton*> allButtons { m_searchButton, m_selectButton, m_relinkButton, m_editButton, m_revealButton, m_effectsButton, m_propertiesButton };
+	for (QPushButton* button : allButtons)
+	{
+		if (button)
+			button->hide();
+	}
+
+	for (int i = 0; i < visibleButtons.count(); ++i)
+	{
+		QPushButton* button = visibleButtons.at(i);
+		if (!button)
+			continue;
+
+		int row = i / 2;
+		int column = i % 2;
+		if (i == visibleButtons.count() - 1 && (visibleButtons.count() % 2) == 1
+			&& button == m_propertiesButton && m_scopeTabs->currentIndex() == 2)
+			column = 1;
+
+		const Qt::Alignment alignment = column == 0 ? Qt::AlignRight : Qt::AlignLeft;
+		m_actionLayout->addWidget(button, row, column, alignment);
+		button->show();
+	}
+}
+
+void ImagePalette::updateInspectorLabelWidth()
+{
+	const QList<QLabel*> labels {
+		m_statusLabel,
+		m_placementLabel,
+		m_pageLabel,
+		m_frameLabel,
+		m_formatLabel,
+		m_colorSpaceLabel,
+		m_dimensionsLabel,
+		m_nativeResolutionLabel,
+		m_resolutionLabel,
+		m_scaleLabel,
+		m_frameSizeLabel,
+		m_scaledImageSizeLabel
+	};
+
+	int gutterWidth = 0;
+	for (QLabel* label : labels)
+	{
+		if (!label)
+			continue;
+		label->setMinimumWidth(0);
+		gutterWidth = qMax(gutterWidth, label->sizeHint().width());
+	}
+
+	const QList<QPushButton*> buttons { m_searchButton, m_selectButton, m_relinkButton, m_editButton, m_revealButton, m_effectsButton, m_propertiesButton };
+	for (QPushButton* button : buttons)
+	{
+		if (button)
+			gutterWidth = qMax(gutterWidth, button->minimumWidth());
+	}
+
+	for (QLabel* label : labels)
+	{
+		if (!label)
+			continue;
+		label->setMinimumWidth(gutterWidth);
+		label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+	}
+
+	if (m_infoLayout)
+		m_infoLayout->setColumnMinimumWidth(0, gutterWidth);
+	if (m_actionLayout)
+		m_actionLayout->setColumnMinimumWidth(0, gutterWidth);
+	if (m_detailsLayout)
+		m_detailsLayout->setColumnMinimumWidth(0, gutterWidth);
+}
+
+void ImagePalette::updateInspectorMinimumWidth()
+{
+	if (!m_inspector || !m_splitter || !m_imageView || !m_infoLayout || !m_actionLayout)
 		return;
 
-	const QSignalBlocker blocker(m_imageView->selectionModel());
-	m_imageView->setCurrentIndex(index);
-	showItem(item);
+	const int gutterWidth = m_infoLayout->columnMinimumWidth(0);
+	const int valueWidth = qMax(qMax(m_visibleCheck->sizeHint().width(), m_printCheck->sizeHint().width()),
+		qMax(m_relinkButton->minimumWidth(), qMax(m_revealButton->minimumWidth(), m_propertiesButton->minimumWidth())));
+	const int columnSpacing = qMax(m_infoLayout->horizontalSpacing(), m_actionLayout->horizontalSpacing());
+	const QMargins margins = m_inspector->layout()->contentsMargins();
+	const int inspectorMinimumWidth = gutterWidth + columnSpacing + valueWidth + margins.left() + margins.right();
+
+	m_inspector->setMinimumWidth(inspectorMinimumWidth);
+
+	const int imageViewMinimumWidth = qMax(m_imageView->minimumWidth(), m_imageView->minimumSizeHint().width());
+	const int splitterMinimumWidth = imageViewMinimumWidth + m_splitter->handleWidth() + inspectorMinimumWidth;
+	m_splitter->setMinimumWidth(splitterMinimumWidth);
+}
+
+void ImagePalette::updateElidedLabels()
+{
+	auto setElidedText = [](QLabel* label, const QString& fullText)
+	{
+		const int availableWidth = qMax(20, label->contentsRect().width());
+		label->setText(label->fontMetrics().elidedText(fullText, Qt::ElideMiddle, availableWidth));
+		label->setToolTip(fullText);
+	};
+
+	setElidedText(m_nameValue, m_nameText);
+	setElidedText(m_pathValue, m_pathText);
+	setElidedText(m_pageValue, m_pageText);
+	setElidedText(m_frameValue, m_frameText);
+}
+
+void ImagePalette::updatePlacementState(PageItem* item)
+{
+	const QVector<PageItem*> placements = m_model->placementsForItem(item);
+	const int count = placements.count();
+	const int current = placements.indexOf(item);
+	if (count <= 0 || current < 0)
+	{
+		m_placementPositionLabel->setText(QString::fromUtf8("—"));
+		m_previousPlacementButton->hide();
+		m_nextPlacementButton->hide();
+		return;
+	}
+
+	m_placementPositionLabel->setText(tr("%1 of %2").arg(current + 1).arg(count));
+	const bool multiple = count > 1;
+	m_previousPlacementButton->setVisible(multiple);
+	m_nextPlacementButton->setVisible(multiple);
+	m_previousPlacementButton->setEnabled(current > 0);
+	m_nextPlacementButton->setEnabled(current + 1 < count);
 }
 
 void ImagePalette::updateSummary()
 {
+	m_scopeTabs->setTabText(2, tr("Missing (%1)").arg(m_model->missingCount()));
 	if (!m_doc)
 	{
-		m_summaryLabel->setText(tr("No document"));
+		m_summaryLabel->setText(tr("No document open"));
 		return;
 	}
 
 	const int count = m_model->rowCount();
-	m_summaryLabel->setText(count == 1 ? tr("1 image frame") : tr("%1 image frames").arg(count));
+	if (count == 0)
+	{
+		if (m_scopeTabs->currentIndex() == 2)
+			m_summaryLabel->setText(tr("No missing images"));
+		else if (m_scopeTabs->currentIndex() == 1)
+			m_summaryLabel->setText(tr("No image frames on this page"));
+		else
+			m_summaryLabel->setText(tr("No image frames in this document"));
+		return;
+	}
+
+	const bool selected = m_imageView->currentIndex().isValid();
+	if (count == 1)
+		m_summaryLabel->setText(selected ? tr("1 image frame · 1 selected") : tr("1 image frame"));
+	else
+		m_summaryLabel->setText(selected ? tr("%1 image frames · 1 selected").arg(count) : tr("%1 image frames").arg(count));
 }
img3-images-folder-browsing-v1.04.patch (39,015 bytes)   
Index: scribus/CMakeLists_Sources.txt
===================================================================
--- scribus/CMakeLists_Sources.txt	(revision 27816)
+++ scribus/CMakeLists_Sources.txt	(working copy)
@@ -367,7 +367,10 @@
 	ui/imagelayers.cpp
 	ui/imageinfodialog.cpp
 	ui/imagepalette.cpp
+	ui/imagepalette_browse.cpp
 	ui/imagepalette_documentmodel.cpp
+	ui/imagepalette_filemodel.cpp
+	ui/imagepalette_fileworker.cpp
 	ui/inlinepalette.cpp
 	ui/insertTable.cpp
 	ui/insertaframe.cpp
Index: scribus/ui/imagepalette.cpp
===================================================================
--- scribus/ui/imagepalette.cpp	(revision 27816)
+++ scribus/ui/imagepalette.cpp	(working copy)
@@ -25,6 +25,7 @@
 #include <QSignalBlocker>
 #include <QSizePolicy>
 #include <QSplitter>
+#include <QStackedWidget>
 #include <QStyle>
 #include <QTabBar>
 #include <QTimer>
@@ -32,6 +33,7 @@
 #include <QUrl>
 #include <QVBoxLayout>
 
+#include "imagepalette_browse.h"
 #include "imagepalette_documentmodel.h"
 #include "pageitem.h"
 #include "pageitemiterator.h"
@@ -59,17 +61,33 @@
 	layout->setContentsMargins(6, 6, 6, 6);
 	layout->setSpacing(6);
 
-	m_scopeTabs = new QTabBar(panel);
+	m_modeTabs = new QTabBar(panel);
+	m_modeTabs->setDocumentMode(true);
+	m_modeTabs->setExpanding(false);
+	m_modeTabs->addTab(QString());
+	m_modeTabs->addTab(QString());
+	layout->addWidget(m_modeTabs);
+
+	m_modeStack = new QStackedWidget(panel);
+	layout->addWidget(m_modeStack, 1);
+
+	auto* documentPage = new QWidget(m_modeStack);
+	auto* documentLayout = new QVBoxLayout(documentPage);
+	documentLayout->setContentsMargins(0, 0, 0, 0);
+	documentLayout->setSpacing(6);
+	m_modeStack->addWidget(documentPage);
+
+	m_scopeTabs = new QTabBar(documentPage);
 	m_scopeTabs->setDocumentMode(true);
 	m_scopeTabs->setExpanding(false);
 	m_scopeTabs->addTab(QString());
 	m_scopeTabs->addTab(QString());
 	m_scopeTabs->addTab(QString());
-	layout->addWidget(m_scopeTabs);
+	documentLayout->addWidget(m_scopeTabs);
 
-	m_splitter = new QSplitter(Qt::Horizontal, panel);
+	m_splitter = new QSplitter(Qt::Horizontal, documentPage);
 	m_splitter->setChildrenCollapsible(false);
-	layout->addWidget(m_splitter, 1);
+	documentLayout->addWidget(m_splitter, 1);
 
 	m_imageView = new QListView(m_splitter);
 	m_imageView->setMinimumSize(180, 180);
@@ -243,14 +261,18 @@
 	m_splitter->setStretchFactor(0, 3);
 	m_splitter->setStretchFactor(1, 2);
 
-	m_summaryLabel = new QLabel(panel);
-	layout->addWidget(m_summaryLabel);
+	m_summaryLabel = new QLabel(documentPage);
+	documentLayout->addWidget(m_summaryLabel);
+
+	m_browsePage = new ImagePaletteBrowse(m_modeStack);
+	m_modeStack->addWidget(m_browsePage);
 
 	setWidget(panel);
 
 	m_refreshTimer->setSingleShot(true);
 	m_refreshTimer->setInterval(75);
 
+	connect(m_modeTabs, &QTabBar::currentChanged, this, &ImagePalette::modeChanged);
 	connect(m_scopeTabs, &QTabBar::currentChanged, this, &ImagePalette::scopeChanged);
 	connect(m_imageView->selectionModel(), &QItemSelectionModel::selectionChanged,
 		this, &ImagePalette::handleViewSelectionChanged);
@@ -291,8 +313,9 @@
 {
 	if (doc == static_cast<ScribusDoc*>(m_doc))
 	{
+		m_browsePage->setDocumentDirectory(documentDirectory());
 		m_dirty = true;
-		if (isVisible())
+		if (isVisible() && m_modeTabs->currentIndex() == 0)
 			rebuild();
 		return;
 	}
@@ -305,6 +328,7 @@
 
 	m_doc = doc;
 	m_model->setDocument(m_doc);
+	m_browsePage->setDocumentDirectory(documentDirectory());
 	m_syncSelectionAfterRebuild = true;
 
 	if (m_doc)
@@ -314,7 +338,7 @@
 	}
 
 	m_dirty = true;
-	if (isVisible())
+	if (isVisible() && m_modeTabs->currentIndex() == 0)
 		rebuild();
 	else
 		updateSummary();
@@ -332,6 +356,7 @@
 	m_syncSelectionAfterRebuild = false;
 	m_doc = nullptr;
 	m_model->setDocument(nullptr);
+	m_browsePage->setDocumentDirectory(QString());
 	m_dirty = false;
 	clearInspector();
 	updateSummary();
@@ -350,6 +375,28 @@
 	updateElidedLabels();
 }
 
+void ImagePalette::modeChanged(int index)
+{
+	if (!m_modeStack)
+		return;
+
+	m_modeStack->setCurrentIndex(index);
+	if (index == 1)
+	{
+		m_refreshTimer->stop();
+		m_browsePage->ensureInitialized(documentDirectory());
+		return;
+	}
+
+	if (m_dirty)
+		rebuild();
+	else
+	{
+		syncSelectionFromDocument();
+		updateSummary();
+	}
+}
+
 void ImagePalette::scopeChanged(int index)
 {
 	ImagePaletteDocumentModel::Scope scope = ImagePaletteDocumentModel::Scope::Document;
@@ -386,6 +433,9 @@
 
 void ImagePalette::handleDocumentSelectionChanged()
 {
+	if (m_modeTabs->currentIndex() != 0)
+		return;
+
 	if (m_scopeTabs->currentIndex() == 1)
 	{
 		m_syncSelectionAfterRebuild = true;
@@ -410,9 +460,17 @@
 
 void ImagePalette::handleVisibilityChanged(bool visible)
 {
-	if (!visible || !m_dirty)
+	if (!visible)
+		return;
+
+	if (m_modeTabs->currentIndex() == 1)
+	{
+		m_browsePage->ensureInitialized(documentDirectory());
 		return;
+	}
 
+	if (!m_dirty)
+		return;
 	m_refreshTimer->stop();
 	rebuild();
 }
@@ -611,6 +669,17 @@
 	updateActionState(nullptr);
 }
 
+QString ImagePalette::documentDirectory() const
+{
+	if (!m_doc || !m_doc->hasName)
+		return QString();
+
+	const QString fileName = m_doc->documentFileName();
+	if (fileName.isEmpty())
+		return QString();
+	return QFileInfo(fileName).absolutePath();
+}
+
 PageItem* ImagePalette::currentItem() const
 {
 	return m_model->itemAt(m_imageView->currentIndex());
@@ -648,6 +717,8 @@
 void ImagePalette::languageChange()
 {
 	setWindowTitle(tr("Images"));
+	m_modeTabs->setTabText(0, tr("Document"));
+	m_modeTabs->setTabText(1, tr("Browse"));
 	m_scopeTabs->setTabText(0, tr("Document"));
 	m_scopeTabs->setTabText(1, tr("Current Page"));
 	m_scopeTabs->setTabText(2, tr("Missing (%1)").arg(m_model->missingCount()));
@@ -711,7 +782,7 @@
 	updateInspectorLabelWidth();
 	updateInspectorMinimumWidth();
 	m_dirty = true;
-	if (isVisible())
+	if (isVisible() && m_modeTabs->currentIndex() == 0)
 		rebuild();
 	else
 		updateSummary();
@@ -821,7 +892,7 @@
 
 void ImagePalette::scheduleRebuild()
 {
-	if (!isVisible())
+	if (!isVisible() || m_modeTabs->currentIndex() != 0)
 		return;
 	m_refreshTimer->start();
 }
@@ -1124,6 +1195,7 @@
 	const int imageViewMinimumWidth = qMax(m_imageView->minimumWidth(), m_imageView->minimumSizeHint().width());
 	const int splitterMinimumWidth = imageViewMinimumWidth + m_splitter->handleWidth() + inspectorMinimumWidth;
 	m_splitter->setMinimumWidth(splitterMinimumWidth);
+	m_modeStack->setMinimumWidth(splitterMinimumWidth);
 }
 
 void ImagePalette::updateElidedLabels()
Index: scribus/ui/imagepalette.h
===================================================================
--- scribus/ui/imagepalette.h	(revision 27816)
+++ scribus/ui/imagepalette.h	(working copy)
@@ -22,12 +22,14 @@
 class QGridLayout;
 class QResizeEvent;
 class QSplitter;
+class QStackedWidget;
 class QTabBar;
 class QTimer;
 class QToolButton;
 class QWidget;
 class QItemSelection;
 
+class ImagePaletteBrowse;
 class ImagePaletteDocumentModel;
 class PageItem;
 class ScribusDoc;
@@ -52,6 +54,7 @@
 	void resizeEvent(QResizeEvent* event) override;
 
 private slots:
+	void modeChanged(int index);
 	void scopeChanged(int index);
 	void handleDocumentChanged();
 	void handleDocumentSelectionChanged();
@@ -72,6 +75,7 @@
 
 private:
 	void clearInspector();
+	QString documentDirectory() const;
 	PageItem* currentItem() const;
 	QString formatText(PageItem* item) const;
 	void languageChange();
@@ -100,6 +104,9 @@
 	bool m_syncSelectionAfterRebuild {false};
 	bool m_syncingSelection {false};
 
+	QTabBar* m_modeTabs {nullptr};
+	QStackedWidget* m_modeStack {nullptr};
+	ImagePaletteBrowse* m_browsePage {nullptr};
 	QTabBar* m_scopeTabs {nullptr};
 	QListView* m_imageView {nullptr};
 	QSplitter* m_splitter {nullptr};
Index: scribus/ui/imagepalette_browse.cpp
===================================================================
--- scribus/ui/imagepalette_browse.cpp	(nonexistent)
+++ scribus/ui/imagepalette_browse.cpp	(working copy)
@@ -0,0 +1,478 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#include "imagepalette_browse.h"
+
+#include <QAbstractItemView>
+#include <QDir>
+#include <QEvent>
+#include <QFileDialog>
+#include <QFileInfo>
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QItemSelectionModel>
+#include <QLabel>
+#include <QLineEdit>
+#include <QListView>
+#include <QResizeEvent>
+#include <QSizePolicy>
+#include <QSplitter>
+#include <QStandardPaths>
+#include <QThread>
+#include <QToolButton>
+#include <QVBoxLayout>
+
+#include "iconmanager.h"
+#include "imagepalette_fileworker.h"
+#include "loadsaveplugin.h"
+#include "plugins/formatidlist.h"
+#include "prefscontext.h"
+#include "prefsfile.h"
+#include "prefsmanager.h"
+#include "util_color.h"
+#include "util_formats.h"
+
+ImagePaletteBrowse::ImagePaletteBrowse(QWidget* parent)
+	: QWidget(parent),
+	  m_model(new ImagePaletteFileModel(this))
+{
+	auto* layout = new QVBoxLayout(this);
+	layout->setContentsMargins(0, 0, 0, 0);
+	layout->setSpacing(6);
+
+	auto* folderLayout = new QHBoxLayout();
+	folderLayout->setContentsMargins(0, 0, 0, 0);
+	m_folderLabel = new QLabel(this);
+	m_folderEdit = new QLineEdit(this);
+	m_folderButton = new QToolButton(this);
+	m_folderButton->setText(QString::fromUtf8("…"));
+	folderLayout->addWidget(m_folderLabel);
+	folderLayout->addWidget(m_folderEdit, 1);
+	folderLayout->addWidget(m_folderButton);
+	layout->addLayout(folderLayout);
+
+	m_splitter = new QSplitter(Qt::Horizontal, this);
+	m_splitter->setChildrenCollapsible(false);
+	layout->addWidget(m_splitter, 1);
+
+	m_imageView = new QListView(m_splitter);
+	m_imageView->setMinimumSize(180, 180);
+	m_imageView->setModel(m_model);
+	m_imageView->setIconSize(QSize(128, 128));
+	m_imageView->setGridSize(QSize(156, 166));
+	m_imageView->setResizeMode(QListView::Adjust);
+	m_imageView->setViewMode(QListView::IconMode);
+	m_imageView->setMovement(QListView::Static);
+	m_imageView->setFlow(QListView::LeftToRight);
+	m_imageView->setWrapping(true);
+	m_imageView->setWordWrap(false);
+	m_imageView->setTextElideMode(Qt::ElideMiddle);
+	m_imageView->setEditTriggers(QAbstractItemView::NoEditTriggers);
+	m_imageView->setSelectionMode(QAbstractItemView::SingleSelection);
+	m_model->setPlaceholderIcon(IconManager::instance().loadIcon("image-collection"));
+
+	m_inspector = new QWidget(m_splitter);
+	auto* inspectorLayout = new QVBoxLayout(m_inspector);
+	inspectorLayout->setContentsMargins(8, 0, 8, 0);
+
+	m_nameValue = new QLabel(m_inspector);
+	QFont nameFont = m_nameValue->font();
+	nameFont.setBold(true);
+	m_nameValue->setFont(nameFont);
+	m_nameValue->setWordWrap(false);
+	m_nameValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+	inspectorLayout->addWidget(m_nameValue);
+
+	m_pathValue = new QLabel(m_inspector);
+	m_pathValue->setWordWrap(false);
+	m_pathValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
+	inspectorLayout->addWidget(m_pathValue);
+	inspectorLayout->addStretch(1);
+
+	m_detailsLabel = new QLabel(m_inspector);
+	QFont detailsFont = m_detailsLabel->font();
+	detailsFont.setBold(true);
+	m_detailsLabel->setFont(detailsFont);
+	inspectorLayout->addWidget(m_detailsLabel);
+
+	auto* detailsWidget = new QWidget(m_inspector);
+	m_detailsLayout = new QGridLayout(detailsWidget);
+	m_detailsLayout->setContentsMargins(0, 0, 0, 0);
+	m_detailsLayout->setHorizontalSpacing(12);
+	m_detailsLayout->setColumnStretch(0, 0);
+	m_detailsLayout->setColumnStretch(1, 1);
+
+	m_formatLabel = new QLabel(detailsWidget);
+	m_formatValue = new QLabel(detailsWidget);
+	m_formatValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_formatLabel, 0, 0);
+	m_detailsLayout->addWidget(m_formatValue, 0, 1);
+
+	m_colorSpaceLabel = new QLabel(detailsWidget);
+	m_colorSpaceValue = new QLabel(detailsWidget);
+	m_colorSpaceValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_colorSpaceLabel, 1, 0);
+	m_detailsLayout->addWidget(m_colorSpaceValue, 1, 1);
+
+	m_dimensionsLabel = new QLabel(detailsWidget);
+	m_dimensionsValue = new QLabel(detailsWidget);
+	m_dimensionsValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_dimensionsLabel, 2, 0);
+	m_detailsLayout->addWidget(m_dimensionsValue, 2, 1);
+
+	m_nativeResolutionLabel = new QLabel(detailsWidget);
+	m_nativeResolutionValue = new QLabel(detailsWidget);
+	m_nativeResolutionValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_detailsLayout->addWidget(m_nativeResolutionLabel, 3, 0);
+	m_detailsLayout->addWidget(m_nativeResolutionValue, 3, 1);
+	inspectorLayout->addWidget(detailsWidget);
+	inspectorLayout->addStretch(12);
+
+	m_splitter->setStretchFactor(0, 3);
+	m_splitter->setStretchFactor(1, 2);
+
+	m_summaryLabel = new QLabel(this);
+	layout->addWidget(m_summaryLabel);
+
+	connect(m_folderButton, &QToolButton::clicked, this, &ImagePaletteBrowse::chooseFolder);
+	connect(m_folderEdit, &QLineEdit::returnPressed, this, &ImagePaletteBrowse::folderEntered);
+	connect(m_imageView->selectionModel(), &QItemSelectionModel::currentChanged,
+		this, &ImagePaletteBrowse::currentFileChanged);
+	connect(m_splitter, &QSplitter::splitterMoved, this, [this](int, int) { updateElidedLabels(); });
+
+	languageChange();
+	showFile(QModelIndex());
+	updateSummary();
+}
+
+ImagePaletteBrowse::~ImagePaletteBrowse()
+{
+	if (!m_workerThread)
+		return;
+
+	++m_generation;
+	if (m_worker)
+		m_worker->setRequestedGeneration(m_generation);
+	m_workerThread->quit();
+	m_workerThread->wait();
+}
+
+void ImagePaletteBrowse::ensureInitialized(const QString& documentDirectory)
+{
+	setDocumentDirectory(documentDirectory);
+	if (m_initialized)
+		return;
+
+	startWorker();
+	m_initialized = true;
+	refreshSupportedExtensions();
+	setFolder(initialFolder());
+}
+
+void ImagePaletteBrowse::setDocumentDirectory(const QString& documentDirectory)
+{
+	m_documentDirectory = documentDirectory;
+}
+
+void ImagePaletteBrowse::changeEvent(QEvent* event)
+{
+	QWidget::changeEvent(event);
+	if (event->type() == QEvent::LanguageChange)
+		languageChange();
+}
+
+void ImagePaletteBrowse::resizeEvent(QResizeEvent* event)
+{
+	QWidget::resizeEvent(event);
+	updateElidedLabels();
+}
+
+void ImagePaletteBrowse::chooseFolder()
+{
+	QString startPath = m_folderPath;
+	if (startPath.isEmpty() || !QDir(startPath).exists())
+		startPath = initialFolder();
+	const QString folderPath = QFileDialog::getExistingDirectory(this, tr("Choose Image Folder"), startPath);
+	if (!folderPath.isEmpty())
+		setFolder(folderPath);
+}
+
+void ImagePaletteBrowse::folderEntered()
+{
+	setFolder(QDir::fromNativeSeparators(m_folderEdit->text().trimmed()));
+}
+
+void ImagePaletteBrowse::currentFileChanged(const QModelIndex& current, const QModelIndex&)
+{
+	showFile(current);
+}
+
+void ImagePaletteBrowse::handleFilesFound(quint64 generation, const QString& folderPath, const QStringList& filePaths)
+{
+	if (generation != m_generation || folderPath != m_folderPath)
+		return;
+
+	m_scanning = false;
+	m_loadingThumbnails = !filePaths.isEmpty();
+	m_model->setFiles(filePaths);
+	if (!filePaths.isEmpty())
+		m_imageView->setCurrentIndex(m_model->index(0, 0));
+	else
+		showFile(QModelIndex());
+	updateSummary();
+}
+
+void ImagePaletteBrowse::handleThumbnailLoaded(quint64 generation, const QString& filePath, const QImage& image,
+	const ImagePaletteFileMetadata& metadata)
+{
+	if (generation != m_generation)
+		return;
+
+	m_model->setThumbnail(filePath, image, metadata);
+	const QModelIndex current = m_imageView->currentIndex();
+	if (m_model->filePathAt(current) == filePath)
+		showFile(current);
+}
+
+void ImagePaletteBrowse::handleLoadingFinished(quint64 generation)
+{
+	if (generation != m_generation)
+		return;
+
+	m_loadingThumbnails = false;
+	updateSummary();
+}
+
+QString ImagePaletteBrowse::initialFolder() const
+{
+	PrefsContext* prefs = PrefsManager::instance().prefsFile->getContext("ImagePalette");
+	const QString savedFolder = prefs->get("browseFolder");
+	if (!savedFolder.isEmpty() && QDir(savedFolder).exists())
+		return savedFolder;
+	if (!m_documentDirectory.isEmpty() && QDir(m_documentDirectory).exists())
+		return m_documentDirectory;
+
+	QString folder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
+	if (folder.isEmpty() || !QDir(folder).exists())
+		folder = QDir::homePath();
+	return folder;
+}
+
+QString ImagePaletteBrowse::formatText(const QString& filePath, const ImagePaletteFileMetadata& metadata) const
+{
+	const QString extension = QFileInfo(filePath).suffix().toLower();
+	if (!metadata.valid)
+		return extension.isEmpty() ? tr("Unknown") : extension.toUpper();
+
+	switch (metadata.type)
+	{
+	case 0:
+		return tr("JPEG");
+	case 1:
+		return tr("TIFF");
+	case 2:
+		return tr("PSD");
+	case 3:
+		return tr("EPS/PS");
+	case 4:
+		return tr("PDF");
+	case 5:
+		return tr("JPEG 2000");
+	case 6:
+		return extension.toUpper();
+	case 7:
+		return tr("Embedded PSD");
+	default:
+		return extension.isEmpty() ? tr("Unknown") : extension.toUpper();
+	}
+}
+
+void ImagePaletteBrowse::languageChange()
+{
+	m_folderLabel->setText(tr("Folder:"));
+	m_folderButton->setToolTip(tr("Choose an image folder."));
+	m_folderEdit->setToolTip(tr("Folder whose supported image files are shown below."));
+	m_detailsLabel->setText(tr("Details"));
+	m_formatLabel->setText(tr("Format:"));
+	m_colorSpaceLabel->setText(tr("Color Space:"));
+	m_dimensionsLabel->setText(tr("Pixel Size:"));
+	m_nativeResolutionLabel->setText(tr("Native DPI:"));
+	updateInspectorLabelWidth();
+	showFile(m_imageView->currentIndex());
+	updateSummary();
+}
+
+void ImagePaletteBrowse::refreshSupportedExtensions()
+{
+	m_supportedExtensions.clear();
+	const QString filter = FormatsManager::instance()->extensionListForFormat(FormatsManager::IMAGESIMGFRAME, 0);
+	const QStringList patterns = filter.split(QLatin1Char(' '), Qt::SkipEmptyParts);
+	for (QString pattern : patterns)
+	{
+		pattern = pattern.trimmed().toLower();
+		if (pattern.startsWith(QStringLiteral("*.")))
+			pattern.remove(0, 2);
+		if (!pattern.isEmpty() && !pattern.contains(QLatin1Char('*')))
+			m_supportedExtensions.append(pattern);
+	}
+
+	m_supportedExtensions.append(QStringLiteral("svg"));
+	m_previewExtensions = LoadSavePlugin::getExtensionsForPreview(FORMATID_FIRSTUSER);
+	for (QString& extension : m_previewExtensions)
+		extension = extension.toLower();
+	m_supportedExtensions.append(m_previewExtensions);
+	m_supportedExtensions.removeDuplicates();
+	m_supportedExtensions.sort(Qt::CaseInsensitive);
+}
+
+void ImagePaletteBrowse::setFolder(const QString& folderPath)
+{
+	const QString cleanInput = folderPath.trimmed();
+	const QDir dir(cleanInput);
+	if (cleanInput.isEmpty() || !dir.exists())
+	{
+		++m_generation;
+		m_worker->setRequestedGeneration(m_generation);
+		m_invalidFolder = true;
+		m_scanning = false;
+		m_loadingThumbnails = false;
+		m_model->clear();
+		showFile(QModelIndex());
+		updateSummary();
+		return;
+	}
+
+	m_invalidFolder = false;
+	m_folderPath = QDir::cleanPath(dir.absolutePath());
+	m_folderEdit->setText(QDir::toNativeSeparators(m_folderPath));
+	PrefsManager::instance().prefsFile->getContext("ImagePalette")->set("browseFolder", m_folderPath);
+
+	++m_generation;
+	m_worker->setRequestedGeneration(m_generation);
+	m_scanning = true;
+	m_loadingThumbnails = false;
+	m_model->clear();
+	showFile(QModelIndex());
+	updateSummary();
+	emit scanRequested(m_generation, m_folderPath, m_supportedExtensions, m_previewExtensions, m_imageView->iconSize().width());
+}
+
+void ImagePaletteBrowse::startWorker()
+{
+	if (m_workerThread)
+		return;
+
+	qRegisterMetaType<ImagePaletteFileMetadata>("ImagePaletteFileMetadata");
+	m_workerThread = new QThread(this);
+	m_worker = new ImagePaletteFileWorker();
+	m_worker->moveToThread(m_workerThread);
+	connect(m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater);
+	connect(this, &ImagePaletteBrowse::scanRequested, m_worker, &ImagePaletteFileWorker::scanFolder, Qt::QueuedConnection);
+	connect(m_worker, &ImagePaletteFileWorker::filesFound, this, &ImagePaletteBrowse::handleFilesFound, Qt::QueuedConnection);
+	connect(m_worker, &ImagePaletteFileWorker::thumbnailLoaded, this, &ImagePaletteBrowse::handleThumbnailLoaded, Qt::QueuedConnection);
+	connect(m_worker, &ImagePaletteFileWorker::loadingFinished, this, &ImagePaletteBrowse::handleLoadingFinished, Qt::QueuedConnection);
+	m_workerThread->start();
+}
+
+void ImagePaletteBrowse::showFile(const QModelIndex& index)
+{
+	const QString empty = QString::fromUtf8("—");
+	const QString filePath = m_model->filePathAt(index);
+	if (filePath.isEmpty())
+	{
+		m_nameText = empty;
+		m_pathText = empty;
+		m_formatValue->setText(empty);
+		m_colorSpaceValue->setText(empty);
+		m_dimensionsValue->setText(empty);
+		m_nativeResolutionValue->setText(empty);
+		updateElidedLabels();
+		return;
+	}
+
+	const QFileInfo fileInfo(filePath);
+	m_nameText = fileInfo.fileName();
+	m_pathText = QDir::toNativeSeparators(filePath);
+	updateElidedLabels();
+
+	const ImagePaletteFileMetadata metadata = m_model->metadataAt(index);
+	m_formatValue->setText(formatText(filePath, metadata));
+	if (!m_model->metadataReadyAt(index) || !metadata.valid)
+	{
+		m_colorSpaceValue->setText(empty);
+		m_dimensionsValue->setText(empty);
+		m_nativeResolutionValue->setText(empty);
+		return;
+	}
+
+	const QString extension = fileInfo.suffix().toLower();
+	if (metadata.colorSpace < 0 || ((extensionIndicatesPDF(extension) || extensionIndicatesEPSorPS(extension)) && metadata.type != 7))
+		m_colorSpaceValue->setText(tr("Unknown"));
+	else
+		m_colorSpaceValue->setText(colorSpaceText(metadata.colorSpace));
+	m_dimensionsValue->setText(metadata.width > 0 && metadata.height > 0
+		? tr("%1 × %2 px").arg(metadata.width).arg(metadata.height) : empty);
+	m_nativeResolutionValue->setText(metadata.xResolution > 0 && metadata.yResolution > 0
+		? tr("%1 × %2 dpi").arg(metadata.xResolution).arg(metadata.yResolution) : empty);
+}
+
+void ImagePaletteBrowse::updateElidedLabels()
+{
+	auto setElidedText = [](QLabel* label, const QString& fullText)
+	{
+		const int availableWidth = qMax(20, label->contentsRect().width());
+		label->setText(label->fontMetrics().elidedText(fullText, Qt::ElideMiddle, availableWidth));
+		label->setToolTip(fullText);
+	};
+
+	setElidedText(m_nameValue, m_nameText);
+	setElidedText(m_pathValue, m_pathText);
+}
+
+void ImagePaletteBrowse::updateInspectorLabelWidth()
+{
+	const QList<QLabel*> labels { m_formatLabel, m_colorSpaceLabel, m_dimensionsLabel, m_nativeResolutionLabel };
+	int gutterWidth = 0;
+	for (QLabel* label : labels)
+	{
+		label->setMinimumWidth(0);
+		gutterWidth = qMax(gutterWidth, label->sizeHint().width());
+	}
+	for (QLabel* label : labels)
+	{
+		label->setMinimumWidth(gutterWidth);
+		label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+	}
+	m_detailsLayout->setColumnMinimumWidth(0, gutterWidth);
+}
+
+void ImagePaletteBrowse::updateSummary()
+{
+	if (m_invalidFolder)
+	{
+		m_summaryLabel->setText(tr("Folder not found"));
+		return;
+	}
+	if (m_scanning)
+	{
+		m_summaryLabel->setText(tr("Scanning folder…"));
+		return;
+	}
+
+	const int count = m_model->rowCount();
+	if (count == 0)
+	{
+		m_summaryLabel->setText(m_folderPath.isEmpty() ? tr("Choose a folder") : tr("No supported images in this folder"));
+		return;
+	}
+	if (m_loadingThumbnails)
+	{
+		m_summaryLabel->setText(tr("%1 images · loading thumbnails…").arg(count));
+		return;
+	}
+	m_summaryLabel->setText(count == 1 ? tr("1 image") : tr("%1 images").arg(count));
+}
Index: scribus/ui/imagepalette_browse.h
===================================================================
--- scribus/ui/imagepalette_browse.h	(nonexistent)
+++ scribus/ui/imagepalette_browse.h	(working copy)
@@ -0,0 +1,107 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#ifndef IMAGEPALETTE_BROWSE_H
+#define IMAGEPALETTE_BROWSE_H
+
+#include <QImage>
+#include <QString>
+#include <QStringList>
+#include <QWidget>
+
+#include "imagepalette_filemodel.h"
+
+class QEvent;
+class QLabel;
+class QGridLayout;
+class QLineEdit;
+class QListView;
+class QModelIndex;
+class QResizeEvent;
+class QSplitter;
+class QThread;
+class QToolButton;
+
+class ImagePaletteFileWorker;
+
+class ImagePaletteBrowse : public QWidget
+{
+	Q_OBJECT
+
+public:
+	explicit ImagePaletteBrowse(QWidget* parent = nullptr);
+	~ImagePaletteBrowse() override;
+
+	void ensureInitialized(const QString& documentDirectory);
+	void setDocumentDirectory(const QString& documentDirectory);
+
+signals:
+	void scanRequested(quint64 generation, const QString& folderPath, const QStringList& supportedExtensions,
+		const QStringList& previewExtensions, int thumbnailSize);
+
+protected:
+	void changeEvent(QEvent* event) override;
+	void resizeEvent(QResizeEvent* event) override;
+
+private slots:
+	void chooseFolder();
+	void folderEntered();
+	void currentFileChanged(const QModelIndex& current, const QModelIndex& previous);
+	void handleFilesFound(quint64 generation, const QString& folderPath, const QStringList& filePaths);
+	void handleThumbnailLoaded(quint64 generation, const QString& filePath, const QImage& image,
+		const ImagePaletteFileMetadata& metadata);
+	void handleLoadingFinished(quint64 generation);
+
+private:
+	QString initialFolder() const;
+	QString formatText(const QString& filePath, const ImagePaletteFileMetadata& metadata) const;
+	void languageChange();
+	void refreshSupportedExtensions();
+	void setFolder(const QString& folderPath);
+	void showFile(const QModelIndex& index);
+	void startWorker();
+	void updateElidedLabels();
+	void updateInspectorLabelWidth();
+	void updateSummary();
+
+	ImagePaletteFileModel* m_model {nullptr};
+	ImagePaletteFileWorker* m_worker {nullptr};
+	QThread* m_workerThread {nullptr};
+	quint64 m_generation {0};
+	bool m_initialized {false};
+	bool m_scanning {false};
+	bool m_loadingThumbnails {false};
+	bool m_invalidFolder {false};
+	QString m_documentDirectory;
+	QString m_folderPath;
+	QStringList m_supportedExtensions;
+	QStringList m_previewExtensions;
+
+	QLabel* m_folderLabel {nullptr};
+	QLineEdit* m_folderEdit {nullptr};
+	QToolButton* m_folderButton {nullptr};
+	QSplitter* m_splitter {nullptr};
+	QListView* m_imageView {nullptr};
+	QWidget* m_inspector {nullptr};
+	QLabel* m_nameValue {nullptr};
+	QLabel* m_pathValue {nullptr};
+	QString m_nameText;
+	QString m_pathText;
+	QLabel* m_detailsLabel {nullptr};
+	QGridLayout* m_detailsLayout {nullptr};
+	QLabel* m_formatLabel {nullptr};
+	QLabel* m_formatValue {nullptr};
+	QLabel* m_colorSpaceLabel {nullptr};
+	QLabel* m_colorSpaceValue {nullptr};
+	QLabel* m_dimensionsLabel {nullptr};
+	QLabel* m_dimensionsValue {nullptr};
+	QLabel* m_nativeResolutionLabel {nullptr};
+	QLabel* m_nativeResolutionValue {nullptr};
+	QLabel* m_summaryLabel {nullptr};
+};
+
+#endif // IMAGEPALETTE_BROWSE_H
Index: scribus/ui/imagepalette_filemodel.cpp
===================================================================
--- scribus/ui/imagepalette_filemodel.cpp	(nonexistent)
+++ scribus/ui/imagepalette_filemodel.cpp	(working copy)
@@ -0,0 +1,116 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#include "imagepalette_filemodel.h"
+
+#include <QDir>
+#include <QFileInfo>
+#include <QImage>
+#include <QPixmap>
+
+ImagePaletteFileModel::ImagePaletteFileModel(QObject* parent)
+	: QAbstractListModel(parent)
+{
+}
+
+int ImagePaletteFileModel::rowCount(const QModelIndex& parent) const
+{
+	if (parent.isValid())
+		return 0;
+	return m_entries.count();
+}
+
+QVariant ImagePaletteFileModel::data(const QModelIndex& index, int role) const
+{
+	if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+		return QVariant();
+
+	const Entry& entry = m_entries.at(index.row());
+	switch (role)
+	{
+	case Qt::DisplayRole:
+		return entry.displayName;
+	case Qt::DecorationRole:
+		return entry.thumbnail.isNull() ? m_placeholderIcon : entry.thumbnail;
+	case Qt::ToolTipRole:
+		return QDir::toNativeSeparators(entry.filePath);
+	default:
+		break;
+	}
+	return QVariant();
+}
+
+void ImagePaletteFileModel::clear()
+{
+	beginResetModel();
+	m_entries.clear();
+	m_rowByPath.clear();
+	endResetModel();
+}
+
+void ImagePaletteFileModel::setFiles(const QStringList& filePaths)
+{
+	beginResetModel();
+	m_entries.clear();
+	m_rowByPath.clear();
+	m_entries.reserve(filePaths.count());
+	for (const QString& filePath : filePaths)
+	{
+		Entry entry;
+		entry.filePath = filePath;
+		entry.displayName = QFileInfo(filePath).fileName();
+		m_rowByPath.insert(filePath, m_entries.count());
+		m_entries.append(entry);
+	}
+	endResetModel();
+}
+
+void ImagePaletteFileModel::setPlaceholderIcon(const QIcon& icon)
+{
+	m_placeholderIcon = icon;
+	if (!m_entries.isEmpty())
+		emit dataChanged(index(0, 0), index(m_entries.count() - 1, 0), { Qt::DecorationRole });
+}
+
+void ImagePaletteFileModel::setThumbnail(const QString& filePath, const QImage& image, const ImagePaletteFileMetadata& metadata)
+{
+	const int row = rowForPath(filePath);
+	if (row < 0)
+		return;
+
+	Entry& entry = m_entries[row];
+	entry.thumbnail = image.isNull() ? QIcon() : QIcon(QPixmap::fromImage(image));
+	entry.metadata = metadata;
+	entry.metadataReady = true;
+	emit dataChanged(index(row, 0), index(row, 0), { Qt::DecorationRole });
+}
+
+QString ImagePaletteFileModel::filePathAt(const QModelIndex& index) const
+{
+	if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+		return QString();
+	return m_entries.at(index.row()).filePath;
+}
+
+ImagePaletteFileMetadata ImagePaletteFileModel::metadataAt(const QModelIndex& index) const
+{
+	if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+		return ImagePaletteFileMetadata();
+	return m_entries.at(index.row()).metadata;
+}
+
+bool ImagePaletteFileModel::metadataReadyAt(const QModelIndex& index) const
+{
+	if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+		return false;
+	return m_entries.at(index.row()).metadataReady;
+}
+
+int ImagePaletteFileModel::rowForPath(const QString& filePath) const
+{
+	return m_rowByPath.value(filePath, -1);
+}
Index: scribus/ui/imagepalette_filemodel.h
===================================================================
--- scribus/ui/imagepalette_filemodel.h	(nonexistent)
+++ scribus/ui/imagepalette_filemodel.h	(working copy)
@@ -0,0 +1,69 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#ifndef IMAGEPALETTE_FILEMODEL_H
+#define IMAGEPALETTE_FILEMODEL_H
+
+#include <QAbstractListModel>
+#include <QHash>
+#include <QIcon>
+#include <QMetaType>
+#include <QStringList>
+#include <QVector>
+
+class QImage;
+
+struct ImagePaletteFileMetadata
+{
+	int width {0};
+	int height {0};
+	int type {-1};
+	int colorSpace {-1};
+	int xResolution {0};
+	int yResolution {0};
+	bool valid {false};
+};
+
+Q_DECLARE_METATYPE(ImagePaletteFileMetadata)
+
+class ImagePaletteFileModel : public QAbstractListModel
+{
+	Q_OBJECT
+
+public:
+	explicit ImagePaletteFileModel(QObject* parent = nullptr);
+
+	int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+	QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+	void clear();
+	void setFiles(const QStringList& filePaths);
+	void setPlaceholderIcon(const QIcon& icon);
+	void setThumbnail(const QString& filePath, const QImage& image, const ImagePaletteFileMetadata& metadata);
+
+	QString filePathAt(const QModelIndex& index) const;
+	ImagePaletteFileMetadata metadataAt(const QModelIndex& index) const;
+	bool metadataReadyAt(const QModelIndex& index) const;
+
+private:
+	struct Entry
+	{
+		QString filePath;
+		QString displayName;
+		QIcon thumbnail;
+		ImagePaletteFileMetadata metadata;
+		bool metadataReady {false};
+	};
+
+	int rowForPath(const QString& filePath) const;
+
+	QIcon m_placeholderIcon;
+	QHash<QString, int> m_rowByPath;
+	QVector<Entry> m_entries;
+};
+
+#endif // IMAGEPALETTE_FILEMODEL_H
Index: scribus/ui/imagepalette_fileworker.cpp
===================================================================
--- scribus/ui/imagepalette_fileworker.cpp	(nonexistent)
+++ scribus/ui/imagepalette_fileworker.cpp	(working copy)
@@ -0,0 +1,129 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#include "imagepalette_fileworker.h"
+
+#include <utility>
+
+#include <QDir>
+#include <QFileInfo>
+#include <QImage>
+
+#include "cmsettings.h"
+#include "fileloader.h"
+#include "loadsaveplugin.h"
+#include "scimage.h"
+
+ImagePaletteFileWorker::ImagePaletteFileWorker(QObject* parent)
+	: QObject(parent)
+{
+}
+
+void ImagePaletteFileWorker::setRequestedGeneration(quint64 generation)
+{
+	m_requestedGeneration.store(generation, std::memory_order_relaxed);
+}
+
+void ImagePaletteFileWorker::scanFolder(quint64 generation, const QString& folderPath,
+	const QStringList& supportedExtensions, const QStringList& previewExtensions, int thumbnailSize)
+{
+	if (!isCurrent(generation))
+		return;
+
+	QDir dir(folderPath);
+	QStringList filePaths;
+	if (dir.exists())
+	{
+		const QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::Readable | QDir::NoSymLinks,
+			QDir::Name | QDir::IgnoreCase);
+		for (const QFileInfo& fileInfo : entries)
+		{
+			if (!isCurrent(generation))
+				return;
+			if (supportedExtensions.contains(fileInfo.suffix().toLower()))
+				filePaths.append(fileInfo.absoluteFilePath());
+		}
+	}
+
+	if (!isCurrent(generation))
+		return;
+	emit filesFound(generation, folderPath, filePaths);
+
+	for (const QString& filePath : std::as_const(filePaths))
+	{
+		if (!isCurrent(generation))
+			return;
+
+		QImage thumbnail;
+		ImagePaletteFileMetadata metadata;
+		loadThumbnail(filePath, previewExtensions, thumbnailSize, thumbnail, metadata);
+		if (!isCurrent(generation))
+			return;
+		emit thumbnailLoaded(generation, filePath, thumbnail, metadata);
+	}
+
+	if (isCurrent(generation))
+		emit loadingFinished(generation);
+}
+
+bool ImagePaletteFileWorker::isCurrent(quint64 generation) const
+{
+	return m_requestedGeneration.load(std::memory_order_relaxed) == generation;
+}
+
+void ImagePaletteFileWorker::loadThumbnail(const QString& filePath, const QStringList& previewExtensions,
+	int thumbnailSize, QImage& thumbnail, ImagePaletteFileMetadata& metadata) const
+{
+	const QString extension = QFileInfo(filePath).suffix().toLower();
+	if (previewExtensions.contains(extension))
+	{
+		FileLoader fileLoader(filePath);
+		const int formatId = fileLoader.testFile();
+		if (formatId >= FORMATID_FIRSTUSER)
+		{
+			const FileFormat* format = LoadSavePlugin::getFormatById(formatId);
+			if (format)
+			{
+				QImage image = format->readThumbnail(filePath);
+				if (!image.isNull())
+				{
+					metadata.width = qRound(image.text(QStringLiteral("XSize")).toDouble());
+					metadata.height = qRound(image.text(QStringLiteral("YSize")).toDouble());
+					metadata.type = 6;
+					metadata.valid = true;
+					thumbnail = image.scaled(thumbnailSize, thumbnailSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+					return;
+				}
+			}
+		}
+	}
+
+	ScImage image;
+	bool realCMYK = false;
+	CMSettings cms(nullptr, "", Intent_Perceptual);
+	cms.allowColorManagement(false);
+	cms.setUseEmbeddedProfile(true);
+	if (!image.loadPicture(filePath, 1, cms, ScImage::Thumbnail, 72, &realCMYK))
+		return;
+
+	if (image.imgInfo.exifDataValid && !image.imgInfo.exifInfo.thumbnail.isNull())
+	{
+		metadata.width = image.imgInfo.exifInfo.width;
+		metadata.height = image.imgInfo.exifInfo.height;
+	}
+	else
+	{
+		metadata.width = image.width();
+		metadata.height = image.height();
+	}
+	metadata.type = image.imgInfo.type;
+	metadata.colorSpace = image.imgInfo.colorspace;
+	metadata.xResolution = image.imgInfo.xres;
+	metadata.yResolution = image.imgInfo.yres;
+	metadata.valid = true;
+	thumbnail = image.scaled(thumbnailSize, thumbnailSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+}
Index: scribus/ui/imagepalette_fileworker.h
===================================================================
--- scribus/ui/imagepalette_fileworker.h	(nonexistent)
+++ scribus/ui/imagepalette_fileworker.h	(working copy)
@@ -0,0 +1,47 @@
+/*
+For general Scribus (>=1.3.2) copyright and licensing information please refer
+to the COPYING file provided with the program. Following this notice may exist
+a copyright and/or license notice that predates the release of Scribus 1.3.2
+for which a new license (GPL+exception) is in place.
+*/
+
+#ifndef IMAGEPALETTE_FILEWORKER_H
+#define IMAGEPALETTE_FILEWORKER_H
+
+#include <atomic>
+
+#include <QImage>
+#include <QObject>
+#include <QStringList>
+
+#include "imagepalette_filemodel.h"
+
+class ImagePaletteFileWorker : public QObject
+{
+	Q_OBJECT
+
+public:
+	explicit ImagePaletteFileWorker(QObject* parent = nullptr);
+
+	// Thread-safe cancellation token used by the GUI thread before queuing a new scan.
+	void setRequestedGeneration(quint64 generation);
+
+public slots:
+	void scanFolder(quint64 generation, const QString& folderPath, const QStringList& supportedExtensions,
+		const QStringList& previewExtensions, int thumbnailSize);
+
+signals:
+	void filesFound(quint64 generation, const QString& folderPath, const QStringList& filePaths);
+	void thumbnailLoaded(quint64 generation, const QString& filePath, const QImage& image,
+		const ImagePaletteFileMetadata& metadata);
+	void loadingFinished(quint64 generation);
+
+private:
+	bool isCurrent(quint64 generation) const;
+	void loadThumbnail(const QString& filePath, const QStringList& previewExtensions, int thumbnailSize,
+		QImage& thumbnail, ImagePaletteFileMetadata& metadata) const;
+
+	std::atomic<quint64> m_requestedGeneration {0};
+};
+
+#endif // IMAGEPALETTE_FILEWORKER_H
img4-images-browse-drag-drop-v1.06.patch (8,900 bytes)   
Index: scribus/ui/imagepalette_browse.cpp
===================================================================
--- scribus/ui/imagepalette_browse.cpp	(revision 27816)
+++ scribus/ui/imagepalette_browse.cpp	(working copy)
@@ -18,6 +18,7 @@
 #include <QLabel>
 #include <QLineEdit>
 #include <QListView>
+#include <QMargins>
 #include <QResizeEvent>
 #include <QSizePolicy>
 #include <QSplitter>
@@ -72,7 +73,9 @@
 	m_imageView->setWordWrap(false);
 	m_imageView->setTextElideMode(Qt::ElideMiddle);
 	m_imageView->setEditTriggers(QAbstractItemView::NoEditTriggers);
-	m_imageView->setSelectionMode(QAbstractItemView::SingleSelection);
+	m_imageView->setSelectionMode(QAbstractItemView::ExtendedSelection);
+	m_imageView->setDragEnabled(true);
+	m_imageView->setDragDropMode(QAbstractItemView::DragOnly);
 	m_model->setPlaceholderIcon(IconManager::instance().loadIcon("image-collection"));
 
 	m_inspector = new QWidget(m_splitter);
@@ -109,24 +112,28 @@
 	m_formatLabel = new QLabel(detailsWidget);
 	m_formatValue = new QLabel(detailsWidget);
 	m_formatValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_formatValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
 	m_detailsLayout->addWidget(m_formatLabel, 0, 0);
 	m_detailsLayout->addWidget(m_formatValue, 0, 1);
 
 	m_colorSpaceLabel = new QLabel(detailsWidget);
 	m_colorSpaceValue = new QLabel(detailsWidget);
 	m_colorSpaceValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_colorSpaceValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
 	m_detailsLayout->addWidget(m_colorSpaceLabel, 1, 0);
 	m_detailsLayout->addWidget(m_colorSpaceValue, 1, 1);
 
 	m_dimensionsLabel = new QLabel(detailsWidget);
 	m_dimensionsValue = new QLabel(detailsWidget);
 	m_dimensionsValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_dimensionsValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
 	m_detailsLayout->addWidget(m_dimensionsLabel, 2, 0);
 	m_detailsLayout->addWidget(m_dimensionsValue, 2, 1);
 
 	m_nativeResolutionLabel = new QLabel(detailsWidget);
 	m_nativeResolutionValue = new QLabel(detailsWidget);
 	m_nativeResolutionValue->setTextInteractionFlags(Qt::TextSelectableByMouse);
+	m_nativeResolutionValue->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
 	m_detailsLayout->addWidget(m_nativeResolutionLabel, 3, 0);
 	m_detailsLayout->addWidget(m_nativeResolutionValue, 3, 1);
 	inspectorLayout->addWidget(detailsWidget);
@@ -142,6 +149,8 @@
 	connect(m_folderEdit, &QLineEdit::returnPressed, this, &ImagePaletteBrowse::folderEntered);
 	connect(m_imageView->selectionModel(), &QItemSelectionModel::currentChanged,
 		this, &ImagePaletteBrowse::currentFileChanged);
+	connect(m_imageView->selectionModel(), &QItemSelectionModel::selectionChanged,
+		this, [this] { updateSelectionInspector(); });
 	connect(m_splitter, &QSplitter::splitterMoved, this, [this](int, int) { updateElidedLabels(); });
 
 	languageChange();
@@ -206,9 +215,9 @@
 	setFolder(QDir::fromNativeSeparators(m_folderEdit->text().trimmed()));
 }
 
-void ImagePaletteBrowse::currentFileChanged(const QModelIndex& current, const QModelIndex&)
-{
-	showFile(current);
+void ImagePaletteBrowse::currentFileChanged(const QModelIndex&, const QModelIndex&)
+{
+	updateSelectionInspector();
 }
 
 void ImagePaletteBrowse::handleFilesFound(quint64 generation, const QString& folderPath, const QStringList& filePaths)
@@ -235,7 +244,7 @@
 	m_model->setThumbnail(filePath, image, metadata);
 	const QModelIndex current = m_imageView->currentIndex();
 	if (m_model->filePathAt(current) == filePath)
-		showFile(current);
+		updateSelectionInspector();
 }
 
 void ImagePaletteBrowse::handleLoadingFinished(quint64 generation)
@@ -302,7 +311,8 @@
 	m_dimensionsLabel->setText(tr("Pixel Size:"));
 	m_nativeResolutionLabel->setText(tr("Native DPI:"));
 	updateInspectorLabelWidth();
-	showFile(m_imageView->currentIndex());
+	updateInspectorMinimumWidth();
+	updateSelectionInspector();
 	updateSummary();
 }
 
@@ -433,6 +443,25 @@
 	setElidedText(m_pathValue, m_pathText);
 }
 
+void ImagePaletteBrowse::updateSelectionInspector()
+{
+	const QModelIndexList selectedIndexes = m_imageView->selectionModel()->selectedIndexes();
+	if (selectedIndexes.count() <= 1)
+	{
+		showFile(selectedIndexes.isEmpty() ? QModelIndex() : selectedIndexes.constFirst());
+		return;
+	}
+
+	const QString empty = QString::fromUtf8("—");
+	m_nameText = tr("%n files selected", nullptr, selectedIndexes.count());
+	m_pathText = QDir::toNativeSeparators(m_folderPath);
+	m_formatValue->setText(empty);
+	m_colorSpaceValue->setText(empty);
+	m_dimensionsValue->setText(empty);
+	m_nativeResolutionValue->setText(empty);
+	updateElidedLabels();
+}
+
 void ImagePaletteBrowse::updateInspectorLabelWidth()
 {
 	const QList<QLabel*> labels { m_formatLabel, m_colorSpaceLabel, m_dimensionsLabel, m_nativeResolutionLabel };
@@ -450,6 +479,26 @@
 	m_detailsLayout->setColumnMinimumWidth(0, gutterWidth);
 }
 
+void ImagePaletteBrowse::updateInspectorMinimumWidth()
+{
+	if (!m_inspector || !m_splitter || !m_imageView || !m_detailsLayout)
+		return;
+
+	const int gutterWidth = m_detailsLayout->columnMinimumWidth(0);
+	int valueWidth = m_formatValue->fontMetrics().horizontalAdvance(tr("JPEG 2000"));
+	valueWidth = qMax(valueWidth, m_colorSpaceValue->fontMetrics().horizontalAdvance(tr("Unknown")));
+	valueWidth = qMax(valueWidth, m_dimensionsValue->fontMetrics().horizontalAdvance(QStringLiteral("99999 × 99999 px")));
+	valueWidth = qMax(valueWidth, m_nativeResolutionValue->fontMetrics().horizontalAdvance(QStringLiteral("9999 × 9999 dpi")));
+	const int columnSpacing = qMax(0, m_detailsLayout->horizontalSpacing());
+	const QMargins margins = m_inspector->layout()->contentsMargins();
+	const int inspectorMinimumWidth = gutterWidth + columnSpacing + valueWidth + margins.left() + margins.right();
+
+	m_inspector->setMinimumWidth(inspectorMinimumWidth);
+
+	const int imageViewMinimumWidth = qMax(m_imageView->minimumWidth(), m_imageView->minimumSizeHint().width());
+	m_splitter->setMinimumWidth(imageViewMinimumWidth + m_splitter->handleWidth() + inspectorMinimumWidth);
+}
+
 void ImagePaletteBrowse::updateSummary()
 {
 	if (m_invalidFolder)
Index: scribus/ui/imagepalette_browse.h
===================================================================
--- scribus/ui/imagepalette_browse.h	(revision 27816)
+++ scribus/ui/imagepalette_browse.h	(working copy)
@@ -66,6 +66,8 @@
 	void startWorker();
 	void updateElidedLabels();
 	void updateInspectorLabelWidth();
+	void updateInspectorMinimumWidth();
+	void updateSelectionInspector();
 	void updateSummary();
 
 	ImagePaletteFileModel* m_model {nullptr};
Index: scribus/ui/imagepalette_filemodel.cpp
===================================================================
--- scribus/ui/imagepalette_filemodel.cpp	(revision 27816)
+++ scribus/ui/imagepalette_filemodel.cpp	(working copy)
@@ -10,7 +10,9 @@
 #include <QDir>
 #include <QFileInfo>
 #include <QImage>
+#include <QMimeData>
 #include <QPixmap>
+#include <QUrl>
 
 ImagePaletteFileModel::ImagePaletteFileModel(QObject* parent)
 	: QAbstractListModel(parent)
@@ -42,6 +44,41 @@
 		break;
 	}
 	return QVariant();
+}
+
+Qt::ItemFlags ImagePaletteFileModel::flags(const QModelIndex& index) const
+{
+	Qt::ItemFlags itemFlags = QAbstractListModel::flags(index);
+	if (index.isValid())
+		itemFlags |= Qt::ItemIsDragEnabled;
+	return itemFlags;
+}
+
+QStringList ImagePaletteFileModel::mimeTypes() const
+{
+	return { QStringLiteral("text/uri-list") };
+}
+
+QMimeData* ImagePaletteFileModel::mimeData(const QModelIndexList& indexes) const
+{
+	auto* mimeData = new QMimeData();
+	QList<QUrl> urls;
+	urls.reserve(indexes.count());
+
+	for (const QModelIndex& index : indexes)
+	{
+		const QString filePath = filePathAt(index);
+		if (!filePath.isEmpty())
+			urls.append(QUrl::fromLocalFile(filePath));
+	}
+
+	mimeData->setUrls(urls);
+	return mimeData;
+}
+
+Qt::DropActions ImagePaletteFileModel::supportedDragActions() const
+{
+	return Qt::CopyAction;
 }
 
 void ImagePaletteFileModel::clear()
Index: scribus/ui/imagepalette_filemodel.h
===================================================================
--- scribus/ui/imagepalette_filemodel.h	(revision 27816)
+++ scribus/ui/imagepalette_filemodel.h	(working copy)
@@ -16,6 +16,7 @@
 #include <QVector>
 
 class QImage;
+class QMimeData;
 
 struct ImagePaletteFileMetadata
 {
@@ -39,6 +40,10 @@
 
 	int rowCount(const QModelIndex& parent = QModelIndex()) const override;
 	QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+	Qt::ItemFlags flags(const QModelIndex& index) const override;
+	QStringList mimeTypes() const override;
+	QMimeData* mimeData(const QModelIndexList& indexes) const override;
+	Qt::DropActions supportedDragActions() const override;
 
 	void clear();
 	void setFiles(const QStringList& filePaths);
PatchYes

Activities

buttar

2026-09-05 12:16

reporter   ~0054421

Some screenshots.
images-palette-browser.png (61,976 bytes)   
images-palette-browser.png (61,976 bytes)   
images-palette-missing.png (53,683 bytes)   
images-palette-missing.png (53,683 bytes)   

Issue History

Date Modified Username Field Change
2026-09-05 10:19 buttar New Issue
2026-09-05 10:19 buttar Tag Attached: #please_test
2026-09-05 10:19 buttar File Added: img1-images-palette-foundation-v1.03.patch
2026-09-05 10:19 buttar File Added: img2-images-document-management-v1.33.patch
2026-09-05 10:19 buttar File Added: img3-images-folder-browsing-v1.04.patch
2026-09-05 10:19 buttar File Added: img4-images-browse-drag-drop-v1.06.patch
2026-09-05 12:16 buttar Note Added: 0054421
2026-09-05 12:16 buttar File Added: images-palette-browser.png
2026-09-05 12:16 buttar File Added: images-palette-info-and-go-to-frame.png
2026-09-05 12:16 buttar File Added: images-palette-missing.png