From 8b712f4c36adb8226fc3377ef486f67bbaa6ed95 Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Tue, 23 Sep 2025 07:09:04 +0300 Subject: [PATCH 1/6] frontend: Natural sort drag&drop sources Sort drag&dropped sources using locale-aware natural sorting with QCollator and system locale. --- frontend/widgets/OBSBasic.hpp | 9 +- frontend/widgets/OBSBasic_Dropfiles.cpp | 212 ++++++++++++++---------- shared/qt/wrappers/qt-wrappers.hpp | 23 +++ 3 files changed, 152 insertions(+), 92 deletions(-) diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 4343e16054c4da..c5c14050544173 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -227,6 +227,8 @@ class OBSBasic : public OBSMainWindow { DropType_Html, DropType_Url, }; + static const std::map DropTypes; + struct DropItem; enum ContextBarSize { ContextBarSize_Minimized, ContextBarSize_Reduced, ContextBarSize_Normal }; @@ -471,9 +473,10 @@ private slots: * ------------------------------------- */ private: - void AddDropSource(const char *file, DropType image); - void AddDropURL(QUrl url, QString &name, obs_data_t *settings, const obs_video_info &ovi); - void ConfirmDropUrl(const QString &url); + QString GetUrlDisplayName(QUrl url); + void AddDropSource(const DropItem &dropItem); + void AddDropURL(QUrl url, obs_data_t *settings, const obs_video_info &ovi); + bool ConfirmDropUrl(const QString &url); void dragEnterEvent(QDragEnterEvent *event) override; void dragLeaveEvent(QDragLeaveEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; diff --git a/frontend/widgets/OBSBasic_Dropfiles.cpp b/frontend/widgets/OBSBasic_Dropfiles.cpp index 3ab81d9d4564a1..1a501899dc5c51 100644 --- a/frontend/widgets/OBSBasic_Dropfiles.cpp +++ b/frontend/widgets/OBSBasic_Dropfiles.cpp @@ -51,6 +51,39 @@ static const char *mediaExtensions[] = { "nuv", "ogg", "ogm", "ogv", "ogx", "ps", "rec", "rm", "rmvb", "rpl", "thp", "tod", "ts", "tts", "txd", "vob", "vro", "webm", "wm", "wmv", "wtv", nullptr}; +const std::map OBSBasic::DropTypes = { +#ifdef _WIN32 + {DropType_RawText, "text_gdiplus"}, {DropType_Text, "text_gdiplus"}, +#else + {DropType_RawText, "text_ft2_source"}, {DropType_Text, "text_ft2_source"}, +#endif + {DropType_Image, "image_source"}, {DropType_Media, "ffmpeg_source"}, + {DropType_Html, "browser_source"}, {DropType_Url, "browser_source"}}; + +struct OBSBasic::DropItem { + QString url; + QString displayName; + DropType dropType; + const char *typeId; + + DropItem(const QString &url, DropType dropType) : DropItem(url, QString(), dropType) {} + DropItem(const QString &url, const QString &displayName, DropType dropType) + { + this->url = url; + this->dropType = dropType; + this->typeId = obs_get_latest_input_type_id(unversionedType()); + + if (this->typeId == nullptr) { + throw invalid_argument("Unsupported drop type: " + std::string(unversionedType())); + } + + this->displayName = displayName.isEmpty() ? QString::fromUtf8(defaultName()) : displayName; + }; + + const char *unversionedType() const { return DropTypes.at(dropType); } + const char *defaultName() const { return obs_source_get_display_name(typeId); } +}; + static string GenerateSourceName(const char *base) { string name; @@ -82,7 +115,13 @@ static QString ReadWindowsURLFile(const QString &file) } #endif -void OBSBasic::AddDropURL(QUrl url, QString &name, obs_data_t *settings, const obs_video_info &ovi) +QString OBSBasic::GetUrlDisplayName(QUrl url) +{ + QUrlQuery query = QUrlQuery(url.query(QUrl::FullyEncoded)); + return query.hasQueryItem("layer-name") ? query.queryItemValue("layer-name", QUrl::FullyDecoded) : url.host(); +} + +void OBSBasic::AddDropURL(QUrl url, obs_data_t *settings, const obs_video_info &ovi) { QUrlQuery query = QUrlQuery(url.query(QUrl::FullyEncoded)); @@ -111,8 +150,6 @@ void OBSBasic::AddDropURL(QUrl url, QString &name, obs_data_t *settings, const o obs_data_set_int(settings, "width", cx); obs_data_set_int(settings, "height", cy); - name = query.hasQueryItem("layer-name") ? query.queryItemValue("layer-name", QUrl::FullyDecoded) : url.host(); - query.removeQueryItem("layer-width"); query.removeQueryItem("layer-height"); query.removeQueryItem("layer-name"); @@ -122,71 +159,48 @@ void OBSBasic::AddDropURL(QUrl url, QString &name, obs_data_t *settings, const o obs_data_set_string(settings, "url", QT_TO_UTF8(url.url())); } -void OBSBasic::AddDropSource(const char *data, DropType image) +void OBSBasic::AddDropSource(const DropItem &dropItem) { OBSBasic *main = OBSBasic::Get(); OBSDataAutoRelease settings = obs_data_create(); - const char *type = nullptr; - QString name; + QByteArray dataArr = dropItem.url.toUtf8(); + const char *data = dataArr.constData(); + const char *type = dropItem.typeId; obs_video_info ovi; obs_get_video_info(&ovi); - switch (image) { + switch (dropItem.dropType) { case DropType_RawText: obs_data_set_string(settings, "text", data); -#ifdef _WIN32 - type = "text_gdiplus"; -#else - type = "text_ft2_source"; -#endif break; case DropType_Text: #ifdef _WIN32 obs_data_set_bool(settings, "read_from_file", true); obs_data_set_string(settings, "file", data); - name = QUrl::fromLocalFile(QString(data)).fileName(); - type = "text_gdiplus"; #else obs_data_set_bool(settings, "from_file", true); obs_data_set_string(settings, "text_file", data); - type = "text_ft2_source"; #endif break; case DropType_Image: obs_data_set_string(settings, "file", data); - name = QUrl::fromLocalFile(QString(data)).fileName(); - type = "image_source"; break; case DropType_Media: obs_data_set_string(settings, "local_file", data); - name = QUrl::fromLocalFile(QString(data)).fileName(); - type = "ffmpeg_source"; break; case DropType_Html: obs_data_set_bool(settings, "is_local_file", true); obs_data_set_string(settings, "local_file", data); obs_data_set_int(settings, "width", ovi.base_width); obs_data_set_int(settings, "height", ovi.base_height); - name = QUrl::fromLocalFile(QString(data)).fileName(); - type = "browser_source"; break; case DropType_Url: - AddDropURL(QUrl(data), name, settings, ovi); - type = "browser_source"; + AddDropURL(QUrl(dropItem.url), settings, ovi); break; } - type = obs_get_latest_input_type_id(type); - - if (type == nullptr || !obs_source_get_display_name(type)) { - return; - } - - if (name.isEmpty()) { - name = obs_source_get_display_name(type); - } - std::string sourceName = GenerateSourceName(QT_TO_UTF8(name)); + std::string sourceName = GenerateSourceName(QT_TO_UTF8(dropItem.displayName)); OBSSourceAutoRelease source = obs_source_create(type, sourceName.c_str(), settings, nullptr); if (source) { OBSDataAutoRelease wrapper = obs_save_source(source); @@ -241,7 +255,7 @@ void OBSBasic::dragMoveEvent(QDragMoveEvent *event) event->acceptProposedAction(); } -void OBSBasic::ConfirmDropUrl(const QString &url) +bool OBSBasic::ConfirmDropUrl(const QString &url) { if (url.left(7).compare("http://", Qt::CaseInsensitive) == 0 || url.left(8).compare("https://", Qt::CaseInsensitive) == 0) { @@ -263,80 +277,100 @@ void OBSBasic::ConfirmDropUrl(const QString &url) messageBox.setIcon(QMessageBox::Question); messageBox.exec(); - if (messageBox.clickedButton() == yesButton) { - AddDropSource(QT_TO_UTF8(url), DropType_Url); - } + return messageBox.clickedButton() == yesButton; } + return false; } void OBSBasic::dropEvent(QDropEvent *event) { const QMimeData *mimeData = event->mimeData(); + QList addList{}; - if (mimeData->hasUrls()) { - QList urls = mimeData->urls(); + try { + if (mimeData->hasUrls()) { + QList urls = mimeData->urls(); - for (int i = 0; i < urls.size(); i++) { - QUrl url = urls[i]; - QString file = url.toLocalFile(); - QFileInfo fileInfo(file); + for (int i = 0; i < urls.size(); i++) { + QUrl url = urls[i]; + QString file = url.toLocalFile(); + QFileInfo fileInfo(file); - if (!fileInfo.exists()) { - ConfirmDropUrl(url.url()); - continue; - } + if (!fileInfo.exists()) { -#ifdef _WIN32 - if (fileInfo.suffix().compare("url", Qt::CaseInsensitive) == 0) { - QString urlTarget = ReadWindowsURLFile(file); - if (!urlTarget.isEmpty()) { - ConfirmDropUrl(urlTarget); + if (ConfirmDropUrl(url.url())) { + addList << DropItem{url.url(), GetUrlDisplayName(url), DropType_Url}; + } + continue; } - continue; - } else if (fileInfo.isShortcut()) { - file = fileInfo.symLinkTarget(); - fileInfo = QFileInfo(file); - if (!fileInfo.exists()) { + +#ifdef _WIN32 + if (fileInfo.suffix().compare("url", Qt::CaseInsensitive) == 0) { + QString urlTarget = ReadWindowsURLFile(file); + if (ConfirmDropUrl(urlTarget)) { + addList << DropItem{urlTarget, GetUrlDisplayName(QUrl(urlTarget)), + DropType_Url}; + } continue; + } else if (fileInfo.isShortcut()) { + file = fileInfo.symLinkTarget(); + fileInfo = QFileInfo(file); + if (!fileInfo.exists()) { + continue; + } } - } #endif - - QString suffixQStr = fileInfo.suffix(); - QByteArray suffixArray = suffixQStr.toUtf8(); - const char *suffix = suffixArray.constData(); - bool found = false; - - const char **cmp; - -#define CHECK_SUFFIX(extensions, type) \ - cmp = extensions; \ - while (*cmp) { \ - if (astrcmpi(*cmp, suffix) == 0) { \ - AddDropSource(QT_TO_UTF8(file), type); \ - found = true; \ - break; \ - } \ - \ - cmp++; \ - } \ - \ - if (found) \ + QString suffixQStr = fileInfo.suffix(); + QByteArray suffixArray = suffixQStr.toUtf8(); + const char *suffix = suffixArray.constData(); + bool found = false; + + const char **cmp; + +#define CHECK_SUFFIX(extensions, type) \ + cmp = extensions; \ + while (*cmp) { \ + if (astrcmpi(*cmp, suffix) == 0) { \ + addList << DropItem{file, fileInfo.fileName(), type};\ + found = true; \ + break; \ + } \ + \ + cmp++; \ + } \ + \ + if (found) \ continue; - CHECK_SUFFIX(textExtensions, DropType_Text); - CHECK_SUFFIX(htmlExtensions, DropType_Html); - CHECK_SUFFIX(imageExtensions, DropType_Image); - CHECK_SUFFIX(mediaExtensions, DropType_Media); + CHECK_SUFFIX(textExtensions, DropType_Text); + CHECK_SUFFIX(htmlExtensions, DropType_Html); + CHECK_SUFFIX(imageExtensions, DropType_Image); + CHECK_SUFFIX(mediaExtensions, DropType_Media); #undef CHECK_SUFFIX + } + } else if (mimeData->hasText()) { + AddDropSource(DropItem{mimeData->text(), DropType_RawText}); + } else if (event->mimeData()->hasFormat("application/x-obs-source-uuid")) { + QString uuid = QString::fromUtf8(event->mimeData()->data("application/x-obs-source-uuid")); + + emit sourceUuidDropped(uuid); + event->acceptProposedAction(); } - } else if (mimeData->hasText()) { - AddDropSource(QT_TO_UTF8(mimeData->text()), DropType_RawText); - } else if (event->mimeData()->hasFormat("application/x-obs-source-uuid")) { - QString uuid = QString::fromUtf8(event->mimeData()->data("application/x-obs-source-uuid")); + } - emit sourceUuidDropped(uuid); - event->acceptProposedAction(); + catch (const invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } + + if (addList.isEmpty()) { + return; + } + + // Reverse sort order, since AddDropSource() sends signals that are processed in reverse order + NaturalSort(addList, [](const DropItem &item) { return item.displayName; }, true); + + for (const auto &item : addList) { + AddDropSource(item); } } diff --git a/shared/qt/wrappers/qt-wrappers.hpp b/shared/qt/wrappers/qt-wrappers.hpp index 72ab917a5636c7..d0d6854d01b7ea 100644 --- a/shared/qt/wrappers/qt-wrappers.hpp +++ b/shared/qt/wrappers/qt-wrappers.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -99,3 +100,25 @@ QStringList OpenFiles(QWidget *parent, QString title, QString path, QString exte void TruncateLabel(QLabel *label, QString newText, int length = MAX_LABEL_LENGTH); void RefreshToolBarStyling(QToolBar *toolBar); + +template void NaturalSort(QList &list, Callback extractor, bool reverse = false) +{ + QCollator collator(QLocale::system()); + collator.setNumericMode(true); + collator.setCaseSensitivity(Qt::CaseInsensitive); + + if (reverse) { + std::sort(list.begin(), list.end(), [&collator, &extractor](const T &a, const T &b) { + return collator.compare(extractor(a), extractor(b)) > 0; + }); + } else { + std::sort(list.begin(), list.end(), [&collator, &extractor](const T &a, const T &b) { + return collator.compare(extractor(a), extractor(b)) < 0; + }); + } +} + +template void NaturalSort(QList &list, bool reverse = false) +{ + NaturalSort(list, [](const T &item) { return item; }, reverse); +} From 2b460fd73f4d2c46c224c8e099985e2090a2702c Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Tue, 23 Sep 2025 07:12:23 +0300 Subject: [PATCH 2/6] frontend: Natural sort profiles and scenes listing in menus Change locale-aware sorting of profiles and scene collections in menus to locale-aware natural sorting, so number sequences in names are treated by value rather than their alphabetical order. --- frontend/widgets/OBSBasic_Profiles.cpp | 7 +------ frontend/widgets/OBSBasic_SceneCollections.cpp | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/frontend/widgets/OBSBasic_Profiles.cpp b/frontend/widgets/OBSBasic_Profiles.cpp index 836be77ac0743d..2c95114e5f509b 100644 --- a/frontend/widgets/OBSBasic_Profiles.cpp +++ b/frontend/widgets/OBSBasic_Profiles.cpp @@ -47,7 +47,6 @@ QList sortedProfiles{}; void updateSortedProfiles(const OBSProfileCache &profiles) { - const QLocale locale = QLocale::system(); QList newList{}; for (auto [profileName, _] : profiles) { @@ -55,11 +54,7 @@ void updateSortedProfiles(const OBSProfileCache &profiles) newList.append(entry); } - std::sort(newList.begin(), newList.end(), [&locale](const QString &lhs, const QString &rhs) -> bool { - int result = QString::localeAwareCompare(locale.toLower(lhs), locale.toLower(rhs)); - - return (result < 0); - }); + NaturalSort(newList); sortedProfiles.swap(newList); } diff --git a/frontend/widgets/OBSBasic_SceneCollections.cpp b/frontend/widgets/OBSBasic_SceneCollections.cpp index fbbcc37b5d9725..cb451121d6e781 100644 --- a/frontend/widgets/OBSBasic_SceneCollections.cpp +++ b/frontend/widgets/OBSBasic_SceneCollections.cpp @@ -60,7 +60,6 @@ QList sortedSceneCollections{}; void updateSortedSceneCollections(const OBSSceneCollectionCache &collections) { - const QLocale locale = QLocale::system(); QList newList{}; for (auto [collectionName, _] : collections) { @@ -68,11 +67,7 @@ void updateSortedSceneCollections(const OBSSceneCollectionCache &collections) newList.append(entry); } - std::sort(newList.begin(), newList.end(), [&locale](const QString &lhs, const QString &rhs) -> bool { - int result = QString::localeAwareCompare(locale.toLower(lhs), locale.toLower(rhs)); - - return (result < 0); - }); + NaturalSort(newList); sortedSceneCollections.swap(newList); } From 4bbf63cfbcb6266fd7b0f7ca9650a7a34dc99c10 Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Tue, 23 Sep 2025 07:20:59 +0300 Subject: [PATCH 3/6] libobs: Add natural comparison methods for strings New comparison methods astrnatcmpi() and wstrnatcmpi() for sorting strings and documentation for them. Uses locale aware strcol() / wcscoll() and a custom implementation of natural comparison for number sequences. Supports multibyte and whcar surrogate characters. This is intended to be used for parts that do not have direct access to higher level methods like Qt QCollator. --- docs/sphinx/reference-libobs-util-dstr.rst | 22 ++ libobs/util/dstr.c | 234 +++++++++++++++++++++ libobs/util/dstr.h | 2 + 3 files changed, 258 insertions(+) diff --git a/docs/sphinx/reference-libobs-util-dstr.rst b/docs/sphinx/reference-libobs-util-dstr.rst index 91794e2d60ac93..7f0eba47640e5f 100644 --- a/docs/sphinx/reference-libobs-util-dstr.rst +++ b/docs/sphinx/reference-libobs-util-dstr.rst @@ -59,6 +59,28 @@ General String Helper Functions ---------------------- +.. function:: int astrnatcmp(const char *str1, const char *str2) + + Natural comparison function for strings. + + In natural comparison numbers are compared by their value instead of the alphabetical order. + For example "z2" < "z11" while in alphabetical comparison it would be "z11" < "z2". + + Otherwise the function uses locale aware string comparison function strcol(). + +---------------------- + +.. function:: int wstrnatcmp(const wchar_t *str1, const wchar_t *str2) + + Natural comparison function for wide strings. + + In natural comparison numbers are compared by their value instead of the alphabetical order. + For example "z2" < "z11" while in alphabetical comparison it would be "z11" < "z2". + + Otherwise the function uses locale aware wide string comparison function wcscoll(). + +---------------------- + .. function:: char *astrstri(const char *str, const char *find) Case insensitive version of strstr. diff --git a/libobs/util/dstr.c b/libobs/util/dstr.c index 4dc415cb356841..c8b977cc4728b6 100644 --- a/libobs/util/dstr.c +++ b/libobs/util/dstr.c @@ -35,6 +35,13 @@ static const char *astrblank = ""; static const wchar_t *wstrblank = L""; +static int astrncoll(const char *str1, const char *str2, size_t read1, size_t read2); +static int wstrncoll(const wchar_t *str1, const wchar_t *str2, size_t read1, size_t read2); +static inline int a_compare_number(const char *a, const char *b, size_t *read); +static inline int w_compare_number(const wchar_t *a, const wchar_t *b, size_t *read); +static inline size_t a_mblen(const char *str); +static inline size_t w_mblen(const wchar_t *str); + int astrcmpi(const char *str1, const char *str2) { if (!str1) @@ -163,6 +170,233 @@ int wstrcmpi_n(const wchar_t *str1, const wchar_t *str2, size_t n) return 0; } +int astrnatcmp(const char *str1, const char *str2) +{ + if (!str1) + str1 = astrblank; + if (!str2) + str2 = astrblank; + + size_t len1 = strlen(str1); + size_t len2 = strlen(str2); + + if (len1 == 0 || len2 == 0) + return strcoll(str1, str2); + + // Track indexes separately, because we may have multibyte characters. + size_t i1 = 0; + size_t i2 = 0; + size_t read1; + size_t read2; + int r; + + for (;;) { + + if (isdigit((unsigned char)str1[i1]) && isdigit((unsigned char)str2[i2])) { + if ((r = a_compare_number(str1 + i1, str2 + i2, &read1)) != 0) + return r; + read2 = read1; + } else { + read1 = a_mblen(str1 + i1); + read2 = a_mblen(str2 + i2); + } + + /* + * Scan forward until next number comparison can be made or end of string is reached. + * astrncoll considers locale, so a longer string could be before or after a shorter string. + */ + while (read1 + i1 < len1 && read2 + i2 < len2 && + !(isdigit((unsigned char)str1[i1 + read1]) && isdigit((unsigned char)str2[i2 + read2]))) { + read1 += a_mblen(str1 + i1 + read1); + read2 += a_mblen(str2 + i2 + read2); + } + + if (read1 + i1 >= len1 || read2 + i2 >= len2) + break; + + if ((r = astrncoll(str1 + i1, str2 + i2, read1, read2)) != 0) + return r; + + i1 += read1; + i2 += read2; + } + return strcoll(str1 + i1, str2 + i2); +} + +int wstrnatcmp(const wchar_t *str1, const wchar_t *str2) +{ + if (!str1) + str1 = wstrblank; + if (!str2) + str2 = wstrblank; + + size_t len1 = wcslen(str1); + size_t len2 = wcslen(str2); + + if (len1 == 0 || len2 == 0) + return wcscoll(str1, str2); + + // Track indexes separately, because we may have surrogate pairs. + size_t i1 = 0; + size_t i2 = 0; + size_t read1; + size_t read2; + int r; + + for (;;) { + + if (iswdigit(str1[i1]) && iswdigit(str2[i2])) { + if ((r = w_compare_number(str1 + i1, str2 + i2, &read1)) != 0) + return r; + read2 = read1; + } else { + read1 = w_mblen(str1 + i1); + read2 = w_mblen(str2 + i2); + } + + /* + * Scan forward until next number comparison can be made or end of string is reached. + * wstrncoll considers locale, so a longer string could be before or after a shorter string. + */ + while (read1 + i1 < len1 && read2 + i2 < len2 && + !(iswdigit(str1[i1 + read1]) && iswdigit(str2[i2 + read2]))) { + read1 += w_mblen(str1 + i1 + read1); + read2 += w_mblen(str2 + i2 + read2); + } + + if (read1 + i1 >= len1 || read2 + i2 >= len2) + break; + + if ((r = wstrncoll(str1 + i1, str2 + i2, read1, read2)) != 0) + return r; + + i1 += read1; + i2 += read2; + } + return wcscoll(str1 + i1, str2 + i2); +} + +static int astrncoll(const char *str1, const char *str2, const size_t read1, size_t read2) +{ + char *buf1 = bstrdup_n(str1, read1); + char *buf2 = bstrdup_n(str2, read2); + + int r = strcoll(buf1, buf2); + + bfree(buf1); + bfree(buf2); + + return r; +} + +static int wstrncoll(const wchar_t *str1, const wchar_t *str2, size_t read1, size_t read2) +{ + wchar_t *buf1 = bwstrdup_n(str1, read1); + wchar_t *buf2 = bwstrdup_n(str2, read2); + + int r = wcscoll(buf1, buf2); + + bfree(buf1); + bfree(buf2); + + return r; +} + +static inline int a_compare_number(const char *a, const char *b, size_t *skip) +{ + int r = 0; + size_t ai = 0; + size_t bi = 0; + + while (a[ai] == '0') + ai++; + + while (b[bi] == '0') + bi++; + + for (; isdigit((unsigned char)a[ai]) || isdigit((unsigned char)b[bi]); ai++, bi++) { + + if (!isdigit((unsigned char)a[ai])) + return -1; + if (!isdigit((unsigned char)b[bi])) + return 1; + if (r == 0) { + if (a[ai] < b[bi]) + r = -1; + else if (a[ai] > b[bi]) + r = 1; + } + }; + // We can skip only shorter numbers length, since we may have multibyte characters + *skip = (ai > bi) ? bi : ai; + return r; +} + +static inline int w_compare_number(const wchar_t *a, const wchar_t *b, size_t *skip) +{ + int r = 0; + size_t ai = 0; + size_t bi = 0; + + while (a[ai] == '0') + ai++; + + while (b[bi] == '0') + bi++; + + for (; iswdigit(a[ai]) || iswdigit(b[bi]); ai++, bi++) { + + if (!iswdigit(a[ai])) + return -1; + if (!iswdigit(b[bi])) + return 1; + if (r == 0) { + if (a[ai] < b[bi]) + r = -1; + else if (a[ai] > b[bi]) + r = 1; + } + }; + // We can skip only shorter numbers length, since we may have multibyte characters + *skip = (ai > bi) ? bi : ai; + return r; +} + +// Returns length of multibyte character or 1 for malformations and single byte characters +// str must be null terminated +static inline size_t a_mblen(const char *str) +{ + if ((unsigned char)str[0] >> 7 == 0b0) // (Not) Multibyte start bit + return 1; + + if ((unsigned char)str[1] >> 6 != 0b10) // (Not) Multibyte continuation bit + return 1; + if ((unsigned char)str[2] >> 6 != 0b10) + return 2; + if ((unsigned char)str[3] >> 6 != 0b10) + return 3; + + return 4; +} + +// Windows wchar_t is 16bit while unix is 32bit, thus Windows needs to check for surrogates +// str must be null terminated +static inline size_t w_mblen(const wchar_t *str) +{ +#ifdef WIN32 + if (str[0] < 0xD800 || str[0] > 0xDBFF) // (Not) high-surrogate + return 1; + + if (str[1] < 0xDC00 || str[1] > 0xDFFF) // (Not) low-surrogate + return 1; + + return 2; +#else + UNUSED_PARAMETER(str); + return 1; +#endif +} + char *astrstri(const char *str, const char *find) { size_t len; diff --git a/libobs/util/dstr.h b/libobs/util/dstr.h index 5ef11f3c485473..bf1371c023bac4 100644 --- a/libobs/util/dstr.h +++ b/libobs/util/dstr.h @@ -51,6 +51,8 @@ EXPORT int astrcmp_n(const char *str1, const char *str2, size_t n); EXPORT int wstrcmp_n(const wchar_t *str1, const wchar_t *str2, size_t n); EXPORT int astrcmpi_n(const char *str1, const char *str2, size_t n); EXPORT int wstrcmpi_n(const wchar_t *str1, const wchar_t *str2, size_t n); +EXPORT int astrnatcmp(const char *str1, const char *str2); +EXPORT int wstrnatcmp(const wchar_t *str1, const wchar_t *str2); EXPORT char *astrstri(const char *str, const char *find); EXPORT wchar_t *wstrstri(const wchar_t *str, const wchar_t *find); From d400a561c31c2caafe8ad630ed8cca6d09ff6f0c Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Tue, 23 Sep 2025 07:34:28 +0300 Subject: [PATCH 4/6] plugins: Natural sort slideshow mk2 images in directories Applies locale-aware natural sorting to the cycled files when adding directory to image slideshow. Previously files in directories were cycled in arbitrary order by os_readdir(). Locale-awareness is limited on Windows currently, because we are using minimal 'C' locale for it. This is to be addressed separately. --- plugins/image-source/obs-slideshow-mk2.c | 40 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/plugins/image-source/obs-slideshow-mk2.c b/plugins/image-source/obs-slideshow-mk2.c index 4d756961de4651..6ef0f595d822ed 100644 --- a/plugins/image-source/obs-slideshow-mk2.c +++ b/plugins/image-source/obs-slideshow-mk2.c @@ -405,6 +405,14 @@ static void restart_slides(struct slideshow *ss) ssd->slides = new_slides; } +static int compare_files(const void *a, const void *b) +{ + const struct image_file_data *aa = a; + const struct image_file_data *bb = b; + + return astrnatcmp(aa->path, bb->path); +} + static void ss_update(void *data, obs_data_t *settings) { struct slideshow *ss = data; @@ -498,9 +506,11 @@ static void ss_update(void *data, obs_data_t *settings) } if (dir) { - struct dstr dir_path = {0}; struct os_dirent *ent; + image_file_array_t sortable_files = {0}; + da_init(sortable_files); + for (;;) { const char *ext; @@ -514,14 +524,30 @@ static void ss_update(void *data, obs_data_t *settings) if (!valid_extension(ext)) continue; - dstr_copy(&dir_path, path); - dstr_cat_ch(&dir_path, '/'); - dstr_cat(&dir_path, ent->d_name); - add_file(&new_data.files, dir_path.array); + add_file(&sortable_files, ent->d_name); } - - dstr_free(&dir_path); os_closedir(dir); + + if (sortable_files.num > 0) { + qsort(sortable_files.array, sortable_files.num, sizeof(struct image_file_data), + compare_files); + + struct dstr dir_path = {0}; + + for (size_t i = 0; i < sortable_files.num; i++) { + + dstr_copy(&dir_path, path); + dstr_cat_ch(&dir_path, '/'); + dstr_cat(&dir_path, sortable_files.array[i].path); + + add_file(&new_data.files, dir_path.array); + + bfree(sortable_files.array[i].path); + } + dstr_free(&dir_path); + } + da_free(sortable_files); + } else { add_file(&new_data.files, path); } From 36cd96039bfb78c8b4b1164f27cf3a6b02cc88fa Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Sat, 31 Jan 2026 04:14:18 +0200 Subject: [PATCH 5/6] vlc-video: Apply natsort to vlc playlist dirs Applies the same natural, alphabetical sort to vlc playlist directories that was added for slideshow. --- plugins/vlc-video/vlc-video-source.c | 44 +++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/plugins/vlc-video/vlc-video-source.c b/plugins/vlc-video/vlc-video-source.c index 0be3ced5365d6a..da0574bbb9d9a3 100644 --- a/plugins/vlc-video/vlc-video-source.c +++ b/plugins/vlc-video/vlc-video-source.c @@ -589,6 +589,14 @@ static bool valid_extension(const char *ext) return valid; } +static int compare_files(const void *a, const void *b) +{ + const struct media_file_data *aa = a; + const struct media_file_data *bb = b; + + return astrnatcmp(aa->path, bb->path); +} + static void vlcs_update(void *data, obs_data_t *settings) { media_file_array_t new_files; @@ -642,9 +650,11 @@ static void vlcs_update(void *data, obs_data_t *settings) os_dir_t *dir = os_opendir(path); if (dir) { - struct dstr dir_path = {0}; struct os_dirent *ent; + media_file_array_t sortable_files = {0}; + da_init(sortable_files); + for (;;) { const char *ext; @@ -658,15 +668,35 @@ static void vlcs_update(void *data, obs_data_t *settings) if (!valid_extension(ext)) continue; - dstr_copy(&dir_path, path); - dstr_cat_ch(&dir_path, '/'); - dstr_cat(&dir_path, ent->d_name); - add_file(c, &new_files, dir_path.array, network_caching, track_index, subtitle_index, - subtitle_enable); + struct media_file_data media_file; + media_file.path = bstrdup(ent->d_name); + + da_push_back(sortable_files, &media_file); } - dstr_free(&dir_path); os_closedir(dir); + + if (sortable_files.num > 0) { + qsort(sortable_files.array, sortable_files.num, sizeof(struct media_file_data), + compare_files); + + struct dstr dir_path = {0}; + + for (size_t i = 0; i < sortable_files.num; i++) { + + dstr_copy(&dir_path, path); + dstr_cat_ch(&dir_path, '/'); + dstr_cat(&dir_path, sortable_files.array[i].path); + + add_file(c, &new_files, dir_path.array, network_caching, track_index, + subtitle_index, subtitle_enable); + + bfree(sortable_files.array[i].path); + } + dstr_free(&dir_path); + } + da_free(sortable_files); + } else { add_file(c, &new_files, path, network_caching, track_index, subtitle_index, subtitle_enable); } From 1801f2916ec5ad4d6878b019c877d70d724e33bb Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Mon, 16 Feb 2026 14:59:32 +0200 Subject: [PATCH 6/6] libobs, plugins: Add locale functions for libobs Adds locale aware functions for libobs and makes slideshow and vlc plugins use them for natural, locale aware file sorting. Adds documentation for the new locale apis. --- docs/sphinx/reference-libobs-util-dstr.rst | 8 +- .../sphinx/reference-libobs-util-platform.rst | 70 ++++++++++++++++ libobs/util/dstr.c | 31 +++---- libobs/util/dstr.h | 5 +- libobs/util/platform-nix.c | 80 +++++++++++++++++- libobs/util/platform-windows.c | 83 ++++++++++++++++++- libobs/util/platform.h | 20 +++++ plugins/image-source/obs-slideshow-mk2.c | 56 ++++--------- plugins/vlc-video/vlc-video-source.c | 62 ++++---------- 9 files changed, 307 insertions(+), 108 deletions(-) diff --git a/docs/sphinx/reference-libobs-util-dstr.rst b/docs/sphinx/reference-libobs-util-dstr.rst index 7f0eba47640e5f..a180f1fc9fd8ce 100644 --- a/docs/sphinx/reference-libobs-util-dstr.rst +++ b/docs/sphinx/reference-libobs-util-dstr.rst @@ -59,25 +59,25 @@ General String Helper Functions ---------------------- -.. function:: int astrnatcmp(const char *str1, const char *str2) +.. function:: int astrnatcmp(const char *str1, const char *str2, os_locale_t locale) Natural comparison function for strings. In natural comparison numbers are compared by their value instead of the alphabetical order. For example "z2" < "z11" while in alphabetical comparison it would be "z11" < "z2". - Otherwise the function uses locale aware string comparison function strcol(). + Otherwise the function uses locale aware string comparison function strcoll_l() / _strcoll_l(). ---------------------- -.. function:: int wstrnatcmp(const wchar_t *str1, const wchar_t *str2) +.. function:: int wstrnatcmp(const wchar_t *str1, const wchar_t *str2, os_locale_t locale) Natural comparison function for wide strings. In natural comparison numbers are compared by their value instead of the alphabetical order. For example "z2" < "z11" while in alphabetical comparison it would be "z11" < "z2". - Otherwise the function uses locale aware wide string comparison function wcscoll(). + Otherwise the function uses locale aware wide string comparison function wcscoll_l() / _wcscoll_l(). ---------------------- diff --git a/docs/sphinx/reference-libobs-util-platform.rst b/docs/sphinx/reference-libobs-util-platform.rst index 3584e282e3456c..9f978963019c57 100644 --- a/docs/sphinx/reference-libobs-util-platform.rst +++ b/docs/sphinx/reference-libobs-util-platform.rst @@ -131,6 +131,60 @@ Number/String Conversion Functions --------------------- +Locale Awareness Functions +---------------------------------- + +.. type:: os_locale_t + + Platform specific locale. Either _locale_t for Windows or locale_t for posix. + +.. type:: int (*os_locale_aware_cmp)(const void *a, const void *b, os_locale_t locale) + + Locale aware comparison function. + +--------------------- + +.. function:: int os_strcoll_l(const char *a, const char *b, os_locale_t locale) + + Performs locale aware comparison with strcoll_l() or _strcoll_l() depending on platform. + +--------------------- + +.. function:: int os_wcscoll_l(const wchar_t *a, const wchar_t *b, os_locale_t locale) + + Performs locale aware comparison with wcscoll_l() or _wcscoll_l() depending on platform. + +--------------------- + +.. function:: os_locale_t os_get_locale(const char *locale) + + Returns platform specific locale type. Must be freed with os_free_locale(). + + :param locale: Locale to be created. (ie. "en_US.UTF-8") + If NULL or empty, system default with UTF-8 encoding (for char apis) is returned. + +--------------------- + +.. function:: void os_free_locale(os_locale_t locale) + + Frees locale acquired with os_get_locale(). + +--------------------- + +.. function:: void os_locale_aware_sort(void *base, size_t elements, size_t element_size, os_locale_aware_cmp cmp, os_locale_t locale) + + Performs locale aware sort with parameters similiar to qsort(). + + :param cmp: Comparison callback to use. Takes os_locale_t as the 3rd parameter. + This can be for example os_wcscoll_l or wstrnatcoll if the sort list is whcar_t* directly. + + :param locale: Locale acquired from os_get_locale(). + If NULL, system default with UTF-8 encoding is used. + If the comparison is for wide chars, then platform specific encoding is used. + +--------------------- + + Dynamic Link Library Functions ------------------------------ @@ -285,6 +339,22 @@ Other Path/File Functions --------------------- +.. type:: bool (*dir_filter_func)(struct os_dirent *) + + Filter function when iterating files in directory. Should return false to remove entries and true to keep them. + +--------------------- + +.. function:: void os_sortdir_natural(os_dir_t *dir, struct darray *sorted, dir_filter_func filter_func) + + Returns files in directory sorted by a natural (number aware) and locale aware sort. + + :param sorted: Pointer to darray of struct os_dirent to append files to. + + :param filter_func: Filter function for directory files. + +--------------------- + .. struct:: os_globent A glob entry. diff --git a/libobs/util/dstr.c b/libobs/util/dstr.c index c8b977cc4728b6..03b39c7f817091 100644 --- a/libobs/util/dstr.c +++ b/libobs/util/dstr.c @@ -30,13 +30,12 @@ #include "bmem.h" #include "utf8.h" #include "lexer.h" -#include "platform.h" static const char *astrblank = ""; static const wchar_t *wstrblank = L""; -static int astrncoll(const char *str1, const char *str2, size_t read1, size_t read2); -static int wstrncoll(const wchar_t *str1, const wchar_t *str2, size_t read1, size_t read2); +static int astrncoll(const char *str1, const char *str2, size_t read1, size_t read2, os_locale_t locale); +static int wstrncoll(const wchar_t *str1, const wchar_t *str2, size_t read1, size_t read2, os_locale_t locale); static inline int a_compare_number(const char *a, const char *b, size_t *read); static inline int w_compare_number(const wchar_t *a, const wchar_t *b, size_t *read); static inline size_t a_mblen(const char *str); @@ -170,7 +169,9 @@ int wstrcmpi_n(const wchar_t *str1, const wchar_t *str2, size_t n) return 0; } -int astrnatcmp(const char *str1, const char *str2) +// Windows shouldn't do collation with strcoll() astrnatcmp() is based on, because it's limited to single bytes unlike unix +// Instead use the wide variant wstrnatcmp() based on wcscoll() +int astrnatcmp(const char *str1, const char *str2, os_locale_t locale) { if (!str1) str1 = astrblank; @@ -181,7 +182,7 @@ int astrnatcmp(const char *str1, const char *str2) size_t len2 = strlen(str2); if (len1 == 0 || len2 == 0) - return strcoll(str1, str2); + return os_strcoll_l(str1, str2, locale); // Track indexes separately, because we may have multibyte characters. size_t i1 = 0; @@ -214,16 +215,16 @@ int astrnatcmp(const char *str1, const char *str2) if (read1 + i1 >= len1 || read2 + i2 >= len2) break; - if ((r = astrncoll(str1 + i1, str2 + i2, read1, read2)) != 0) + if ((r = astrncoll(str1 + i1, str2 + i2, read1, read2, locale)) != 0) return r; i1 += read1; i2 += read2; } - return strcoll(str1 + i1, str2 + i2); + return os_strcoll_l(str1 + i1, str2 + i2, locale); } -int wstrnatcmp(const wchar_t *str1, const wchar_t *str2) +int wstrnatcmp(const wchar_t *str1, const wchar_t *str2, os_locale_t locale) { if (!str1) str1 = wstrblank; @@ -234,7 +235,7 @@ int wstrnatcmp(const wchar_t *str1, const wchar_t *str2) size_t len2 = wcslen(str2); if (len1 == 0 || len2 == 0) - return wcscoll(str1, str2); + return os_wcscoll_l(str1, str2, locale); // Track indexes separately, because we may have surrogate pairs. size_t i1 = 0; @@ -267,21 +268,21 @@ int wstrnatcmp(const wchar_t *str1, const wchar_t *str2) if (read1 + i1 >= len1 || read2 + i2 >= len2) break; - if ((r = wstrncoll(str1 + i1, str2 + i2, read1, read2)) != 0) + if ((r = wstrncoll(str1 + i1, str2 + i2, read1, read2, locale)) != 0) return r; i1 += read1; i2 += read2; } - return wcscoll(str1 + i1, str2 + i2); + return os_wcscoll_l(str1 + i1, str2 + i2, locale); } -static int astrncoll(const char *str1, const char *str2, const size_t read1, size_t read2) +static int astrncoll(const char *str1, const char *str2, const size_t read1, size_t read2, os_locale_t locale) { char *buf1 = bstrdup_n(str1, read1); char *buf2 = bstrdup_n(str2, read2); - int r = strcoll(buf1, buf2); + int r = os_strcoll_l(buf1, buf2, locale); bfree(buf1); bfree(buf2); @@ -289,12 +290,12 @@ static int astrncoll(const char *str1, const char *str2, const size_t read1, siz return r; } -static int wstrncoll(const wchar_t *str1, const wchar_t *str2, size_t read1, size_t read2) +static int wstrncoll(const wchar_t *str1, const wchar_t *str2, size_t read1, size_t read2, os_locale_t locale) { wchar_t *buf1 = bwstrdup_n(str1, read1); wchar_t *buf2 = bwstrdup_n(str2, read2); - int r = wcscoll(buf1, buf2); + int r = os_wcscoll_l(buf1, buf2, locale); bfree(buf1); bfree(buf2); diff --git a/libobs/util/dstr.h b/libobs/util/dstr.h index bf1371c023bac4..9d7a661cd01ade 100644 --- a/libobs/util/dstr.h +++ b/libobs/util/dstr.h @@ -20,6 +20,7 @@ #include #include "c99defs.h" #include "bmem.h" +#include "platform.h" /* * Dynamic string @@ -51,8 +52,8 @@ EXPORT int astrcmp_n(const char *str1, const char *str2, size_t n); EXPORT int wstrcmp_n(const wchar_t *str1, const wchar_t *str2, size_t n); EXPORT int astrcmpi_n(const char *str1, const char *str2, size_t n); EXPORT int wstrcmpi_n(const wchar_t *str1, const wchar_t *str2, size_t n); -EXPORT int astrnatcmp(const char *str1, const char *str2); -EXPORT int wstrnatcmp(const wchar_t *str1, const wchar_t *str2); +EXPORT int astrnatcmp(const char *str1, const char *str2, os_locale_t locale); +EXPORT int wstrnatcmp(const wchar_t *str1, const wchar_t *str2, os_locale_t locale); EXPORT char *astrstri(const char *str, const char *find); EXPORT wchar_t *wstrstri(const wchar_t *str, const wchar_t *find); diff --git a/libobs/util/platform-nix.c b/libobs/util/platform-nix.c index 5648ca61b03a2b..4eb7ad8c16a972 100644 --- a/libobs/util/platform-nix.c +++ b/libobs/util/platform-nix.c @@ -28,6 +28,9 @@ #include #include #include +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) +#include +#endif #include "obsconfig.h" @@ -54,7 +57,6 @@ #include #endif -#include "darray.h" #include "dstr.h" #include "platform.h" #include "threading.h" @@ -425,6 +427,54 @@ char *os_get_abs_path_ptr(const char *path) return ptr; } +int os_strcoll_l(const char *a, const char *b, os_locale_t locale) +{ + return strcoll_l(a, b, locale); +} +int os_wcscoll_l(const wchar_t *a, const wchar_t *b, os_locale_t locale) +{ + return wcscoll_l(a, b, locale); +} + +os_locale_t os_get_locale(const char *locale) +{ + return newlocale(LC_ALL_MASK, locale ? locale : ".UTF-8", (locale_t)0); +} + +void os_free_locale(os_locale_t locale) +{ + freelocale(locale); +} + +struct _locale_aware_sort_context { + os_locale_aware_cmp cmp; + os_locale_t locale; +}; +typedef struct _locale_aware_sort_context locale_aware_sort_ctx; + +static THREAD_LOCAL locale_aware_sort_ctx *sort_ctx = NULL; + +int _locale_aware_comparison(const void *a, const void *b) +{ + return sort_ctx->cmp(a, b, sort_ctx->locale); +} + +void os_locale_aware_sort(void *base, size_t elements, size_t element_size, os_locale_aware_cmp cmp, os_locale_t locale) +{ + bool tmplocale = !locale; + + locale_aware_sort_ctx context = {.cmp = cmp, .locale = tmplocale ? os_get_locale(NULL) : locale}; + + // Restore old context after, so this function is re-entrant capable despite static thread locals + locale_aware_sort_ctx *old_ctx = sort_ctx; + sort_ctx = &context; + qsort(base, elements, element_size, _locale_aware_comparison); + sort_ctx = old_ctx; + + if (tmplocale) + os_free_locale(context.locale); +} + struct os_dir { const char *path; DIR *dir; @@ -492,6 +542,34 @@ void os_closedir(os_dir_t *dir) } } +int natural_compare_files(const void *a, const void *b, os_locale_t locale) +{ + const struct os_dirent *aa = (const struct os_dirent *)a; + const struct os_dirent *bb = (const struct os_dirent *)b; + + return astrnatcmp(aa->d_name, bb->d_name, locale); +} + +void os_sortdir_natural(os_dir_t *dir, struct darray *sorted, dir_filter_func filter_func) +{ + size_t start = sorted->num; + size_t item_size = sizeof(struct os_dirent); + + while (os_readdir(dir)) { + + if (!filter_func || filter_func(&dir->out)) { + darray_push_back(item_size, sorted, &dir->out); + // da_push_back(*sorted, &dir->out); + } + } + + size_t count = sorted->num - start; + if (count) { + struct os_dirent *start_iter = &((struct os_dirent *)sorted->array)[start]; + os_locale_aware_sort(start_iter, count, item_size, natural_compare_files, NULL); + } +} + #ifndef __APPLE__ int64_t os_get_free_space(const char *path) { diff --git a/libobs/util/platform-windows.c b/libobs/util/platform-windows.c index df4efb7848dd27..953e61d73cf91f 100644 --- a/libobs/util/platform-windows.c +++ b/libobs/util/platform-windows.c @@ -25,7 +25,6 @@ #include "base.h" #include "platform.h" -#include "darray.h" #include "dstr.h" #include "util_uint64.h" #include "windows/win-registry.h" @@ -525,6 +524,47 @@ char *os_get_abs_path_ptr(const char *path) return ptr; } +int os_strcoll_l(const char *a, const char *b, os_locale_t locale) +{ + return _strcoll_l(a, b, locale); +} +int os_wcscoll_l(const wchar_t *a, const wchar_t *b, os_locale_t locale) +{ + return _wcscoll_l(a, b, locale); +} + +os_locale_t os_get_locale(const char *locale) +{ + return _create_locale(LC_ALL, locale ? locale : ".UTF-8"); +} + +void os_free_locale(os_locale_t locale) +{ + _free_locale(locale); +} + +struct _locale_aware_sort_context { + os_locale_aware_cmp cmp; + os_locale_t locale; +}; +typedef struct _locale_aware_sort_context locale_aware_sort_ctx; + +int _locale_aware_comparison(locale_aware_sort_ctx *context, const void *a, const void *b) +{ + return context->cmp(a, b, context->locale); +} + +void os_locale_aware_sort(void *base, size_t elements, size_t element_size, os_locale_aware_cmp cmp, os_locale_t locale) +{ + bool tmplocale = !locale; + + locale_aware_sort_ctx context = {.cmp = cmp, .locale = tmplocale ? os_get_locale(NULL) : locale}; + qsort_s(base, elements, element_size, _locale_aware_comparison, &context); + + if (tmplocale) + os_free_locale(context.locale); +} + struct os_dir { HANDLE handle; WIN32_FIND_DATA wfd; @@ -592,6 +632,47 @@ void os_closedir(os_dir_t *dir) } } +struct _sort_file_data { + struct os_dirent out; + wchar_t *wname; +}; +typedef struct _sort_file_data sort_file_data_t; + +int natural_compare_files(const sort_file_data_t *a, const sort_file_data_t *b, os_locale_t locale) +{ + return wstrnatcmp(a->wname, b->wname, locale); +} + +void os_sortdir_natural(os_dir_t *dir, struct darray *sorted, dir_filter_func filter_func) +{ + DARRAY(sort_file_data_t) files; + da_init(files); + + while (os_readdir(dir)) { + + if (!filter_func || filter_func(&dir->out)) { + sort_file_data_t sort_data = {.out = dir->out, .wname = bwstrdup(dir->wfd.cFileName)}; + da_push_back(files, &sort_data); + } + } + + if (files.num) { + + os_locale_aware_sort(&files.array[0], files.num, sizeof(sort_file_data_t), natural_compare_files, NULL); + + size_t item_size = sizeof(struct os_dirent); + darray_reserve(item_size, sorted, sorted->num + files.num); + //da_reserve(*sorted, sorted->num + files.num); + + for (size_t i = 0; i < files.num; i++) { + darray_push_back(item_size, sorted, &files.array[i].out); + //da_push_back(*sorted, &files.array[i].out); + bfree(files.array[i].wname); + } + } + da_free(files); +} + int64_t os_get_free_space(const char *path) { ULARGE_INTEGER remainingSpace; diff --git a/libobs/util/platform.h b/libobs/util/platform.h index 592d9eca6e5c8c..44c3e360a55774 100644 --- a/libobs/util/platform.h +++ b/libobs/util/platform.h @@ -19,7 +19,9 @@ #include #include #include +#include #include "c99defs.h" +#include "darray.h" /* * Platform-independent functions for Accessing files, encoding, DLLs, @@ -117,6 +119,21 @@ EXPORT const char *os_get_path_extension(const char *path); EXPORT bool os_get_emulation_status(void); +#ifdef _WIN32 +typedef _locale_t os_locale_t; +#else +typedef locale_t os_locale_t; +#endif + +typedef int (*os_locale_aware_cmp)(const void *a, const void *b, os_locale_t locale); + +EXPORT int os_strcoll_l(const char *a, const char *b, os_locale_t locale); +EXPORT int os_wcscoll_l(const wchar_t *a, const wchar_t *b, os_locale_t locale); +EXPORT os_locale_t os_get_locale(const char *locale); +EXPORT void os_free_locale(os_locale_t locale); +EXPORT void os_locale_aware_sort(void *base, size_t elements, size_t element_size, os_locale_aware_cmp cmp, + os_locale_t locale); + struct os_dir; typedef struct os_dir os_dir_t; @@ -125,9 +142,12 @@ struct os_dirent { bool directory; }; +typedef bool (*dir_filter_func)(struct os_dirent *); + EXPORT os_dir_t *os_opendir(const char *path); EXPORT struct os_dirent *os_readdir(os_dir_t *dir); EXPORT void os_closedir(os_dir_t *dir); +EXPORT void os_sortdir_natural(os_dir_t *dir, struct darray *sorted, dir_filter_func filter_func); struct os_globent { char *path; diff --git a/plugins/image-source/obs-slideshow-mk2.c b/plugins/image-source/obs-slideshow-mk2.c index 6ef0f595d822ed..ae1e6aa3d92c84 100644 --- a/plugins/image-source/obs-slideshow-mk2.c +++ b/plugins/image-source/obs-slideshow-mk2.c @@ -405,12 +405,13 @@ static void restart_slides(struct slideshow *ss) ssd->slides = new_slides; } -static int compare_files(const void *a, const void *b) +bool dir_file_filter(struct os_dirent *ent) { - const struct image_file_data *aa = a; - const struct image_file_data *bb = b; + if (ent->directory) + return false; - return astrnatcmp(aa->path, bb->path); + const char *ext = os_get_path_extension(ent->d_name); + return valid_extension(ext); } static void ss_update(void *data, obs_data_t *settings) @@ -506,47 +507,22 @@ static void ss_update(void *data, obs_data_t *settings) } if (dir) { - struct os_dirent *ent; - - image_file_array_t sortable_files = {0}; - da_init(sortable_files); - - for (;;) { - const char *ext; - - ent = os_readdir(dir); - if (!ent) - break; - if (ent->directory) - continue; - - ext = os_get_path_extension(ent->d_name); - if (!valid_extension(ext)) - continue; - - add_file(&sortable_files, ent->d_name); - } + DARRAY(struct os_dirent) files; + da_init(files); + os_sortdir_natural(dir, &files.da, dir_file_filter); os_closedir(dir); - if (sortable_files.num > 0) { - qsort(sortable_files.array, sortable_files.num, sizeof(struct image_file_data), - compare_files); - - struct dstr dir_path = {0}; - - for (size_t i = 0; i < sortable_files.num; i++) { - - dstr_copy(&dir_path, path); - dstr_cat_ch(&dir_path, '/'); - dstr_cat(&dir_path, sortable_files.array[i].path); + struct dstr dir_path = {0}; + for (size_t j = 0; j < files.num; j++) { - add_file(&new_data.files, dir_path.array); + dstr_copy(&dir_path, path); + dstr_cat_ch(&dir_path, '/'); + dstr_cat(&dir_path, files.array[j].d_name); - bfree(sortable_files.array[i].path); - } - dstr_free(&dir_path); + add_file(&new_data.files, dir_path.array); } - da_free(sortable_files); + dstr_free(&dir_path); + da_free(files); } else { add_file(&new_data.files, path); diff --git a/plugins/vlc-video/vlc-video-source.c b/plugins/vlc-video/vlc-video-source.c index da0574bbb9d9a3..caa68d05c225fc 100644 --- a/plugins/vlc-video/vlc-video-source.c +++ b/plugins/vlc-video/vlc-video-source.c @@ -589,12 +589,13 @@ static bool valid_extension(const char *ext) return valid; } -static int compare_files(const void *a, const void *b) +bool dir_file_filter(struct os_dirent *ent) { - const struct media_file_data *aa = a; - const struct media_file_data *bb = b; + if (ent->directory) + return false; - return astrnatcmp(aa->path, bb->path); + const char *ext = os_get_path_extension(ent->d_name); + return valid_extension(ext); } static void vlcs_update(void *data, obs_data_t *settings) @@ -650,52 +651,23 @@ static void vlcs_update(void *data, obs_data_t *settings) os_dir_t *dir = os_opendir(path); if (dir) { - struct os_dirent *ent; - - media_file_array_t sortable_files = {0}; - da_init(sortable_files); - - for (;;) { - const char *ext; - - ent = os_readdir(dir); - if (!ent) - break; - if (ent->directory) - continue; - - ext = os_get_path_extension(ent->d_name); - if (!valid_extension(ext)) - continue; - - struct media_file_data media_file; - media_file.path = bstrdup(ent->d_name); - - da_push_back(sortable_files, &media_file); - } - + DARRAY(struct os_dirent) files = {0}; + da_init(files); + os_sortdir_natural(dir, &files.da, dir_file_filter); os_closedir(dir); - if (sortable_files.num > 0) { - qsort(sortable_files.array, sortable_files.num, sizeof(struct media_file_data), - compare_files); - - struct dstr dir_path = {0}; - - for (size_t i = 0; i < sortable_files.num; i++) { - - dstr_copy(&dir_path, path); - dstr_cat_ch(&dir_path, '/'); - dstr_cat(&dir_path, sortable_files.array[i].path); + struct dstr dir_path = {0}; + for (size_t j = 0; j < files.num; j++) { - add_file(c, &new_files, dir_path.array, network_caching, track_index, - subtitle_index, subtitle_enable); + dstr_copy(&dir_path, path); + dstr_cat_ch(&dir_path, '/'); + dstr_cat(&dir_path, files.array[j].d_name); - bfree(sortable_files.array[i].path); - } - dstr_free(&dir_path); + add_file(c, &new_files, dir_path.array, network_caching, track_index, subtitle_index, + subtitle_enable); } - da_free(sortable_files); + dstr_free(&dir_path); + da_free(files); } else { add_file(c, &new_files, path, network_caching, track_index, subtitle_index, subtitle_enable);