diff --git a/assets/stop_small.svg b/assets/stop_small.svg new file mode 100644 index 0000000..a35cf7e --- /dev/null +++ b/assets/stop_small.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/assets/stop_small.svg.import b/assets/stop_small.svg.import new file mode 100644 index 0000000..b675e3d --- /dev/null +++ b/assets/stop_small.svg.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/stop_small.svg-d7819f3a82ff689d7390798487abb123.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/stop_small.svg" +dest_files=[ "res://.import/stop_small.svg-d7819f3a82ff689d7390798487abb123.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=0.6 diff --git a/project.godot b/project.godot index dd59537..30a23c8 100644 --- a/project.godot +++ b/project.godot @@ -40,6 +40,11 @@ _global_script_classes=[ { "path": "res://scripts/time_entry.gd" }, { "base": "Reference", +"class": "TimeEntryTreeItem", +"language": "GDScript", +"path": "res://scripts/time_entry_tree_item.gd" +}, { +"base": "Reference", "class": "TimeSheet", "language": "GDScript", "path": "res://scripts/time_sheet.gd" @@ -56,6 +61,7 @@ _global_script_class_icons={ "FileWatcher": "", "TimeEntriesItemsTree": "", "TimeEntry": "", +"TimeEntryTreeItem": "", "TimeSheet": "", "TimeStamp": "" } diff --git a/scripts/config_manager.gd b/scripts/config_manager.gd index 758eeb5..6d2dc6d 100644 --- a/scripts/config_manager.gd +++ b/scripts/config_manager.gd @@ -1,16 +1,16 @@ class_name ConfigManager extends Resource -signal entry_started -signal entry_stopped signal file_changed +signal time_sheet_reloaded const CONFIG_PATH := "user://settings.cfg" -var _config := ConfigFile.new() -var _watcher: FileWatcher var timesheet: TimeSheet setget ,get_timesheet +var _config := ConfigFile.new() +var _watcher: FileWatcher + func get_timesheet() -> TimeSheet: if timesheet == null: timesheet = _load_timesheet(get_current_file()) @@ -20,29 +20,26 @@ func get_timesheet() -> TimeSheet: func _load_timesheet(path: String) -> TimeSheet: timesheet = TimeSheet.restore(path) if timesheet == null: - _watcher = null return null _watcher = FileWatcher.new() _watcher.file_name = path + # warning-ignore:return_value_discarded + _watcher.connect("file_changed", self, "reload_timesheet") _watcher.start() # warning-ignore:return_value_discarded - _watcher.connect("file_changed", self, "_on_file_changed") + #timesheet.connect("entry_started", self, "_on_entry_started") # warning-ignore:return_value_discarded - timesheet.connect("entry_started", self, "_on_entry_started") - # warning-ignore:return_value_discarded - timesheet.connect("entry_stopped", self, "_on_entry_stopped") + #timesheet.connect("entry_stopped", self, "_on_entry_stopped") return timesheet -func _on_entry_started() -> void: - emit_signal("entry_started") - -func _on_entry_stopped() -> void: - emit_signal("entry_stopped") - - -func _on_file_changed() -> void: - emit_signal("file_changed") +func reload_timesheet() -> void: + var new_timesheet = _load_timesheet(get_current_file()) + if new_timesheet == null: + printerr("failed to load new timesheet") + return + timesheet = new_timesheet + emit_signal("time_sheet_reloaded") var current_file: String = "" setget set_current_file, get_current_file diff --git a/scripts/time_entry.gd b/scripts/time_entry.gd index f511184..bf23f88 100644 --- a/scripts/time_entry.gd +++ b/scripts/time_entry.gd @@ -2,11 +2,14 @@ ## Has a beginning and an end class_name TimeEntry + +signal end_time_updated +signal closed + var name := "" -var closed := false +var is_closed := false var start_time := TimeStamp.new() var end_time := TimeStamp.new() -var previous_total := 0 func start_recording() -> TimeEntry: @@ -17,6 +20,13 @@ func start_recording() -> TimeEntry: func update() -> void: end_time = end_time.from_current_time() + emit_signal("end_time_updated") + + +func close() -> void: + update() + is_closed = true + emit_signal("closed") func get_elapsed_seconds() -> int: @@ -24,21 +34,11 @@ func get_elapsed_seconds() -> int: 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 time_to_period(time_in_secs) -func get_total_period() -> String: - var time_in_secs := get_total_elapsed_seconds() - return time_to_period(time_in_secs) - - static func time_to_period(time_in_secs: int) -> String: # warning-ignore:integer_division var seconds := time_in_secs%60 @@ -48,14 +48,16 @@ static func time_to_period(time_in_secs: int) -> String: var hours := (time_in_secs/60)/60 return "%02d:%02d:%02d" % [hours, minutes, seconds] + func to_csv_line() -> PoolStringArray: return PoolStringArray([ name, start_time, end_time, - str(get_elapsed_seconds()) if closed else tr(Consts.ONGOING) + str(get_elapsed_seconds()) if is_closed else tr(Consts.ONGOING) ]) + static func is_csv_line_valid(line: PoolStringArray) -> bool: return line.size() > 3 @@ -68,9 +70,9 @@ func from_csv_line(line: PoolStringArray) -> TimeEntry: start_time.from_string(start_time_string) var elapsed_seconds = int(line[3]) if line[3].is_valid_integer() else 0 - closed = elapsed_seconds > 0 + is_closed = elapsed_seconds > 0 - if closed == true: + if is_closed == true: var end_time_string = line[2] # warning-ignore:return_value_discarded end_time.from_string(end_time_string) @@ -79,5 +81,12 @@ func from_csv_line(line: PoolStringArray) -> TimeEntry: end_time.from_current_time() return self + +func to_dict() -> Dictionary: + return { + start_time = start_time, + closed = is_closed, + } + func _to_string() -> String: - return "%s\t%s\t%s"%[name, Consts.ONGOING if closed == false else "", start_time] + return "%s\t%s\t%s"%[name, tr(Consts.ONGOING) if is_closed == false else end_time.to_string(), start_time] diff --git a/scripts/time_entry_tree_item.gd b/scripts/time_entry_tree_item.gd new file mode 100644 index 0000000..d638772 --- /dev/null +++ b/scripts/time_entry_tree_item.gd @@ -0,0 +1,70 @@ +class_name TimeEntryTreeItem + +signal end_time_updated + +var time_entry: TimeEntry +var children := {} +var time_entries := [] + +func get_child(parts: Array, or_create := false): + var TimeEntryTreeItem = load("res://scripts/time_entry_tree_item.gd") + if parts.size() == 0: + return self + var part: String = parts.pop_front() + if not children.has(part): + if or_create == false: + return null + var time_entry_tree_item = TimeEntryTreeItem.new() + # warning-ignore:return_value_discarded + time_entry_tree_item.connect("end_time_updated", self, "_on_end_time_updated") + children[part] = time_entry_tree_item + return children[part].get_child(parts, or_create) + + +func append(new_time_entry: TimeEntry) -> void: + var TimeEntryTreeItem = load("res://scripts/time_entry_tree_item.gd") + var time_entry_tree_item = TimeEntryTreeItem.new() + time_entry_tree_item.time_entry = new_time_entry + # warning-ignore:return_value_discarded + new_time_entry.connect("end_time_updated", time_entry_tree_item, "_on_end_time_updated") + # warning-ignore:return_value_discarded + time_entry_tree_item.connect("end_time_updated", self, "_on_end_time_updated") + time_entries.append(time_entry_tree_item) + + +func _on_end_time_updated() -> void: + emit_signal("end_time_updated") + + +func get_elapsed_seconds() -> int: + var seconds := time_entry.get_elapsed_seconds() if time_entry != null else 0 + for child_name in children: + var child = children[child_name] + seconds += child.get_elapsed_seconds() + for entry in time_entries: + seconds += entry.get_elapsed_seconds() + return seconds + + +func get_period() -> String: + var time_in_secs := get_elapsed_seconds() + return TimeEntry.time_to_period(time_in_secs) + + +func to_dict() -> Dictionary: + var json := {} + var times := [] + if time_entries.size() > 0: + for entry in time_entries: + times.append(entry.to_dict()) + json["__time"] = times + if children.size() > 0: + for name in children: + json[name] = children[name].to_dict() + return json + + +func _to_string() -> String: + var json := to_dict() + var json_string := JSON.print(json, "\t") + return json_string diff --git a/scripts/time_sheet.gd b/scripts/time_sheet.gd index 9a0745a..6d70af3 100644 --- a/scripts/time_sheet.gd +++ b/scripts/time_sheet.gd @@ -1,13 +1,9 @@ class_name TimeSheet -signal entry_started -signal entry_stopped var source_path := "" var entries := [] -var entries_names := {} -var current_entry: TimeEntry - +var tree := TimeEntryTreeItem.new() ## Loads the data file func load_file() -> bool: @@ -19,6 +15,7 @@ func load_file() -> bool: printerr("Failed to open file %s"%[ProjectSettings.globalize_path(source_path)]) return false return true + while not file.eof_reached(): var line := file.get_csv_line() if line.size() == 0 or "".join(line).length() == 0: @@ -28,22 +25,32 @@ func load_file() -> bool: continue var entry := TimeEntry.new().from_csv_line(line) entries.append(entry) - if entry.closed == false: - current_entry = entry - if not entries_names.has(entry.name): - entries_names[entry.name] = 0 - entries_names[entry.name] += entry.get_elapsed_seconds() + # warning-ignore:return_value_discarded + entry.connect("closed", self, "save") file.close() + + entries.sort_custom(self, "_sort_entries") + + + + for entry_index in entries.size(): + var entry: TimeEntry = entries[entry_index] + var parts: PoolStringArray = entry.name.split("/") + var repo: TimeEntryTreeItem = tree.get_child(parts, true) + repo.append(entry) + return true +func _sort_entries(a: TimeEntry, b: TimeEntry) -> bool: + return a.name < b.name + + ## Adds a new time entry to the tree and to the data file func start_entry(entry_name: String) -> void: - current_entry = TimeEntry.new().start_recording() + var current_entry := TimeEntry.new().start_recording() current_entry.name = entry_name current_entry.closed = false - if entry_name in entries_names: - current_entry.previous_total = entries_names[entry_name] var file := File.new() var success := file.open(source_path, File.READ_WRITE) if success != OK: @@ -55,22 +62,10 @@ func start_entry(entry_name: String) -> void: func update() -> void: - current_entry.update() - entries_names[current_entry.name] = current_entry.get_total_elapsed_seconds() - - -func close_entry() -> void: - current_entry.closed = true - save() - emit_signal("entry_stopped") - - -func get_period() -> String: - return current_entry.get_period() - - -func get_total_elapsed_seconds() -> int: - return current_entry.get_total_elapsed_seconds() + for entry in entries: + var time_entry := entry as TimeEntry + if time_entry.is_closed == false: + time_entry.update() func save() -> void: @@ -90,5 +85,3 @@ static func restore(file_path: String) -> TimeSheet: if success: return timesheet return null - - diff --git a/test.gd b/test.gd new file mode 100644 index 0000000..e7e263b --- /dev/null +++ b/test.gd @@ -0,0 +1,6 @@ +tool +extends EditorScript + + +func _run(): + print ("c" < "b") diff --git a/ui/tasks_list.gd b/ui/tasks_list.gd index b1098b8..f739e9b 100644 --- a/ui/tasks_list.gd +++ b/ui/tasks_list.gd @@ -7,55 +7,91 @@ enum COL{ var config: ConfigManager = preload("res://config_manager.tres") -var current_item: TreeItem +var _timer := Timer.new() + func _ready() -> void: + hide_root = true + add_child(_timer) + # warning-ignore:return_value_discarded + connect("button_pressed", self, "_on_button_pressed") + # warning-ignore:return_value_discarded + _timer.connect("timeout", self, "_on_timer_timeout") # warning-ignore:return_value_discarded config.connect("file_changed", self, "populate_entries") + # warning-ignore:return_value_discarded + config.connect("time_sheet_reloaded", self, "populate_entries") populate_entries() func populate_entries() -> void: clear() - var _root := create_item() - var entries_names := config.timesheet.entries_names - for entry_name in entries_names: + var tree_items_root := create_item() + var item_entries_tree := config.timesheet.tree + _populate_from_entry(tree_items_root, item_entries_tree) + _timer.start() + #for item in entries: # warning-ignore:return_value_discarded - append_name_to_tree(entry_name, entries_names[entry_name]) + # set_entry(entry) +# warning-ignore:unused_argument func set_time_elapsed(total_elapsed: int) -> void: - current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed)) - current_item.set_metadata(COL.TIME, total_elapsed) + #current_item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed)) + #current_item.set_metadata(COL.TIME, total_elapsed) + pass -func set_current_item(task_name: String) -> void: - current_item = append_name_to_tree(task_name, 0) +func set_current_item(_task_name: String) -> void: + #current_item = append_name_to_tree(task_name, 0) + pass - -func get_current_text() -> String: - var item := get_selected() - if not item: - return "" - - var resp = item.get_metadata(COL.TEXT) - if resp is String: - return resp - return "" +func _on_timer_timeout() -> void: + config.timesheet.update() -## Adds a new item to the tree, or returns the old item if it exists -func append_name_to_tree(task_name: String, total_elapsed := 0) -> TreeItem: - var item := 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) - item.set_text(COL.TIME, TimeEntry.time_to_period(total_elapsed)) - item.set_metadata(COL.TIME, total_elapsed) - scroll_to_item(item) - return item +func _populate_from_entry(tree_item_root: TreeItem, time_entry_item_root: TimeEntryTreeItem): + var children := time_entry_item_root.children + for time_entry_name in children: + var time_entry_item: TimeEntryTreeItem = children[time_entry_name] + var item := find_or_create_item(tree_item_root, time_entry_name) + item.set_metadata(COL.TEXT, time_entry_item) + item.set_text(COL.TIME, time_entry_item.get_period()) + # warning-ignore:return_value_discarded + time_entry_item.connect("end_time_updated", self, "_on_time_entry_changed_update_item", [time_entry_item, item]) + _populate_from_entry(item, time_entry_item) + var entries = time_entry_item_root.time_entries + for entry_item in entries: + var time_entry_item := entry_item as TimeEntryTreeItem + var item := create_item(tree_item_root) + var time_entry := time_entry_item.time_entry + item.set_text(COL.TEXT, time_entry.start_time.to_string()) + item.set_metadata(COL.TEXT, time_entry_item) + item.set_text(COL.TIME, time_entry_item.get_period()) + if time_entry.is_closed == false: + var texture := preload("res://assets/stop_small.svg") + item.add_button(COL.TIME, texture) + # warning-ignore:return_value_discarded + time_entry_item.connect("end_time_updated", self, "_on_time_entry_changed_update_item", [time_entry_item, item]) + + +func _on_time_entry_changed_update_item(time_entry_item: TimeEntryTreeItem, item: TreeItem) -> void: + item.set_text(COL.TIME, time_entry_item.get_period()) + + +func _on_button_pressed(item: TreeItem, _column: int, _id: int) -> void: + var time_entry_tree_item: TimeEntryTreeItem = item.get_metadata(COL.TEXT) + if time_entry_tree_item == null: + return + var time_entry := time_entry_tree_item.time_entry + if time_entry == null: + return + if time_entry.is_closed: + return + else: + time_entry.close() + item.erase_button(COL.TIME, 0) ## Unecessary in Godot 4, can bre replaced with get_children() @@ -73,13 +109,17 @@ static func _get_tree_item_children(item: TreeItem): ## Finds an item in the tree by text -func find_item(root: TreeItem, item_name: String, or_create: bool) -> TreeItem: +func find_item(root: TreeItem, item_name: String) -> TreeItem: for child in _get_tree_item_children(root): if child.get_text(COL.TEXT) == item_name: return child - if or_create: - var child := create_item(root) - child.set_text(COL.TEXT, item_name) - child.set_text(COL.TIME, Consts.NO_TIME) - return child return null + + +func find_or_create_item(root: TreeItem, item_name: String) -> TreeItem: + var child := find_item(root, item_name) + if child != null: + return child + child = create_item(root) + child.set_text(COL.TEXT, item_name) + return child diff --git a/ui/time_counter.gd b/ui/time_counter.gd index 5545cf6..05ad102 100644 --- a/ui/time_counter.gd +++ b/ui/time_counter.gd @@ -14,7 +14,7 @@ onready var audio_stream_player := $"%AudioStreamPlayer" as AudioStreamPlayer func _ready() -> void: # warning-ignore:return_value_discarded timer.connect("timeout", self, "_on_timer_timeout") - start_button.tooltip_text = tr(Consts.START) + start_button.hint_tooltip = tr(Consts.START) start_button.toggle_mode = true # warning-ignore:return_value_discarded start_button.connect("toggled", self, "_on_start_button_toggled") @@ -57,7 +57,7 @@ func _on_task_name_line_edit_text_changed(new_text: String) -> void: func set_button_as_stopped() -> void: start_button.set_pressed_no_signal(false) - start_button.tooltip_text = tr(Consts.START) + start_button.hint_tooltip = tr(Consts.START) time_label.text = Consts.NO_TIME start_button.theme_type_variation = Consts.THEME_OVERRIDE_START timer.stop() @@ -67,7 +67,7 @@ func set_button_as_started() -> void: ## TODO: make this independent # time_entries_items_tree.set_current_item(config.timesheet.current_entry.name) start_button.set_pressed_no_signal(true) - start_button.tooltip_text = tr(Consts.STOP) + start_button.hint_tooltip = tr(Consts.STOP) start_button.theme_type_variation = Consts.THEME_OVERRIDE_STOP timer.start()