Skip to content

Commit

Permalink
Dockable font list window with extended search filter.
Browse files Browse the repository at this point in the history
  • Loading branch information
t-paul committed Apr 27, 2024
1 parent 6581795 commit 614bcf7
Show file tree
Hide file tree
Showing 9 changed files with 520 additions and 28 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Expand Up @@ -982,6 +982,7 @@ set(GUI_SOURCES
src/gui/MainWindow.cc
src/gui/Measurement.cc
src/gui/Animate.cc
src/gui/FontList.cc
src/gui/MouseSelector.cc
src/gui/OctoPrint.cc
src/gui/OpenCSGWarningDialog.cc
Expand Down Expand Up @@ -1040,6 +1041,7 @@ set(GUI_HEADERS
src/gui/ErrorLog.h
src/gui/EventFilter.h
src/gui/ExportPdfDialog.h
src/gui/FontList.h
src/gui/FontListDialog.h
src/gui/FontListTableView.h
src/gui/IgnoreWheelWhenNotFocused.h
Expand Down Expand Up @@ -1092,6 +1094,7 @@ set(GUI_UIS
src/gui/Console.ui
src/gui/ErrorLog.ui
src/gui/ExportPdfDialog.ui
src/gui/FontList.ui
src/gui/FontListDialog.ui
src/gui/LaunchingScreen.ui
src/gui/LibraryInfoDialog.ui
Expand Down
46 changes: 41 additions & 5 deletions src/FontCache.cc
Expand Up @@ -65,7 +65,8 @@ const std::string get_freetype_version()
return FontCache::instance()->get_freetype_version();
}

FontInfo::FontInfo(std::string family, std::string style, std::string file) : family(std::move(family)), style(std::move(style)), file(std::move(file))
FontInfo::FontInfo(std::string family, std::string style, std::string file, uint32_t hash)
: family(std::move(family)), style(std::move(style)), file(std::move(file)), hash(hash)
{
}

Expand Down Expand Up @@ -95,6 +96,11 @@ const std::string& FontInfo::get_file() const
return file;
}

const uint32_t FontInfo::get_hash() const
{
return hash;
}

FontCache *FontCache::self = nullptr;
FontCache::InitHandlerFunc *FontCache::cb_handler = FontCache::defaultInitHandler;
void *FontCache::cb_userdata = nullptr;
Expand Down Expand Up @@ -223,6 +229,34 @@ void FontCache::add_font_dir(const std::string& path)
}
}

std::vector<uint32_t> FontCache::filter(const std::u32string& str) const
{
FcObjectSet *object_set = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, nullptr);
FcPattern *pattern = FcPatternCreate();
init_pattern(pattern);
FcCharSet *charSet = FcCharSetCreate();
for (char32_t a : str) {
FcCharSetAddChar(charSet, a);
}
FcValue charSetValue;
charSetValue.type = FcTypeCharSet;
charSetValue.u.c = charSet;
FcPatternAdd(pattern, FC_CHARSET, charSetValue, true);

FcFontSet *font_set = FcFontList(this->config, pattern, object_set);
FcObjectSetDestroy(object_set);
FcPatternDestroy(pattern);
FcCharSetDestroy(charSet);

std::vector<uint32_t> result;
result.reserve(font_set->nfont);
for (int a = 0;a < font_set->nfont;++a) {
result.push_back(FcPatternHash(font_set->fonts[a]));
}
FcFontSetDestroy(font_set);
return result;
}

FontInfoList *FontCache::list_fonts() const
{
FcObjectSet *object_set = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, nullptr);
Expand All @@ -234,20 +268,22 @@ FontInfoList *FontCache::list_fonts() const

auto *list = new FontInfoList();
for (int a = 0; a < font_set->nfont; ++a) {
FcPattern *p = font_set->fonts[a];

FcValue file_value;
FcPatternGet(font_set->fonts[a], FC_FILE, 0, &file_value);
FcPatternGet(p, FC_FILE, 0, &file_value);

FcValue family_value;
FcPatternGet(font_set->fonts[a], FC_FAMILY, 0, &family_value);
FcPatternGet(p, FC_FAMILY, 0, &family_value);

FcValue style_value;
FcPatternGet(font_set->fonts[a], FC_STYLE, 0, &style_value);
FcPatternGet(p, FC_STYLE, 0, &style_value);

std::string family((const char *) family_value.u.s);
std::string style((const char *) style_value.u.s);
std::string file((const char *) file_value.u.s);

list->push_back(FontInfo(family, style, file));
list->push_back(FontInfo(family, style, file, FcPatternHash(p)));
}
FcFontSetDestroy(font_set);

