Introducing transparent colors

Koji Yokota <[email protected]> Sat, 16 May 2026 10:51:08 +0900
Newsgroups gmane.editors.lyx.devel
Message-ID <[email protected]>
Hi all,

I’m in need of transparent UI background color so that the background of preedit strings fits to any colored insets. So I’d like to propose new RGBAColor as a derived struct of RGBColor, adding an alpha value as the attached patch.

Any comments on this?

The patch adds the opacity handle to the color dialog so that existing colors can be changed to transparent colors. Note that the string representation of RGBAColor is #AARRGGBB following Qt’s QColor::HexArgb, not #RRGGBBAA. The format of \setcolor in preferences will be changed to use this 9-chars representation.

At this moment, this extension is limited to UI colors.

Koji

-- 
lyx-devel mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-devel
alpha_colors.patch (application/octet-stream, 10.6 KB)
diff --git a/src/Color.cpp b/src/Color.cpp
index 36629c015b..d79a17b10e 100644
--- a/src/Color.cpp
+++ b/src/Color.cpp
@@ -67,8 +67,12 @@ string const X11hexname(RGBColor const & col)
 {
 	ostringstream ostr;
 
-	ostr << '#' << setbase(16) << setfill('0')
-	     << setw(2) << col.r
+	ostr << '#' << setbase(16) << setfill('0');
+
+	if (typeid(col) == typeid(RGBAColor))
+		ostr << setw(2) << ((RGBAColor const &)col).a;
+
+	ostr << setw(2) << col.r
 	     << setw(2) << col.g
 	     << setw(2) << col.b;
 
@@ -78,13 +82,22 @@ string const X11hexname(RGBColor const & col)
 
 RGBColor rgbFromHexName(string const & x11hexname)
 {
-	RGBColor c;
-	LASSERT(x11hexname.size() == 7 && x11hexname[0] == '#',
-		return c);
-	c.r = hexstrToInt(x11hexname.substr(1, 2));
-	c.g = hexstrToInt(x11hexname.substr(3, 2));
-	c.b = hexstrToInt(x11hexname.substr(5, 2));
-	return c;
+	LASSERT((x11hexname.size() == 7 || x11hexname.size() == 9)
+	        && x11hexname[0] == '#', return RGBColor());
+	if (x11hexname.size() == 7) {
+		RGBColor c;
+		c.r = hexstrToInt(x11hexname.substr(1, 2));
+		c.g = hexstrToInt(x11hexname.substr(3, 2));
+		c.b = hexstrToInt(x11hexname.substr(5, 2));
+		return c;
+	} else {
+		RGBAColor c;
+		c.a = hexstrToInt(x11hexname.substr(1, 2));
+		c.r = hexstrToInt(x11hexname.substr(3, 2));
+		c.g = hexstrToInt(x11hexname.substr(5, 2));
+		c.b = hexstrToInt(x11hexname.substr(7, 2));
+		return c;
+	}
 }
 
 
diff --git a/src/ColorCode.h b/src/ColorCode.h
index 60837a3c2c..d832b9ab76 100644
--- a/src/ColorCode.h
+++ b/src/ColorCode.h
@@ -270,6 +270,14 @@ struct RGBColor {
 		: r(red), g(green), b(blue) {}
 };
 
+struct RGBAColor : public RGBColor {
+	unsigned int a;
+	RGBAColor() : RGBColor(), a(255) {}
+	RGBAColor(unsigned int red, unsigned int green, unsigned int blue,
+	          unsigned int alpha = 255)
+		: RGBColor(red, green, blue), a(alpha) {}
+};
+
 inline bool operator==(RGBColor const & c1, RGBColor const & c2)
 {
 	return (c1.r == c2.r && c1.g == c2.g && c1.b == c2.b);
@@ -281,6 +289,18 @@ inline bool operator!=(RGBColor const & c1, RGBColor const & c2)
 	return !(c1 == c2);
 }
 
+
+inline bool operator==(RGBAColor const & c1, RGBAColor const & c2)
+{
+	return (c1.r == c2.r && c1.g == c2.g && c1.b == c2.b && c1.a == c2.a);
+}
+
+
+inline bool operator!=(RGBAColor const & c1, RGBAColor const & c2)
+{
+	return !(c1 == c2);
+}
+
 } // namespace lyx
 
 #endif
diff --git a/src/frontends/qt/GuiDialog.cpp b/src/frontends/qt/GuiDialog.cpp
index c52bbb4d9c..af6ebc31d6 100644
--- a/src/frontends/qt/GuiDialog.cpp
+++ b/src/frontends/qt/GuiDialog.cpp
@@ -286,9 +286,11 @@ QString GuiDialog::browseRelToSub(
 }
 
 
