diff --git a/docs/sphinx/reference-libobs-util-dstr.rst b/docs/sphinx/reference-libobs-util-dstr.rst index 91794e2d60ac93..a180f1fc9fd8ce 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, 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 strcoll_l() / _strcoll_l(). + +---------------------- + +.. 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_l() / _wcscoll_l(). + +---------------------- + .. function:: char *astrstri(const char *str, const char *find) Case insensitive version of strstr. 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/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/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); } diff --git a/libobs/util/dstr.c b/libobs/util/dstr.c index 4dc415cb356841..03b39c7f817091 100644 --- a/libobs/util/dstr.c +++ b/libobs/util/dstr.c @@ -30,11 +30,17 @@ #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, 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); +static inline size_t w_mblen(const wchar_t *str); + int astrcmpi(const char *str1, const char *str2) { if (!str1) @@ -163,6 +169,235 @@ int wstrcmpi_n(const wchar_t *str1, const wchar_t *str2, size_t n) return 0; } +// 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; + if (!str2) + str2 = astrblank; + + size_t len1 = strlen(str1); + size_t len2 = strlen(str2); + + if (len1 == 0 || len2 == 0) + return os_strcoll_l(str1, str2, locale); + + // 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, locale)) != 0) + return r; + + i1 += read1; + i2 += read2; + } + return os_strcoll_l(str1 + i1, str2 + i2, locale); +} + +int wstrnatcmp(const wchar_t *str1, const wchar_t *str2, os_locale_t locale) +{ + if (!str1) + str1 = wstrblank; + if (!str2) + str2 = wstrblank; + + size_t len1 = wcslen(str1); + size_t len2 = wcslen(str2); + + if (len1 == 0 || len2 == 0) + return os_wcscoll_l(str1, str2, locale); + + // 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, locale)) != 0) + return r; + + i1 += read1; + i2 += read2; + } + 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, os_locale_t locale) +{ + char *buf1 = bstrdup_n(str1, read1); + char *buf2 = bstrdup_n(str2, read2); + + int r = os_strcoll_l(buf1, buf2, locale); + + bfree(buf1); + bfree(buf2); + + return r; +} + +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 = os_wcscoll_l(buf1, buf2, locale); + + 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..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,6 +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, 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 4d756961de4651..ae1e6aa3d92c84 100644 --- a/plugins/image-source/obs-slideshow-mk2.c +++ b/plugins/image-source/obs-slideshow-mk2.c @@ -405,6 +405,15 @@ static void restart_slides(struct slideshow *ss) ssd->slides = new_slides; } +bool dir_file_filter(struct os_dirent *ent) +{ + if (ent->directory) + return false; + + const char *ext = os_get_path_extension(ent->d_name); + return valid_extension(ext); +} + static void ss_update(void *data, obs_data_t *settings) { struct slideshow *ss = data; @@ -498,30 +507,23 @@ static void ss_update(void *data, obs_data_t *settings) } if (dir) { - struct dstr dir_path = {0}; - struct os_dirent *ent; - - for (;;) { - const char *ext; - - ent = os_readdir(dir); - if (!ent) - break; - if (ent->directory) - continue; + DARRAY(struct os_dirent) files; + da_init(files); + os_sortdir_natural(dir, &files.da, dir_file_filter); + os_closedir(dir); - ext = os_get_path_extension(ent->d_name); - if (!valid_extension(ext)) - continue; + struct dstr dir_path = {0}; + for (size_t j = 0; j < files.num; j++) { dstr_copy(&dir_path, path); dstr_cat_ch(&dir_path, '/'); - dstr_cat(&dir_path, ent->d_name); + dstr_cat(&dir_path, files.array[j].d_name); + add_file(&new_data.files, dir_path.array); } - dstr_free(&dir_path); - os_closedir(dir); + 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 0be3ced5365d6a..caa68d05c225fc 100644 --- a/plugins/vlc-video/vlc-video-source.c +++ b/plugins/vlc-video/vlc-video-source.c @@ -589,6 +589,15 @@ static bool valid_extension(const char *ext) return valid; } +bool dir_file_filter(struct os_dirent *ent) +{ + if (ent->directory) + return false; + + const char *ext = os_get_path_extension(ent->d_name); + return valid_extension(ext); +} + static void vlcs_update(void *data, obs_data_t *settings) { media_file_array_t new_files; @@ -642,31 +651,24 @@ 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; - - for (;;) { - const char *ext; - - ent = os_readdir(dir); - if (!ent) - break; - if (ent->directory) - continue; + DARRAY(struct os_dirent) files = {0}; + da_init(files); + os_sortdir_natural(dir, &files.da, dir_file_filter); + os_closedir(dir); - ext = os_get_path_extension(ent->d_name); - if (!valid_extension(ext)) - continue; + struct dstr dir_path = {0}; + for (size_t j = 0; j < files.num; j++) { dstr_copy(&dir_path, path); dstr_cat_ch(&dir_path, '/'); - dstr_cat(&dir_path, ent->d_name); + dstr_cat(&dir_path, files.array[j].d_name); + add_file(c, &new_files, dir_path.array, network_caching, track_index, subtitle_index, subtitle_enable); } - dstr_free(&dir_path); - os_closedir(dir); + da_free(files); + } else { add_file(c, &new_files, path, network_caching, track_index, subtitle_index, subtitle_enable); } 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); +}