Compare commits
5 Commits
plugin-loa
...
e76608a67e
Author | SHA1 | Date | |
---|---|---|---|
e76608a67e | |||
d69af271fe | |||
ef15d4696a | |||
5c4e0eec6a | |||
5209f26ec5 |
365
Main.gd
365
Main.gd
@ -1,365 +0,0 @@
|
||||
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)
|
236
Main.tscn
236
Main.tscn
@ -1,236 +0,0 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://bmlciwscreowf"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bd8ancgbfsvmd" path="res://assets/default_theme.theme" id="1_1mila"]
|
||||
[ext_resource type="Script" path="res://Main.gd" id="1_vrowr"]
|
||||
[ext_resource type="AudioStream" uid="uid://cdsbhoidgyx70" path="res://assets/pop.ogg" id="3_o37py"]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_guuii"]
|
||||
device = -1
|
||||
pressed = true
|
||||
keycode = 32
|
||||
unicode = 32
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_irhvi"]
|
||||
events = [SubResource("InputEventKey_guuii")]
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("1_1mila")
|
||||
theme_type_variation = &"background"
|
||||
|
||||
[node name="Main" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
script = ExtResource("1_vrowr")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Main"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="Main/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SettingsButton" type="Button" parent="Main/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Settings"
|
||||
theme_type_variation = &"settings_button"
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="TaskNameLineEdit" type="LineEdit" parent="Main/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Task Name. Use \"/\" to create subtasks"
|
||||
caret_blink = true
|
||||
caret_blink_interval = 0.5
|
||||
|
||||
[node name="TasksButton" type="Button" parent="Main/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Tasks"
|
||||
theme_type_variation = &"tasks_button"
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Main/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="TimeLabel" type="Label" parent="Main/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"time_label"
|
||||
text = "00:00:00"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="StartButton" type="Button" parent="Main/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"play_button"
|
||||
shortcut = SubResource("Shortcut_irhvi")
|
||||
|
||||
[node name="Timer" type="Timer" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="FilePathFileDialog" type="FileDialog" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
title = "Pick Tracker File"
|
||||
size = Vector2i(800, 600)
|
||||
ok_button_text = "Save"
|
||||
access = 2
|
||||
filters = PackedStringArray("*.csv ; Comma Separated Files")
|
||||
|
||||
[node name="ThemePathFileDialog" type="FileDialog" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
title = "Open a File"
|
||||
size = Vector2i(800, 600)
|
||||
ok_button_text = "Open"
|
||||
file_mode = 0
|
||||
access = 2
|
||||
filters = PackedStringArray("*.theme ; Theme Files")
|
||||
|
||||
[node name="PreviousTasksWindow" type="Window" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
title = "Tasks"
|
||||
size = Vector2i(300, 300)
|
||||
visible = false
|
||||
always_on_top = true
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Main/PreviousTasksWindow"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"background"
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Main/PreviousTasksWindow/PanelContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="PreviousTasksTree" type="Tree" parent="Main/PreviousTasksWindow/PanelContainer/MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
columns = 2
|
||||
|
||||
[node name="SettingsWindow" type="Window" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
title = "Settings"
|
||||
size = Vector2i(300, 300)
|
||||
visible = false
|
||||
always_on_top = true
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Main/SettingsWindow"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"background"
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Main/SettingsWindow/PanelContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "File Path"
|
||||
|
||||
[node name="FilePathLineEdit" type="LineEdit" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
caret_blink = true
|
||||
caret_blink_interval = 0.5
|
||||
|
||||
[node name="FilePathButton" type="Button" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "..."
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Alternative theme
|
||||
"
|
||||
|
||||
[node name="ThemePathButton" type="Button" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "load"
|
||||
|
||||
[node name="HBoxContainer3" type="HBoxContainer" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SoundCheckBox" type="CheckBox" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer3"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
text = "Sounds"
|
||||
|
||||
[node name="OpenDataDirButton" type="Button" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "open data dir"
|
||||
|
||||
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="Main/SettingsWindow/PanelContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"small_text"
|
||||
bbcode_enabled = true
|
||||
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
|
||||
|
||||
Sound: [url]https://opengameart.org/content/bubbles-pop[/url]
|
||||
|
||||
This game uses Godot Engine, available under the following license:
|
||||
|
||||
Copyright (c) 2014-present Godot Engine contributors. Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("3_o37py")
|
16
README.md
16
README.md
@ -4,6 +4,18 @@ Track your time, save it to a CSV file.
|
||||
|
||||

|
||||
|
||||
That's it I guess
|
||||
|
||||
Uses Godot 4
|
||||
|
||||
## Features
|
||||
|
||||
- Saves to custom CSV file. Sync this file with syncthing/nextcloud/dropbox/whatever you like
|
||||
- Versions for Linux, Windows, Android, and presumably IOS and Mac (untested)
|
||||
- Custom theming possible
|
||||
- Does not depend on a timer, so you can close the app immediately after starting a task
|
||||
- Create nested tasks with `task/subtask` notation
|
||||
|
||||
## Coming Up
|
||||
|
||||
- Command line version supplied for your scripting needs or for terminal lovers
|
||||
- Plugin interface to change/add functionality
|
||||
- price per task
|
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -3,15 +3,15 @@
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cpmyyivxx0dlt"
|
||||
path="res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"
|
||||
path="res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://logo.png"
|
||||
dest_files=["res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"]
|
||||
source_file="res://assets/logo.png"
|
||||
dest_files=["res://.godot/imported/logo.png-e2220799298e3631eb0e245316e0501a.ctex"]
|
||||
|
||||
[params]
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -3,15 +3,15 @@
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://fk5s6m8qlsei"
|
||||
path="res://.godot/imported/logo.svg-8d8cf086b974db23ad31f8a2f3ea7d0f.ctex"
|
||||
path="res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://logo.svg"
|
||||
dest_files=["res://.godot/imported/logo.svg-8d8cf086b974db23ad31f8a2f3ea7d0f.ctex"]
|
||||
source_file="res://assets/logo.svg"
|
||||
dest_files=["res://.godot/imported/logo.svg-01597fe4b7eb446be26a49e8a22b6f42.ctex"]
|
||||
|
||||
[params]
|
||||
|
57
assets/stop.svg
Normal file
57
assets/stop.svg
Normal file
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 16.933333 16.933333"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="stop.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#323232"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
inkscape:zoom="8.8391522"
|
||||
inkscape:cx="39.766257"
|
||||
inkscape:cy="42.424883"
|
||||
inkscape:window-width="1896"
|
||||
inkscape:window-height="977"
|
||||
inkscape:window-x="12"
|
||||
inkscape:window-y="91"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid7036"
|
||||
originx="0"
|
||||
originy="0"
|
||||
empspacing="8" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:0.994836;stroke-width:4;stroke-linecap:round;stroke-opacity:0;paint-order:stroke markers fill;stop-color:#000000"
|
||||
id="rect341"
|
||||
width="11.641667"
|
||||
height="11.641667"
|
||||
x="2.6458325"
|
||||
y="2.6458325" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
37
assets/stop.svg.import
Normal file
37
assets/stop.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c56y8w3k75rxc"
|
||||
path="res://.godot/imported/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/stop.svg"
|
||||
dest_files=["res://.godot/imported/stop.svg-fc65124eb2fb3129fbdd4f17f48ea6f3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
6
config_manager.tres
Normal file
6
config_manager.tres
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_resource type="Resource" script_class="ConfigManager" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/config_manager.gd" id="1_xfu8y"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_xfu8y")
|
@ -25,7 +25,6 @@ texture_format/bptc=false
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
texture_format/etc2=false
|
||||
texture_format/no_bptc_fallbacks=true
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
@ -50,7 +49,7 @@ custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="exports/Rat Times.app"
|
||||
export_path="exports/Rat Times.zip"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
@ -67,7 +66,7 @@ application/icon="res://logo.icns"
|
||||
application/icon_interpolation=4
|
||||
application/bundle_identifier="org.mutnt.io.rat-times"
|
||||
application/signature=""
|
||||
application/app_category="Productivity"
|
||||
application/app_category="Developer-tools"
|
||||
application/short_version="1.0"
|
||||
application/version="1.0"
|
||||
application/copyright=""
|
||||
@ -361,7 +360,7 @@ custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path=""
|
||||
export_path="exports/rat-times.ipa"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
@ -454,7 +453,6 @@ texture_format/bptc=false
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
texture_format/etc2=false
|
||||
texture_format/no_bptc_fallbacks=true
|
||||
binary_format/architecture="x86_64"
|
||||
codesign/enable=false
|
||||
codesign/identity_type=0
|
||||
|
34
main.gd
Normal file
34
main.gd
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env -S godot --headless -s
|
||||
extends SceneTree
|
||||
|
||||
var config: ConfigManager = preload("res://config_manager.tres")
|
||||
|
||||
func _init():
|
||||
var cmd := CMD.new()
|
||||
for command in ["list", "stop", "start", "current"]:
|
||||
if cmd.has_argument(command):
|
||||
call(command)
|
||||
return
|
||||
print("no command provided -- exiting")
|
||||
quit()
|
||||
|
||||
|
||||
func list() -> void:
|
||||
var entries := config.timesheet.entries
|
||||
for item in entries:
|
||||
print(item)
|
||||
quit()
|
||||
|
||||
|
||||
func stop() -> void:
|
||||
if config.timesheet.current_entry:
|
||||
config.timesheet.close_entry()
|
||||
|
||||
|
||||
func start() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func current() -> void:
|
||||
if config.timesheet.current_entry:
|
||||
print("{name}\t{start_time}\t{end_time}"%config.timesheet.current_entry)
|
@ -12,13 +12,14 @@ config_version=5
|
||||
|
||||
config/name="Rat Times"
|
||||
config/description="Track your time(s)s"
|
||||
run/main_scene="res://Main.tscn"
|
||||
run/main_scene="res://ui/Main.tscn"
|
||||
config/use_custom_user_dir=true
|
||||
config/custom_user_dir_name="rat_times"
|
||||
config/features=PackedStringArray("4.0")
|
||||
run/low_processor_mode=true
|
||||
boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 0)
|
||||
boot_splash/show_image=false
|
||||
config/icon="res://logo.svg"
|
||||
config/icon="res://assets/logo.svg"
|
||||
config/macos_native_icon="res://logo.icns"
|
||||
config/windows_native_icon="res://logo.ico"
|
||||
|
||||
|
43
scripts/cmd.gd
Normal file
43
scripts/cmd.gd
Normal file
@ -0,0 +1,43 @@
|
||||
class_name CMD
|
||||
|
||||
## Returns a dictionary of all arguments passed after `--` on the command line
|
||||
## arguments take one of 2 forms:
|
||||
## - `--arg` which is a boolean (using `--no-arg` for `false` is possible)
|
||||
## - `--arg=value`. If the value is quoted with `"` or `'`, this function will
|
||||
## unsurround the string
|
||||
## This function does no evaluation and does not attempt to guess the type of
|
||||
## arguments. You will receive either bools, or strings.
|
||||
var command_line_arguments: Dictionary = (func () -> Dictionary:
|
||||
var unsurround := func unsurround(value: String, quotes := PackedStringArray(['"', "'"])) -> String:
|
||||
for quote_str in quotes:
|
||||
if value.begins_with(quote_str) \
|
||||
and value.ends_with(quote_str) \
|
||||
and value[value.length() - 2] != "\\":
|
||||
return value.trim_prefix(quote_str).trim_suffix(quote_str).strip_edges()
|
||||
return value
|
||||
var arguments := {}
|
||||
for argument in OS.get_cmdline_user_args():
|
||||
argument = argument.lstrip("--").to_lower()
|
||||
if argument.find("=") > -1:
|
||||
var arg_tuple := argument.split("=")
|
||||
var key := arg_tuple[0]
|
||||
var value:String = unsurround.call(arg_tuple[1])
|
||||
arguments[key] = value
|
||||
else:
|
||||
var key := argument
|
||||
var value := true
|
||||
if argument.begins_with("no-"):
|
||||
value = false
|
||||
key = argument.lstrip("no-")
|
||||
arguments[key] = value
|
||||
return arguments).call()
|
||||
|
||||
|
||||
func get_argument(name: String, default: Variant = null) -> Variant:
|
||||
if command_line_arguments.has(name):
|
||||
return command_line_arguments[name]
|
||||
return default
|
||||
|
||||
|
||||
func has_argument(name: String) -> bool:
|
||||
return command_line_arguments.has(name)
|
85
scripts/config_manager.gd
Normal file
85
scripts/config_manager.gd
Normal file
@ -0,0 +1,85 @@
|
||||
class_name ConfigManager extends Resource
|
||||
|
||||
|
||||
const CONFIG_PATH := "user://settings.cfg"
|
||||
var _config := ConfigFile.new()
|
||||
|
||||
|
||||
var timesheet: TimeSheet:
|
||||
get:
|
||||
if timesheet == null:
|
||||
timesheet = TimeSheet.restore(current_file)
|
||||
return timesheet
|
||||
|
||||
signal file_changed
|
||||
var current_file: String = "":
|
||||
set = set_current_file,
|
||||
get = get_current_file
|
||||
|
||||
|
||||
func set_current_file(value: String) -> void:
|
||||
timesheet = TimeSheet.restore(value)
|
||||
if timesheet == null:
|
||||
return
|
||||
current_file = value
|
||||
_config.set_value("MAIN", "file", value)
|
||||
file_changed.emit()
|
||||
save()
|
||||
|
||||
|
||||
func get_current_file() -> String:
|
||||
var _default_path := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS, true).path_join("mouse_timer.csv")
|
||||
return _config.get_value("MAIN", "file", _default_path)
|
||||
|
||||
|
||||
var theme: Theme:
|
||||
get:
|
||||
if theme == null:
|
||||
theme = ResourceLoader.load(theme_path, "Theme")
|
||||
return theme
|
||||
|
||||
|
||||
signal theme_changed
|
||||
var theme_path: String = "":
|
||||
set = set_theme_path,
|
||||
get = get_theme_path
|
||||
|
||||
|
||||
func set_theme_path(value: String) -> void:
|
||||
var new_theme: Theme = ResourceLoader.load(value, "Theme")
|
||||
if new_theme != null:
|
||||
theme = new_theme
|
||||
theme_path = value
|
||||
_config.set_value("MAIN", "theme", value)
|
||||
theme_changed.emit()
|
||||
save()
|
||||
|
||||
|
||||
func get_theme_path() -> String:
|
||||
return _config.get_value("MAIN", "theme", preload("res://assets/default_theme.theme").resource_path)
|
||||
|
||||
|
||||
var last_task_name: String = "":
|
||||
set(value):
|
||||
last_task_name = value
|
||||
_config.set_value("MAIN", "last_task_name", value)
|
||||
save()
|
||||
get:
|
||||
return _config.get_value("MAIN", "last_task_name", "")
|
||||
|
||||
|
||||
var sound_fx_on: bool = true:
|
||||
set(value):
|
||||
sound_fx_on = value
|
||||
_config.set_value("MAIN", "sound_fx_on", value)
|
||||
save()
|
||||
get:
|
||||
return _config.get_value("MAIN", "sound_fx", true)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_config.load(CONFIG_PATH)
|
||||
|
||||
|
||||
func save() -> void:
|
||||
_config.save(CONFIG_PATH)
|
8
scripts/consts.gd
Normal file
8
scripts/consts.gd
Normal file
@ -0,0 +1,8 @@
|
||||
class_name Consts
|
||||
|
||||
const START := "start"
|
||||
const STOP := "stop"
|
||||
const NO_TIME := "00:00:00"
|
||||
const ONGOING := "ongoing"
|
||||
const THEME_OVERRIDE_START := "play_button"
|
||||
const THEME_OVERRIDE_STOP := "stop_button"
|
79
scripts/time_entry.gd
Normal file
79
scripts/time_entry.gd
Normal file
@ -0,0 +1,79 @@
|
||||
## Describes a row in a timesheet
|
||||
## Has a beginning and an end
|
||||
class_name TimeEntry
|
||||
|
||||
var name := ""
|
||||
var closed := false
|
||||
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()) if closed else tr(Consts.ONGOING)
|
||||
])
|
||||
|
||||
static func is_csv_line_valid(line: PackedStringArray) -> bool:
|
||||
return line.size() > 3
|
||||
|
||||
|
||||
func from_csv_line(line: PackedStringArray) -> TimeEntry:
|
||||
name = line[0]
|
||||
|
||||
var start_time_string = line[1]
|
||||
start_time.from_string(start_time_string)
|
||||
|
||||
var elapsed_seconds = int(line[3]) if line[3].is_valid_int() else 0
|
||||
closed = elapsed_seconds > 0
|
||||
|
||||
if closed == true:
|
||||
var end_time_string = line[2]
|
||||
end_time.from_string(end_time_string)
|
||||
else:
|
||||
end_time.from_current_time()
|
||||
return self
|
||||
|
||||
func _to_string() -> String:
|
||||
return "%s\t%s\t%s"%[name, Consts.ONGOING if closed == false else "", start_time]
|
84
scripts/time_sheet.gd
Normal file
84
scripts/time_sheet.gd
Normal file
@ -0,0 +1,84 @@
|
||||
class_name TimeSheet
|
||||
|
||||
|
||||
var source_path := ""
|
||||
var entries: Array[TimeEntry] = []
|
||||
var entries_names := {}
|
||||
var current_entry: TimeEntry
|
||||
|
||||
## Loads the data file
|
||||
func load_file() -> 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
|
||||
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)
|
||||
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()
|
||||
file.close()
|
||||
return true
|
||||
|
||||
|
||||
## 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()
|
||||
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 := FileAccess.open(source_path, FileAccess.READ_WRITE)
|
||||
if file == null:
|
||||
printerr("Could not open file")
|
||||
entries.append(current_entry)
|
||||
file.store_csv_line(current_entry.to_csv_line())
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
func get_period() -> String:
|
||||
return current_entry.get_period()
|
||||
|
||||
|
||||
func get_total_elapsed_seconds() -> int:
|
||||
return current_entry.get_total_elapsed_seconds()
|
||||
|
||||
|
||||
func save() -> void:
|
||||
var file := FileAccess.open(source_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
printerr("Could not open file")
|
||||
for time_entry in entries:
|
||||
file.store_csv_line(time_entry.to_csv_line())
|
||||
|
||||
|
||||
static func restore(file_path: String) -> TimeSheet:
|
||||
var timesheet := TimeSheet.new()
|
||||
timesheet.source_path = file_path
|
||||
var success := timesheet.load_file()
|
||||
if success:
|
||||
return timesheet
|
||||
return null
|
||||
|
||||
|
54
scripts/time_stamp.gd
Normal file
54
scripts/time_stamp.gd
Normal file
@ -0,0 +1,54 @@
|
||||
## A simple proxy for the object returned by Godot's time functions
|
||||
## Ensures proper typing
|
||||
class_name 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)
|
118
ui/Main.gd
Normal file
118
ui/Main.gd
Normal file
@ -0,0 +1,118 @@
|
||||
extends Control
|
||||
|
||||
|
||||
@onready var time_label: Label = %TimeLabel
|
||||
@onready var start_button: Button = %StartButton
|
||||
@onready var task_name_line_edit: LineEdit = %TaskNameLineEdit
|
||||
@onready var time_entries_items_tree: TimeEntriesItemsTree = %TimeEntriesItemsTree
|
||||
@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 audio_stream_player: AudioStreamPlayer = %AudioStreamPlayer
|
||||
|
||||
|
||||
var config: ConfigManager = preload("res://config_manager.tres")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
||||
config.file_changed.connect(set_initial_state)
|
||||
config.theme_changed.connect(
|
||||
func theme_changed() -> void:
|
||||
theme = config.them
|
||||
)
|
||||
|
||||
get_tree().set_auto_accept_quit(false)
|
||||
|
||||
previous_tasks_window.hide()
|
||||
settings_window.hide()
|
||||
|
||||
timer.timeout.connect(
|
||||
func on_timer_timeout() -> void:
|
||||
config.timesheet.update()
|
||||
|
||||
time_label.text = config.timesheet.get_period()
|
||||
|
||||
var total_elapsed: int = config.timesheet.get_total_elapsed_seconds()
|
||||
time_entries_items_tree.set_time_elapsed(total_elapsed)
|
||||
)
|
||||
|
||||
start_button.tooltip_text = tr(Consts.START)
|
||||
start_button.toggle_mode = true
|
||||
start_button.toggled.connect(
|
||||
func start(is_on: bool) -> void:
|
||||
if config.sound_fx_on:
|
||||
audio_stream_player.play()
|
||||
if is_on:
|
||||
config.timesheet.start_entry(task_name_line_edit.text)
|
||||
set_button_as_started()
|
||||
else:
|
||||
config.timesheet.close_entry()
|
||||
set_button_as_stopped()
|
||||
)
|
||||
|
||||
|
||||
task_name_line_edit.text = config.last_task_name
|
||||
task_name_line_edit.text_changed.connect(
|
||||
func(new_text: String) -> void:
|
||||
config.last_task_name = new_text
|
||||
)
|
||||
time_entries_items_tree.item_selected.connect(
|
||||
func item_selected() -> void:
|
||||
task_name_line_edit.text = time_entries_items_tree.get_current_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()
|
||||
)
|
||||
|
||||
|
||||
|
||||
func set_button_as_started() -> void:
|
||||
time_entries_items_tree.set_current_item(config.timesheet.current_entry.name)
|
||||
start_button.tooltip_text = tr(Consts.STOP)
|
||||
start_button.theme_type_variation = Consts.THEME_OVERRIDE_STOP
|
||||
timer.start()
|
||||
|
||||
|
||||
func set_button_as_stopped() -> void:
|
||||
start_button.tooltip_text = tr(Consts.START)
|
||||
time_label.text = Consts.NO_TIME
|
||||
start_button.theme_type_variation = Consts.THEME_OVERRIDE_START
|
||||
timer.stop()
|
||||
|
||||
|
||||
func set_initial_state() -> void:
|
||||
if config.timesheet.current_entry != null and config.timesheet.current_entry.closed == false:
|
||||
start_button.set_pressed_no_signal(true)
|
||||
set_button_as_started()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||
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)
|
132
ui/Main.tscn
Normal file
132
ui/Main.tscn
Normal file
@ -0,0 +1,132 @@
|
||||
[gd_scene load_steps=8 format=3 uid="uid://bmlciwscreowf"]
|
||||
|
||||
[ext_resource type="Theme" uid="uid://bd8ancgbfsvmd" path="res://assets/default_theme.theme" id="1_2s8h2"]
|
||||
[ext_resource type="Script" path="res://ui/Main.gd" id="2_sl5q6"]
|
||||
[ext_resource type="Script" path="res://ui/time_entries_items_tree.gd" id="3_oxqux"]
|
||||
[ext_resource type="PackedScene" uid="uid://b07v41toqw355" path="res://ui/settings.tscn" id="4_4fa2j"]
|
||||
[ext_resource type="AudioStream" uid="uid://cdsbhoidgyx70" path="res://assets/pop.ogg" id="4_6ajaq"]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_guuii"]
|
||||
device = -1
|
||||
pressed = true
|
||||
keycode = 32
|
||||
unicode = 32
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_irhvi"]
|
||||
events = [SubResource("InputEventKey_guuii")]
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("1_2s8h2")
|
||||
theme_type_variation = &"background"
|
||||
|
||||
[node name="Main" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
script = ExtResource("2_sl5q6")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Main"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="Main/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SettingsButton" type="Button" parent="Main/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Settings"
|
||||
theme_type_variation = &"settings_button"
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="TaskNameLineEdit" type="LineEdit" parent="Main/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Task Name. Use \"/\" to create subtasks"
|
||||
caret_blink = true
|
||||
caret_blink_interval = 0.5
|
||||
|
||||
[node name="TasksButton" type="Button" parent="Main/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Tasks"
|
||||
theme_type_variation = &"tasks_button"
|
||||
icon_alignment = 1
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Main/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="TimeLabel" type="Label" parent="Main/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
theme_type_variation = &"time_label"
|
||||
text = "00:00:00"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="StartButton" type="Button" parent="Main/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_type_variation = &"play_button"
|
||||
shortcut = SubResource("Shortcut_irhvi")
|
||||
|
||||
[node name="Timer" type="Timer" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="PreviousTasksWindow" type="Window" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
title = "Tasks"
|
||||
size = Vector2i(300, 300)
|
||||
visible = false
|
||||
always_on_top = true
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="Main/PreviousTasksWindow"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"background"
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Main/PreviousTasksWindow/PanelContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="TimeEntriesItemsTree" type="Tree" parent="Main/PreviousTasksWindow/PanelContainer/MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
columns = 2
|
||||
script = ExtResource("3_oxqux")
|
||||
|
||||
[node name="SettingsWindow" type="Window" parent="Main"]
|
||||
unique_name_in_owner = true
|
||||
title = "Settings"
|
||||
size = Vector2i(300, 300)
|
||||
visible = false
|
||||
always_on_top = true
|
||||
|
||||
[node name="Settings" parent="Main/SettingsWindow" instance=ExtResource("4_4fa2j")]
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("4_6ajaq")
|
59
ui/settings.gd
Normal file
59
ui/settings.gd
Normal file
@ -0,0 +1,59 @@
|
||||
extends PanelContainer
|
||||
|
||||
|
||||
var config: ConfigManager = preload("res://config_manager.tres")
|
||||
|
||||
|
||||
@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 open_data_dir_button: Button = %OpenDataDirButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
||||
config.theme_changed.connect(
|
||||
func set_current_theme() -> void:
|
||||
theme = config.theme
|
||||
)
|
||||
|
||||
config.file_changed.connect(
|
||||
func set_current_file() -> void:
|
||||
file_path_file_dialog.current_path = config.current_file
|
||||
file_path_file_dialog.current_dir = config.current_file.get_base_dir()
|
||||
file_path_line_edit.text = config.current_file
|
||||
)
|
||||
|
||||
file_path_button.pressed.connect(
|
||||
file_path_file_dialog.popup_centered
|
||||
)
|
||||
|
||||
file_path_file_dialog.file_selected.connect(config.set_current_file)
|
||||
file_path_line_edit.text_submitted.connect(config.set_current_file)
|
||||
|
||||
theme_path_button.pressed.connect(
|
||||
theme_path_file_dialog.popup_centered
|
||||
)
|
||||
theme_path_file_dialog.file_selected.connect(config.set_theme_path)
|
||||
|
||||
theme_path_file_dialog.hide()
|
||||
file_path_file_dialog.hide()
|
||||
|
||||
sound_check_box.button_pressed = config.sound_fx_on
|
||||
sound_check_box.toggled.connect(
|
||||
func sound_toggle(is_on: bool) -> void:
|
||||
config.sound_fx_on = is_on
|
||||
)
|
||||
|
||||
attributions_rich_text_label.meta_clicked.connect(OS.shell_open)
|
||||
|
||||
open_data_dir_button.pressed.connect(
|
||||
OS.shell_open.bind(OS.get_user_data_dir())
|
||||
)
|
||||
|
||||
|
||||
|
113
ui/settings.tscn
Normal file
113
ui/settings.tscn
Normal file
@ -0,0 +1,113 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b07v41toqw355"]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/settings.gd" id="1_cmilf"]
|
||||
|
||||
[node name="Settings" type="PanelContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"background"
|
||||
script = ExtResource("1_cmilf")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 20
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 20
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "File Path"
|
||||
|
||||
[node name="FilePathLineEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
caret_blink = true
|
||||
caret_blink_interval = 0.5
|
||||
|
||||
[node name="FilePathButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "..."
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Alternative theme
|
||||
"
|
||||
|
||||
[node name="ThemePathButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "load"
|
||||
|
||||
[node name="HBoxContainer3" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SoundCheckBox" type="CheckBox" parent="MarginContainer/VBoxContainer/HBoxContainer3"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
button_pressed = true
|
||||
text = "Sounds"
|
||||
|
||||
[node name="OpenDataDirButton" type="Button" parent="MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "open data dir"
|
||||
|
||||
[node name="AttributionsRichTextLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_type_variation = &"small_text"
|
||||
bbcode_enabled = true
|
||||
text = "Font: Cairo, Designed by Mohamed Gaber, Accademia di Belle Arti di Urbino
|
||||
|
||||
Sound: [url]https://opengameart.org/content/bubbles-pop[/url]
|
||||
|
||||
This game uses Godot Engine, available under the following license:
|
||||
|
||||
Copyright (c) 2014-present Godot Engine contributors. Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"
|
||||
|
||||
[node name="FilePathFileDialog" type="FileDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
title = "Pick Tracker File"
|
||||
size = Vector2i(800, 600)
|
||||
ok_button_text = "Save"
|
||||
access = 2
|
||||
filters = PackedStringArray("*.csv ; Comma Separated Files")
|
||||
|
||||
[node name="ThemePathFileDialog" type="FileDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
title = "Open a File"
|
||||
size = Vector2i(800, 600)
|
||||
ok_button_text = "Open"
|
||||
file_mode = 0
|
||||
access = 2
|
||||
filters = PackedStringArray("*.theme ; Theme Files")
|
66
ui/time_entries_items_tree.gd
Normal file
66
ui/time_entries_items_tree.gd
Normal file
@ -0,0 +1,66 @@
|
||||
class_name TimeEntriesItemsTree extends Tree
|
||||
|
||||
enum COL{
|
||||
TEXT,
|
||||
TIME
|
||||
}
|
||||
|
||||
var config: ConfigManager = preload("res://config_manager.tres")
|
||||
|
||||
var current_item: TreeItem
|
||||
|
||||
func _ready() -> void:
|
||||
config.file_changed.connect(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:
|
||||
append_name_to_tree(entry_name, entries_names[entry_name])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
func set_current_item(task_name: String) -> void:
|
||||
current_item = append_name_to_tree(task_name, 0)
|
||||
|
||||
|
||||
|
||||
func get_current_text() -> String:
|
||||
var item := get_selected()
|
||||
if not item:
|
||||
return ""
|
||||
var resp: String = item.get_metadata(COL.TEXT)
|
||||
return resp
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
## 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, Consts.NO_TIME)
|
||||
return child
|
||||
return null
|
Reference in New Issue
Block a user