Expand Down
5 changes: 4 additions & 1 deletion src/FontCache.h
Expand Up @@ -45,17 +45,19 @@
class FontInfo
{
public:
FontInfo(std::string family, std::string style, std::string file);
FontInfo(std::string family, std::string style, std::string file, uint32_t hash);
virtual ~FontInfo() = default;

[[nodiscard]] const std::string& get_family() const;
[[nodiscard]] const std::string& get_style() const;
[[nodiscard]] const std::string& get_file() const;
[[nodiscard]] const uint32_t get_hash() const;
bool operator<(const FontInfo& rhs) const;
private:
std::string family;
std::string style;
std::string file;
uint32_t hash;
};

using FontInfoList = std::vector<FontInfo>;
Expand Down Expand Up @@ -89,6 +91,7 @@ class FontCache
void register_font_file(const std::string& path);
void clear();
[[nodiscard]] FontInfoList *list_fonts() const;
[[nodiscard]] std::vector<uint32_t> filter(const std::u32string&) const;
[[nodiscard]] const std::string get_freetype_version() const;

static FontCache *instance();
Expand Down
165 changes: 165 additions & 0 deletions src/gui/FontList.cc
@@ -0,0 +1,165 @@
#include <QClipboard>

#include "FontList.h"
#include "FontCache.h"
#include "printutils.h"

FontSortFilterProxyModel::FontSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
{
}

void FontSortFilterProxyModel::clearFilter()
{
filterHashes.clear();
invalidateFilter();
}

void FontSortFilterProxyModel::setFilterHashes(const std::vector<uint32_t>& hashes)
{
filterHashes.clear();
for (const auto hash : hashes) {
filterHashes.insert(QString::number(hash, 16));
}
invalidateFilter();
}

bool FontSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (filterHashes.empty()) {
return true;
}

QModelIndex idx = sourceModel()->index(sourceRow, FontList::COL_HASH, sourceParent);
const auto &data = sourceModel()->data(idx);
const bool result = filterHashes.contains(data.toString());
return result;
}

FontList::FontList(QWidget *parent) : QWidget(parent), model(nullptr), proxy(nullptr)
{
setupUi(this);
}

void FontList::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
}

void FontList::on_copyButton_clicked()
{
font_selected(selection);

QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(selection);
}

void FontList::on_filterLineEdit_textChanged(const QString& text)
{
proxy->setFilterWildcard(text);
groupBox->setTitle(QString("Filter (%1 fonts found)").arg(proxy->rowCount()));
}

void FontList::on_charsLineEdit_textChanged(const QString& text)
{
if (text.length() == 0) {
proxy->clearFilter();
} else {
const auto hashes = FontCache::instance()->filter(text.toStdU32String());
proxy->setFilterHashes(hashes);
}
groupBox->setTitle(QString("Filter (%1 fonts found)").arg(proxy->rowCount()));
}

void FontList::selection_changed(const QItemSelection& current, const QItemSelection&)
{
if (current.count() == 0) {
copyButton->setEnabled(false);
tableView->setDragText("");
return;
}

const QModelIndex& idx = proxy->mapToSource(current.indexes().at(0));
const QString name = model->item(idx.row(), COL_FONT_NAME)->text();
const QString style = model->item(idx.row(), COL_FONT_STYLE)->text();
selection = QString("\"%1:style=%2\"").arg(quote(name)).arg(quote(style));
copyButton->setEnabled(true);
tableView->setDragText(selection);
}