-QColor GuiDialog::getColor(const QColor &initial, QWidget *parent)
+QColor GuiDialog::getColor(const QColor &initial, QWidget *parent,
+                           QString const & title,
+                           QColorDialog::ColorDialogOptions options)
 {
-	const QColor color = QColorDialog::getColor(initial, parent);
+	const QColor color = QColorDialog::getColor(initial, parent, title, options);
 	if (guiApp->platformName() == "cocoa") {
 		QWidget * dialog = parent->window();
 		// On Mac explicitly activate the parents top-level widget
@@ -299,9 +301,10 @@ QColor GuiDialog::getColor(const QColor &initial, QWidget *parent)
 	return color;
 }
 
-QColor GuiDialog::getColor(const QColor &initial)
+QColor GuiDialog::getColor(const QColor &initial, QString const & title,
+                           QColorDialog::ColorDialogOptions options)
 {
-	return getColor(initial, asQWidget());
+	return getColor(initial, asQWidget(), title, options);
 }
 
 } // namespace frontend
diff --git a/src/frontends/qt/GuiDialog.h b/src/frontends/qt/GuiDialog.h
index f2a7efe727..016fca19a9 100644
--- a/src/frontends/qt/GuiDialog.h
+++ b/src/frontends/qt/GuiDialog.h
@@ -16,6 +16,7 @@
 #include "ButtonController.h"
 
 #include <QAbstractButton>
+#include <QColorDialog>
 #include <QDialog>
 
 
@@ -179,8 +180,13 @@ public:
 		QString const & label2 = QString(),
 		QString const & dir2 = QString());
 
-	static QColor getColor(const QColor &initial, QWidget *parent);
-	QColor getColor(const QColor &initial);
+	static QColor getColor(const QColor &initial, QWidget *parent,
+	                       QString const & title = QString(),
+	                       QColorDialog::ColorDialogOptions options
+	                       = QColorDialog::ColorDialogOptions());
+	QColor getColor(const QColor &initial, QString const & title = QString(),
+	                QColorDialog::ColorDialogOptions options
+	                = QColorDialog::ColorDialogOptions());
 
 private:
 	ButtonController bc_;
diff --git a/src/frontends/qt/GuiPrefs.cpp b/src/frontends/qt/GuiPrefs.cpp
index 1290556215..f75f914a3c 100644
--- a/src/frontends/qt/GuiPrefs.cpp
+++ b/src/frontends/qt/GuiPrefs.cpp
@@ -1112,9 +1112,9 @@ void PrefColors::updateRC(LyXRC const & rc)
 		ColorPair colors =
 		        guiApp->colorCache().getAll(lcolors_[i], false);
 		theme_colors_[i].first  = newcolors_[i].first
-		        = curcolors_[i].first  = colors.first.name();
+		        = curcolors_[i].first  = colors.first.name(QColor::HexArgb);
 		theme_colors_[i].second = newcolors_[i].second
-		        = curcolors_[i].second = colors.second.name();
+		        = curcolors_[i].second = colors.second.name(QColor::HexArgb);
 		setSwatches(i, colors);
 	}
 
@@ -1187,13 +1187,14 @@ void PrefColors::changeColor(int const &row, bool const &is_dark_mode)
 	else
 		color = newcolors_[size_t(row)].first;
 
