MapSelect.qml 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776
  1. import QtQuick 2.15
  2. import QtQuick.Controls 2.15
  3. import QtQuick.Layouts 1.15
  4. import StandardOfIron 1.0
  5. Item {
  6. id: root
  7. property var mapsModel: (typeof game !== "undefined" && game.available_maps) ? game.available_maps : []
  8. property bool mapsLoading: (typeof game !== "undefined" && game.maps_loading) ? game.maps_loading : false
  9. property int selectedMapIndex: -1
  10. property var selectedMapData: null
  11. property string selectedMapPath: ""
  12. property string validationError: ""
  13. property var availableNations: []
  14. signal mapChosen(string map_path, var playerConfigs)
  15. signal cancelled()
  16. function refreshAvailableNations() {
  17. if (typeof game !== "undefined" && game.available_nations)
  18. availableNations = game.available_nations;
  19. else
  20. availableNations = [];
  21. }
  22. function defaultNationEntry() {
  23. if (availableNations && availableNations.length > 0)
  24. return availableNations[0];
  25. return {
  26. "id": "roman_republic",
  27. "name": qsTr("Roman Republic")
  28. };
  29. }
  30. function hasMinimumDistinctTeams() {
  31. let enabledPlayers = [];
  32. for (let i = 0; i < playersModel.count; i++) {
  33. let p = playersModel.get(i);
  34. if (p.isEnabled)
  35. enabledPlayers.push(p);
  36. }
  37. if (enabledPlayers.length < 2)
  38. return false;
  39. let teams = new Set();
  40. for (let i = 0; i < enabledPlayers.length; i++) {
  41. teams.add(enabledPlayers[i].team_id);
  42. }
  43. return teams.size >= 2;
  44. }
  45. function updateValidationError() {
  46. let enabledCount = 0;
  47. for (let i = 0; i < playersModel.count; i++) {
  48. if (playersModel.get(i).isEnabled)
  49. enabledCount++;
  50. }
  51. if (enabledCount < 2)
  52. validationError = "Need at least 2 enabled players to start";
  53. else if (!hasMinimumDistinctTeams())
  54. validationError = "At least two teams must be selected to start a match";
  55. else
  56. validationError = "";
  57. }
  58. function field(obj, key) {
  59. return (obj && obj[key] !== undefined) ? String(obj[key]) : "";
  60. }
  61. function nationEmblemFor(nationId) {
  62. if (!nationId || !Theme.nationEmblems)
  63. return "";
  64. let emblem = Theme.nationEmblems[nationId];
  65. if (emblem === undefined || emblem === null)
  66. return "";
  67. return String(emblem);
  68. }
  69. function getMapData(index) {
  70. if (!mapsModel || index < 0 || index >= (mapsModel.length || list.count))
  71. return null;
  72. let m = (mapsModel.length !== undefined) ? mapsModel[index] : null;
  73. if (m && m.get)
  74. return m.get(index);
  75. return m;
  76. }
  77. function initializePlayers(mapData) {
  78. playersModel.clear();
  79. if (!mapData || !mapData.player_ids || mapData.player_ids.length === 0)
  80. return ;
  81. let humanPlayerId = mapData.player_ids.length > 0 ? mapData.player_ids[0] : 1;
  82. let defaultNation = defaultNationEntry();
  83. playersModel.append({
  84. "player_id": humanPlayerId,
  85. "playerName": "Player " + (humanPlayerId + 1),
  86. "colorIndex": 0,
  87. "colorHex": Theme.playerColors[0].hex,
  88. "colorName": Theme.playerColors[0].name,
  89. "team_id": 0,
  90. "teamIcon": Theme.teamIcons[0],
  91. "nationId": defaultNation.id,
  92. "nationName": defaultNation.name,
  93. "isHuman": true,
  94. "isEnabled": true
  95. });
  96. let cpuId = mapData.player_ids.find(function(id) {
  97. return id !== humanPlayerId;
  98. });
  99. if (cpuId !== undefined)
  100. addCPU();
  101. updateValidationError();
  102. }
  103. function addCPU() {
  104. if (!selectedMapData || !selectedMapData.player_ids)
  105. return ;
  106. if (playersModel.count >= selectedMapData.player_ids.length)
  107. return ;
  108. let usedIds = [];
  109. for (let i = 0; i < playersModel.count; i++) usedIds.push(playersModel.get(i).player_id)
  110. let nextId = -1;
  111. for (let j = 0; j < selectedMapData.player_ids.length; j++) {
  112. if (usedIds.indexOf(selectedMapData.player_ids[j]) === -1) {
  113. nextId = selectedMapData.player_ids[j];
  114. break;
  115. }
  116. }
  117. if (nextId === -1)
  118. return ;
  119. let usedColors = [];
  120. for (let k = 0; k < playersModel.count; k++) usedColors.push(playersModel.get(k).colorIndex)
  121. let colorIdx = 0;
  122. for (let c = 0; c < Theme.playerColors.length; c++) {
  123. if (usedColors.indexOf(c) === -1) {
  124. colorIdx = c;
  125. break;
  126. }
  127. }
  128. let defaultTeamId = playersModel.count > 0 ? 1 : 0;
  129. let defaultNation = defaultNationEntry();
  130. playersModel.append({
  131. "player_id": nextId,
  132. "playerName": "CPU " + nextId,
  133. "colorIndex": colorIdx,
  134. "colorHex": Theme.playerColors[colorIdx].hex,
  135. "colorName": Theme.playerColors[colorIdx].name,
  136. "team_id": defaultTeamId,
  137. "teamIcon": Theme.teamIcons[defaultTeamId],
  138. "nationId": defaultNation.id,
  139. "nationName": defaultNation.name,
  140. "isHuman": false,
  141. "isEnabled": true
  142. });
  143. updateValidationError();
  144. }
  145. function removePlayer(index) {
  146. if (index < 0 || index >= playersModel.count)
  147. return ;
  148. let p = playersModel.get(index);
  149. if (p.isHuman)
  150. return ;
  151. playersModel.remove(index);
  152. updateValidationError();
  153. }
  154. function cyclePlayerColor(index) {
  155. if (index < 0 || index >= playersModel.count)
  156. return ;
  157. let p = playersModel.get(index);
  158. let usedColors = [];
  159. for (let i = 0; i < playersModel.count; i++) {
  160. if (i !== index)
  161. usedColors.push(playersModel.get(i).colorIndex);
  162. }
  163. let startIdx = p.colorIndex;
  164. let newIdx = (startIdx + 1) % Theme.playerColors.length;
  165. let attempts = 0;
  166. while (usedColors.indexOf(newIdx) !== -1 && attempts < Theme.playerColors.length) {
  167. newIdx = (newIdx + 1) % Theme.playerColors.length;
  168. attempts++;
  169. }
  170. if (attempts >= Theme.playerColors.length)
  171. newIdx = (startIdx + 1) % Theme.playerColors.length;
  172. playersModel.setProperty(index, "colorIndex", newIdx);
  173. playersModel.setProperty(index, "colorHex", Theme.playerColors[newIdx].hex);
  174. playersModel.setProperty(index, "colorName", Theme.playerColors[newIdx].name);
  175. }
  176. function cyclePlayerTeam(index) {
  177. if (index < 0 || index >= playersModel.count)
  178. return ;
  179. let p = playersModel.get(index);
  180. let maxTeam = Math.min(8, playersModel.count);
  181. let newTeamId = (p.team_id + 1) % (maxTeam + 1);
  182. playersModel.setProperty(index, "team_id", newTeamId);
  183. playersModel.setProperty(index, "teamIcon", Theme.teamIcons[newTeamId]);
  184. updateValidationError();
  185. }
  186. function cyclePlayerNation(index) {
  187. if (index < 0 || index >= playersModel.count)
  188. return ;
  189. if (!availableNations || availableNations.length === 0)
  190. return ;
  191. let p = playersModel.get(index);
  192. let currentId = p.nationId || availableNations[0].id;
  193. let nextIndex = 0;
  194. for (let i = 0; i < availableNations.length; i++) {
  195. if (availableNations[i].id === currentId) {
  196. nextIndex = (i + 1) % availableNations.length;
  197. break;
  198. }
  199. }
  200. let nextNation = availableNations[nextIndex];
  201. playersModel.setProperty(index, "nationId", nextNation.id);
  202. playersModel.setProperty(index, "nationName", nextNation.name);
  203. }
  204. function togglePlayerEnabled(index) {
  205. if (index < 0 || index >= playersModel.count)
  206. return ;
  207. let p = playersModel.get(index);
  208. let newEnabled = !p.isEnabled;
  209. playersModel.setProperty(index, "isEnabled", newEnabled);
  210. updateValidationError();
  211. }
  212. function getPlayerConfigs() {
  213. let configs = [];
  214. for (let i = 0; i < playersModel.count; i++) {
  215. let p = playersModel.get(i);
  216. if (!p.isEnabled)
  217. continue;
  218. let config = {
  219. "player_id": p.player_id,
  220. "colorHex": p.colorHex,
  221. "team_id": p.team_id,
  222. "nationId": p.nationId,
  223. "isHuman": p.isHuman
  224. };
  225. console.log("MapSelect: Player", p.player_id, "config - Team:", p.team_id, "Color:", p.colorHex, "Nation:", p.nationId, "Human:", p.isHuman);
  226. configs.push(config);
  227. }
  228. return configs;
  229. }
  230. function acceptSelection() {
  231. if (selectedMapIndex < 0 || !selectedMapPath)
  232. return ;
  233. let enabledCount = 0;
  234. for (let i = 0; i < playersModel.count; i++) {
  235. if (playersModel.get(i).isEnabled)
  236. enabledCount++;
  237. }
  238. if (enabledCount < 2) {
  239. console.log("MapSelect: Need at least 2 enabled players to start");
  240. updateValidationError();
  241. return ;
  242. }
  243. if (!hasMinimumDistinctTeams()) {
  244. console.log("MapSelect: Need at least 2 different teams to start");
  245. updateValidationError();
  246. return ;
  247. }
  248. validationError = "";
  249. let configs = getPlayerConfigs();
  250. console.log("MapSelect: Starting game with", configs.length, "enabled players");
  251. root.mapChosen(selectedMapPath, configs);
  252. }
  253. function playerColorClicked(index) {
  254. cyclePlayerColor(index);
  255. }
  256. function playerTeamClicked(index) {
  257. cyclePlayerTeam(index);
  258. }
  259. function addCPUClicked() {
  260. addCPU();
  261. }
  262. function removePlayerClicked(index) {
  263. removePlayer(index);
  264. }
  265. Component.onCompleted: refreshAvailableNations()
  266. anchors.fill: parent
  267. focus: true
  268. onVisibleChanged: {
  269. if (visible) {
  270. root.focus = true;
  271. selectedMapIndex = -1;
  272. selectedMapData = null;
  273. selectedMapPath = "";
  274. playersModel.clear();
  275. refreshAvailableNations();
  276. if (typeof game !== "undefined" && game.start_loading_maps)
  277. game.start_loading_maps();
  278. }
  279. }
  280. Keys.onPressed: function(event) {
  281. if (!visible)
  282. return ;
  283. if (event.key === Qt.Key_Escape) {
  284. root.cancelled();
  285. event.accepted = true;
  286. } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
  287. acceptSelection();
  288. event.accepted = true;
  289. } else if (event.key === Qt.Key_Down) {
  290. if (list.count > 0)
  291. list.currentIndex = Math.min(list.currentIndex + 1, list.count - 1);
  292. event.accepted = true;
  293. } else if (event.key === Qt.Key_Up) {
  294. if (list.count > 0)
  295. list.currentIndex = Math.max(list.currentIndex - 1, 0);
  296. event.accepted = true;
  297. }
  298. }
  299. ListModel {
  300. id: playersModel
  301. }
  302. Rectangle {
  303. anchors.fill: parent
  304. color: Theme.dim
  305. }
  306. Rectangle {
  307. id: panel
  308. anchors.centerIn: parent
  309. width: Math.min(parent.width * 0.9, 1300)
  310. height: Math.min(parent.height * 0.88, 800)
  311. radius: Theme.radiusPanel
  312. color: Theme.panelBase
  313. border.color: Theme.panelBr
  314. border.width: 1
  315. opacity: 0.98
  316. clip: true
  317. Item {
  318. id: left
  319. width: Math.max(360, Math.min(panel.width * 0.38, 460))
  320. anchors {
  321. top: parent.top
  322. left: parent.left
  323. bottom: footer.top
  324. topMargin: Theme.spacingXLarge
  325. leftMargin: Theme.spacingXLarge
  326. bottomMargin: Theme.spacingMedium
  327. }
  328. ColumnLayout {
  329. anchors.fill: parent
  330. anchors.margins: 0
  331. spacing: Theme.spacingMedium
  332. Text {
  333. id: leftTitle
  334. text: qsTr("Maps")
  335. color: Theme.textMain
  336. font.pixelSize: 20
  337. font.bold: true
  338. Layout.fillWidth: true
  339. }
  340. MapPreview {
  341. id: mapPreviewLeft
  342. Layout.fillWidth: true
  343. Layout.preferredHeight: visible ? 240 : 0
  344. visible: selectedMapData !== null
  345. mapPath: selectedMapPath
  346. playerConfigs: getPlayerConfigs()
  347. }
  348. Rectangle {
  349. id: listFrame
  350. color: Qt.rgba(0, 0, 0, 0)
  351. radius: Theme.radiusLarge
  352. border.color: Theme.panelBr
  353. border.width: 1
  354. clip: true
  355. Layout.fillWidth: true
  356. Layout.fillHeight: (!mapsLoading && list.count > 0)
  357. ListView {
  358. id: list
  359. anchors.fill: parent
  360. anchors.margins: Theme.spacingSmall
  361. model: mapsModel
  362. spacing: Theme.spacingSmall
  363. currentIndex: (count > 0 ? 0 : -1)
  364. keyNavigationWraps: false
  365. boundsBehavior: Flickable.StopAtBounds
  366. onCurrentIndexChanged: {
  367. if (currentIndex < 0) {
  368. selectedMapIndex = -1;
  369. selectedMapData = null;
  370. selectedMapPath = "";
  371. playersModel.clear();
  372. return ;
  373. }
  374. selectedMapIndex = currentIndex;
  375. selectedMapData = getMapData(currentIndex);
  376. selectedMapPath = selectedMapData ? (selectedMapData.path || selectedMapData.file || "") : "";
  377. initializePlayers(selectedMapData);
  378. }
  379. delegate: Item {
  380. id: row
  381. width: list.width
  382. height: 72
  383. MouseArea {
  384. id: rowMouse
  385. anchors.fill: parent
  386. hoverEnabled: true
  387. acceptedButtons: Qt.LeftButton
  388. cursorShape: Qt.PointingHandCursor
  389. onClicked: list.currentIndex = index
  390. onDoubleClicked: acceptSelection()
  391. }
  392. Rectangle {
  393. id: card
  394. anchors.fill: parent
  395. radius: Theme.radiusLarge
  396. clip: true
  397. color: rowMouse.containsPress ? Theme.hoverBg : (index === list.currentIndex ? Theme.selectedBg : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.03) : Qt.rgba(0, 0, 0, 0)))
  398. border.width: 1
  399. border.color: (index === list.currentIndex) ? Theme.selectedBr : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.15) : Theme.thumbBr)
  400. Rectangle {
  401. id: thumbWrap
  402. width: 76
  403. height: 54
  404. radius: Theme.radiusMedium
  405. color: Theme.cardBase
  406. border.color: Theme.thumbBr
  407. border.width: 1
  408. clip: true
  409. anchors {
  410. left: parent.left
  411. leftMargin: Theme.spacingSmall
  412. verticalCenter: parent.verticalCenter
  413. }
  414. Image {
  415. id: thumbImage
  416. anchors.fill: parent
  417. source: (typeof thumbnail !== "undefined" && thumbnail !== "") ? thumbnail : ""
  418. asynchronous: true
  419. fillMode: Image.PreserveAspectCrop
  420. visible: status === Image.Ready
  421. }
  422. Rectangle {
  423. anchors.fill: parent
  424. color: Theme.cardBase
  425. visible: !thumbImage.visible
  426. Text {
  427. anchors.centerIn: parent
  428. text: "🗺"
  429. font.pixelSize: 24
  430. color: Theme.textDim
  431. }
  432. }
  433. }
  434. Item {
  435. height: 54
  436. anchors {
  437. left: thumbWrap.right
  438. right: parent.right
  439. leftMargin: Theme.spacingSmall
  440. rightMargin: Theme.spacingSmall
  441. verticalCenter: parent.verticalCenter
  442. }
  443. Text {
  444. id: map_name
  445. text: (typeof name !== "undefined") ? String(name) : (typeof modelData === "string" ? modelData : (modelData && modelData.name ? String(modelData.name) : ""))
  446. color: (index === list.currentIndex) ? Theme.textMain : Theme.textBright
  447. font.pixelSize: (index === list.currentIndex) ? 17 : 15
  448. font.bold: (index === list.currentIndex)
  449. elide: Text.ElideRight
  450. anchors {
  451. top: parent.top
  452. left: parent.left
  453. right: parent.right
  454. }
  455. Behavior on font.pixelSize {
  456. NumberAnimation {
  457. duration: Theme.animNormal
  458. }
  459. }
  460. }
  461. Text {
  462. text: (typeof description !== "undefined") ? String(description) : (modelData && modelData.description ? String(modelData.description) : "")
  463. color: (index === list.currentIndex) ? Theme.accentBright : Theme.textSub
  464. font.pixelSize: 12
  465. elide: Text.ElideRight
  466. anchors {
  467. left: parent.left
  468. right: parent.right
  469. bottom: parent.bottom
  470. }
  471. }
  472. Text {
  473. text: "›"
  474. font.pointSize: 18
  475. color: (index === list.currentIndex) ? Theme.textMain : Theme.textHint
  476. anchors {
  477. right: parent.right
  478. rightMargin: 0
  479. verticalCenter: parent.verticalCenter
  480. }
  481. }
  482. }
  483. Behavior on color {
  484. ColorAnimation {
  485. duration: Theme.animNormal
  486. }
  487. }
  488. Behavior on border.color {
  489. ColorAnimation {
  490. duration: Theme.animNormal
  491. }
  492. }
  493. }
  494. }
  495. }
  496. }
  497. Item {
  498. Layout.fillWidth: true
  499. Layout.fillHeight: visible
  500. visible: list.count === 0 && !mapsLoading
  501. Text {
  502. text: qsTr("No maps available")
  503. color: Theme.textSub
  504. font.pixelSize: 14
  505. anchors.centerIn: parent
  506. }
  507. }
  508. Item {
  509. Layout.fillWidth: true
  510. Layout.fillHeight: visible
  511. visible: mapsLoading
  512. Column {
  513. anchors.centerIn: parent
  514. spacing: Theme.spacingSmall
  515. Text {
  516. text: "⟳"
  517. font.pixelSize: 24
  518. color: Theme.accent
  519. anchors.horizontalCenter: parent.horizontalCenter
  520. RotationAnimator on rotation {
  521. from: 0
  522. to: 360
  523. duration: 1500
  524. loops: Animation.Infinite
  525. running: mapsLoading
  526. }
  527. }
  528. Text {
  529. text: qsTr("Loading maps...")
  530. color: Theme.textSub
  531. font.pixelSize: 12
  532. anchors.horizontalCenter: parent.horizontalCenter
  533. }
  534. }
  535. }
  536. }
  537. }
  538. Item {
  539. id: right
  540. anchors {
  541. top: parent.top
  542. bottom: footer.top
  543. right: parent.right
  544. left: left.right
  545. leftMargin: Theme.spacingXLarge
  546. rightMargin: Theme.spacingXLarge
  547. topMargin: Theme.spacingXLarge
  548. bottomMargin: Theme.spacingMedium
  549. }
  550. Text {
  551. id: breadcrumb
  552. text: selectedMapData ? qsTr("► %1").arg(field(selectedMapData, "name")) : qsTr("Select a map to continue")
  553. color: selectedMapData ? Theme.accent : Theme.textHint
  554. font.pixelSize: 13
  555. font.italic: !selectedMapData
  556. elide: Text.ElideRight
  557. anchors {
  558. top: parent.top
  559. left: parent.left
  560. right: parent.right
  561. }
  562. }
  563. Item {
  564. id: loadingIndicator
  565. visible: mapsLoading && list.count === 0
  566. height: 100
  567. anchors {
  568. top: breadcrumb.bottom
  569. left: parent.left
  570. right: parent.right
  571. topMargin: Theme.spacingXLarge * 2
  572. }
  573. Column {
  574. anchors.centerIn: parent
  575. spacing: Theme.spacingMedium
  576. Text {
  577. text: "⟳"
  578. font.pixelSize: 40
  579. color: Theme.accent
  580. anchors.horizontalCenter: parent.horizontalCenter
  581. RotationAnimator on rotation {
  582. from: 0
  583. to: 360
  584. duration: 1500
  585. loops: Animation.Infinite
  586. running: loadingIndicator.visible
  587. }
  588. }
  589. Text {
  590. text: qsTr("Loading maps...")
  591. color: Theme.textSub
  592. font.pixelSize: 14
  593. anchors.horizontalCenter: parent.horizontalCenter
  594. }
  595. }
  596. }
  597. Item {
  598. id: loadingSkeleton
  599. visible: !selectedMapData && !mapsLoading && list.currentIndex >= 0
  600. height: 200
  601. anchors {
  602. top: breadcrumb.bottom
  603. left: parent.left
  604. right: parent.right
  605. topMargin: Theme.spacingMedium
  606. }
  607. Column {
  608. anchors.fill: parent
  609. spacing: Theme.spacingMedium
  610. Rectangle {
  611. width: parent.width * 0.6
  612. height: 28
  613. radius: Theme.radiusSmall
  614. color: Theme.cardBase
  615. opacity: 0.3
  616. SequentialAnimation on opacity {
  617. loops: Animation.Infinite
  618. running: loadingSkeleton.visible
  619. NumberAnimation {
  620. to: 0.6
  621. duration: 800
  622. }
  623. NumberAnimation {
  624. to: 0.3
  625. duration: 800
  626. }
  627. }
  628. }
  629. Rectangle {
  630. width: parent.width * 0.8
  631. height: 16
  632. radius: Theme.radiusSmall
  633. color: Theme.cardBase
  634. opacity: 0.3
  635. SequentialAnimation on opacity {
  636. loops: Animation.Infinite
  637. running: loadingSkeleton.visible
  638. NumberAnimation {
  639. to: 0.6
  640. duration: 800
  641. easing.type: Easing.InOutQuad
  642. }
  643. NumberAnimation {
  644. to: 0.3
  645. duration: 800
  646. easing.type: Easing.InOutQuad
  647. }
  648. }
  649. }
  650. Rectangle {
  651. width: parent.width * 0.7
  652. height: 16
  653. radius: Theme.radiusSmall
  654. color: Theme.cardBase
  655. opacity: 0.3
  656. SequentialAnimation on opacity {
  657. loops: Animation.Infinite
  658. running: loadingSkeleton.visible
  659. NumberAnimation {
  660. to: 0.6
  661. duration: 800
  662. easing.type: Easing.InOutQuad
  663. }
  664. NumberAnimation {
  665. to: 0.3
  666. duration: 800
  667. easing.type: Easing.InOutQuad
  668. }
  669. }
  670. }
  671. Text {
  672. text: qsTr("Loading map details...")
  673. color: Theme.textHint
  674. font.pixelSize: 12
  675. font.italic: true
  676. anchors.horizontalCenter: parent.horizontalCenter
  677. }
  678. }
  679. }
  680. Text {
  681. id: title
  682. text: {
  683. var it = selectedMapData;
  684. var t = field(it, "name");
  685. return t || field(it, "path") || qsTr("No Map Selected");
  686. }
  687. visible: selectedMapData !== null
  688. color: Theme.textMain
  689. font.pixelSize: 24
  690. font.bold: true
  691. elide: Text.ElideRight
  692. anchors {
  693. top: breadcrumb.bottom
  694. left: parent.left
  695. right: parent.right
  696. topMargin: Theme.spacingSmall
  697. }
  698. }
  699. Text {
  700. id: descr
  701. text: field(selectedMapData, "description")
  702. visible: selectedMapData !== null
  703. color: Theme.textSubLite
  704. font.pixelSize: 13
  705. wrapMode: Text.WordWrap
  706. maximumLineCount: 2
  707. lineHeight: 1.3
  708. anchors {
  709. top: title.bottom
  710. left: parent.left
  711. right: parent.right
  712. topMargin: Theme.spacingSmall
  713. }
  714. }
  715. Rectangle {
  716. id: playerConfigPanel
  717. height: Math.min(240, (playersModel.count * 60) + 90)
  718. radius: Theme.radiusLarge
  719. color: Theme.cardBaseA
  720. border.color: Theme.panelBr
  721. border.width: 1
  722. visible: selectedMapData !== null
  723. anchors {
  724. top: descr.bottom
  725. left: parent.left
  726. right: parent.right
  727. topMargin: Theme.spacingMedium
  728. }
  729. Column {
  730. spacing: Theme.spacingMedium
  731. anchors {
  732. fill: parent
  733. margins: Theme.spacingMedium + 2
  734. }
  735. Row {
  736. spacing: Theme.spacingSmall + 2
  737. Text {
  738. text: qsTr("Players")
  739. color: Theme.textMain
  740. font.pixelSize: 17
  741. font.bold: true
  742. }
  743. Rectangle {
  744. width: 30
  745. height: 22
  746. radius: Theme.radiusSmall
  747. color: Theme.selectedBg
  748. anchors.verticalCenter: parent.verticalCenter
  749. Text {
  750. anchors.centerIn: parent
  751. text: playersModel.count
  752. color: Theme.textMain
  753. font.pixelSize: 13
  754. font.bold: true
  755. }
  756. }
  757. Text {
  758. text: qsTr("• Click color/team to cycle")
  759. color: Theme.textSubLite
  760. font.pixelSize: 11
  761. font.italic: true
  762. anchors.verticalCenter: parent.verticalCenter
  763. }
  764. Text {
  765. text: qsTr("• Click nation tag to change")
  766. color: Theme.textSubLite
  767. font.pixelSize: 11
  768. font.italic: true
  769. anchors.verticalCenter: parent.verticalCenter
  770. }
  771. }
  772. ListView {
  773. id: playersList
  774. width: parent.width
  775. height: Math.min(200, playersModel.count * 60)
  776. model: playersModel
  777. spacing: Theme.spacingMedium
  778. clip: true
  779. delegate: Rectangle {
  780. id: playerCard
  781. width: playersList.width
  782. height: 52
  783. radius: Theme.radiusMedium
  784. color: playerCardMouse.containsMouse ? Qt.lighter(Theme.cardBaseB, 1.1) : Theme.cardBaseB
  785. border.color: model.isHuman ? Theme.accent : (playerCardMouse.containsMouse ? Theme.selectedBr : Theme.thumbBr)
  786. border.width: model.isHuman ? 1.5 : (playerCardMouse.containsMouse ? 1.5 : 1)
  787. MouseArea {
  788. id: playerCardMouse
  789. anchors.fill: parent
  790. hoverEnabled: true
  791. acceptedButtons: Qt.NoButton
  792. }
  793. Rectangle {
  794. height: 1
  795. color: Qt.rgba(1, 1, 1, 0.05)
  796. anchors {
  797. left: parent.left
  798. right: parent.right
  799. top: parent.top
  800. }
  801. }
  802. Item {
  803. anchors.fill: parent
  804. anchors.margins: Theme.spacingSmall + 2
  805. Rectangle {
  806. id: enabledCheckbox
  807. width: 32
  808. height: 32
  809. radius: Theme.radiusSmall
  810. anchors.left: parent.left
  811. anchors.leftMargin: 4
  812. anchors.verticalCenter: parent.verticalCenter
  813. color: enabledCheckMA.containsMouse ? Qt.lighter(Theme.cardBase, 1.2) : Theme.cardBase
  814. border.color: model.isEnabled ? Theme.accent : Theme.thumbBr
  815. border.width: enabledCheckMA.containsMouse ? 2 : 1
  816. ToolTip.visible: enabledCheckMA.containsMouse
  817. ToolTip.text: model.isEnabled ? qsTr("Disable player (spectator mode)") : qsTr("Enable player")
  818. Text {
  819. anchors.centerIn: parent
  820. text: model.isEnabled ? "✓" : ""
  821. color: Theme.accent
  822. font.pixelSize: 18
  823. font.bold: true
  824. }
  825. MouseArea {
  826. id: enabledCheckMA
  827. anchors.fill: parent
  828. hoverEnabled: true
  829. cursorShape: Qt.PointingHandCursor
  830. onClicked: togglePlayerEnabled(index)
  831. }
  832. Behavior on color {
  833. ColorAnimation {
  834. duration: Theme.animFast
  835. }
  836. }
  837. Behavior on border.color {
  838. ColorAnimation {
  839. duration: Theme.animFast
  840. }
  841. }
  842. Behavior on border.width {
  843. NumberAnimation {
  844. duration: Theme.animFast
  845. }
  846. }
  847. }
  848. Text {
  849. id: playerNameText
  850. anchors.left: enabledCheckbox.right
  851. anchors.leftMargin: Theme.spacingSmall
  852. anchors.verticalCenter: parent.verticalCenter
  853. text: model.playerName || ""
  854. color: model.isEnabled ? (model.isHuman ? Theme.accentBright : Theme.textBright) : Theme.textDim
  855. font.pixelSize: model.isHuman ? 15 : 14
  856. font.bold: true
  857. opacity: model.isEnabled ? 1 : 0.5
  858. }
  859. Row {
  860. anchors.right: parent.right
  861. anchors.verticalCenter: parent.verticalCenter
  862. spacing: Theme.spacingMedium
  863. opacity: model.isEnabled ? 1 : 0.4
  864. Rectangle {
  865. width: 105
  866. height: playerCard.height - (Theme.spacingSmall + 2) * 2 - 4
  867. radius: Theme.radiusSmall + 1
  868. anchors.verticalCenter: parent.verticalCenter
  869. color: Theme.cardBase
  870. border.color: model.colorHex || Theme.textDim
  871. border.width: colorMA.containsMouse ? 3 : 2
  872. ToolTip.visible: colorMA.containsMouse
  873. ToolTip.text: qsTr("Player color: %1 - Click to change").arg(model.colorName || qsTr("Color"))
  874. Rectangle {
  875. anchors.fill: parent
  876. anchors.margins: 1
  877. radius: parent.radius - 1
  878. color: "transparent"
  879. border.color: model.colorHex || Theme.textDim
  880. border.width: 1
  881. opacity: 0.3
  882. }
  883. Text {
  884. anchors.centerIn: parent
  885. text: model.colorName || "Color"
  886. color: model.colorHex || Theme.textMain
  887. font.pixelSize: 13
  888. font.bold: true
  889. }
  890. MouseArea {
  891. id: colorMA
  892. anchors.fill: parent
  893. hoverEnabled: true
  894. cursorShape: Qt.PointingHandCursor
  895. onClicked: cyclePlayerColor(index)
  896. }
  897. Rectangle {
  898. anchors.fill: parent
  899. radius: parent.radius
  900. color: model.colorHex || Theme.textDim
  901. opacity: colorMA.containsMouse ? 0.15 : 0
  902. Behavior on opacity {
  903. NumberAnimation {
  904. duration: Theme.animFast
  905. }
  906. }
  907. }
  908. Behavior on border.width {
  909. NumberAnimation {
  910. duration: Theme.animFast
  911. }
  912. }
  913. }
  914. Rectangle {
  915. readonly property real emblemSize: Math.min(playerCard.height - Theme.spacingSmall * 2, 72)
  916. property string emblemSource: root.nationEmblemFor(model.nationId)
  917. width: emblemSize
  918. height: emblemSize
  919. radius: Theme.radiusSmall
  920. anchors.verticalCenter: parent.verticalCenter
  921. color: nationMA.containsMouse ? Qt.lighter(Theme.cardBaseB, 1.1) : Theme.cardBaseB
  922. border.color: nationMA.containsMouse ? Theme.selectedBr : Theme.thumbBr
  923. border.width: nationMA.containsMouse ? 2 : 1
  924. ToolTip.visible: nationMA.containsMouse
  925. ToolTip.text: qsTr("Nation: %1 - Click to change").arg(model.nationName || qsTr("Nation"))
  926. Image {
  927. anchors.centerIn: parent
  928. visible: parent.emblemSource !== ""
  929. source: parent.emblemSource
  930. width: parent.width * 0.8
  931. height: width
  932. fillMode: Image.PreserveAspectFit
  933. smooth: true
  934. mipmap: true
  935. }
  936. Text {
  937. anchors.centerIn: parent
  938. visible: parent.emblemSource === ""
  939. text: model.nationName || qsTr("Nation")
  940. color: Theme.textMain
  941. font.pixelSize: 11
  942. font.bold: true
  943. }
  944. MouseArea {
  945. id: nationMA
  946. anchors.fill: parent
  947. hoverEnabled: true
  948. cursorShape: Qt.PointingHandCursor
  949. onClicked: cyclePlayerNation(index)
  950. }
  951. Behavior on color {
  952. ColorAnimation {
  953. duration: Theme.animFast
  954. }
  955. }
  956. Behavior on border.color {
  957. ColorAnimation {
  958. duration: Theme.animFast
  959. }
  960. }
  961. Behavior on border.width {
  962. NumberAnimation {
  963. duration: Theme.animFast
  964. }
  965. }
  966. }
  967. Rectangle {
  968. width: 70
  969. height: playerCard.height - (Theme.spacingSmall + 2) * 2 - 4
  970. radius: Theme.radiusSmall
  971. anchors.verticalCenter: parent.verticalCenter
  972. color: teamMA.containsMouse ? Qt.lighter(Theme.hoverBg, 1.2) : Theme.hoverBg
  973. border.color: teamMA.containsMouse ? Theme.selectedBr : Theme.thumbBr
  974. border.width: teamMA.containsMouse ? 2 : 1
  975. ToolTip.visible: teamMA.containsMouse
  976. ToolTip.text: qsTr("Team %1 - Click to change").arg(model.team_id || 0)
  977. Column {
  978. anchors.centerIn: parent
  979. spacing: 2
  980. Text {
  981. anchors.horizontalCenter: parent.horizontalCenter
  982. text: model.teamIcon || "⚪"
  983. color: Theme.textMain
  984. font.pixelSize: 20
  985. font.bold: true
  986. }
  987. Text {
  988. anchors.horizontalCenter: parent.horizontalCenter
  989. text: qsTr("Team %1").arg(model.team_id || 0)
  990. color: Theme.textBright
  991. font.pixelSize: 10
  992. font.bold: true
  993. }
  994. }
  995. MouseArea {
  996. id: teamMA
  997. anchors.fill: parent
  998. hoverEnabled: true
  999. cursorShape: Qt.PointingHandCursor
  1000. onClicked: cyclePlayerTeam(index)
  1001. }
  1002. Behavior on color {
  1003. ColorAnimation {
  1004. duration: Theme.animFast
  1005. }
  1006. }
  1007. Behavior on border.color {
  1008. ColorAnimation {
  1009. duration: Theme.animFast
  1010. }
  1011. }
  1012. Behavior on border.width {
  1013. NumberAnimation {
  1014. duration: Theme.animFast
  1015. }
  1016. }
  1017. }
  1018. Rectangle {
  1019. width: 36
  1020. height: playerCard.height - (Theme.spacingSmall + 2) * 2 - 4
  1021. radius: Theme.radiusSmall
  1022. anchors.verticalCenter: parent.verticalCenter
  1023. color: removeMA.containsMouse ? Theme.removeColor : Theme.cardBaseA
  1024. border.color: Theme.removeColor
  1025. border.width: removeMA.containsMouse ? 2 : 1
  1026. visible: !model.isHuman
  1027. ToolTip.visible: removeMA.containsMouse
  1028. ToolTip.text: qsTr("Remove player")
  1029. Text {
  1030. anchors.centerIn: parent
  1031. text: "✕"
  1032. color: Theme.textMain
  1033. font.pixelSize: 16
  1034. font.bold: true
  1035. }
  1036. MouseArea {
  1037. id: removeMA
  1038. anchors.fill: parent
  1039. hoverEnabled: true
  1040. cursorShape: Qt.PointingHandCursor
  1041. onClicked: removePlayer(index)
  1042. }
  1043. Behavior on color {
  1044. ColorAnimation {
  1045. duration: Theme.animFast
  1046. }
  1047. }
  1048. Behavior on border.width {
  1049. NumberAnimation {
  1050. duration: Theme.animFast
  1051. }
  1052. }
  1053. }
  1054. }
  1055. }
  1056. Behavior on color {
  1057. ColorAnimation {
  1058. duration: Theme.animNormal
  1059. }
  1060. }
  1061. Behavior on border.color {
  1062. ColorAnimation {
  1063. duration: Theme.animNormal
  1064. }
  1065. }
  1066. Behavior on border.width {
  1067. NumberAnimation {
  1068. duration: Theme.animNormal
  1069. }
  1070. }
  1071. }
  1072. }
  1073. Rectangle {
  1074. width: parent.width
  1075. height: 8
  1076. color: "transparent"
  1077. }
  1078. Button {
  1079. text: qsTr("+ Add CPU")
  1080. enabled: playersModel.count < (selectedMapData && selectedMapData.player_ids ? selectedMapData.player_ids.length : 0)
  1081. onClicked: addCPU()
  1082. hoverEnabled: true
  1083. implicitHeight: 38
  1084. implicitWidth: 120
  1085. ToolTip.visible: addCpuHover.containsMouse && parent.enabled
  1086. ToolTip.text: qsTr("Add AI opponent")
  1087. MouseArea {
  1088. id: addCpuHover
  1089. anchors.fill: parent
  1090. hoverEnabled: true
  1091. acceptedButtons: Qt.NoButton
  1092. cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
  1093. }
  1094. contentItem: Text {
  1095. text: parent.text
  1096. font.pixelSize: 13
  1097. font.bold: true
  1098. color: parent.enabled ? Theme.textMain : Theme.textDim
  1099. horizontalAlignment: Text.AlignHCenter
  1100. verticalAlignment: Text.AlignVCenter
  1101. }
  1102. background: Rectangle {
  1103. radius: Theme.radiusMedium
  1104. color: {
  1105. if (!parent.enabled)
  1106. return Theme.cardBase;
  1107. if (parent.down)
  1108. return Qt.darker(Theme.addColor, 1.2);
  1109. if (addCpuHover.containsMouse)
  1110. return Qt.lighter(Theme.addColor, 1.2);
  1111. return Theme.addColor;
  1112. }
  1113. border.width: parent.enabled && addCpuHover.containsMouse ? 2 : 1
  1114. border.color: parent.enabled ? Qt.lighter(Theme.addColor, 1.3) : Theme.thumbBr
  1115. Behavior on color {
  1116. ColorAnimation {
  1117. duration: Theme.animFast
  1118. }
  1119. }
  1120. Behavior on border.width {
  1121. NumberAnimation {
  1122. duration: Theme.animFast
  1123. }
  1124. }
  1125. }
  1126. }
  1127. }
  1128. }
  1129. Rectangle {
  1130. id: playerSelectionPanel
  1131. radius: Theme.radiusLarge
  1132. color: Theme.cardBaseA
  1133. border.color: Theme.panelBr
  1134. border.width: 1
  1135. visible: false
  1136. height: playerSelectionContent.height + 20
  1137. anchors {
  1138. top: playerConfigPanel.bottom
  1139. left: parent.left
  1140. right: parent.right
  1141. topMargin: Theme.spacingMedium
  1142. }
  1143. Column {
  1144. id: playerSelectionContent
  1145. spacing: Theme.spacingSmall
  1146. anchors {
  1147. left: parent.left
  1148. right: parent.right
  1149. top: parent.top
  1150. margins: Theme.spacingSmall
  1151. }
  1152. Text {
  1153. text: "Available Player Slots: " + (function() {
  1154. var it = selectedMapData;
  1155. return (it && typeof it.playerCount !== 'undefined') ? it.playerCount : 0;
  1156. })()
  1157. color: Theme.textMain
  1158. font.pixelSize: 14
  1159. font.bold: true
  1160. }
  1161. Text {
  1162. text: "Select your player ID:"
  1163. color: Theme.textSubLite
  1164. font.pixelSize: 12
  1165. }
  1166. Flow {
  1167. width: parent.width
  1168. spacing: Theme.spacingSmall
  1169. Repeater {
  1170. model: {
  1171. var it = selectedMapData;
  1172. return (it && it.player_ids) ? it.player_ids : [];
  1173. }
  1174. delegate: Rectangle {
  1175. width: 60
  1176. height: 32
  1177. radius: Theme.radiusMedium
  1178. color: {
  1179. var pid = modelData;
  1180. if (typeof game === 'undefined')
  1181. return Theme.cardBaseB;
  1182. return (game.selected_player_id === pid) ? Theme.selectedBg : Theme.cardBaseB;
  1183. }
  1184. border.color: {
  1185. var pid = modelData;
  1186. if (typeof game === 'undefined')
  1187. return Theme.thumbBr;
  1188. return (game.selected_player_id === pid) ? Theme.selectedBr : Theme.thumbBr;
  1189. }
  1190. border.width: 1
  1191. Text {
  1192. anchors.centerIn: parent
  1193. text: "ID " + modelData
  1194. color: {
  1195. var pid = modelData;
  1196. if (typeof game === 'undefined')
  1197. return Theme.textSub;
  1198. return (game.selected_player_id === pid) ? Theme.textMain : Theme.textSub;
  1199. }
  1200. font.pixelSize: 12
  1201. font.bold: {
  1202. var pid = modelData;
  1203. if (typeof game === 'undefined')
  1204. return false;
  1205. return game.selected_player_id === pid;
  1206. }
  1207. }
  1208. MouseArea {
  1209. anchors.fill: parent
  1210. cursorShape: Qt.PointingHandCursor
  1211. onClicked: {
  1212. if (typeof game !== 'undefined')
  1213. game.selected_player_id = modelData;
  1214. }
  1215. }
  1216. }
  1217. }
  1218. }
  1219. Text {
  1220. text: {
  1221. if (typeof game === 'undefined')
  1222. return "";
  1223. var it = selectedMapData;
  1224. if (!it || !it.player_ids)
  1225. return "";
  1226. var others = [];
  1227. for (var i = 0; i < it.player_ids.length; i++) {
  1228. if (it.player_ids[i] !== game.selected_player_id)
  1229. others.push(it.player_ids[i]);
  1230. }
  1231. if (others.length === 0)
  1232. return "All other slots will be CPU-controlled";
  1233. return "CPU will control: ID " + others.join(", ID ");
  1234. }
  1235. color: Theme.textSubLite
  1236. font.pixelSize: 11
  1237. wrapMode: Text.WordWrap
  1238. width: parent.width
  1239. }
  1240. }
  1241. }
  1242. }
  1243. Rectangle {
  1244. id: footer
  1245. height: 60
  1246. color: "transparent"
  1247. anchors {
  1248. left: parent.left
  1249. right: parent.right
  1250. bottom: parent.bottom
  1251. leftMargin: Theme.spacingXLarge
  1252. rightMargin: Theme.spacingXLarge
  1253. bottomMargin: Theme.spacingMedium
  1254. }
  1255. Rectangle {
  1256. height: 1
  1257. color: Theme.panelBr
  1258. anchors {
  1259. left: parent.left
  1260. right: parent.right
  1261. top: parent.top
  1262. }
  1263. }
  1264. Text {
  1265. id: validationErrorText
  1266. text: validationError
  1267. visible: validationError !== ""
  1268. color: Theme.removeColor
  1269. font.pixelSize: 13
  1270. font.bold: true
  1271. wrapMode: Text.WordWrap
  1272. horizontalAlignment: Text.AlignHCenter
  1273. anchors {
  1274. left: parent.left
  1275. right: parent.right
  1276. verticalCenter: parent.verticalCenter
  1277. leftMargin: 140
  1278. rightMargin: 140
  1279. }
  1280. }
  1281. Button {
  1282. text: qsTr("Back")
  1283. onClicked: root.cancelled()
  1284. hoverEnabled: true
  1285. implicitHeight: 42
  1286. implicitWidth: 120
  1287. ToolTip.visible: backHover.containsMouse
  1288. ToolTip.text: qsTr("Return to main menu (Esc)")
  1289. anchors {
  1290. left: parent.left
  1291. verticalCenter: parent.verticalCenter
  1292. topMargin: Theme.spacingSmall
  1293. }
  1294. MouseArea {
  1295. id: backHover
  1296. anchors.fill: parent
  1297. hoverEnabled: true
  1298. acceptedButtons: Qt.NoButton
  1299. cursorShape: Qt.PointingHandCursor
  1300. }
  1301. contentItem: Text {
  1302. text: parent.text
  1303. font.pixelSize: backHover.containsMouse ? 14 : 13
  1304. font.bold: backHover.containsMouse
  1305. color: Theme.textBright
  1306. horizontalAlignment: Text.AlignHCenter
  1307. verticalAlignment: Text.AlignVCenter
  1308. Behavior on font.pixelSize {
  1309. NumberAnimation {
  1310. duration: Theme.animFast
  1311. }
  1312. }
  1313. }
  1314. background: Rectangle {
  1315. radius: Theme.radiusLarge
  1316. color: {
  1317. if (parent.down)
  1318. return Theme.hover;
  1319. if (backHover.containsMouse)
  1320. return Theme.cardBase;
  1321. return Qt.rgba(0, 0, 0, 0);
  1322. }
  1323. border.width: backHover.containsMouse ? 2 : 1
  1324. border.color: backHover.containsMouse ? Theme.thumbBr : Theme.panelBr
  1325. Behavior on color {
  1326. ColorAnimation {
  1327. duration: Theme.animFast
  1328. }
  1329. }
  1330. Behavior on border.color {
  1331. ColorAnimation {
  1332. duration: Theme.animFast
  1333. }
  1334. }
  1335. Behavior on border.width {
  1336. NumberAnimation {
  1337. duration: Theme.animFast
  1338. }
  1339. }
  1340. }
  1341. }
  1342. Button {
  1343. text: qsTr("Play")
  1344. enabled: list.currentIndex >= 0 && list.count > 0 && playersModel.count >= 2 && hasMinimumDistinctTeams()
  1345. onClicked: acceptSelection()
  1346. hoverEnabled: true
  1347. implicitHeight: 42
  1348. implicitWidth: 130
  1349. ToolTip.visible: playHover.containsMouse
  1350. ToolTip.text: {
  1351. if (validationError !== "")
  1352. return validationError;
  1353. return qsTr("Start game (Enter)");
  1354. }
  1355. anchors {
  1356. right: parent.right
  1357. verticalCenter: parent.verticalCenter
  1358. topMargin: Theme.spacingSmall
  1359. }
  1360. MouseArea {
  1361. id: playHover
  1362. anchors.fill: parent
  1363. hoverEnabled: true
  1364. acceptedButtons: Qt.NoButton
  1365. cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
  1366. }
  1367. contentItem: Text {
  1368. text: parent.text
  1369. font.pixelSize: parent.enabled ? (playHover.containsMouse ? 15 : 14) : 14
  1370. font.bold: true
  1371. color: parent.enabled ? Theme.textMain : Theme.textDim
  1372. horizontalAlignment: Text.AlignHCenter
  1373. verticalAlignment: Text.AlignVCenter
  1374. Behavior on font.pixelSize {
  1375. NumberAnimation {
  1376. duration: Theme.animFast
  1377. }
  1378. }
  1379. }
  1380. background: Rectangle {
  1381. radius: Theme.radiusLarge
  1382. color: {
  1383. if (!parent.enabled)
  1384. return Theme.cardBaseB;
  1385. if (parent.down)
  1386. return Theme.selectedBr;
  1387. if (playHover.containsMouse)
  1388. return Qt.lighter(Theme.selectedBg, 1.2);
  1389. return Theme.selectedBg;
  1390. }
  1391. border.width: parent.enabled && playHover.containsMouse ? 2 : 1
  1392. border.color: parent.enabled ? Theme.selectedBr : Theme.panelBr
  1393. Behavior on color {
  1394. ColorAnimation {
  1395. duration: Theme.animFast
  1396. }
  1397. }
  1398. Behavior on border.color {
  1399. ColorAnimation {
  1400. duration: Theme.animFast
  1401. }
  1402. }
  1403. Behavior on border.width {
  1404. NumberAnimation {
  1405. duration: Theme.animFast
  1406. }
  1407. }
  1408. }
  1409. }
  1410. }
  1411. }
  1412. }