extends Control const CONFIG_PATH := "user://settings.cfg" enum COL{ TEXT, TIME } const STRINGS := { START = "start", STOP = "stop", NO_TIME = "00:00:00", } @onready var time_label: Label = %TimeLabel @onready var start_button: Button = %StartButton @onready var task_name_line_edit: LineEdit = %TaskNameLineEdit @onready var previous_tasks_tree: Tree = %PreviousTasksTree @onready var timer: Timer = %Timer @onready var tasks_button: Button = %TasksButton @onready var settings_button: Button = %SettingsButton @onready var previous_tasks_window: Window = %PreviousTasksWindow @onready var settings_window: Window = %SettingsWindow @onready var file_path_file_dialog: FileDialog = %FilePathFileDialog @onready var file_path_line_edit: LineEdit = %FilePathLineEdit @onready var file_path_button: Button = %FilePathButton @onready var theme_path_file_dialog: FileDialog = %ThemePathFileDialog @onready var theme_path_button: Button = %ThemePathButton @onready var sound_check_box: CheckBox = %SoundCheckBox @onready var attributions_rich_text_label: RichTextLabel = %AttributionsRichTextLabel @onready var audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer @onready var open_data_dir_button: Button = %OpenDataDirButton var previous_entries: Array[TimeEntry] = [] var current_entry := TimeEntry.new() var current_item: TreeItem var config := ConfigFile.new() var current_file := "" func _init() -> void: config.load(CONFIG_PATH) var default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).path_join("mouse_timer.csv") current_file = config.get_value("MAIN", "file", default_path) if config.has_section_key("MAIN", "theme"): var new_theme: Theme = ResourceLoader.load(config.get_value("MAIN", "theme", "res://default_theme.theme"), "Theme") if new_theme != null: theme = new_theme func _ready() -> void: get_tree().set_auto_accept_quit(false) var _root := previous_tasks_tree.create_item() file_path_button.pressed.connect( file_path_file_dialog.popup_centered ) file_path_file_dialog.file_selected.connect(set_current_file) file_path_line_edit.text_submitted.connect(set_current_file) theme_path_button.pressed.connect( theme_path_file_dialog.popup_centered ) theme_path_file_dialog.file_selected.connect( func theme_selected(theme_path: String) -> void: var new_theme: Theme = ResourceLoader.load(theme_path, "Theme") if new_theme != null: theme = new_theme config.set_value("MAIN", "theme", theme_path) config.save(CONFIG_PATH) ) theme_path_file_dialog.hide() file_path_file_dialog.hide() previous_tasks_window.hide() settings_window.hide() timer.timeout.connect( func on_timer_timeout() -> void: current_entry.update() time_label.text = current_entry.get_period() var total_elapsed: int = current_entry.get_total_elapsed_seconds() current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed)) current_item.set_metadata(COL.TIME, total_elapsed) ) start_button.tooltip_text = tr(STRINGS.START) start_button.toggle_mode = true start_button.toggled.connect( func start(is_on: bool) -> void: if sound_check_box.button_pressed: audio_stream_player.play() if is_on: current_entry = TimeEntry.new().start_recording() current_entry.name = task_name_line_edit.text current_item = append_name_to_tree(task_name_line_edit.text) current_entry.previous_total = current_item.get_metadata(COL.TIME) start_button.tooltip_text = tr(STRINGS.STOP) timer.start() else: add_to_entries() start_button.tooltip_text = tr(STRINGS.START) time_label.text = STRINGS.NO_TIME timer.stop() ) task_name_line_edit.text = config.get_value("MAIN", "last_task_name", "") task_name_line_edit.text_changed.connect( func(new_text: String) -> void: config.set_value("MAIN", "last_task_name", new_text) config.save(CONFIG_PATH) ) previous_tasks_tree.item_selected.connect( func item_selected() -> void: var item := previous_tasks_tree.get_selected() task_name_line_edit.text = item.get_metadata(COL.TEXT) ) tasks_button.toggle_mode = true tasks_button.toggled.connect( func tasks_toggled(is_on: bool) -> void: previous_tasks_window.visible = is_on ) previous_tasks_window.close_requested.connect( func close() -> void: tasks_button.set_pressed_no_signal(false) previous_tasks_window.hide() ) settings_button.toggle_mode = true settings_button.toggled.connect( func settings_toggled(is_on: bool) -> void: settings_window.visible = is_on ) settings_window.close_requested.connect( func close() -> void: settings_button.set_pressed_no_signal(false) settings_window.hide() ) sound_check_box.button_pressed = config.get_value("MAIN", "sound_fx", true) sound_check_box.toggled.connect( func sound_toggle(is_on: bool) -> void: config.set_value("MAIN", "sound_fx", is_on) ) open_data_dir_button.pressed.connect( OS.shell_open.bind(OS.get_user_data_dir()) ) attributions_rich_text_label.meta_clicked.connect(OS.shell_open) set_current_file(current_file) ## Adds a new time entry to the tree and to the data file func add_to_entries() -> void: previous_entries.append(current_entry) var file := FileAccess.open(current_file, FileAccess.WRITE) if file == null: printerr("Could not open file") file.store_csv_line(current_entry.to_csv_line()) ## Adds a new item to the tree, or returns the old item if it exists func append_name_to_tree(task_name: String, total_time := 0) -> TreeItem: var item := previous_tasks_tree.get_root() for item_name in task_name.split("/"): item.collapsed = false item = find_item(item, item_name, true) item.set_metadata(COL.TEXT, task_name) if not item.get_metadata(COL.TIME): item.set_metadata(COL.TIME, total_time) previous_tasks_tree.scroll_to_item(item) return item ## Finds an item in the tree by text func find_item(root: TreeItem, item_name: String, or_create: bool) -> TreeItem: for child in root.get_children(): if child.get_text(COL.TEXT) == item_name: return child if or_create: var child := root.create_child() child.set_text(COL.TEXT, item_name) child.set_text(COL.TIME, STRINGS.NO_TIME) return child return null ## Changes the data file path, and loads the new path func set_current_file(new_current_file: String) -> void: if not load_file(new_current_file): return previous_entries.clear() current_file = new_current_file config.set_value("MAIN", "file", current_file) config.save(CONFIG_PATH) file_path_file_dialog.current_path = current_file file_path_file_dialog.current_dir = current_file.get_base_dir() file_path_line_edit.text = current_file ## Loads the data file func load_file(source_path: String) -> bool: var file := FileAccess.open(source_path, FileAccess.READ) if file == null: file = FileAccess.open(source_path, FileAccess.WRITE) if file == null: printerr("Failed to open file %s"%[ProjectSettings.globalize_path(source_path)]) return false return true var collector := {} while not file.eof_reached(): var line := file.get_csv_line() if line.size() == 0 or "".join(line).length() == 0: continue if not TimeEntry.is_csv_line_valid(line): push_warning("CSV Line `%s` is not conform"%[",".join(line)]) continue var entry := TimeEntry.new().from_csv_line(line) previous_entries.append(entry) if not collector.has(entry.name): collector[entry.name] = 0 collector[entry.name] += entry.get_elapsed_seconds() for entry_name in collector: append_name_to_tree(entry_name, collector[entry_name]) file.close() return true func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: if start_button.button_pressed: add_to_entries() get_tree().quit() ## Unused; if a manual quit button is added, this would be used func quit() -> void: get_tree().notification(NOTIFICATION_WM_CLOSE_REQUEST) class TimeEntry: var name := "" var start_time := TimeStamp.new() var end_time := TimeStamp.new() var previous_total := 0 func start_recording() -> TimeEntry: start_time = start_time.from_current_time() end_time = end_time.from_current_time() return self func update() -> void: end_time = end_time.from_current_time() func get_elapsed_seconds() -> int: var elapsed := end_time.get_difference(start_time) return elapsed func get_total_elapsed_seconds() -> int: var elapsed := get_elapsed_seconds() + previous_total return elapsed func get_period() -> String: var time_in_secs := get_elapsed_seconds() return TimeEntry.time_to_period(time_in_secs) func get_total_period() -> String: var time_in_secs := get_total_elapsed_seconds() return TimeEntry.time_to_period(time_in_secs) static func time_to_period(time_in_secs: int) -> String: var seconds := time_in_secs%60 @warning_ignore("integer_division") var minutes := (time_in_secs/60)%60 @warning_ignore("integer_division") var hours := (time_in_secs/60)/60 return "%02d:%02d:%02d" % [hours, minutes, seconds] func to_csv_line() -> PackedStringArray: return PackedStringArray([ name, start_time, end_time, str(get_elapsed_seconds()), ]) static func is_csv_line_valid(line: PackedStringArray) -> bool: return line.size() > 2 func from_csv_line(line: PackedStringArray) -> TimeEntry: name = line[0] start_time.from_string(line[1]) end_time.from_string(line[2]) return self class TimeStamp: var year := 0 var month := 0 var day := 0 var weekday := 0 var hour := 0 var minute := 0 var second := 0 var unix := 0 func from_current_time() -> TimeStamp: return from_dict(Time.get_datetime_dict_from_system()) func from_dict(time: Dictionary) -> TimeStamp: year = time.year month = time.month day = time.day weekday = time.weekday hour = time.hour minute = time.minute second = time.second unix = Time.get_unix_time_from_datetime_dict(time) return self func to_dict() -> Dictionary: return { year = year, month = month, day = day, weekday = weekday, hour = hour, minute = minute, second = second, } func get_difference(other_timestamp: TimeStamp) -> int: return unix - other_timestamp.unix func from_string(time_string: String) -> TimeStamp: var time := Time.get_datetime_dict_from_datetime_string(time_string, true) return from_dict(time) func _to_string() -> String: return Time.get_datetime_string_from_datetime_dict(to_dict(), false)