-	QColor const c = form_->getColor(QColor(color));
+	QColor const c = form_->getColor(QColor(color), QString(),
+	                                 QColorDialog::ShowAlphaChannel);
 
 	if (setColor(colorsTV_model_.item(row, is_dark_mode), c, color)) {
 		if (is_dark_mode)
-			newcolors_[size_t(row)].second = c.name();
+			newcolors_[size_t(row)].second = c.name(QColor::HexArgb);
 		else
-			newcolors_[size_t(row)].first  = c.name();
+			newcolors_[size_t(row)].first  = c.name(QColor::HexArgb);
 		findThemeFromColorSet();
 		// emit signal
 		changed();
@@ -1332,7 +1333,7 @@ bool PrefColors::resetAllColor()
 bool PrefColors::setColor(QStandardItem *pitem,
                           QColor const &new_color, QString const &old_color)
 {
-	if (new_color.isValid() && new_color.name() != old_color) {
+	if (new_color.isValid() && new_color.name(QColor::HexArgb) != old_color) {
 		QUndoCommand* setColorCmd =
 		        new SetColor(pitem, new_color, old_color, newcolors_,
 		                     autoapply_, this);
@@ -1623,8 +1624,8 @@ ColorNamePairs PrefColors::readTheme(FileName const & fullpath, LyXRC & rc) cons
 	for (size_type row = 0; row < lcolors_.size(); ++row) {
 		// get colors from extern lcolor
 		colors[size_t(row)] =
-		    {getCurrentColor(lcolors_[row], false).name(),
-		     getCurrentColor(lcolors_[row], true).name()};
+		    {getCurrentColor(lcolors_[row], false).name(QColor::HexArgb),
+		     getCurrentColor(lcolors_[row], true).name(QColor::HexArgb)};
 	}
 	return colors;
 }
@@ -2010,10 +2011,10 @@ void PrefColors::filterByColor(const QColor &color)
 {
 	QList<QStandardItem *>rows_found;
 	for (int i=0; i<colorsTV_model_.rowCount(); ++i) {
-		if (colorsTV_model_.item(i, 0)->data(Qt::DecorationRole).value<QColor>().name()
-		        == color.name() ||
-		        colorsTV_model_.item(i, 1)->data(Qt::DecorationRole).value<QColor>().name()
-		        == color.name()) {
+		if (colorsTV_model_.item(i, 0)->data(Qt::DecorationRole).value<QColor>().name(QColor::HexArgb)
+		        == color.name(QColor::HexArgb) ||
+		        colorsTV_model_.item(i, 1)->data(Qt::DecorationRole).value<QColor>().name(QColor::HexArgb)
+		        == color.name(QColor::HexArgb)) {
 			rows_found.push_back(colorsTV_model_.item(i));
 		}
 	}
@@ -2060,7 +2061,8 @@ void PrefColors::openColorChooser()
 	}
 
 	// open color dialog
-	QColor color = cdlg.getColor(initial_color, form_);
+	QColor color = cdlg.getColor(initial_color, form_, QString(),
+	                             QColorDialog::ShowAlphaChannel);
 
 	form_->raise();
 
@@ -4691,9 +4693,9 @@ void SetColor::undo()
 void SetColor::setColor(QColor const &color)
 {
 	if (item_.column() == 1)
-		newcolors_[size_t(item_.row())].second = color.name();
+		newcolors_[size_t(item_.row())].second = color.name(QColor::HexArgb);
 	else
-		newcolors_[size_t(item_.row())].first = color.name();
+		newcolors_[size_t(item_.row())].first = color.name(QColor::HexArgb);
 	setSwatch(&item_, color);
 
 	parent_->form_->update();
@@ -4718,6 +4720,26 @@ ColorSwatchDelegate::ColorSwatchDelegate(QObject *parent)
     : QStyledItemDelegate(parent), button_(new QPushButton)
 {
 	pane_ = static_cast<PrefColors*>(parent);
+
+	// construct the background image of swatches
+	bg_pixmap_ = new QPixmap(pane_->swatch_width_, pane_->swatch_height_);
+	bg_pixmap_->fill(Qt::white);
+	QPainter painter(bg_pixmap_);
+	painter.setRenderHint(QPainter::Antialiasing);
+	QPolygon polygon;
+	polygon << QPoint(pane_->swatch_width_, 0)
+	        << QPoint(0, pane_->swatch_height_)
+	        << QPoint(0, 0);
+	painter.setPen(QColor(Qt::black));
+	painter.setBrush(QColor(Qt::black));
+	painter.drawPolygon(polygon);
+	painter.end();
+}
+
+
+ColorSwatchDelegate::~ColorSwatchDelegate()
+{
+	delete bg_pixmap_;
 }
 
 
@@ -4741,9 +4763,10 @@ void ColorSwatchDelegate::paint(QPainter *painter,
 		QColor color =
 		        pane_->colorsTV_model_.data(index, Qt::DecorationRole).
 		        value<QColor>();
-		if (index.flags().testFlag(Qt::ItemIsEnabled))
+		if (index.flags().testFlag(Qt::ItemIsEnabled)) {
 			pixmap.fill(color);
-		else
+			pixmap = mergePixmaps(&pixmap, bg_pixmap_);
+		} else
 			pixmap.fill(Qt::transparent);
 		style->drawItemPixmap(painter, opt.rect, Qt::AlignCenter, pixmap);
 	} else {
@@ -4753,6 +4776,16 @@ void ColorSwatchDelegate::paint(QPainter *painter,
 }
 
 
+QPixmap ColorSwatchDelegate::mergePixmaps(QPixmap const *fg, QPixmap const *bg) const
+{
+	// make a copy to leave bg untouched
+	QPixmap merged(*bg);
+	QPainter painter(&merged);
+	painter.drawPixmap(0, 0, *bg);
+	painter.drawPixmap(0, 0, *fg);
+	return merged;
+}
+
 } // namespace frontend
 } // namespace lyx
 
diff --git a/src/frontends/qt/GuiPrefs.h b/src/frontends/qt/GuiPrefs.h
index ab67f69ebc..441adc81c3 100644
--- a/src/frontends/qt/GuiPrefs.h
+++ b/src/frontends/qt/GuiPrefs.h
@@ -735,12 +735,15 @@ class ColorSwatchDelegate : public QStyledItemDelegate
 
 public:
 	ColorSwatchDelegate(QObject *parent = nullptr);
+	~ColorSwatchDelegate();
 
 	void paint(QPainter *painter, const QStyleOptionViewItem &option,
 	           const QModelIndex &index) const override;
 
 private:
+	QPixmap mergePixmaps(QPixmap const * fg, QPixmap const * bg) const;
 	PrefColors* pane_;
+	QPixmap* bg_pixmap_;
 	QFont font_;
 
 	// To give impression that a button is pressed, flip the direction of