@@ -455,8 +455,15 @@ std::unique_ptr<PluginModule> PythonFarAdapter::CreatePluginModule(const wchar_t
455455 LOG_TRACE (" Successfully loaded plugin module" );
456456
457457 auto pluginModule = std::make_unique<PluginModule>(filename, pModule.release (), pPluginClass.release ());
458-
459-
458+
459+ // Plugin must have a non-empty title class attribute.
460+ if (pluginModule->GetTitle ().empty ()) {
461+ LOG_TRACE (" Plugin has no title" );
462+ m_ErrorSummary = L" Plugin Error" ;
463+ m_ErrorDescription = L" Plugin class must define a non-empty 'title' attribute" ;
464+ return nullptr ;
465+ }
466+
460467 return pluginModule;
461468
462469 } catch (...) {
@@ -1024,22 +1031,22 @@ void PluginModule::ReportPluginInfoFailure(const std::wstring& summary, const st
10241031
10251032void PluginModule::GetPluginInfoW (PluginInfo* Info) {
10261033 LOG_TRACE (" PluginModule::GetPluginInfoW" );
1027-
1034+
10281035 if (!Info) return ;
10291036
10301037 Info->StructSize = sizeof (PluginInfo);
10311038 Info->Flags = 0 ;
10321039 Info->CommandPrefix = nullptr ;
10331040 Info->Instance = nullptr ;
1034-
1041+
10351042 Info->DiskMenu .Count = 0 ;
10361043 Info->DiskMenu .Strings = nullptr ;
10371044 Info->DiskMenu .Guids = nullptr ;
1038-
1045+
10391046 Info->PluginMenu .Count = 0 ;
10401047 Info->PluginMenu .Strings = nullptr ;
10411048 Info->PluginMenu .Guids = nullptr ;
1042-
1049+
10431050 Info->PluginConfig .Count = 0 ;
10441051 Info->PluginConfig .Strings = nullptr ;
10451052 Info->PluginConfig .Guids = nullptr ;
@@ -1067,7 +1074,7 @@ void PluginModule::GetPluginInfoW(PluginInfo* Info) {
10671074 if (PyErr_Occurred ()) PyErr_Clear ();
10681075 ReportPluginInfoFailure (
10691076 L" Plugin does not implement get_plugin_info()." ,
1070- L" Define get_plugin_info() returning a dict with at least a 'title' , "
1077+ L" Define get_plugin_info() returning a dict, "
10711078 L" or inherit from the far.Plugin base class which provides it." );
10721079 return ;
10731080 }
@@ -1088,84 +1095,135 @@ void PluginModule::GetPluginInfoW(PluginInfo* Info) {
10881095 if (PyErr_Occurred ()) PyErr_Clear ();
10891096 ReportPluginInfoFailure (
10901097 L" get_plugin_info() did not return a dict." ,
1091- L" It must return a dict with at least a 'title' key ." );
1098+ L" It must return a dict." );
10921099 return ;
10931100 }
10941101
1095- // A plugin must provide at least a non-empty title (its name).
1096- {
1097- PyObject* titleCheck = PyDict_GetItemString (result, " title" ); // borrowed
1098- bool hasTitle = titleCheck && PyUnicode_Check (titleCheck)
1099- && PyUnicode_GetLength (titleCheck) > 0 ;
1100- if (!hasTitle) {
1101- ReportPluginInfoFailure (
1102- L" get_plugin_info() did not provide a plugin name." ,
1103- L" The returned dict must contain a non-empty 'title'." );
1104- return ;
1105- }
1106- }
1107-
1108- // Parse the dictionary result
1102+ // --- Parse the dictionary ---
1103+
11091104 PyObject* flagsObj = PyDict_GetItemString (result, " flags" );
11101105 if (flagsObj && PyLong_Check (flagsObj)) {
11111106 Info->Flags = PyLong_AsLong (flagsObj);
11121107 }
11131108
1114- PyObject* titleObj = PyDict_GetItemString (result, " title" );
1115- if (titleObj && PyUnicode_Check (titleObj)) {
1116- const char * titleStr = PyUnicode_AsUTF8 (titleObj);
1117- if (titleStr) {
1118- int len = MultiByteToWideChar (CP_UTF8 , 0 , titleStr, -1 , nullptr , 0 );
1119- titleBuffer.resize (len);
1120- MultiByteToWideChar (CP_UTF8 , 0 , titleStr, -1 , &titleBuffer[0 ], len);
1121- menuStrings.clear ();
1122- menuStrings.push_back (&titleBuffer[0 ]);
1123- menuGuids.clear ();
1124- menuGuids.push_back (m_Guid); // Use the plugin's GUID
1125- Info->PluginMenu .Guids = menuGuids.data ();
1126- Info->PluginMenu .Strings = menuStrings.data ();
1127- Info->PluginMenu .Count = menuStrings.size ();
1109+ auto parseGuid = [](const wchar_t * s) -> GUID {
1110+ GUID g = {};
1111+ CLSIDFromString (s, &g);
1112+ return g;
1113+ };
1114+
1115+ auto pyToWide = [](PyObject* obj, std::vector<wchar_t >& buf) -> bool {
1116+ const char * utf8 = PyUnicode_AsUTF8 (obj);
1117+ if (!utf8) return false ;
1118+ int len = MultiByteToWideChar (CP_UTF8 , 0 , utf8, -1 , nullptr , 0 );
1119+ if (len <= 0 ) return false ;
1120+ buf.resize (len);
1121+ MultiByteToWideChar (CP_UTF8 , 0 , utf8, -1 , buf.data (), len);
1122+ return true ;
1123+ };
1124+
1125+ auto readPair = [&](PyObject* item,
1126+ GUID & outGuid,
1127+ std::wstring& outText) -> bool {
1128+ if (!PySequence_Check (item)) return false ;
1129+ Py_ssize_t itemLen = PySequence_Size (item);
1130+ if (itemLen != 2 ) return false ;
1131+
1132+ PyObject* guidObj = PySequence_GetItem (item, 0 );
1133+ PyObject* textObj = PySequence_GetItem (item, 1 );
1134+ if (!guidObj || !textObj || !PyUnicode_Check (guidObj) || !PyUnicode_Check (textObj)) {
1135+ Py_XDECREF (guidObj); Py_XDECREF (textObj);
1136+ return false ;
11281137 }
1129- }
1138+ std::vector<wchar_t > tmp;
1139+ bool ok = pyToWide (textObj, tmp);
1140+ if (ok) outText = tmp.data ();
1141+ ok = ok && pyToWide (guidObj, tmp);
1142+ if (ok) outGuid = parseGuid (tmp.data ());
1143+ Py_XDECREF (guidObj); Py_XDECREF (textObj);
1144+ return ok;
1145+ };
11301146
1131- PyObject* descObj = PyDict_GetItemString (result, " description" );
1132- if (descObj && PyUnicode_Check (descObj)) {
1133- const char * descStr = PyUnicode_AsUTF8 (descObj);
1134- if (descStr) {
1135- int len = MultiByteToWideChar (CP_UTF8 , 0 , descStr, -1 , nullptr , 0 );
1136- descBuffer.resize (len);
1137- MultiByteToWideChar (CP_UTF8 , 0 , descStr, -1 , &descBuffer[0 ], len);
1138- configStrings.clear ();
1139- configStrings.push_back (&descBuffer[0 ]);
1140- // Re-use menuGuids or populate a new guids array for config. We can just point it to menuGuids since it has m_Guid
1141- if (menuGuids.empty ()) {
1142- menuGuids.push_back (m_Guid);
1143- }
1144- Info->PluginConfig .Guids = menuGuids.data ();
1145- Info->PluginConfig .Strings = configStrings.data ();
1146- Info->PluginConfig .Count = configStrings.size ();
1147+ auto buildMenu = [&](const char * key,
1148+ std::vector<wchar_t >& buf,
1149+ std::vector<const wchar_t *>& strs,
1150+ std::vector<GUID >& guids,
1151+ PluginMenuItem& menuItem) {
1152+ buf.clear (); strs.clear (); guids.clear ();
1153+ PyObject* obj = PyDict_GetItemString (result, key);
1154+ if (!obj) return ;
1155+
1156+ // plain string → single item, plugin GUID
1157+ if (PyUnicode_Check (obj)) {
1158+ const char * utf8 = PyUnicode_AsUTF8 (obj);
1159+ if (!utf8 || !*utf8) return ;
1160+ int len = MultiByteToWideChar (CP_UTF8 , 0 , utf8, -1 , nullptr , 0 );
1161+ if (len <= 0 ) return ;
1162+ buf.resize (len);
1163+ MultiByteToWideChar (CP_UTF8 , 0 , utf8, -1 , buf.data (), len);
1164+ strs.push_back (buf.data ());
1165+ guids.push_back (m_Guid);
1166+ menuItem.Guids = guids.data ();
1167+ menuItem.Strings = strs.data ();
1168+ menuItem.Count = 1 ;
1169+ return ;
11471170 }
1148- }
11491171
1150- PyObject* authorObj = PyDict_GetItemString (result, " author" );
1151- if (authorObj && PyUnicode_Check (authorObj)) {
1152- const char * authorStr = PyUnicode_AsUTF8 (authorObj);
1153- if (authorStr) {
1154- int len = MultiByteToWideChar (CP_UTF8 , 0 , authorStr, -1 , nullptr , 0 );
1155- authorBuffer.resize (len);
1156- MultiByteToWideChar (CP_UTF8 , 0 , authorStr, -1 , &authorBuffer[0 ], len);
1172+ if (!PySequence_Check (obj)) return ;
1173+ Py_ssize_t seqLen = PySequence_Size (obj);
1174+ if (seqLen <= 0 ) return ;
1175+
1176+ // [guid, str] → single item, explicit GUID
1177+ if (seqLen == 2 ) {
1178+ PyObject* first = PySequence_GetItem (obj, 0 );
1179+ bool firstIsStr = PyUnicode_Check (first);
1180+ Py_XDECREF (first);
1181+ if (firstIsStr) {
1182+ GUID itemGuid = {};
1183+ std::wstring itemText;
1184+ if (readPair (obj, itemGuid, itemText)) {
1185+ buf.resize (itemText.size () + 1 );
1186+ wcscpy_s (buf.data (), buf.size (), itemText.c_str ());
1187+ strs.push_back (buf.data ());
1188+ guids.push_back (itemGuid);
1189+ }
1190+ menuItem.Guids = guids.data ();
1191+ menuItem.Strings = strs.data ();
1192+ menuItem.Count = static_cast <size_t >(strs.size ());
1193+ return ;
1194+ }
11571195 }
1158- }
11591196
1160- PyObject* versionObj = PyDict_GetItemString (result, " version" );
1161- if (versionObj && PyUnicode_Check (versionObj)) {
1162- const char * versionStr = PyUnicode_AsUTF8 (versionObj);
1163- if (versionStr) {
1164- int len = MultiByteToWideChar (CP_UTF8 , 0 , versionStr, -1 , nullptr , 0 );
1165- versionBuffer.resize (len);
1166- MultiByteToWideChar (CP_UTF8 , 0 , versionStr, -1 , &versionBuffer[0 ], len);
1197+ // [[guid, str], …] → multiple items
1198+ struct Item { GUID guid; std::wstring text; };
1199+ std::vector<Item> items;
1200+ for (Py_ssize_t i = 0 ; i < seqLen; ++i) {
1201+ PyObject* elem = PySequence_GetItem (obj, i);
1202+ if (!elem) continue ;
1203+ Item it = {};
1204+ if (readPair (elem, it.guid , it.text )) {
1205+ items.push_back (std::move (it));
1206+ }
1207+ Py_XDECREF (elem);
11671208 }
1168- }
1209+ if (items.empty ()) return ;
1210+
1211+ for (const auto & it : items) {
1212+ size_t pos = buf.size ();
1213+ for (wchar_t c : it.text )
1214+ buf.push_back (c);
1215+ buf.push_back (L' \0 ' );
1216+ strs.push_back (buf.data () + pos);
1217+ guids.push_back (it.guid );
1218+ }
1219+ menuItem.Guids = guids.data ();
1220+ menuItem.Strings = strs.data ();
1221+ menuItem.Count = static_cast <size_t >(strs.size ());
1222+ };
1223+
1224+ buildMenu (" plugin_menu" , menuBuffer, menuStrings, menuGuids, Info->PluginMenu );
1225+ buildMenu (" plugin_config" , configBuffer, configStrings, configGuids, Info->PluginConfig );
1226+ buildMenu (" disk_menu" , diskBuffer, diskStrings, diskGuids, Info->DiskMenu );
11691227
11701228 PyObject* prefixObj = PyDict_GetItemString (result, " command_prefix" );
11711229 if (prefixObj && PyUnicode_Check (prefixObj)) {
0 commit comments