diff --git a/src/langs/bg.json b/src/langs/bg.json index d9936da..c570d09 100644 --- a/src/langs/bg.json +++ b/src/langs/bg.json @@ -36,94 +36,109 @@ }, "options_menu": { "options_text": "Опции", - "playback_menu": { - "playback_text": "Възпроизвеждане", - "speed_text": "Скорост", - "speed_settings": { - "title": "Настройки на скоростта", - "sub_text": "Въведете число за скорост между 0.1 и 10", - "error_new_value": "Стойността на скоростта трябва да бъде между 0.1 и 10!" - }, - "repeat_text": "Повтаряне", - "repeat_settings": { - "title": "Настройки за повтаряне", - "infinite_repeat": "Безкрайно повторение?", - "sub_text": "Въведете броя на повторенията", - "error_new_value": "Не можете да имате по-малко от 1 повторение." - }, - "for_text": "За", - "for_settings": { - "title": "Настройки за За" - }, - "interval_text": "Интервал", - "interval_settings": { - "title": "Настройки на интервала" - }, - "for_interval_settings": { - "hours_text": "Часове", - "minutes_text": "Минути", - "seconds_text": "Секунди", - "error_new_value_multiple": "Въведените стойности са грешни", - "error_new_value_single": "Въведената стойност е грешна" - }, - "scheduled_text": "Планирани", - "scheduled_settings": { - "title": "Настройки за планирани" - }, - "delay_text": "Закъснение", - "delay_settings": { - "title": "Настройки за закъснение", - "sub_text": "Въведете закъснение между повторенията", - "error_new_value": "Не можете да имате закъснение по-малко от 0." - } - }, - "recordings_menu": { - "recordings_text": "Записи", - "mouse_movement_text": "Движение на мишката", - "mouse_click_text": "Щракване с мишка", - "keyboard_text": "Клавиатура", - "show_events_statut": "Показване на събития в лентата за състояние" - }, - "json_compact": "Компактирани макро данни", - "settings_menu": { - "settings_text": "Настройки", - "always_import_macro_settings": "Винаги импортирай макро настройки", - "lang_text": "Език", - "lang_settings": { - "title": "Настройки на езика", - "sub_text": "Изберете вашия език" + "playback_menu": { + "playback_text": "Възпроизвеждане", + "speed_text": "Скорост", + "speed_settings": { + "title": "Настройки на скоростта", + "sub_text": "Въведете число за скорост между 0,1 и 10\nНастройката за скорост ще влияе само на интервала от време между действията,\nняма да влияе на други настройки, свързани с времето.", + "error_new_value": "Стойността на скоростта трябва да бъде между 0.1 и 10!" + }, + "repeat_text": "Повтаряне", + "repeat_settings": { + "title": "Настройки за повтаряне", + "infinite_repeat": "Безкрайно повторение?", + "sub_text": "Въведете броя на повторенията", + "error_new_value": "Не можете да имате по-малко от 1 повторение." + }, + "for_text": "За", + "for_settings": { + "title": "Настройки за За" + }, + "interval_text": "Интервал", + "interval_settings": { + "title": "Настройки на интервала" + }, + "for_interval_settings": { + "hours_text": "Часове", + "minutes_text": "Минути", + "seconds_text": "Секунди", + "error_new_value_multiple": "Въведените стойности са грешни", + "error_new_value_single": "Въведената стойност е грешна" + }, + "scheduled_text": "Планирани", + "scheduled_settings": { + "title": "Настройки за планирани" + }, + "delay_text": "Закъснение", + "delay_settings": { + "title": "Настройки за закъснение", + "sub_text": "Въведете закъснение между повторенията", + "error_new_value": "Не можете да имате закъснение по-малко от 0." + }, + "randomized_delay_text": "Случайно забавяне", + "randomized_delay_settings": { + "title": "Настройки за случайно забавяне", + "sub_text": "Конфигурирайте границите на случайното забавяне между действията в милисекунди.\nСтойности под нулата ще намалят времето до следващото действие.\nСтойности над нулата ще увеличат времето до следващото действие.", + "lower_text": "Долна граница:", + "upper_text": "Горна граница:", + "error_new_value": "И двете граници на случайното забавяне трябва да бъдат валидни числа." + }, + "distribution_drawer_settings": { + "title": "Персонализиране на разпределението", + "sub_text": "Начертайте колко често трябва да се среща всяко забавяне.", + "more_text": "По-често", + "less_text": "По-рядко", + "clear_text": "Изчистване" + } }, - "hotkeys_text": "Бързи клавиши", - "hotkeys_settings": { - "title": "Настройки за бързи клавиши", - "start_record_text": "Започни запис", - "stop_record_text": "Спри запис", - "start_playback_text": "Започни възпроизвеждане", - "stop_playback_text": "Спри възпроизвеждане", - "clear_text": "Изчисти", - "please_key_text": "Моля, натиснете клавиш", - "error_hotkeys": "Не можете да имате същите бързи клавиши за започване на запис и започване на възпроизвеждане" + "recordings_menu": { + "recordings_text": "Записи", + "mouse_movement_text": "Движение на мишката", + "mouse_click_text": "Щракване с мишка", + "keyboard_text": "Клавиатура", + "show_events_statut": "Показване на събития в лентата за състояние" }, - "minimization_text": "Минимизиране", - "minimization_toast": "PyMacroRecord беше минимизиран.", - "minimization_menu": { - "minimization_when_playing_text": "Минимизиране повреме на възпроизвеждане", - "minimization_when_recording_text": "Минимизиране повреме на запис" + "json_compact": "Компактирани макро данни", + "settings_menu": { + "settings_text": "Настройки", + "always_import_macro_settings": "Винаги импортирай макро настройки", + "lang_text": "Език", + "lang_settings": { + "title": "Настройки на езика", + "sub_text": "Изберете вашия език" + }, + "hotkeys_text": "Бързи клавиши", + "hotkeys_settings": { + "title": "Настройки за бързи клавиши", + "start_record_text": "Започни запис", + "stop_record_text": "Спри запис", + "start_playback_text": "Започни възпроизвеждане", + "stop_playback_text": "Спри възпроизвеждане", + "clear_text": "Изчисти", + "please_key_text": "Моля, натиснете клавиш", + "error_hotkeys": "Не можете да имате същите бързи клавиши за започване на запис и започване на възпроизвеждане" + }, + "minimization_text": "Минимизиране", + "minimization_toast": "PyMacroRecord беше минимизиран.", + "minimization_menu": { + "minimization_when_playing_text": "Минимизиране повреме на възпроизвеждане", + "minimization_when_recording_text": "Минимизиране повреме на запис" + }, + "after_playback_text": "След възпроизвеждане", + "after_playback_settings": { + "when_playback_complete_text": "След завършване на възпроизвеждането", + "title": "Настройки след възпроизвеждане", + "sub_text": "При завършване на възпроизвеждането", + "idle": "Неактивно", + "quit_software": "Изключване на софтуера", + "standby": "Изчакване", + "log_off_computer": "Излизане от компютъра", + "turn_off_computer": "Изключване на компютъра", + "restart_computer": "Рестартиране на компютъра", + "hibernate_if_enabled": "Хибернация (ако е възможно)" + } }, - "after_playback_text": "След възпроизвеждане", - "after_playback_settings": { - "when_playback_complete_text": "След завършване на възпроизвеждането", - "title": "Настройки след възпроизвеждане", - "sub_text": "При завършване на възпроизвеждането", - "idle": "Неактивно", - "quit_software": "Изключване на софтуера", - "standby": "Изчакване", - "log_off_computer": "Излизане от компютъра", - "turn_off_computer": "Изключване на компютъра", - "restart_computer": "Рестартиране на компютъра", - "hibernate_if_enabled": "Хибернация (ако е възможно)" - } - }, "others_menu": { "others_text": "Други", "check_update_text": "Проверка за актуализации", diff --git a/src/langs/de.json b/src/langs/de.json index 9164a24..2158c02 100644 --- a/src/langs/de.json +++ b/src/langs/de.json @@ -41,7 +41,7 @@ "speed_text": "Geschwindigkeit", "speed_settings": { "title": "Geschwindigkeit", - "sub_text": "Wählen Sie eine Geschwindigkeit zwischen 0.1 und 10", + "sub_text": "Wählen Sie eine Geschwindigkeit zwischen 0,1 und 10.\nDie Geschwindigkeitseinstellung wirkt sich nur auf das Zeitintervall zwischen den Aktionen aus,\nsie beeinflusst keine anderen zeitbezogenen Einstellungen.", "error_new_value": "Die Geschwindigkeit muss zwischen 0.1 und 10 liegen!" }, "repeat_text": "Wiederholen", @@ -75,6 +75,21 @@ "title": "Verzögerungseinstellung", "sub_text": "Verzögerung zwischen den Ausführungen", "error_new_value": "Eine Minimum-Verzögerung von Null ist nötig." + }, + "randomized_delay_text": "Zufällige Verzögerung", + "randomized_delay_settings": { + "title": "Einstellungen für zufällige Verzögerung", + "sub_text": "Konfigurieren Sie die Grenzen der zufälligen Verzögerung zwischen Aktionen in Millisekunden.\nWerte unter null verkürzen die Zeit bis zur nächsten Aktion.\nWerte über null verlängern die Zeit bis zur nächsten Aktion.", + "lower_text": "Untergrenze:", + "upper_text": "Obergrenze:", + "error_new_value": "Beide Grenzen der zufälligen Verzögerung müssen gültige Zahlen sein." + }, + "distribution_drawer_settings": { + "title": "Verteilung anpassen", + "sub_text": "Zeichnen Sie, wie häufig jede Verzögerung vorkommen soll.", + "more_text": "Häufiger", + "less_text": "Seltener", + "clear_text": "Löschen" } }, "recordings_menu": { diff --git a/src/langs/en.json b/src/langs/en.json index ea16cfe..d9585f7 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -41,7 +41,7 @@ "speed_text": "Speed", "speed_settings": { "title": "Speed settings", - "sub_text": "Enter speed number between 0.1 and 10", + "sub_text": "Enter speed number between 0.1 and 10\nThe speed setting will only affect the time interval between actions,\nthis will not affect other settings that are time related.", "error_new_value": "Your speed value must be between 0.1 and 10!" }, "repeat_text": "Repeat", @@ -75,6 +75,21 @@ "title": "Delay settings", "sub_text": "Enter delay between repeat", "error_new_value": "You cannot have less than 0 delay." + }, + "randomized_delay_text": "Randomized delay", + "randomized_delay_settings": { + "title": "Randomized delay settings", + "sub_text": "Configure randomized delay bounds between actions in milliseconds.\nValues below zero will allow for decreased time to next action.\nValues above zero will allow for increased time to next action.", + "lower_text": "Lower:", + "upper_text": "Upper:", + "error_new_value": "Both randomized delay bounds must be valid numbers." + }, + "distribution_drawer_settings": { + "title": "Customize distribution", + "sub_text": "Draw how common each delay should be.", + "more_text": "More common", + "less_text": "Less common", + "clear_text": "Clear" } }, "recordings_menu": { diff --git a/src/langs/eo.json b/src/langs/eo.json index 1ce9026..857df85 100644 --- a/src/langs/eo.json +++ b/src/langs/eo.json @@ -41,7 +41,7 @@ "speed_text": "Rapideco", "speed_settings": { "title": "Rapid-agordoj", - "sub_text": "Enmetu rapidecon inter 0.1 kaj 10", + "sub_text": "Enmetu rapidecon inter 0,1 kaj 10\nLa rapideca agordo influos nur la tempintervalon inter agoj,\nĝi ne influos aliajn agordojn rilatajn al tempo.", "error_new_value": "Via rapideco devas esti inter 0.1 kaj 10!" }, "repeat_text": "Ripeto", @@ -75,6 +75,21 @@ "title": "Prokrast-agordoj", "sub_text": "Enmetu prokraston inter ripetoj", "error_new_value": "Vi ne povas havi malpli ol 0 da prokrasto." + }, + "randomized_delay_text": "Hazarda prokrasto", + "randomized_delay_settings": { + "title": "Agordoj de hazarda prokrasto", + "sub_text": "Agordu la limojn de la hazarda prokrasto inter agoj en milisekundoj.\nValoroj sub nul reduktos la tempon ĝis la sekva ago.\nValoroj super nul pliigos la tempon ĝis la sekva ago.", + "lower_text": "Malsupra:", + "upper_text": "Supra:", + "error_new_value": "Ambaŭ limoj de la hazarda prokrasto devas esti validaj nombroj." + }, + "distribution_drawer_settings": { + "title": "Agordi distribuon", + "sub_text": "Desegnu, kiom ofta estu ĉiu prokrasto.", + "more_text": "Pli ofta", + "less_text": "Malpli ofta", + "clear_text": "Forigi" } }, "recordings_menu": { diff --git a/src/langs/es.json b/src/langs/es.json index e6d07fc..dd1c32a 100644 --- a/src/langs/es.json +++ b/src/langs/es.json @@ -41,7 +41,7 @@ "speed_text": "Velocidad", "speed_settings": { "title": "Ajustes de velocidad", - "sub_text": "Introduzca el número de velocidad entre 0.1 y 10", + "sub_text": "Introduzca el número de velocidad entre 0,1 y 10\nLa configuración de velocidad solo afectará al intervalo de tiempo entre acciones,\nesto no afectará a otras configuraciones relacionadas con el tiempo.", "error_new_value": "¡Su valor de velocidad debe estar entre 0.1 y 10!" }, "repeat_text": "Repetir", @@ -75,6 +75,21 @@ "title": "Ajustes de retrazo", "sub_text": "Introduzca el retraso entre repetición", "error_new_value": "No puede tener menos de 0 retrasos" + }, + "randomized_delay_text": "Retraso aleatorio", + "randomized_delay_settings": { + "title": "Configuración del retraso aleatorio", + "sub_text": "Configure los límites del retraso aleatorio entre acciones en milisegundos.\nLos valores inferiores a cero reducirán el tiempo hasta la siguiente acción.\nLos valores superiores a cero aumentarán el tiempo hasta la siguiente acción.", + "lower_text": "Inferior:", + "upper_text": "Superior:", + "error_new_value": "Ambos límites del retraso aleatorio deben ser números válidos." + }, + "distribution_drawer_settings": { + "title": "Personalizar la distribución", + "sub_text": "Dibuja con qué frecuencia debería aparecer cada retraso.", + "more_text": "Más frecuente", + "less_text": "Menos frecuente", + "clear_text": "Borrar" } }, "recordings_menu": { diff --git a/src/langs/fr.json b/src/langs/fr.json index 08cf4db..fb0495e 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -41,7 +41,7 @@ "speed_text": "Vitesse", "speed_settings": { "title": "Réglagle de la vitesse", - "sub_text": "Entrez un nombre entre 0.1 et 10", + "sub_text": "Entrez un nombre de vitesse compris entre 0,1 et 10\nLe réglage de la vitesse affectera uniquement l'intervalle de temps entre les actions,\nil n'affectera pas les autres paramètres liés au temps.", "error_new_value": "La valeur de votre vitesse doit être entre 0,1 et 10 !" }, "repeat_text": "Répéter", @@ -75,6 +75,21 @@ "title": "Réglage du délai", "sub_text": "Entrez un délai entre chaque répétition", "error_new_value": "Vous ne pouvez pas allez en dessous de 0 délai." + }, + "randomized_delay_text": "Délai aléatoire", + "randomized_delay_settings": { + "title": "Paramètres du délai aléatoire", + "sub_text": "Configurez les limites du délai aléatoire entre les actions en millisecondes.\nLes valeurs inférieures à zéro réduiront le temps avant l'action suivante.\nLes valeurs supérieures à zéro augmenteront le temps avant l'action suivante.", + "lower_text": "Inférieure :", + "upper_text": "Supérieure :", + "error_new_value": "Les deux limites du délai aléatoire doivent être des nombres valides." + }, + "distribution_drawer_settings": { + "title": "Personnaliser la distribution", + "sub_text": "Dessinez la fréquence à laquelle chaque délai doit apparaître.", + "more_text": "Plus fréquent", + "less_text": "Moins fréquent", + "clear_text": "Effacer" } }, "recordings_menu": { diff --git a/src/langs/it.json b/src/langs/it.json index da28191..de51182 100644 --- a/src/langs/it.json +++ b/src/langs/it.json @@ -1,176 +1,191 @@ { - "information": { - "author": "takiem", - "lang_short": "it", - "lang_long": "Italiano" + "information": { + "author": "takiem", + "lang_short": "it", + "lang_long": "Italiano" + }, + "content": { + "global": { + "confirm_button": "Conferma", + "cancel_button": "Annulla", + "close_button": "Chiudi", + "error": "Errore", + "previous_text": "Precedente", + "next_text": "Successivo", + "confirm_save": "Vuoi salvare la tua registrazione?", + "confirm": "Conferma", + "information": "Informazione", + "restart_software_text": "È necessario riavviare il software per applicare le modifiche.", + "load_macro_settings": "Import macro settings too?" }, - "content": { - "global": { - "confirm_button": "Conferma", - "cancel_button": "Annulla", - "close_button": "Chiudi", - "error": "Errore", - "previous_text": "Precedente", - "next_text": "Successivo", - "confirm_save": "Vuoi salvare la tua registrazione?", - "confirm": "Conferma", - "information": "Informazione", - "restart_software_text": "È necessario riavviare il software per applicare le modifiche.", - "load_macro_settings": "Import macro settings too?" + "new_version": { + "title": "Aggiornamento del software", + "sub_text_1": "Nuova versione", + "sub_text_2": "disponibile!", + "sub_text_3": "Vuoi scaricarlo ora?", + "ignore_button": "Ignora", + "remind_later_button": "Ricordamelo più tardi", + "download_button": "Scarica l'aggiornamento" + }, + "file_menu": { + "file_text": "File", + "new_text": "Nuovo", + "load_text": "Carica", + "save_text": "Salva", + "save_as_text": "Salva come" + }, + "options_menu": { + "options_text": "Opzioni", + "playback_menu": { + "playback_text": "Riproduzione", + "speed_text": "Velocità", + "speed_settings": { + "title": "Impostazioni della velocità", + "sub_text": "Inserisci un numero di velocità compreso tra 0,1 e 10\nL'impostazione della velocità influirà solo sull'intervallo di tempo tra le azioni,\nnon influirà sulle altre impostazioni relative al tempo.", + "error_new_value": "Il valore della velocità deve essere compreso tra 0,1 e 10!" + }, + "repeat_text": "Ripeti", + "repeat_settings": { + "title": "Impostazioni della ripetizione", + "infinite_repeat": "Ripetizione infinita?", + "sub_text": "Inserisci il numero di ripetizioni", + "error_new_value": "Non puoi avere meno di una ripetizione." + }, + "for_text": "Per", + "for_settings": { + "title": "Impostazioni per" }, - "new_version": { - "title": "Aggiornamento del software", - "sub_text_1": "Nuova versione", - "sub_text_2": "disponibile!", - "sub_text_3": "Vuoi scaricarlo ora?", - "ignore_button": "Ignora", - "remind_later_button": "Ricordamelo più tardi", - "download_button": "Scarica l'aggiornamento" + "interval_text": "Intervallo", + "interval_settings": { + "title": "Impostazioni dell'intervallo" }, - "file_menu": { - "file_text": "File", - "new_text": "Nuovo", - "load_text": "Carica", - "save_text": "Salva", - "save_as_text": "Salva come" + "for_interval_settings": { + "hours_text": "Ore", + "minutes_text": "Minuti", + "seconds_text": "Secondi", + "error_new_value_multiple": "L'input non è corretto", + "error_new_value_single": "L'input non è corretto" }, - "options_menu": { - "options_text": "Opzioni", - "playback_menu": { - "playback_text": "Riproduzione", - "speed_text": "Velocità", - "speed_settings": { - "title": "Impostazioni della velocità", - "sub_text": "Inserisci un numero di velocità tra 0,1 e 10", - "error_new_value": "Il valore della velocità deve essere compreso tra 0,1 e 10!" - }, - "repeat_text": "Ripeti", - "repeat_settings": { - "title": "Impostazioni della ripetizione", - "infinite_repeat": "Ripetizione infinita?", - "sub_text": "Inserisci il numero di ripetizioni", - "error_new_value": "Non puoi avere meno di una ripetizione." - }, - "for_text": "Per", - "for_settings": { - "title": "Impostazioni per" - }, - "interval_text": "Intervallo", - "interval_settings": { - "title": "Impostazioni dell'intervallo" - }, - "for_interval_settings": { - "hours_text": "Ore", - "minutes_text": "Minuti", - "seconds_text": "Secondi", - "error_new_value_multiple": "L'input non è corretto", - "error_new_value_single": "L'input non è corretto" - }, - "scheduled_text": "In programma", - "scheduled_settings": { - "title": "Impostazioni programmate" - }, - "delay_text": "Ritardo", - "delay_settings": { - "title": "Impostazioni del ritardo", - "sub_text": "Inserisci il ritardo tra le ripetizioni", - "error_new_value": "Il ritardo non può essere inferiore a 0." - } - }, - "recordings_menu": { - "recordings_text": "Registrazioni", - "mouse_movement_text": "Movimento del mouse", - "mouse_click_text": "Clic del mouse", - "keyboard_text": "Tastiera", - "show_events_statut": "Mostra eventi sulla barra di stato" - }, - "json_compact": "Dati macro compatti", - "settings_menu": { - "settings_text": "Impostazioni", - "always_import_macro_settings": "Importa sempre le impostazioni macro", - "lang_text": "Lingua", - "lang_settings": { - "title": "Impostazioni della lingua", - "sub_text": "Scegli la tua lingua" - }, - "hotkeys_text": "Tasti di scelta rapida", - "hotkeys_settings": { - "title": "Impostazioni dei tasti di scelta rapida", - "start_record_text": "Inizia registrazione", - "stop_record_text": "Ferma registrazione", - "start_playback_text": "Avvia riproduzione", - "stop_playback_text": "Ferma riproduzione", - "clear_text": "Pulisci", - "please_key_text": "Premi un tasto", - "error_hotkeys": "Non puoi assegnare gli stessi tasti di scelta rapida per avviare la registrazione e la riproduzione" - }, - "minimization_text": "Minimizzazione", - "minimization_toast": "PyMacroRecord è stato ridotto a icona.", - "minimization_menu": { - "minimization_when_playing_text": "Minimizzazione durante la riproduzione", - "minimization_when_recording_text": "Minimizzazione durante la registrazione" - }, - "after_playback_text": "Dopo la riproduzione", - "after_playback_settings": { - "when_playback_complete_text": "Dopo il completamento della riproduzione", - "title": "Impostazioni dopo la riproduzione", - "sub_text": "Al termine della riproduzione", - "idle": "Inattivo", - "quit_software": "Esci dal software", - "standby": "Standby", - "log_off_computer": "Disconnetti computer", - "turn_off_computer": "Spegni computer", - "restart computer": "Riavvia computer", - "hibernate_if_enabled": "Ibernazione (se abilitata)" - } - }, - "others_menu": { - "others_text": "Altri", - "check_update_text": "Verifica aggiornamenti", - "reset_settings_text": "Reimposta impostazioni", - "reset_settings_confirmation": "Sei sicuro di voler reimpostare le impostazioni?", - "fixed_timestamp_text": "Timestamp fisso", - "fixed_timestamp_settings": { - "title": "Impostazioni del timestamp fisso", - "sub_text": "Inserisci timestamp fisso" - } - } + "scheduled_text": "In programma", + "scheduled_settings": { + "title": "Impostazioni programmate" }, - "help_menu": { - "help_text": "Aiuto", - "tutorial_text": "Tutorial", - "about_text": "Informazioni", - "website_text": "Sito web", - "about_settings": { - "title": "Informazioni", - "publisher_text": "Editore", - "version_text": "Versione", - "license_text": "Sotto licenza", - "version_check_update_text": { - "checking": "Verifica...", - "up_to_date": "Aggiornato", - "outdated": "Obsoleto", - "disabled": "Verifica aggiornamenti disabilitata", - "failed": "Impossibile verificare nuovi aggiornamenti" - } - } + "delay_text": "Ritardo", + "delay_settings": { + "title": "Impostazioni del ritardo", + "sub_text": "Inserisci il ritardo tra le ripetizioni", + "error_new_value": "Il ritardo non può essere inferiore a 0." }, - "others_menu": { - "others_text": "Altri", - "donors_text": "Donatori", - "donors_settings": { - "title": "Donatori", - "sub_text": "Tutti i donatori", - "load_donors": "Caricamento donatori...", - "want_be_donor": "Vuoi diventare donatore? Clicca qui!", - "cant_get_donors": "Impossibile ottenere i donatori :(" - }, - "translators_text": "Traduttori", - "translators_settings": { - "title": "Traduttori", - "sub_text": "Tutti i traduttori", - "page": "Pagina" - } + "randomized_delay_text": "Ritardo casuale", + "randomized_delay_settings": { + "title": "Impostazioni del ritardo casuale", + "sub_text": "Configura i limiti del ritardo casuale tra le azioni in millisecondi.\nI valori inferiori a zero ridurranno il tempo fino all'azione successiva.\nI valori superiori a zero aumenteranno il tempo fino all'azione successiva.", + "lower_text": "Inferiore:", + "upper_text": "Superiore:", + "error_new_value": "Entrambi i limiti del ritardo casuale devono essere numeri validi." + }, + "distribution_drawer_settings": { + "title": "Personalizza la distribuzione", + "sub_text": "Disegna la frequenza con cui dovrebbe verificarsi ciascun ritardo.", + "more_text": "Più frequente", + "less_text": "Meno frequente", + "clear_text": "Cancella" } + }, + "recordings_menu": { + "recordings_text": "Registrazioni", + "mouse_movement_text": "Movimento del mouse", + "mouse_click_text": "Clic del mouse", + "keyboard_text": "Tastiera", + "show_events_statut": "Mostra eventi sulla barra di stato" + }, + "json_compact": "Dati macro compatti", + "settings_menu": { + "settings_text": "Impostazioni", + "always_import_macro_settings": "Importa sempre le impostazioni macro", + "lang_text": "Lingua", + "lang_settings": { + "title": "Impostazioni della lingua", + "sub_text": "Scegli la tua lingua" + }, + "hotkeys_text": "Tasti di scelta rapida", + "hotkeys_settings": { + "title": "Impostazioni dei tasti di scelta rapida", + "start_record_text": "Inizia registrazione", + "stop_record_text": "Ferma registrazione", + "start_playback_text": "Avvia riproduzione", + "stop_playback_text": "Ferma riproduzione", + "clear_text": "Pulisci", + "please_key_text": "Premi un tasto", + "error_hotkeys": "Non puoi assegnare gli stessi tasti di scelta rapida per avviare la registrazione e la riproduzione" + }, + "minimization_text": "Minimizzazione", + "minimization_toast": "PyMacroRecord è stato ridotto a icona.", + "minimization_menu": { + "minimization_when_playing_text": "Minimizzazione durante la riproduzione", + "minimization_when_recording_text": "Minimizzazione durante la registrazione" + }, + "after_playback_text": "Dopo la riproduzione", + "after_playback_settings": { + "when_playback_complete_text": "Dopo il completamento della riproduzione", + "title": "Impostazioni dopo la riproduzione", + "sub_text": "Al termine della riproduzione", + "idle": "Inattivo", + "quit_software": "Esci dal software", + "standby": "Standby", + "log_off_computer": "Disconnetti computer", + "turn_off_computer": "Spegni computer", + "restart computer": "Riavvia computer", + "hibernate_if_enabled": "Ibernazione (se abilitata)" + } + }, + "others_menu": { + "others_text": "Altri", + "check_update_text": "Verifica aggiornamenti", + "reset_settings_text": "Reimposta impostazioni", + "reset_settings_confirmation": "Sei sicuro di voler reimpostare le impostazioni?", + "fixed_timestamp_text": "Timestamp fisso", + "fixed_timestamp_settings": { + "title": "Impostazioni del timestamp fisso", + "sub_text": "Inserisci timestamp fisso" + } + } + }, + "help_menu": { + "help_text": "Aiuto", + "tutorial_text": "Tutorial", + "about_text": "Informazioni", + "website_text": "Sito web", + "about_settings": { + "title": "Informazioni", + "publisher_text": "Editore", + "version_text": "Versione", + "license_text": "Sotto licenza", + "version_check_update_text": { + "checking": "Verifica...", + "up_to_date": "Aggiornato", + "outdated": "Obsoleto", + "disabled": "Verifica aggiornamenti disabilitata", + "failed": "Impossibile verificare nuovi aggiornamenti" + } + } + }, + "others_menu": { + "others_text": "Altri", + "donors_text": "Donatori", + "donors_settings": { + "title": "Donatori", + "sub_text": "Tutti i donatori", + "load_donors": "Caricamento donatori...", + "want_be_donor": "Vuoi diventare donatore? Clicca qui!", + "cant_get_donors": "Impossibile ottenere i donatori :(" + }, + "translators_text": "Traduttori", + "translators_settings": { + "title": "Traduttori", + "sub_text": "Tutti i traduttori", + "page": "Pagina" + } } + } } diff --git a/src/langs/ko.json b/src/langs/ko.json index f4764f6..e9f77a1 100644 --- a/src/langs/ko.json +++ b/src/langs/ko.json @@ -1,176 +1,191 @@ { - "information": { - "author": "Jinwoo.Seo", - "lang_short": "ko", - "lang_long": "한국어" + "information": { + "author": "Jinwoo.Seo", + "lang_short": "ko", + "lang_long": "한국어" + }, + "content": { + "global": { + "confirm_button": "확인", + "cancel_button": "취소", + "close_button": "닫기", + "error": "오류", + "previous_text": "이전", + "next_text": "다음", + "confirm_save": "녹화를 저장하시겠습니까?", + "confirm": "확인", + "information": "정보", + "restart_software_text": "변경 사항을 적용하려면 소프트웨어를 다시 시작해야 합니다.", + "load_macro_settings": "매크로 설정도 가져오시겠습니까?" }, - "content": { - "global": { - "confirm_button": "확인", - "cancel_button": "취소", - "close_button": "닫기", - "error": "오류", - "previous_text": "이전", - "next_text": "다음", - "confirm_save": "녹화를 저장하시겠습니까?", - "confirm": "확인", - "information": "정보", - "restart_software_text": "변경 사항을 적용하려면 소프트웨어를 다시 시작해야 합니다.", - "load_macro_settings": "매크로 설정도 가져오시겠습니까?" + "new_version": { + "title": "소프트웨어 업데이트", + "sub_text_1": "새 버전", + "sub_text_2": "사용 가능!", + "sub_text_3": "지금 다운로드하시겠습니까?", + "ignore_button": "무시", + "remind_later_button": "나중에 알림", + "download_button": "업데이트 다운로드" + }, + "file_menu": { + "file_text": "파일", + "new_text": "새로 만들기", + "load_text": "불러오기", + "save_text": "저장", + "save_as_text": "다른 이름으로 저장" + }, + "options_menu": { + "options_text": "옵션", + "playback_menu": { + "playback_text": "재생", + "speed_text": "속도", + "speed_settings": { + "title": "속도 설정", + "sub_text": "0.1에서 10 사이의 속도 값을 입력하세요.\n속도 설정은 동작 사이의 시간 간격에만 영향을 줍니다.\n시간과 관련된 다른 설정에는 영향을 주지 않습니다.", + "error_new_value": "속도 값은 0.1과 10 사이여야 합니다!" + }, + "repeat_text": "반복", + "repeat_settings": { + "title": "반복 설정", + "infinite_repeat": "무한 반복?", + "sub_text": "반복 횟수를 입력하세요", + "error_new_value": "반복 횟수는 최소 1이어야 합니다." + }, + "for_text": "기간", + "for_settings": { + "title": "기간 설정" }, - "new_version": { - "title": "소프트웨어 업데이트", - "sub_text_1": "새 버전", - "sub_text_2": "사용 가능!", - "sub_text_3": "지금 다운로드하시겠습니까?", - "ignore_button": "무시", - "remind_later_button": "나중에 알림", - "download_button": "업데이트 다운로드" + "interval_text": "간격", + "interval_settings": { + "title": "간격 설정" }, - "file_menu": { - "file_text": "파일", - "new_text": "새로 만들기", - "load_text": "불러오기", - "save_text": "저장", - "save_as_text": "다른 이름으로 저장" + "for_interval_settings": { + "hours_text": "시간", + "minutes_text": "분", + "seconds_text": "초", + "error_new_value_multiple": "입력이 올바르지 않습니다", + "error_new_value_single": "입력이 올바르지 않습니다" }, - "options_menu": { - "options_text": "옵션", - "playback_menu": { - "playback_text": "재생", - "speed_text": "속도", - "speed_settings": { - "title": "속도 설정", - "sub_text": "0.1에서 10 사이의 속도 값을 입력하세요", - "error_new_value": "속도 값은 0.1과 10 사이여야 합니다!" - }, - "repeat_text": "반복", - "repeat_settings": { - "title": "반복 설정", - "infinite_repeat": "무한 반복?", - "sub_text": "반복 횟수를 입력하세요", - "error_new_value": "반복 횟수는 최소 1이어야 합니다." - }, - "for_text": "기간", - "for_settings": { - "title": "기간 설정" - }, - "interval_text": "간격", - "interval_settings": { - "title": "간격 설정" - }, - "for_interval_settings": { - "hours_text": "시간", - "minutes_text": "분", - "seconds_text": "초", - "error_new_value_multiple": "입력이 올바르지 않습니다", - "error_new_value_single": "입력이 올바르지 않습니다" - }, - "scheduled_text": "예약됨", - "scheduled_settings": { - "title": "예약 설정" - }, - "delay_text": "지연", - "delay_settings": { - "title": "지연 설정", - "sub_text": "반복 간 지연을 입력하세요", - "error_new_value": "지연 시간은 0보다 작을 수 없습니다." - } - }, - "recordings_menu": { - "recordings_text": "녹화", - "mouse_movement_text": "마우스 이동", - "mouse_click_text": "마우스 클릭", - "keyboard_text": "키보드", - "show_events_statut": "상태 표시줄에 이벤트 표시" - }, - "json_compact": "매크로 데이터 축약", - "settings_menu": { - "settings_text": "설정", - "always_import_macro_settings": "매크로 설정 항상 가져오기", - "lang_text": "언어", - "lang_settings": { - "title": "언어 설정", - "sub_text": "언어를 선택하세요" - }, - "hotkeys_text": "단축키", - "hotkeys_settings": { - "title": "단축키 설정", - "start_record_text": "녹화 시작", - "stop_record_text": "녹화 중지", - "start_playback_text": "재생 시작", - "stop_playback_text": "재생 중지", - "clear_text": "해제", - "please_key_text": "키를 누르세요", - "error_hotkeys": "녹화 시작과 재생 시작에 동일한 단축키를 지정할 수 없습니다" - }, - "minimization_text": "최소화", - "minimization_toast": "PyMacroRecord가 최소화되었습니다.", - "minimization_menu": { - "minimization_when_playing_text": "재생 중 최소화", - "minimization_when_recording_text": "녹화 중 최소화" - }, - "after_playback_text": "재생 후", - "after_playback_settings": { - "when_playback_complete_text": "재생 완료 시", - "title": "재생 후 설정", - "sub_text": "재생이 끝났을 때", - "idle": "휴기", - "quit_software": "프로그램 종료", - "standby": "대기 모드", - "log_off_computer": "로그오프", - "turn_off_computer": "컴퓨터 종료", - "restart computer": "컴퓨터 재시작", - "hibernate_if_enabled": "최대 절전 모드(활성화된 경우)" - } - }, - "others_menu": { - "others_text": "기타", - "check_update_text": "업데이트 확인", - "reset_settings_text": "설정 재설정", - "reset_settings_confirmation": "설정을 재설정하시겠습니까?", - "fixed_timestamp_text": "고정 타임스탬프", - "fixed_timestamp_settings": { - "title": "고정 타임스탬프 설정", - "sub_text": "고정 타임스탬프를 입력하세요" - } - } + "scheduled_text": "예약됨", + "scheduled_settings": { + "title": "예약 설정" }, - "help_menu": { - "help_text": "도움말", - "tutorial_text": "튜토리얼", - "about_text": "정보", - "website_text": "웹사이트", - "about_settings": { - "title": "정보", - "publisher_text": "배포자", - "version_text": "버전", - "license_text": "라이선스", - "version_check_update_text": { - "checking": "확인 중...", - "up_to_date": "최신", - "outdated": "구버전", - "disabled": "업데이트 확인 비활성화됨", - "failed": "업데이트 확인에 실패했습니다" - } - } + "delay_text": "지연", + "delay_settings": { + "title": "지연 설정", + "sub_text": "반복 간 지연을 입력하세요", + "error_new_value": "지연 시간은 0보다 작을 수 없습니다." }, - "others_menu": { - "others_text": "기타", - "donors_text": "기부자", - "donors_settings": { - "title": "기부자", - "sub_text": "모든 기부자", - "load_donors": "기부자 불러오는 중...", - "want_be_donor": "기부자가 되고 싶으신가요? 여기를 클릭하세요!", - "cant_get_donors": "기부자 정보를 가져올 수 없습니다 :(" - }, - "translators_text": "번역자", - "translators_settings": { - "title": "번역자", - "sub_text": "모든 번역자", - "page": "페이지" - } + "randomized_delay_text": "무작위 지연", + "randomized_delay_settings": { + "title": "무작위 지연 설정", + "sub_text": "동작 간 무작위 지연 범위를 밀리초 단위로 설정합니다.\n0보다 작은 값은 다음 동작까지의 시간을 줄입니다.\n0보다 큰 값은 다음 동작까지의 시간을 늘립니다.", + "lower_text": "하한:", + "upper_text": "상한:", + "error_new_value": "무작위 지연의 두 범위 모두 유효한 숫자여야 합니다." + }, + "distribution_drawer_settings": { + "title": "분포 사용자 지정", + "sub_text": "각 지연이 얼마나 자주 발생할지 그려보세요.", + "more_text": "더 자주", + "less_text": "덜 자주", + "clear_text": "지우기" } + }, + "recordings_menu": { + "recordings_text": "녹화", + "mouse_movement_text": "마우스 이동", + "mouse_click_text": "마우스 클릭", + "keyboard_text": "키보드", + "show_events_statut": "상태 표시줄에 이벤트 표시" + }, + "json_compact": "매크로 데이터 축약", + "settings_menu": { + "settings_text": "설정", + "always_import_macro_settings": "매크로 설정 항상 가져오기", + "lang_text": "언어", + "lang_settings": { + "title": "언어 설정", + "sub_text": "언어를 선택하세요" + }, + "hotkeys_text": "단축키", + "hotkeys_settings": { + "title": "단축키 설정", + "start_record_text": "녹화 시작", + "stop_record_text": "녹화 중지", + "start_playback_text": "재생 시작", + "stop_playback_text": "재생 중지", + "clear_text": "해제", + "please_key_text": "키를 누르세요", + "error_hotkeys": "녹화 시작과 재생 시작에 동일한 단축키를 지정할 수 없습니다" + }, + "minimization_text": "최소화", + "minimization_toast": "PyMacroRecord가 최소화되었습니다.", + "minimization_menu": { + "minimization_when_playing_text": "재생 중 최소화", + "minimization_when_recording_text": "녹화 중 최소화" + }, + "after_playback_text": "재생 후", + "after_playback_settings": { + "when_playback_complete_text": "재생 완료 시", + "title": "재생 후 설정", + "sub_text": "재생이 끝났을 때", + "idle": "휴기", + "quit_software": "프로그램 종료", + "standby": "대기 모드", + "log_off_computer": "로그오프", + "turn_off_computer": "컴퓨터 종료", + "restart computer": "컴퓨터 재시작", + "hibernate_if_enabled": "최대 절전 모드(활성화된 경우)" + } + }, + "others_menu": { + "others_text": "기타", + "check_update_text": "업데이트 확인", + "reset_settings_text": "설정 재설정", + "reset_settings_confirmation": "설정을 재설정하시겠습니까?", + "fixed_timestamp_text": "고정 타임스탬프", + "fixed_timestamp_settings": { + "title": "고정 타임스탬프 설정", + "sub_text": "고정 타임스탬프를 입력하세요" + } + } + }, + "help_menu": { + "help_text": "도움말", + "tutorial_text": "튜토리얼", + "about_text": "정보", + "website_text": "웹사이트", + "about_settings": { + "title": "정보", + "publisher_text": "배포자", + "version_text": "버전", + "license_text": "라이선스", + "version_check_update_text": { + "checking": "확인 중...", + "up_to_date": "최신", + "outdated": "구버전", + "disabled": "업데이트 확인 비활성화됨", + "failed": "업데이트 확인에 실패했습니다" + } + } + }, + "others_menu": { + "others_text": "기타", + "donors_text": "기부자", + "donors_settings": { + "title": "기부자", + "sub_text": "모든 기부자", + "load_donors": "기부자 불러오는 중...", + "want_be_donor": "기부자가 되고 싶으신가요? 여기를 클릭하세요!", + "cant_get_donors": "기부자 정보를 가져올 수 없습니다 :(" + }, + "translators_text": "번역자", + "translators_settings": { + "title": "번역자", + "sub_text": "모든 번역자", + "page": "페이지" + } } + } } diff --git a/src/langs/nl.json b/src/langs/nl.json index 05b5071..3b1e8e1 100644 --- a/src/langs/nl.json +++ b/src/langs/nl.json @@ -41,7 +41,7 @@ "speed_text": "Snelheid", "speed_settings": { "title": "Snelheidsinstellingen", - "sub_text": "Geef snelheid tussen 0.1 and 10 in", + "sub_text": "Voer een snelheidsgetal tussen 0,1 en 10 in\nDe snelheidsinstelling heeft alleen invloed op het tijdsinterval tussen acties,\ndit heeft geen invloed op andere tijdgerelateerde instellingen.", "error_new_value": "De snelheid moet tussen 0.1 en 10 zijn!" }, "repeat_text": "Herhaal", @@ -75,6 +75,21 @@ "title": "Uitstel instellingen", "sub_text": "Geef uitstel tussen herhalingen op", "error_new_value": "Je kan niet minder dan 0 uitstel hebben." + }, + "randomized_delay_text": "Willekeurige vertraging", + "randomized_delay_settings": { + "title": "Instellingen voor willekeurige vertraging", + "sub_text": "Configureer de grenzen van de willekeurige vertraging tussen acties in milliseconden.\nWaarden onder nul verkorten de tijd tot de volgende actie.\nWaarden boven nul verlengen de tijd tot de volgende actie.", + "lower_text": "Ondergrens:", + "upper_text": "Bovengrens:", + "error_new_value": "Beide grenzen van de willekeurige vertraging moeten geldige getallen zijn." + }, + "distribution_drawer_settings": { + "title": "Verdeling aanpassen", + "sub_text": "Teken hoe vaak elke vertraging moet voorkomen.", + "more_text": "Vaker", + "less_text": "Minder vaak", + "clear_text": "Wissen" } }, "recordings_menu": { diff --git a/src/langs/pt-BR.json b/src/langs/pt-BR.json index a9f224e..c99fd65 100644 --- a/src/langs/pt-BR.json +++ b/src/langs/pt-BR.json @@ -1,176 +1,191 @@ { - "information": { - "author": "takiem", - "lang_short": "pt-BR", - "lang_long": "Brazilian-Portuguese" + "information": { + "author": "takiem", + "lang_short": "pt-BR", + "lang_long": "Brazilian-Portuguese" + }, + "content": { + "global": { + "confirm_button": "Confirmar", + "cancel_button": "Cancelar", + "close_button": "Fechar", + "error": "Erro", + "previous_text": "Anterior", + "next_text": "Próximo", + "confirm_save": "Deseja salvar sua gravação?", + "confirm": "Confirmar", + "information": "Informação", + "restart_software_text": "Você precisa reiniciar o software para que as alterações tenham efeito.", + "load_macro_settings": "Importar também as configurações macro?" }, - "content": { - "global": { - "confirm_button": "Confirmar", - "cancel_button": "Cancelar", - "close_button": "Fechar", - "error": "Erro", - "previous_text": "Anterior", - "next_text": "Próximo", - "confirm_save": "Deseja salvar sua gravação?", - "confirm": "Confirmar", - "information": "Informação", - "restart_software_text": "Você precisa reiniciar o software para que as alterações tenham efeito.", - "load_macro_settings": "Importar também as configurações macro?" - }, - "new_version": { - "title": "Atualização de Software", - "sub_text_1": "Nova versão", - "sub_text_2": "disponível!", - "sub_text_3": "Deseja baixar agora?", - "ignore_button": "Ignorar", - "remind_later_button": "Lembrar mais tarde", - "download_button": "Baixar atualização" - }, - "file_menu": { - "file_text": "Arquivo", - "new_text": "Novo", - "load_text": "Carregar", - "save_text": "Salvar", - "save_as_text": "Salvar como" - }, - "options_menu": { - "options_text": "Opções", - "playback_menu": { - "playback_text": "Reprodução", - "speed_text": "Velocidade", - "speed_settings": { - "title": "Configurações de Velocidade", - "sub_text": "Digite um valor de velocidade entre 0.1 e 10", - "error_new_value": "O valor de velocidade deve estar entre 0.1 e 10!" - }, - "repeat_text": "Repetir", - "repeat_settings": { - "title": "Configurações de Repetição", - "infinite_repeat": "Repetir infinitamente?", - "sub_text": "Digite o número de repetições", - "error_new_value": "O número de repetições não pode ser inferior a 1." - }, - "for_text": "Por", - "for_settings": { - "title": "Configurações de tempo" - }, - "interval_text": "Intervalo", - "interval_settings": { - "title": "Configurações de Intervalo" - }, - "for_interval_settings": { - "hours_text": "Horas", - "minutes_text": "Minutos", - "seconds_text": "Segundos", - "error_new_value_multiple": "entradas estão incorretas", - "error_new_value_single": "entrada está incorreta" - }, - "scheduled_text": "Programado", - "scheduled_settings": { - "title": "Configurações programadas" - }, - "delay_text": "Atraso", - "delay_settings": { - "title": "Configurações de Atraso", - "sub_text": "Digite o atraso entre as repetições", - "error_new_value": "O atraso não pode ser inferior a 0." - } + "new_version": { + "title": "Atualização de Software", + "sub_text_1": "Nova versão", + "sub_text_2": "disponível!", + "sub_text_3": "Deseja baixar agora?", + "ignore_button": "Ignorar", + "remind_later_button": "Lembrar mais tarde", + "download_button": "Baixar atualização" + }, + "file_menu": { + "file_text": "Arquivo", + "new_text": "Novo", + "load_text": "Carregar", + "save_text": "Salvar", + "save_as_text": "Salvar como" + }, + "options_menu": { + "options_text": "Opções", + "playback_menu": { + "playback_text": "Reprodução", + "speed_text": "Velocidade", + "speed_settings": { + "title": "Configurações de Velocidade", + "sub_text": "Digite um valor de velocidade entre 0,1 e 10\nA configuração de velocidade afetará apenas o intervalo de tempo entre as ações,\nisso não afetará outras configurações relacionadas ao tempo.", + "error_new_value": "O valor de velocidade deve estar entre 0.1 e 10!" + }, + "repeat_text": "Repetir", + "repeat_settings": { + "title": "Configurações de Repetição", + "infinite_repeat": "Repetir infinitamente?", + "sub_text": "Digite o número de repetições", + "error_new_value": "O número de repetições não pode ser inferior a 1." }, - "recordings_menu": { - "recordings_text": "Gravações", - "mouse_movement_text": "Movimento do Mouse", - "mouse_click_text": "Clique do Mouse", - "keyboard_text": "Teclado", - "show_events_statut": "Mostrar eventos na barra de status" + "for_text": "Por", + "for_settings": { + "title": "Configurações de tempo" }, - "json_compact": "Compactar dados do macro", - "settings_menu": { - "settings_text": "Configurações", - "always_import_macro_settings": "Importeer altijd macro-instellingen", - "lang_text": "Idioma", - "lang_settings": { - "title": "Configurações de Idioma", - "sub_text": "Escolha seu idioma" - }, - "hotkeys_text": "Teclas de Atalho", - "hotkeys_settings": { - "title": "Configurações de Teclas de Atalho", - "start_record_text": "Iniciar gravação", - "stop_record_text": "Parar gravação", - "start_playback_text": "Iniciar reprodução", - "stop_playback_text": "Parar reprodução", - "clear_text": "Limpar", - "please_key_text": "Por favor, pressione uma tecla", - "error_hotkeys": "As teclas de atalho para iniciar a gravação e a reprodução não podem ser as mesmas" - }, - "minimization_text": "Minimizar", - "minimization_toast": "PyMacroRecord foi minimizado.", - "minimization_menu": { - "minimization_when_playing_text": "Minimizar ao reproduzir", - "minimization_when_recording_text": "Minimizar ao gravar" - }, - "after_playback_text": "Após a reprodução", - "after_playback_settings": { - "when_playback_complete_text": "Quando a reprodução for concluída", - "title": "Configurações pós-reprodução", - "sub_text": "Ao concluir a reprodução", - "idle": "Ocioso", - "quit_software": "Fechar software", - "standby": "Suspender", - "log_off_computer": "Encerrar sessão", - "turn_off_computer": "Desligar o computador", - "restart computer": "Reiniciar o computador", - "hibernate_if_enabled": "Hibernar (se habilitado)" - } + "interval_text": "Intervalo", + "interval_settings": { + "title": "Configurações de Intervalo" }, - "others_menu": { - "others_text": "Outros", - "check_update_text": "Verificar atualização", - "reset_settings_text": "Redefinir configurações", - "reset_settings_confirmation": "Tem certeza de que deseja redefinir suas configurações?", - "fixed_timestamp_text": "Timestamp fixo", - "fixed_timestamp_settings": { - "title": "Configurações de Timestamp Fixo", - "sub_text": "Digite o timestamp fixo" - } + "for_interval_settings": { + "hours_text": "Horas", + "minutes_text": "Minutos", + "seconds_text": "Segundos", + "error_new_value_multiple": "entradas estão incorretas", + "error_new_value_single": "entrada está incorreta" + }, + "scheduled_text": "Programado", + "scheduled_settings": { + "title": "Configurações programadas" + }, + "delay_text": "Atraso", + "delay_settings": { + "title": "Configurações de Atraso", + "sub_text": "Digite o atraso entre as repetições", + "error_new_value": "O atraso não pode ser inferior a 0." + }, + "randomized_delay_text": "Atraso aleatório", + "randomized_delay_settings": { + "title": "Configurações do atraso aleatório", + "sub_text": "Configure os limites do atraso aleatório entre ações em milissegundos.\nValores abaixo de zero reduzirão o tempo até a próxima ação.\nValores acima de zero aumentarão o tempo até a próxima ação.", + "lower_text": "Inferior:", + "upper_text": "Superior:", + "error_new_value": "Ambos os limites do atraso aleatório devem ser números válidos." + }, + "distribution_drawer_settings": { + "title": "Personalizar distribuição", + "sub_text": "Desenhe com que frequência cada atraso deve ocorrer.", + "more_text": "Mais frequente", + "less_text": "Menos frequente", + "clear_text": "Limpar" } }, - "help_menu": { - "help_text": "Ajuda", - "tutorial_text": "Tutorial", - "about_text": "Sobre", - "website_text": "Website", - "about_settings": { - "title": "Sobre", - "publisher_text": "Editor", - "version_text": "Versão", - "license_text": "Sob licença", - "version_check_update_text": { - "checking": "Verificando...", - "up_to_date": "Atualizado", - "outdated": "Desatualizado", - "disabled": "Verificação de atualização desativada", - "failed": "Não foi possível verificar se há uma nova atualização" - } + "recordings_menu": { + "recordings_text": "Gravações", + "mouse_movement_text": "Movimento do Mouse", + "mouse_click_text": "Clique do Mouse", + "keyboard_text": "Teclado", + "show_events_statut": "Mostrar eventos na barra de status" + }, + "json_compact": "Compactar dados do macro", + "settings_menu": { + "settings_text": "Configurações", + "always_import_macro_settings": "Importeer altijd macro-instellingen", + "lang_text": "Idioma", + "lang_settings": { + "title": "Configurações de Idioma", + "sub_text": "Escolha seu idioma" + }, + "hotkeys_text": "Teclas de Atalho", + "hotkeys_settings": { + "title": "Configurações de Teclas de Atalho", + "start_record_text": "Iniciar gravação", + "stop_record_text": "Parar gravação", + "start_playback_text": "Iniciar reprodução", + "stop_playback_text": "Parar reprodução", + "clear_text": "Limpar", + "please_key_text": "Por favor, pressione uma tecla", + "error_hotkeys": "As teclas de atalho para iniciar a gravação e a reprodução não podem ser as mesmas" + }, + "minimization_text": "Minimizar", + "minimization_toast": "PyMacroRecord foi minimizado.", + "minimization_menu": { + "minimization_when_playing_text": "Minimizar ao reproduzir", + "minimization_when_recording_text": "Minimizar ao gravar" + }, + "after_playback_text": "Após a reprodução", + "after_playback_settings": { + "when_playback_complete_text": "Quando a reprodução for concluída", + "title": "Configurações pós-reprodução", + "sub_text": "Ao concluir a reprodução", + "idle": "Ocioso", + "quit_software": "Fechar software", + "standby": "Suspender", + "log_off_computer": "Encerrar sessão", + "turn_off_computer": "Desligar o computador", + "restart computer": "Reiniciar o computador", + "hibernate_if_enabled": "Hibernar (se habilitado)" } }, "others_menu": { "others_text": "Outros", - "donors_text": "Doadores", - "donors_settings": { - "title": "Doadores", - "sub_text": "Todos os doadores", - "load_donors": "Carregando doadores...", - "want_be_donor": "Quer ser um doador? Clique aqui!", - "cant_get_donors": "Não foi possível obter os doadores :(" - }, - "translators_text": "Tradutores", - "translators_settings": { - "title": "Tradutores", - "sub_text": "Todos os tradutores", - "page": "Página" + "check_update_text": "Verificar atualização", + "reset_settings_text": "Redefinir configurações", + "reset_settings_confirmation": "Tem certeza de que deseja redefinir suas configurações?", + "fixed_timestamp_text": "Timestamp fixo", + "fixed_timestamp_settings": { + "title": "Configurações de Timestamp Fixo", + "sub_text": "Digite o timestamp fixo" + } + } + }, + "help_menu": { + "help_text": "Ajuda", + "tutorial_text": "Tutorial", + "about_text": "Sobre", + "website_text": "Website", + "about_settings": { + "title": "Sobre", + "publisher_text": "Editor", + "version_text": "Versão", + "license_text": "Sob licença", + "version_check_update_text": { + "checking": "Verificando...", + "up_to_date": "Atualizado", + "outdated": "Desatualizado", + "disabled": "Verificação de atualização desativada", + "failed": "Não foi possível verificar se há uma nova atualização" } } + }, + "others_menu": { + "others_text": "Outros", + "donors_text": "Doadores", + "donors_settings": { + "title": "Doadores", + "sub_text": "Todos os doadores", + "load_donors": "Carregando doadores...", + "want_be_donor": "Quer ser um doador? Clique aqui!", + "cant_get_donors": "Não foi possível obter os doadores :(" + }, + "translators_text": "Tradutores", + "translators_settings": { + "title": "Tradutores", + "sub_text": "Todos os tradutores", + "page": "Página" + } } } +} diff --git a/src/langs/ru-RU.json b/src/langs/ru-RU.json index 6d3c6dd..ff3b045 100644 --- a/src/langs/ru-RU.json +++ b/src/langs/ru-RU.json @@ -41,7 +41,7 @@ "speed_text": "Скорость", "speed_settings": { "title": "Настройки скорости", - "sub_text": "Введите скорость от 0.1 до 10", + "sub_text": "Введите значение скорости от 0,1 до 10\nНастройка скорости будет влиять только на интервал времени между действиями,\nона не повлияет на другие настройки, связанные со временем.", "error_new_value": "Значение скорости должно быть между 0.1 и 10!" }, "repeat_text": "Повтор", @@ -75,6 +75,21 @@ "title": "Настройки задержки", "sub_text": "Введите задержку между повторами", "error_new_value": "Задержка не может быть меньше 0." + }, + "randomized_delay_text": "Случайная задержка", + "randomized_delay_settings": { + "title": "Настройки случайной задержки", + "sub_text": "Настройте границы случайной задержки между действиями в миллисекундах.\nЗначения ниже нуля уменьшат время до следующего действия.\nЗначения выше нуля увеличат время до следующего действия.", + "lower_text": "Нижняя граница:", + "upper_text": "Верхняя граница:", + "error_new_value": "Обе границы случайной задержки должны быть допустимыми числами." + }, + "distribution_drawer_settings": { + "title": "Настроить распределение", + "sub_text": "Нарисуйте, насколько часто должна встречаться каждая задержка.", + "more_text": "Чаще", + "less_text": "Реже", + "clear_text": "Очистить" } }, "recordings_menu": { diff --git a/src/langs/tr.json b/src/langs/tr.json index 8c9f524..f7f133b 100644 --- a/src/langs/tr.json +++ b/src/langs/tr.json @@ -42,7 +42,7 @@ "speed_text": "Hız", "speed_settings": { "title": "Hız ayarları", - "sub_text": "Hız değerini 0,1 ile 10 arasında girin", + "sub_text": "0,1 ile 10 arasında bir hız değeri girin\nHız ayarı yalnızca eylemler arasındaki zaman aralığını etkiler,\nzamanla ilgili diğer ayarları etkilemez.", "error_new_value": "Hız değeriniz 0,1 ile 10 arasında olmalıdır!" }, "repeat_text": "Tekrarla", @@ -76,6 +76,21 @@ "title": "Gecikme ayarları", "sub_text": "Tekrarlama arasındaki gecikmeyi girin.", "error_new_value": "Gecikme süresi en az 0 olabilir" + }, + "randomized_delay_text": "Rastgele gecikme", + "randomized_delay_settings": { + "title": "Rastgele gecikme ayarları", + "sub_text": "Eylemler arasındaki rastgele gecikme sınırlarını milisaniye cinsinden yapılandırın.\nSıfırın altındaki değerler bir sonraki eyleme kadar olan süreyi azaltır.\nSıfırın üzerindeki değerler bir sonraki eyleme kadar olan süreyi artırır.", + "lower_text": "Alt:", + "upper_text": "Üst:", + "error_new_value": "Rastgele gecikmenin her iki sınırı da geçerli sayılar olmalıdır." + }, + "distribution_drawer_settings": { + "title": "Dağılımı özelleştir", + "sub_text": "Her gecikmenin ne kadar sık olması gerektiğini çizin.", + "more_text": "Daha sık", + "less_text": "Daha az sık", + "clear_text": "Temizle" } }, "recordings_menu": { @@ -168,9 +183,9 @@ }, "translators_text": "Çevirmenler", "translators_settings": { - "title": "Çevirmenler", - "sub_text": "Tüm çevirmenler", - "page": "Sayfa" + "title": "Çevirmenler", + "sub_text": "Tüm çevirmenler", + "page": "Sayfa" } } } diff --git a/src/langs/zh-CN.json b/src/langs/zh-CN.json index d545843..f53166f 100644 --- a/src/langs/zh-CN.json +++ b/src/langs/zh-CN.json @@ -41,7 +41,7 @@ "speed_text": "速度", "speed_settings": { "title": "速度设置", - "sub_text": "设定速(0.1到10)", + "sub_text": "输入 0.1 到 10 之间的速度值\n速度设置只会影响操作之间的时间间隔,\n不会影响其他与时间相关的设置。", "error_new_value": "速度值必须为0.1到10之间的数值!" }, "repeat_text": "重复", @@ -75,6 +75,21 @@ "title": "延迟设置", "sub_text": "输入每次重复回放之间的延迟", "error_new_value": "延迟必须大于0." + }, + "randomized_delay_text": "随机延迟", + "randomized_delay_settings": { + "title": "随机延迟设置", + "sub_text": "以毫秒为单位配置操作之间的随机延迟范围。\n小于零的值将缩短执行下一操作前的时间。\n大于零的值将延长执行下一操作前的时间。", + "lower_text": "下限:", + "upper_text": "上限:", + "error_new_value": "随机延迟的两个范围都必须是有效数字。" + }, + "distribution_drawer_settings": { + "title": "自定义分布", + "sub_text": "绘制每种延迟出现的频率。", + "more_text": "更常见", + "less_text": "较少见", + "clear_text": "清除" } }, "recordings_menu": { @@ -167,9 +182,9 @@ }, "translators_text": "翻译者", "translators_settings": { - "title": "翻译者", - "sub_text": "所有翻译者", - "page": "第 页" + "title": "翻译者", + "sub_text": "所有翻译者", + "page": "第 页" } } } diff --git a/src/macro/macro.py b/src/macro/macro.py index f50661e..a5367d2 100644 --- a/src/macro/macro.py +++ b/src/macro/macro.py @@ -9,13 +9,13 @@ from pynput.keyboard import Key # FUTURE SELF: DON'T REMOVE THIS!! from pynput.mouse import Button +from utils.random import generate_weighted_random_func, update_random_seed from utils.get_key_pressed import getKeyPressed from utils.keys import vk_nb from utils.record_file_management import RecordFileManagement from utils.show_toast import show_notification_minim from utils.warning_pop_up_save import confirm_save - class Macro: """Init a new Macro""" @@ -210,6 +210,12 @@ def __play_events(self): repeat_count = 0 now = time() + randomized_delay_opts = userSettings["Playback"].get("Randomized_Delay", {}) + randomized_delay_weights = randomized_delay_opts.get("Distribution") + lower_bound = float(randomized_delay_opts["Lower"]) + upper_bound = float(randomized_delay_opts["Upper"]) + get_weighted_random = generate_weighted_random_func(randomized_delay_weights, lower_bound, upper_bound) + while self.playback and (is_infinite or repeat_count < repeat_times): for events in range(len(self.macro_events["events"])): elapsed_time = int(time() - now) @@ -223,9 +229,21 @@ def __play_events(self): self.macro_events["events"][events]["timestamp"] * (1 / userSettings["Playback"]["Speed"]) ) - if timeSleep < 0: - timeSleep = abs(timeSleep) - sleep(timeSleep) + + # base sleep time + base_ms = self.macro_events["events"][events]["timestamp"] + # the speed adjustments only affect the base interval between actions, it should not affect other time modifiers + adjusted_speed = 1/(userSettings["Playback"]["Speed"] or 1) + # randomized delay between actions, default to zero for no change + randomized_delay_ms = get_weighted_random() + update_random_seed() + + # sleep for the adjusted speed delay, and add the randomized delay time + sleep_time = max(0, (base_ms * adjusted_speed) + ((randomized_delay_ms or 0)/1000)) + + sleep(sleep_time) + + # continue to execute action event_type = self.macro_events["events"][events]["type"] if event_type == "cursorMove": # Cursor Move diff --git a/src/utils/random.py b/src/utils/random.py new file mode 100644 index 0000000..5c40d66 --- /dev/null +++ b/src/utils/random.py @@ -0,0 +1,207 @@ +import math +import random + + +def update_random_seed(seed=None, version=2): + """Update the random package seed. + At arbitrary times the randomness should update its seed so that it is harder to reverse-engineer the randomized value usage. + + Args: + seed (int|float|str|bytes|bytearray, optional): The seed to be used. None will default to using system time. Defaults to None (aka, system time). + version (1|2, optional): the version for the generation of seeding algorithm, the higher means the more advanced. Defaults to the maximum valid value. + """ + random.seed(a=seed, version=version) + +def get_weighted_random_float(distribution, lower_bound=0, upper_bound=1): + """ + Select a continuous random float between lower_bound and upper_bound + according to the weighted distribution. + + distribution: + List of [x, weight] pairs, where x is normalized to [0, 1]. + Example: [[0.0, 1], [0.5, 5], [1.0, 2]] + + lower_bound, upper_bound: float + The lower and upper bounds (inclusive) for the range to choose a value from. + + The x positions define the shape of the distribution. The weights are + linearly interpolated between points to form a continuous probability + density function. + """ + if lower_bound > upper_bound: + lower_bound, upper_bound = upper_bound, lower_bound + if lower_bound == upper_bound: + return lower_bound + points = sorted((float(x), max(0.0, float(weight))) for x, weight in distribution) + if not points: + return random.uniform(lower_bound, upper_bound) + # Merge duplicate x positions. + merged = [] + for x, weight in points: + if merged and x == merged[-1][0]: + merged[-1][1] = weight + else: + merged.append([x, weight]) + points = merged + # Clamp the distribution to the usable normalized range. + if points[0][0] > 0.0: + points.insert(0, [0.0, points[0][1]]) + if points[-1][0] < 1.0: + points.append([1.0, points[-1][1]]) + points = [ + [max(0.0, min(1.0, x)), weight] + for x, weight in points + ] + # Calculate the area under the piecewise-linear probability density. + total_area = 0.0 + for i in range(len(points) - 1): + x1, y1 = points[i] + x2, y2 = points[i + 1] + total_area += (x2 - x1) * (y1 + y2) / 2.0 + if total_area <= 0.0: + return random.uniform(lower_bound, upper_bound) + # Select a random area under the density curve. + target_area = random.random() * total_area + accumulated_area = 0.0 + for i in range(len(points) - 1): + x1, y1 = points[i] + x2, y2 = points[i + 1] + width = x2 - x1 + if width <= 0.0: + continue + segment_area = width * (y1 + y2) / 2.0 + if accumulated_area + segment_area >= target_area: + # Solve for x within this piecewise-linear segment. + target = target_area - accumulated_area + slope = (y2 - y1) / width + if abs(slope) < 1e-12: + dx = target / y1 if y1 > 0.0 else 0.0 + else: + # Integral from 0 to dx: + # y1 * dx + 0.5 * slope * dx^2 = target + discriminant = y1 * y1 + 2.0 * slope * target + dx = (-y1 + math.sqrt(max(0.0, discriminant))) / slope + x = x1 + max(0.0, min(width, dx)) + return lower_bound + x * (upper_bound - lower_bound) + accumulated_area += segment_area + return upper_bound + +def generate_weighted_random_func(distribution, lower_bound=0.0, upper_bound=1.0): + """ + Create and return a callable that generates random floats between + lower_bound and upper_bound according to a weighted distribution. + + distribution: + List of [x, weight] pairs, where x is normalized to [0, 1]. + + Example: + [[0.0, 1], [0.5, 5], [1.0, 2]] + + The x positions define the shape of a piecewise-linear probability + density function. The weights are linearly interpolated between points. + + Example: + random_value = generate_weighted_random_func( + [[0.0, 1], [0.5, 5], [1.0, 2]], + lower_bound=10, + upper_bound=20, + ) + + value = random_value() + """ + + if lower_bound > upper_bound: + lower_bound, upper_bound = upper_bound, lower_bound + + lower_bound = float(lower_bound) + upper_bound = float(upper_bound) + + # Prepare the distribution once when creating the generator. + points = [ + ( + max(0.0, min(1.0, float(x))), + max(0.0, float(weight)), + ) + for x, weight in distribution + ] + + # If there is no usable distribution, use a uniform distribution. + if not points: + return lambda: random.uniform(lower_bound, upper_bound) + + # Sort and merge duplicate x positions. + points.sort(key=lambda point: point[0]) + + merged = [] + for x, weight in points: + if merged and x == merged[-1][0]: + merged[-1][1] = weight + else: + merged.append([x, weight]) + + points = merged + + # Extend the distribution to cover the full normalized range. + if points[0][0] > 0.0: + points.insert(0, [0.0, points[0][1]]) + + if points[-1][0] < 1.0: + points.append([1.0, points[-1][1]]) + + # Calculate the total area under the density curve. + total_area = sum( + (x2 - x1) * (y1 + y2) / 2.0 + for (x1, y1), (x2, y2) in zip(points, points[1:]) + ) + + # Fall back to uniform sampling if the density has no area. + if total_area <= 0.0: + return lambda: random.uniform(lower_bound, upper_bound) + + def sample(): + """Generate one random value using the prepared distribution.""" + + # Handle a zero-width output range. + if lower_bound == upper_bound: + return lower_bound + + target_area = random.random() * total_area + accumulated_area = 0.0 + + for (x1, y1), (x2, y2) in zip(points, points[1:]): + width = x2 - x1 + + if width <= 0.0: + continue + + segment_area = width * (y1 + y2) / 2.0 + + if accumulated_area + segment_area >= target_area: + target = target_area - accumulated_area + slope = (y2 - y1) / width + + if abs(slope) < 1e-12: + # Constant-density segment. + dx = target / y1 if y1 > 0.0 else 0.0 + else: + # Solve: + # + # y1 * dx + 0.5 * slope * dx² = target + # + discriminant = y1 * y1 + 2.0 * slope * target + dx = (-y1 + math.sqrt(max(0.0, discriminant))) / slope + + dx = max(0.0, min(width, dx)) + normalized_value = x1 + dx + + return ( + lower_bound + + normalized_value * (upper_bound - lower_bound) + ) + + accumulated_area += segment_area + + return upper_bound + + # Return a new callable function instance. + return sample diff --git a/src/utils/user_settings.py b/src/utils/user_settings.py index be7103d..01eec5b 100644 --- a/src/utils/user_settings.py +++ b/src/utils/user_settings.py @@ -38,6 +38,12 @@ def init_settings(self): userSettings = { "Playback": { "Speed": 1, + "Randomized_Delay": { + "Enabled": False, + "Lower": 0, + "Upper": 0, + "Distribution": [] + }, "Repeat": { "Times": 1, "For": 0, @@ -80,7 +86,7 @@ def init_settings(self): "After_Playback": { "Mode": "Idle" - # Quit, Lock Computer, Lof off computer, Turn off computer, Restart Computer, Standby, Hibernate + # Quit, Lock Computer, Log off computer, Turn off computer, Restart Computer, Standby, Hibernate }, "Language": "en", @@ -147,6 +153,21 @@ def check_new_options(self): userSettings["Others"] = {"Check_update": True} if "Delay" not in userSettings["Playback"]["Repeat"]: userSettings["Playback"]["Repeat"]["Delay"] = 0 + if "Randomized_Delay" not in userSettings["Playback"]: + userSettings["Playback"]["Randomized_Delay"] = { + "Enabled": False, + "Lower": 0, + "Upper": 0, + "Distribution": [] + } + else: + randomized_delay = userSettings["Playback"]["Randomized_Delay"] + if "Enabled" not in randomized_delay: + randomized_delay["Enabled"] = (randomized_delay.get("Lower", 0) != 0 or randomized_delay.get("Upper", 0) != 0) + if "Lower" not in randomized_delay: + randomized_delay["Lower"] = 0 + if "Upper" not in randomized_delay: + randomized_delay["Upper"] = 0 if "Remind_new_ver_at" not in userSettings["Others"]: userSettings["Others"]["Remind_new_ver_at"] = 0 if "Language" not in userSettings: diff --git a/src/windows/main/main_app.py b/src/windows/main/main_app.py index 8ab6d3c..1f97aa1 100644 --- a/src/windows/main/main_app.py +++ b/src/windows/main/main_app.py @@ -67,7 +67,8 @@ def __init__(self): self.menu = MenuBar(self) # Menu Bar self.macro = Macro(self) - self.validate_cmd = self.register(self.validate_input) + self.validate_cmd_float = self.register(self.validate_input_float) + self.validate_cmd_int = self.register(self.validate_input_int) self.hotkeyManager = HotkeysManager(self) @@ -141,7 +142,7 @@ def systemTray(self): self.icon = Icon("name", image, "PyMacroRecord", menu) self.icon.run() - def validate_input(self, action, value_if_allowed): + def validate_input_float(self, action, value_if_allowed): """Prevents from adding letters on an Entry label""" if action == "1": # Insert try: @@ -151,6 +152,16 @@ def validate_input(self, action, value_if_allowed): return False return True + def validate_input_int(self, action, value_if_allowed): + """Prevents from adding letters on an Entry label""" + if action == "1": # Insert + try: + int(value_if_allowed) + return True + except ValueError: + return False + return True + def quit_software(self, force=False): if not self.macro_saved and self.macro_recorded and not force: wantToSave = confirm_save(self) diff --git a/src/windows/main/menu_bar.py b/src/windows/main/menu_bar.py index a42a968..77dafcf 100644 --- a/src/windows/main/menu_bar.py +++ b/src/windows/main/menu_bar.py @@ -4,7 +4,7 @@ from utils.record_file_management import RecordFileManagement from windows.help.about import About -from windows.options.playback import Delay, Repeat, Speed, TimeGui +from windows.options.playback import Delay, RandomizedDelay, Repeat, Speed, TimeGui from windows.options.settings import AfterPlayBack, Hotkeys, SelectLanguage from windows.others.donors import Donors from windows.others.translators import Translators @@ -50,6 +50,7 @@ def __init__(self, parent): playback_sub.add_command(label=self.text_config["options_menu"]["playback_menu"]["for_text"], command=lambda: TimeGui(self, parent, "For")) playback_sub.add_command(label=self.text_config["options_menu"]["playback_menu"]["scheduled_text"], command=lambda: TimeGui(self, parent, "Scheduled")) playback_sub.add_command(label=self.text_config["options_menu"]["playback_menu"]["delay_text"], command=lambda: Delay(self, parent)) + playback_sub.add_command(label=self.text_config["options_menu"]["playback_menu"]["randomized_delay_text"], command=lambda: RandomizedDelay(self, parent)) # Recordings Sub self.mouseMove = BooleanVar(value=userSettings["Recordings"]["Mouse_Move"]) diff --git a/src/windows/options/playback/__init__.py b/src/windows/options/playback/__init__.py index 3592f06..24365c3 100644 --- a/src/windows/options/playback/__init__.py +++ b/src/windows/options/playback/__init__.py @@ -2,3 +2,4 @@ from .repeat import Repeat from .speed import Speed from .time_gui import TimeGui +from .randomized_delay import RandomizedDelay diff --git a/src/windows/options/playback/delay.py b/src/windows/options/playback/delay.py index 8f9afe9..b2be425 100644 --- a/src/windows/options/playback/delay.py +++ b/src/windows/options/playback/delay.py @@ -11,16 +11,18 @@ def __init__(self, parent, main_app): self.settings = main_app.settings Label(self, text=main_app.text_content["options_menu"]["playback_menu"]["delay_settings"]["sub_text"], font=('Segoe UI', 10)).pack(side=TOP, pady=10) userSettings = main_app.settings.settings_dict - setNewDelayInput = Spinbox(self, from_=1, to=100000000, width=7, validate="key", - validatecommand=(main_app.validate_cmd, "%d", "%P")) + setNewDelayInput = Spinbox(self, from_=0, to=100000000, width=7, validate="key", validatecommand=(main_app.validate_cmd_float, "%d", "%P")) setNewDelayInput.delete(0, "end") setNewDelayInput.insert(0, str(userSettings["Playback"]["Repeat"]["Delay"])) setNewDelayInput.pack(pady=20) buttonArea = Frame(self) - Button(buttonArea, text=main_app.text_content["global"]["confirm_button"], command=lambda: self.setNewDelayNumber(setNewDelayInput.get(), main_app)).pack(side=LEFT, - padx=10) + Button(buttonArea, text=main_app.text_content["global"]["confirm_button"], command=lambda: self.setNewDelayNumber(setNewDelayInput.get(), main_app)).pack(side=LEFT, padx=10) Button(buttonArea, text=main_app.text_content["global"]["cancel_button"], command=self.destroy).pack(side=LEFT, padx=10) buttonArea.pack(side=BOTTOM, pady=10) + self.update_idletasks() + popup_width = min(max(300, self.winfo_reqwidth() + 10), 800) + popup_height = min(max(150, self.winfo_reqheight() + 10), 600) + self.geometry(f"{popup_width}x{popup_height}") self.wait_window() main_app.prevent_record = False diff --git a/src/windows/options/playback/randomized_delay.py b/src/windows/options/playback/randomized_delay.py new file mode 100644 index 0000000..5a3c89c --- /dev/null +++ b/src/windows/options/playback/randomized_delay.py @@ -0,0 +1,85 @@ +from tkinter import BOTTOM, LEFT, TOP, Spinbox, messagebox +from tkinter.ttk import Button, Frame, Label +from sys import maxsize as INT_BOUND +from windows.others.distribution import DistributionDrawer +from windows.popup import Popup + +class RandomizedDelay(Popup): + def __init__(self, parent, main_app): + super().__init__(main_app.text_content["options_menu"]["playback_menu"]["randomized_delay_settings"]["title"], 350, 180, parent) + main_app.prevent_record = True + self.main_app = main_app + self.settings = main_app.settings + text = main_app.text_content["options_menu"]["playback_menu"]["randomized_delay_settings"] + Label(self, text=text["sub_text"], font=("Segoe UI", 10)).pack(side=TOP, pady=10) + user_settings = main_app.settings.settings_dict + randomized_delay = user_settings["Playback"].get("Randomized_Delay", + { + "Enabled": False, + "Lower": 0, + "Upper": 0, + "Distribution": None + } + ) + # Keep the currently saved distribution while this popup is open. + self.distribution = randomized_delay.get("Distribution") + input_area = Frame(self) + Label(input_area, text=text["lower_text"]).pack(side=LEFT, padx=5) + self.lowerInput = Spinbox(input_area, from_=-1 * INT_BOUND, to=INT_BOUND, width=9, validate="key", validatecommand=(main_app.validate_cmd_float, "%d", "%P")) + self.lowerInput.delete(0, "end") + self.lowerInput.insert(0, str(randomized_delay.get("Lower", 0))) + self.lowerInput.pack(side=LEFT, padx=5) + Label(input_area, text=text["upper_text"]).pack(side=LEFT, padx=5) + self.upperInput = Spinbox(input_area, from_=-1 * INT_BOUND, to=INT_BOUND, width=9, validate="key", validatecommand=(main_app.validate_cmd_float, "%d", "%P")) + self.upperInput.delete(0, "end") + self.upperInput.insert(0, str(randomized_delay.get("Upper", 0))) + self.upperInput.pack(side=LEFT, padx=5) + input_area.pack(pady=10) + distribution_area = Frame(self) + Button(distribution_area, text=text.get("distribution_button", "Customize distribution"), command=self.open_distribution_editor).pack(side=LEFT, padx=5) + distribution_area.pack(pady=5) + button_area = Frame(self) + Button(button_area, text=main_app.text_content["global"]["confirm_button"], command=lambda: self.setNewValues(self.lowerInput.get(), self.upperInput.get(), main_app)).pack(side=LEFT, padx=10) + Button(button_area, text=main_app.text_content["global"]["cancel_button"], command=self.destroy).pack(side=LEFT, padx=10) + button_area.pack(side=BOTTOM, pady=10) + self.update_idletasks() + popup_width = min(max(350, self.winfo_reqwidth() + 10), 800) + popup_height = min(max(180, self.winfo_reqheight() + 10), 600) + self.geometry(f"{popup_width}x{popup_height}") + self.wait_window() + main_app.prevent_record = False + + def open_distribution_editor(self): + """Open the distribution drawer as a child of this popup.""" + + try: + lower_bound = float(self.lowerInput.get()) + upper_bound = float(self.upperInput.get()) + except ValueError: + messagebox.showerror(self.main_app.text_content["global"]["error"], self.main_app.text_content["options_menu"]["playback_menu"]["randomized_delay_settings"]["error_new_value"]) + return + + if lower_bound > upper_bound: + lower_bound, upper_bound = upper_bound, lower_bound + + DistributionDrawer(self, self.main_app, lower_bound, upper_bound, self.distribution) + + def setNewValues(self, lower_bound, upper_bound, main_app): + """Set the new Randomized Delay values.""" + try: + lower_bound = float(lower_bound) + upper_bound = float(upper_bound) + except ValueError: + messagebox.showerror(main_app.text_content["global"]["error"], main_app.text_content["options_menu"]["playback_menu"]["randomized_delay_settings"]["error_new_value"]) + return + if lower_bound > upper_bound: + lower_bound, upper_bound = upper_bound, lower_bound + enabled = lower_bound != 0 or upper_bound != 0 + self.settings.change_settings("Playback", "Randomized_Delay", None, { + "Enabled": enabled, + "Lower": lower_bound, + "Upper": upper_bound, + "Distribution": self.distribution + } + ) + self.destroy() diff --git a/src/windows/options/playback/repeat.py b/src/windows/options/playback/repeat.py index ca6c027..11e624d 100644 --- a/src/windows/options/playback/repeat.py +++ b/src/windows/options/playback/repeat.py @@ -27,7 +27,7 @@ def __init__(self, parent, main_app): infiniteCheck.pack(pady=5) repeatTimes = Spinbox(self, from_=1, to=100000000, width=7, validate="key", - validatecommand=(main_app.validate_cmd, "%d", "%P")) + validatecommand=(main_app.self.validate_cmd_int, "%d", "%P")) repeatTimes.delete(0, "end") repeatTimes.insert(0, userSettings["Playback"]["Repeat"]["Times"]) repeatTimes.pack(pady=5) @@ -39,6 +39,10 @@ def __init__(self, parent, main_app): command=self.destroy).pack(side=LEFT, padx=5) buttonArea.pack(pady=10) + self.update_idletasks() + popup_width = min(max(300, self.winfo_reqwidth() + 10), 800) + popup_height = min(max(180, self.winfo_reqheight() + 10), 600) + self.geometry(f"{popup_width}x{popup_height}") self.wait_window() main_app.prevent_record = False diff --git a/src/windows/options/playback/speed.py b/src/windows/options/playback/speed.py index a55c300..f28a7ec 100644 --- a/src/windows/options/playback/speed.py +++ b/src/windows/options/playback/speed.py @@ -12,7 +12,7 @@ def __init__(self, parent, main_app): Label(self, text=main_app.text_content["options_menu"]["playback_menu"]["speed_settings"]["sub_text"], font=('Segoe UI', 10)).pack(side=TOP, pady=10) userSettings = main_app.settings.settings_dict setNewSpeedInput = Spinbox(self, from_=0.1, to=10, width=7, validate="key", - validatecommand=(main_app.validate_cmd, "%d", "%P")) + validatecommand=(main_app.validate_cmd_float, "%d", "%P")) setNewSpeedInput.insert(0, str(userSettings["Playback"]["Speed"])) setNewSpeedInput.pack(pady=20) buttonArea = Frame(self) @@ -20,6 +20,10 @@ def __init__(self, parent, main_app): padx=10) Button(buttonArea, text=main_app.text_content["global"]["cancel_button"], command=self.destroy).pack(side=LEFT, padx=10) buttonArea.pack(side=BOTTOM, pady=10) + self.update_idletasks() + popup_width = min(max(300, self.winfo_reqwidth() + 10), 800) + popup_height = min(max(150, self.winfo_reqheight() + 10), 600) + self.geometry(f"{popup_width}x{popup_height}") self.wait_window() main_app.prevent_record = False diff --git a/src/windows/options/playback/time_gui.py b/src/windows/options/playback/time_gui.py index 980aa88..7fbdd66 100644 --- a/src/windows/options/playback/time_gui.py +++ b/src/windows/options/playback/time_gui.py @@ -35,7 +35,7 @@ def __init__(self, parent, main_app, type): to=24, width=10, validate="key", - validatecommand=(main_app.validate_cmd, "%d", "%P"), + validatecommand=(main_app.validate_cmd_int, "%d", "%P"), ) hourValue = str(value // 3600) if self.type == "Scheduled" and self.time_format == "12 hours" and self.time_string == "PM" and int(hourValue) >= 12: @@ -59,7 +59,7 @@ def __init__(self, parent, main_app, type): to=60, width=10, validate="key", - validatecommand=(main_app.validate_cmd, "%d", "%P"), + validatecommand=(main_app.validate_cmd_int, "%d", "%P"), ) minInput.insert( 0, str((value % 3600) // 60) @@ -75,7 +75,7 @@ def __init__(self, parent, main_app, type): to=60, width=10, validate="key", - validatecommand=(main_app.validate_cmd, "%d", "%P"), + validatecommand=(main_app.validate_cmd_float, "%d", "%P"), ) secInput.insert(0, str(value % 60)) secInput.pack() @@ -90,6 +90,10 @@ def __init__(self, parent, main_app, type): ).pack(side=LEFT, padx=10) Button(buttonArea, text=main_app.text_content["global"]["cancel_button"], command=self.destroy).pack(side=LEFT, padx=10) buttonArea.pack(side=BOTTOM, pady=10) + self.update_idletasks() + popup_width = min(max(300, self.winfo_reqwidth() + 10), 800) + popup_height = min(max(height, self.winfo_reqheight() + 10), 600) + self.geometry(f"{popup_width}x{popup_height}") self.wait_window() main_app.prevent_record = False diff --git a/src/windows/others/__init__.py b/src/windows/others/__init__.py index 276a6f2..3cc3b20 100644 --- a/src/windows/others/__init__.py +++ b/src/windows/others/__init__.py @@ -1 +1,2 @@ from .new_ver_avalaible import NewVerAvailable +from .distribution import DistributionDrawer \ No newline at end of file diff --git a/src/windows/others/distribution.py b/src/windows/others/distribution.py new file mode 100644 index 0000000..aca42a7 --- /dev/null +++ b/src/windows/others/distribution.py @@ -0,0 +1,833 @@ +import math +from tkinter import BOTTOM, LEFT, RIGHT, TOP, Canvas, IntVar, Scrollbar, Spinbox, StringVar +from tkinter.ttk import Button, Combobox, Frame, Label + +from windows.popup import Popup + + +class DistributionDrawer(Popup): + """ + Visual distribution editor. + + The curve is stored as normalized points: + [x, y] + + X is the position between Lower and Upper. + Y is how common / likely that value should be. + + Drawing is non-destructive: drawing over an existing curve only replaces + the portion covered by the new stroke. Right-click dragging erases the + covered portion. Green points are individually draggable vertically. + """ + + CANVAS_WIDTH = 560 + CANVAS_HEIGHT = 280 + CONTROL_WIDTH = 230 + EDIT_RADIUS_X = 0.012 + ERASE_RADIUS_X = 0.018 + POINT_HIT_RADIUS_PX = 9 + LINEAR_EDGE_OFFSET = 0.06 + PRESET_SAMPLE_COUNT = 256 + + USER_POINT_COLOR = "#4a890b" + INTERPOLATED_COLOR = "#4a0b89" + GRID_COLOR = "#d9d9d9" + ZERO_GRID_COLOR = "#b5b5b5" + + PRESET_KEYS = ("freehand", "linear", "bell") + INTERPOLATION_KEYS = ("sharp", "curves") + + def __init__(self, parent, main_app, lower_bound, upper_bound, distribution=None): + self.owner = parent + self.main_app = main_app + self.lower_bound = lower_bound + self.upper_bound = upper_bound + + self.points = [] + self._user_points = [] + self._stroke = [] + self._editing = False + self._erasing = False + self._dragged_user_index = None + self._dragged_user_point = None + self._controls_scroll_update_scheduled = False + + settings = main_app.text_content["options_menu"]["playback_menu"]["distribution_drawer_settings"] + super().__init__(settings["title"], 900, 540, parent) + + main_app.prevent_record = True + self.settings = settings + self.interpolation_var = StringVar() + self.preset_var = StringVar() + + Label(self, text=settings["distribution_instruction_text"], font=("Segoe UI", 10)).pack(side=TOP, pady=(10, 8)) + + button_area = Frame(self) + Button(button_area, text=main_app.text_content["global"]["confirm_button"], command=self.apply).pack(side=LEFT, padx=5) + Button(button_area, text=main_app.text_content["global"]["cancel_button"], command=self.destroy).pack(side=LEFT, padx=5) + button_area.pack(side=BOTTOM, pady=8) + + content = Frame(self) + content.pack(fill="both", expand=True, padx=15, pady=(0, 4)) + + graph_frame = Frame(content) + graph_frame.pack(side=LEFT, fill="both", expand=True) + + axis_information = settings["axis_information"] + vertical_high_label = Label(graph_frame, text=axis_information["vertical"]["high_text"],) + vertical_high_label.pack(side=TOP, pady=(0, 2)) + + canvas_frame = Frame(graph_frame, relief="solid", borderwidth=1) + canvas_frame.pack() + + self.canvas = Canvas(canvas_frame, width=self.CANVAS_WIDTH, height=self.CANVAS_HEIGHT, highlightthickness=0, cursor="crosshair",) + self.canvas.pack() + self._draw_grid() + + info_area = Frame(graph_frame) + horizontal_low_label = Label(info_area, text=settings["axis_information"]["horizontal"]["low_value_text"].format(value=self._format_number(lower_bound)),) + horizontal_low_label.pack(side=LEFT) + horizontal_high_label = Label(info_area, text=settings["axis_information"]["horizontal"]["high_value_text"].format(value=self._format_number(upper_bound)),) + horizontal_high_label.pack(side=RIGHT) + info_area.pack(fill="x", pady=(4, 0)) + + vertical_low_label = Label(graph_frame, text=axis_information["vertical"]["low_text"],) + vertical_low_label.pack(side=TOP, pady=(2, 0)) + Label(graph_frame, text=settings["help_instructions_text"], foreground="#666666",).pack(side=TOP, pady=(6, 0)) + + control_frame = Frame(content, relief="solid", borderwidth=1, padding=0) + control_frame.pack(side=RIGHT, fill="y", padx=(15, 0)) + + self.control_canvas = Canvas(control_frame, width=self.CONTROL_WIDTH, highlightthickness=0, borderwidth=0) + self.control_scrollbar = Scrollbar(control_frame, orient="vertical", command=self.control_canvas.yview) + self.control_canvas.configure(yscrollcommand=self.control_scrollbar.set) + self.control_canvas.pack(side=LEFT, fill="both", expand=True) + + controls_inner = Frame(self.control_canvas, padding=10) + controls_window = self.control_canvas.create_window((0, 0), window=controls_inner, anchor="nw",) + + self.controls_inner = controls_inner + self._controls_window = controls_window + controls_inner.bind("", self._update_controls_scroll_region) + self.control_canvas.bind("", self._resize_controls_inner) + self._schedule_controls_scroll_update() + self.control_canvas.bind("", self._scroll_controls, add="+") + self.control_canvas.bind("", self._scroll_controls_up, add="+") + self.control_canvas.bind("", self._scroll_controls_down, add="+") + + settings_information = settings["settings_information"] + Label(controls_inner, text=settings_information["header_text"], font=("Segoe UI", 10, "bold"),).pack(anchor="w", pady=(0, 10)) + + Label(controls_inner, text=settings_information["preset_text"]).pack(anchor="w") + + preset_labels = { + key: settings["presets"][key]["label_text"] for key in self.PRESET_KEYS + } + self._preset_key_by_label = { + label: key for key, label in preset_labels.items() + } + self._preset_label_by_key = preset_labels + + self.preset_var.set(preset_labels["freehand"]) + self.preset_box = Combobox(controls_inner, textvariable=self.preset_var, values=list(preset_labels.values()), state="readonly", width=18,) + self.preset_box.pack(fill="x", pady=(2, 2)) + self.preset_box.bind("<>", self.apply_selected_preset) + + self.preset_description = Label(controls_inner, text=self._preset_description("freehand"), wraplength=195, justify="left", foreground="#555555",) + self.preset_description.pack(anchor="w", pady=(0, 4)) + + self.preset_note = Label(controls_inner, text=self._preset_note("freehand"), wraplength=195, justify="left", foreground="#555555",) + self.preset_note.pack(anchor="w", pady=(0, 12)) + + self.bell_frame = Frame(controls_inner) + self._build_bell_controls(self.bell_frame) + + Label(controls_inner, text=settings_information["interpolate_text"]).pack(anchor="w", pady=(4, 0)) + interpolation_labels = { + key: settings_information["interpolation_types"][key]["label_text"] + for key in self.INTERPOLATION_KEYS + } + self._interpolation_key_by_label = { + label: key for key, label in interpolation_labels.items() + } + self._interpolation_label_by_key = interpolation_labels + + self.interpolation_box = Combobox(controls_inner, textvariable=self.interpolation_var, values=list(interpolation_labels.values()), state="readonly", width=18,) + self.interpolation_box.pack(fill="x", pady=(2, 2)) + self.interpolation_box.bind("<>", self._on_interpolation_changed,) + self.interpolation_var.set(interpolation_labels["curves"]) + + interpolate_button = Button(controls_inner, text=settings_information["interpolate_button_text"], command=self.interpolate_missing,) + interpolate_button.pack(fill="x", pady=(10, 4)) + + clear_button = Button(controls_inner, text=settings_information["clear_text"], command=self.clear,) + clear_button.pack(fill="x", pady=4) + + self._bind_control_scroll(controls_inner) + + self.canvas.bind("", self.start_drawing) + self.canvas.bind("", self.draw) + self.canvas.bind("", self.finish_drawing) + self.canvas.bind("", self.start_erasing) + self.canvas.bind("", self.erase) + self.canvas.bind("", self.finish_erasing) + + self.load_distribution(distribution) + + self.control_canvas.yview_moveto(0) + self._schedule_controls_scroll_update() + self.wait_window() + main_app.prevent_record = False + + def _update_controls_scroll_region(self, _event=None): + self.control_canvas.configure(scrollregion=self.control_canvas.bbox("all")) + self._schedule_controls_scroll_update() + + def _resize_controls_inner(self, event): + self.control_canvas.itemconfigure(self._controls_window, width=event.width) + self._update_controls_scroll_region() + + def _schedule_controls_scroll_update(self): + if self._controls_scroll_update_scheduled: + return + self._controls_scroll_update_scheduled = True + self.after_idle(self._update_controls_scroll_state) + + def _update_controls_scroll_state(self): + self._controls_scroll_update_scheduled = False + bbox = self.control_canvas.bbox("all") + if bbox is None: + return + + content_height = bbox[3] - bbox[1] + canvas_height = self.control_canvas.winfo_height() + needs_scroll = content_height > canvas_height + 1 + + if needs_scroll and not self.control_scrollbar.winfo_ismapped(): + self.control_scrollbar.pack(side=RIGHT, fill="y") + self.control_canvas.update_idletasks() + self.control_canvas.itemconfigure(self._controls_window, width=self.control_canvas.winfo_width()) + self.control_canvas.configure(scrollregion=self.control_canvas.bbox("all")) + return + + if not needs_scroll and self.control_scrollbar.winfo_ismapped(): + self.control_scrollbar.pack_forget() + self.control_canvas.update_idletasks() + self.control_canvas.itemconfigure(self._controls_window, width=self.control_canvas.winfo_width()) + self.control_canvas.configure(scrollregion=self.control_canvas.bbox("all")) + self.control_canvas.yview_moveto(0) + + def _has_control_scroll(self): + bbox = self.control_canvas.bbox("all") + if bbox is None: + return False + return (bbox[3] - bbox[1]) > self.control_canvas.winfo_height() + 1 + + def _scroll_controls(self, event): + if not self._has_control_scroll(): + return + delta = event.delta or 0 + self.control_canvas.yview_scroll(int(-1 * delta / 120), "units") + + def _scroll_controls_up(self, _event=None): + if self._has_control_scroll(): + self.control_canvas.yview_scroll(-1, "units") + + def _scroll_controls_down(self, _event=None): + if self._has_control_scroll(): + self.control_canvas.yview_scroll(1, "units") + + def _control_canvas_yview(self, *args): + self.control_canvas.yview(*args) + self.after_idle(self._update_controls_scroll_state) + + def _bind_control_scroll(self, widget): + widget.bind("", self._scroll_controls, add="+") + widget.bind("", self._scroll_controls_up, add="+") + widget.bind("", self._scroll_controls_down, add="+") + for child in widget.winfo_children(): + self._bind_control_scroll(child) + + def _build_bell_controls(self, parent): + bell_settings = self.settings["presets"]["bell"]["controls"] + + Label(parent, text=bell_settings["header_text"], font=("Segoe UI", 9, "bold"),).pack(anchor="w", pady=(0, 6)) + + self.bell_center_var = IntVar(value=50) + self.bell_width_var = IntVar(value=20) + self.bell_tail_magnitude_var = IntVar(value=0) + self.bell_peaks_var = IntVar(value=1) + + self._add_spin_control(parent, bell_settings["center_text"], self.bell_center_var, 0, 100, bell_settings["center_tooltip_text"],) + self._add_spin_control(parent, bell_settings["width_text"], self.bell_width_var, 1, 100, bell_settings["width_tooltip_text"],) + self._add_spin_control(parent, bell_settings["tail_magnitude_text"], self.bell_tail_magnitude_var, 0, 100, bell_settings["tail_magnitude_tooltip_text"],) + + Label(parent, text=bell_settings["tail_side_text"]).pack(anchor="w", pady=(5, 1)) + + tail_side_labels = { + "negative": bell_settings["tail_side_negative_text"], + "positive": bell_settings["tail_side_positive_text"], + } + self._tail_side_key_by_label = { + label: key for key, label in tail_side_labels.items() + } + self._tail_side_label_by_key = tail_side_labels + + self.bell_tail_side_var = StringVar(value=tail_side_labels["positive"]) + tail_side_box = Combobox(parent, textvariable=self.bell_tail_side_var, values=list(tail_side_labels.values()), state="readonly", width=18,) + tail_side_box.pack(fill="x", pady=(1, 2)) + + self._add_spin_control(parent, bell_settings["peaks_text"], self.bell_peaks_var, 0, 12, bell_settings["peaks_tooltip_text"],) + + reset_button = Button(parent, text=bell_settings["reset_text"], command=self.reset_bell_settings,) + reset_button.pack(fill="x", pady=(6, 4)) + + apply_button = Button(parent, text=bell_settings["apply_text"], command=self.apply_bell_settings,) + apply_button.pack(fill="x", pady=(0, 8)) + + def _add_spin_control(self, parent, label_text, variable, minimum, maximum, tooltip): + Label(parent, text=label_text).pack(anchor="w", pady=(3, 1)) + spinbox = Spinbox(parent, textvariable=variable, from_=minimum, to=maximum, increment=1, width=18,) + spinbox.pack(fill="x", pady=(0, 1)) + + def _preset_description(self, key): + return self.settings["presets"][key]["description_text"] + + def _preset_note(self, key): + return self.settings["presets"][key]["note_text"] + + def _selected_preset_key(self): + return self._preset_key_by_label[self.preset_var.get()] + + def _selected_interpolation_key(self): + return self._interpolation_key_by_label[self.interpolation_var.get()] + + def _on_interpolation_changed(self, _event=None): + self.redraw() + + def start_drawing(self, event): + dragged_index = self._find_user_point(event.x, event.y) + if dragged_index is not None: + self._editing = True + self._erasing = False + self._dragged_user_index = dragged_index + self._dragged_user_point = self._user_points[dragged_index] + return + + self._dragged_user_index = None + self._dragged_user_point = None + self._editing = True + self._erasing = False + self._stroke = [ + (self._canvas_to_normalized_x(event.x), + self._canvas_to_normalized_y(event.y),) + ] + self.redraw() + + def draw(self, event): + if not self._editing or self._erasing: + return + + if self._dragged_user_index is not None: + self._drag_user_point(event.y) + self.redraw() + return + + self._stroke.append((self._canvas_to_normalized_x(event.x), self._canvas_to_normalized_y(event.y),)) + self._merge_stroke_into_curve() + self.redraw() + + def finish_drawing(self, event): + if not self._editing or self._erasing: + return + + if self._dragged_user_index is not None: + self._drag_user_point(event.y) + self._dragged_user_index = None + self._dragged_user_point = None + self._editing = False + self.redraw() + return + + self._stroke.append((self._canvas_to_normalized_x(event.x), self._canvas_to_normalized_y(event.y),)) + self._merge_stroke_into_curve() + self._stroke = [] + self._editing = False + self.points = self._normalize_points(self.points) + self._user_points = self._normalize_points(self._user_points) + self.redraw() + + def _find_user_point(self, canvas_x, canvas_y): + best_index = None + best_distance = self.POINT_HIT_RADIUS_PX + + for index, (x, y) in enumerate(self._user_points): + point_x, point_y = self._normalized_to_canvas(x, y) + distance = math.hypot(point_x - canvas_x, point_y - canvas_y) + if distance <= best_distance: + best_index = index + best_distance = distance + + return best_index + + def _drag_user_point(self, canvas_y): + if self._dragged_user_index is None: + return + + old_x, old_y = self._dragged_user_point + new_y = self._canvas_to_normalized_y(canvas_y) + self._user_points[self._dragged_user_index] = (old_x, new_y) + + for index, (point_x, point_y) in enumerate(self.points): + if abs(point_x - old_x) < 0.002 and abs(point_y - old_y) < 0.01: + self.points[index] = (point_x, new_y) + break + + self._dragged_user_point = (old_x, new_y) + + def start_erasing(self, event): + self._editing = True + self._erasing = True + self._dragged_user_index = None + self._dragged_user_point = None + self._erase_at(event.x, event.y) + self.redraw() + + def erase(self, event): + if not self._editing or not self._erasing: + return + self._erase_at(event.x, event.y) + self.redraw() + + def finish_erasing(self, _event): + self._stroke = [] + self._editing = False + self._erasing = False + self._dragged_user_index = None + self._dragged_user_point = None + self.points = self._normalize_points(self.points) + self._user_points = self._normalize_points(self._user_points) + self.redraw() + + def _merge_stroke_into_curve(self): + if not self._stroke: + return + stroke = self._normalize_points(self._stroke) + if not stroke: + return + + stroke_min = stroke[0][0] + stroke_max = stroke[-1][0] + + if not self.points: + self.points = stroke + self._user_points = stroke.copy() + return + + left_y = self._interpolate(self.points, stroke_min) + right_y = self._interpolate(self.points, stroke_max) + merged = [ + (x, y) + for x, y in self.points + if x < stroke_min or x > stroke_max + ] + + if not any(abs(x - stroke_min) < 1e-6 for x, _ in merged): + merged.append((stroke_min, left_y)) + if not any(abs(x - stroke_max) < 1e-6 for x, _ in merged): + merged.append((stroke_max, right_y)) + + merged.extend(stroke) + self.points = self._normalize_points(merged) + + self._user_points = [ + (x, y) + for x, y in self._user_points + if x < stroke_min or x > stroke_max + ] + self._user_points.extend(stroke) + self._user_points = self._normalize_points(self._user_points) + + def _erase_at(self, canvas_x, canvas_y): + x = self._canvas_to_normalized_x(canvas_x) + y = self._canvas_to_normalized_y(canvas_y) + + self.points = [ + (px, py) + for px, py in self.points + if abs(px - x) > self.ERASE_RADIUS_X or abs(py - y) > 0.08 + ] + self._user_points = [ + (px, py) + for px, py in self._user_points + if abs(px - x) > self.ERASE_RADIUS_X or abs(py - y) > 0.08 + ] + + def _canvas_to_normalized_x(self, x): + x = max(0, min(self.CANVAS_WIDTH, x)) + return x / self.CANVAS_WIDTH + + def _canvas_to_normalized_y(self, y): + y = max(0, min(self.CANVAS_HEIGHT, y)) + return 1.0 - (y / self.CANVAS_HEIGHT) + + def _canvas_to_normalized(self, x, y): + return self._canvas_to_normalized_x(x), self._canvas_to_normalized_y(y) + + def _normalized_to_canvas(self, x, y): + return x * self.CANVAS_WIDTH, (1.0 - y) * self.CANVAS_HEIGHT + + def _draw_grid(self): + self.canvas.delete("grid") + + for index in range(11): + x = self.CANVAS_WIDTH * index / 10 + self.canvas.create_line(x, 0, x, self.CANVAS_HEIGHT, fill=self.GRID_COLOR, tags="grid",) + + for index in range(11): + y = self.CANVAS_HEIGHT * (1.0 - index / 10) + self.canvas.create_line(0, y, self.CANVAS_WIDTH, y, fill=self.GRID_COLOR, tags="grid",) + + span = self.upper_bound - self.lower_bound + if span <= 0 or not (self.lower_bound <= 0 <= self.upper_bound): + return + + zero_x = ((0 - self.lower_bound) / span) * self.CANVAS_WIDTH + nearest_tenth = round(zero_x / (self.CANVAS_WIDTH / 10)) + if abs(zero_x - nearest_tenth * (self.CANVAS_WIDTH / 10)) > 1: + self.canvas.create_line(zero_x, 0, zero_x, self.CANVAS_HEIGHT, fill=self.ZERO_GRID_COLOR, tags="grid",) + + def _normalize_points(self, points): + if not points: + return [] + + cleaned = [] + for x, y in points: + x = max(0.0, min(1.0, float(x))) + y = max(0.0, min(1.0, float(y))) + cleaned.append((x, y)) + + cleaned.sort(key=lambda point: point[0]) + + merged = [] + for x, y in cleaned: + if merged and abs(merged[-1][0] - x) < 0.002: + old_x, old_y = merged[-1] + merged[-1] = ((old_x + x) / 2, (old_y + y) / 2) + else: + merged.append((x, y)) + return merged + + def _is_user_point(self, point): + x, y = point + return any(abs(user_x - x) < 0.003 and abs(user_y - y) < 0.003 for user_x, user_y in self._user_points) + + def redraw(self): + self.canvas.delete("distribution") + + if self.points: + coords = [] + for x, y in self.points: + canvas_x, canvas_y = self._normalized_to_canvas(x, y) + coords.extend((canvas_x, canvas_y)) + + if len(coords) >= 4: + self.canvas.create_line(*coords, width=3, smooth=self._selected_interpolation_key() == "curves", splinesteps=12, fill=self.INTERPOLATED_COLOR, tags="distribution",) + else: + x, y = self._normalized_to_canvas(self.points[0][0], self.points[0][1],) + self.canvas.create_oval(x - 2, y - 2, x + 2, y + 2, fill=self.USER_POINT_COLOR, outline=self.USER_POINT_COLOR, tags="distribution",) + + for x, y in self.points: + if not self._is_user_point((x, y)): + continue + canvas_x, canvas_y = self._normalized_to_canvas(x, y) + radius = 4 if self._dragged_user_index is not None else 3 + self.canvas.create_oval(canvas_x - radius, canvas_y - radius, canvas_x + radius, canvas_y + radius, fill=self.USER_POINT_COLOR, outline=self.USER_POINT_COLOR, tags="distribution",) + + if len(self._stroke) >= 2: + stroke_coords = [] + for x, y in self._stroke: + stroke_coords.extend(self._normalized_to_canvas(x, y)) + + self.canvas.create_line(*stroke_coords, width=3, dash=(5, 3), fill=self.USER_POINT_COLOR, tags="distribution",) + + for x, y in self._stroke: + canvas_x, canvas_y = self._normalized_to_canvas(x, y) + radius = 3 + self.canvas.create_oval(canvas_x - radius, canvas_y - radius, canvas_x + radius, canvas_y + radius, fill=self.USER_POINT_COLOR, outline=self.USER_POINT_COLOR, tags="distribution",) + + def load_distribution(self, distribution): + if not distribution: + return + + try: + points = [] + for point in distribution: + if len(point) != 2: + continue + x = max(0.0, min(1.0, float(point[0]))) + y = max(0.0, min(1.0, float(point[1]))) + points.append((x, y)) + + self.points = self._normalize_points(points) + self._user_points = self.points.copy() + self.redraw() + except (TypeError, ValueError): + self.points = [] + self._user_points = [] + + def clear(self): + self.points = [] + self._user_points = [] + self._stroke = [] + self._dragged_user_index = None + self._dragged_user_point = None + self.redraw() + + def apply_selected_preset(self, _event=None): + key = self._selected_preset_key() + self.preset_description.config(text=self._preset_description(key)) + self.preset_note.config(text=self._preset_note(key)) + + if key == "freehand": + self.bell_frame.pack_forget() + return + + if key == "bell": + self.bell_frame.pack(fill="x", pady=(0, 6)) + self.apply_bell_settings() + return + + self.bell_frame.pack_forget() + self.points = self._make_linear_preset() + self._user_points = self._linear_control_points() + self._stroke = [] + self._dragged_user_index = None + self._dragged_user_point = None + self.redraw() + + def reset_bell_settings(self): + if self._selected_preset_key() != "bell": + return + + bell_settings = self.settings["presets"]["bell"]["controls"] + self.bell_center_var.set(50) + self.bell_width_var.set(20) + self.bell_tail_magnitude_var.set(0) + self.bell_tail_side_var.set(bell_settings["tail_side_positive_text"]) + self.bell_peaks_var.set(1) + self.apply_bell_settings() + + def apply_bell_settings(self): + if self._selected_preset_key() != "bell": + return + + self.points, self._user_points = self._make_bell_preset(center_percent=self.bell_center_var.get(), width_percent=self.bell_width_var.get(), tail_magnitude_percent=self.bell_tail_magnitude_var.get(), tail_side=self._tail_side_key_by_label[self.bell_tail_side_var.get()], peak_count=self.bell_peaks_var.get(),) + self._stroke = [] + self._dragged_user_index = None + self._dragged_user_point = None + self.redraw() + + def _linear_control_points(self): + edge = self.LINEAR_EDGE_OFFSET + return [ + (0.0, 0.5), + (edge, 0.5), + (1.0 - edge, 0.5), + (1.0, 0.5), + ] + + def _make_linear_preset(self): + return self._normalize_points(self._linear_control_points()) + + def _make_bell_preset(self, + center_percent, + width_percent, + tail_magnitude_percent, + tail_side, + peak_count,): + peak_count = max(0, int(peak_count)) + if peak_count == 0: + points = self._make_linear_preset() + return points, self._linear_control_points() + + center = max(0.0, min(1.0, int(center_percent) / 100.0)) + width = max(0.01, min(1.0, int(width_percent) / 100.0)) + tail_magnitude = max(0.0, min(1.0, int(tail_magnitude_percent) / 100.0),) + + centers = self._bell_centers(center, width, peak_count) + sigma = max(0.006, width * 0.30) + + raw = [] + for index in range(self.PRESET_SAMPLE_COUNT): + x = index / (self.PRESET_SAMPLE_COUNT - 1) + y = 0.0 + for peak_center in centers: + y += self._asymmetric_gaussian(x, peak_center, sigma, tail_magnitude, tail_side,) + raw.append((x, y)) + + max_y = max(y for _, y in raw) + if max_y > 0: + raw = [(x, y / max_y) for x, y in raw] + + control_points = [] + for peak_center in centers: + peak_y = self._interpolate(raw, peak_center) + control_points.append((peak_center, peak_y)) + + self._add_bell_tail_control_points(control_points, centers, sigma, center,) + + return self._normalize_points(raw), self._normalize_points(control_points) + + @staticmethod + def _bell_centers(center, width, peak_count): + if peak_count <= 1: + return [center] + + span = min(0.82, max(0.12, width * 4.0)) + left = center - (span / 2.0) + right = center + (span / 2.0) + + if left < 0: + right -= left + left = 0.0 + if right > 1: + left -= right - 1.0 + right = 1.0 + + left = max(0.0, left) + right = min(1.0, right) + + return [ + left + (right - left) * index / (peak_count - 1) + for index in range(peak_count) + ] + + @staticmethod + def _asymmetric_gaussian(x, + center, + sigma, + tail_magnitude, + tail_side,): + effective_sigma = sigma + if tail_magnitude > 0: + if tail_side == "negative" and x < center: + effective_sigma *= 1.0 - (0.85 * tail_magnitude) + elif tail_side == "positive" and x > center: + effective_sigma *= 1.0 - (0.85 * tail_magnitude) + + effective_sigma = max(0.001, effective_sigma) + distance = x - center + return math.exp(-0.5 * (distance / effective_sigma) ** 2) + + def _add_bell_tail_control_points(self, control_points, centers, sigma, center): + for peak_center in centers: + control_points.append((max(0.0, peak_center - sigma * 1.5), self._bell_control_y(peak_center - sigma * 1.5, centers, sigma, center,),)) + control_points.append((min(1.0, peak_center + sigma * 1.5), self._bell_control_y(peak_center + sigma * 1.5, centers, sigma, center,),)) + + def _bell_control_y(self, x, centers, sigma, center): + value = 0.0 + for peak_center in centers: + value += math.exp(-0.5 * ((x - peak_center) / max(0.001, sigma)) ** 2) + return min(1.0, value) + + def interpolate_missing(self): + if len(self.points) < 2: + return + + sample_count = self.PRESET_SAMPLE_COUNT + result = [] + for index in range(sample_count): + x = index / (sample_count - 1) + y = self._interpolate(self.points, x) + result.append((x, max(0.0, min(1.0, y)))) + + self.points = result + self._user_points = self._normalize_points(self._user_points) + self.redraw() + + def apply(self): + if not self.points: + self.owner.distribution = None + self.destroy() + return + + distribution = self._resample_distribution() + self.owner.distribution = distribution + self.destroy() + + def _resample_distribution(self): + points = self._normalize_points(self.points) + if len(points) < 2: + return None + + if self._selected_interpolation_key() == "curves": + return [ + [ + round(index / (self.PRESET_SAMPLE_COUNT - 1), 6), + round(max(0.0, min(1.0, self._interpolate(points, index / (self.PRESET_SAMPLE_COUNT - 1),),),), 6,), + ] + for index in range(self.PRESET_SAMPLE_COUNT) + ] + + return [ + [round(x, 6), round(max(0.0, min(1.0, y)), 6)] + for x, y in points + ] + + def _interpolate(self, points, x): + if not points: + return 0.0 + if len(points) == 1: + return points[0][1] + if x <= points[0][0]: + return points[0][1] + if x >= points[-1][0]: + return points[-1][1] + + for index in range(1, len(points)): + x1, y1 = points[index - 1] + x2, y2 = points[index] + if x <= x2: + if x2 == x1: + return y2 + if self._selected_interpolation_key() == "sharp": + amount = (x - x1) / (x2 - x1) + return y1 + ((y2 - y1) * amount) + return self._catmull_rom_y(points, index - 1, x) + + return points[-1][1] + + @staticmethod + def _catmull_rom_y(points, segment_start_index, x): + p0 = points[max(0, segment_start_index - 1)] + p1 = points[segment_start_index] + p2 = points[segment_start_index + 1] + p3 = points[min(len(points) - 1, segment_start_index + 2)] + + x1, y1 = p1 + x2, y2 = p2 + if x2 == x1: + return y2 + + t = (x - x1) / (x2 - x1) + t2 = t * t + t3 = t2 * t + + # Catmull-Rom is applied in local segment coordinates. Using the + # neighboring Y values controls the curve while X remains normalized. + y0 = p0[1] + y1 = p1[1] + y2 = p2[1] + y3 = p3[1] + value = 0.5 * ((2.0 * y1) + + (-y0 + y2) * t + + (2.0 * y0 - 5.0 * y1 + 4.0 * y2 - y3) * t2 + + (-y0 + 3.0 * y1 - 3.0 * y2 + y3) * t3) + return max(0.0, min(1.0, value)) + + @staticmethod + def _format_number(value): + if float(value).is_integer(): + return str(int(value)) + return str(value)