void FontList::update_font_list()
{
copyButton->setEnabled(false);

if (proxy) {
delete proxy;
proxy = nullptr;
}
if (model) {
delete model;
model = nullptr;
}

FontInfoList *list = FontCache::instance()->list_fonts();
model = new QStandardItemModel(list->size(), 4, this);
model->setHorizontalHeaderItem(COL_FONT_NAME, new QStandardItem(_("Font name")));
model->setHorizontalHeaderItem(COL_FONT_STYLE, new QStandardItem(_("Font style")));
model->setHorizontalHeaderItem(COL_FILE_NAME, new QStandardItem(_("Filename")));
model->setHorizontalHeaderItem(COL_HASH, new QStandardItem(_("Hash")));

int idx = 0;
for (auto it = list->begin(); it != list->end(); it++, idx++) {
FontInfo font_info = (*it);
auto *family = new QStandardItem(QString::fromStdString(font_info.get_family()));
family->setEditable(false);
model->setItem(idx, COL_FONT_NAME, family);
auto *style = new QStandardItem(QString::fromStdString(font_info.get_style()));
style->setEditable(false);
model->setItem(idx, COL_FONT_STYLE, style);
auto *file = new QStandardItem(QString::fromStdString(font_info.get_file()));
file->setEditable(false);
model->setItem(idx, COL_FILE_NAME, file);
auto *hash = new QStandardItem(QString::number(font_info.get_hash(), 16));
hash->setEditable(false);
model->setItem(idx, COL_HASH, hash);
}

proxy = new FontSortFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
groupBox->setTitle(QString("Filter (%1 fonts found)").arg(proxy->rowCount()));

this->tableView->setModel(proxy);
this->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
this->tableView->sortByColumn(COL_FONT_NAME, Qt::AscendingOrder);
this->tableView->resizeColumnsToContents();
this->tableView->setSortingEnabled(true);

connect(tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&,const QItemSelection&)), this, SLOT(selection_changed(const QItemSelection&,const QItemSelection&)));

delete list;
}

/**
* Quote a string according to the requirements of font-config.
* See http://www.freedesktop.org/software/fontconfig/fontconfig-user.html
*
* The '\', '-', ':' and ',' characters in family names must be preceded
* by a '\' character to avoid having them misinterpreted. Similarly, values
* containing '\', '=', '_', ':' and ',' must also have them preceded by a
* '\' character. The '\' characters are stripped out of the family name and
* values as the font name is read.
*
* @param text unquoted string
* @return quoted text
*/
QString FontList::quote(const QString& text)
{
QString result = text;
result.replace('\\', R"(\\\\)")
.replace('-', "\\\\-")
.replace(':', "\\\\:")
.replace(',', "\\\\,")
.replace('=', "\\\\=")
.replace('_', "\\\\_");
return result;
}
67 changes: 67 additions & 0 deletions src/gui/FontList.h
@@ -0,0 +1,67 @@
#pragma once

#include <vector>

#include <QWidget>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>

#include "qtgettext.h"
#include "ui_FontList.h"

class FontSortFilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT

public:
FontSortFilterProxyModel(QObject *parent = nullptr);

void clearFilter();
void setFilterHashes(const std::vector<uint32_t>&);

protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;

private:
QSet<QString> filterHashes;
};

class FontList : public QWidget, public Ui::FontListWidget
{
Q_OBJECT

public:
FontList(QWidget *parent = nullptr);
FontList(const FontList& source) = delete;
FontList(FontList&& source) = delete;
FontList& operator=(const FontList& source) = delete;
FontList& operator=(FontList&& source) = delete;
~FontList() override = default;

void initGUI();
void update_font_list();

static constexpr int COL_FONT_NAME = 0;
static constexpr int COL_FONT_STYLE = 1;
static constexpr int COL_FILE_NAME = 2;
static constexpr int COL_HASH = 3;

public slots:
void on_copyButton_clicked();
void on_filterLineEdit_textChanged(const QString&);
void on_charsLineEdit_textChanged(const QString&);
void selection_changed(const QItemSelection&, const QItemSelection&);

signals:
void font_selected(const QString font);

protected:
void resizeEvent(QResizeEvent *event) override;

private:
QString quote(const QString& text);

QString selection;
QStandardItemModel *model;
FontSortFilterProxyModel *proxy;
};

0 comments on commit 614bcf7

Please sign in to comment.