qtimetrap 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +3 -4
- data/app/components/entries/branch_hierarchy_helpers.rb +4 -4
- data/app/components/entries/entry_row_render_helpers.rb +196 -0
- data/app/components/entries/leaf_archive_helpers.rb +6 -6
- data/app/components/entries/leaf_note_helpers.rb +18 -12
- data/app/components/entries/leaf_task_display_helpers.rb +15 -0
- data/app/components/entries/leaf_task_editor_state_helpers.rb +68 -0
- data/app/components/entries/leaf_task_helpers.rb +130 -46
- data/app/components/entries/leaf_time_helpers.rb +9 -9
- data/app/components/entries/list_component.rb +302 -15
- data/app/components/entries/list_host_helpers.rb +6 -3
- data/app/components/entries/list_state_helpers.rb +77 -4
- data/app/components/entries/list_task_editor_helpers.rb +31 -0
- data/app/components/entries/render_helpers.rb +2 -2
- data/app/components/entries/tree_helpers.rb +9 -7
- data/app/components/entries/tree_toolbar_helpers.rb +12 -12
- data/app/components/project_sidebar/archive_toggle_helpers.rb +6 -6
- data/app/components/project_sidebar/component.rb +18 -7
- data/app/components/project_sidebar/logo_helpers.rb +14 -14
- data/app/components/project_sidebar/project_button_helpers.rb +7 -7
- data/app/components/project_sidebar/task_helpers.rb +11 -11
- data/app/components/qt_ui_helpers.rb +9 -9
- data/app/components/tracker_controls/component.rb +19 -8
- data/app/components/tracker_controls/layout_builder.rb +35 -15
- data/app/components/tracker_controls/layout_helpers.rb +7 -6
- data/app/services/archived_entries_store.rb +11 -6
- data/app/styles/themes/dark/entries_list.qss +12 -4
- data/app/styles/themes/light/entries_list.qss +12 -4
- data/app/view_models/main_view_model.rb +4 -0
- data/app/views/main_window.rb +2 -3
- data/app/views/main_window_layout_builder.rb +6 -6
- data/app/views/main_window_runtime.rb +14 -8
- data/app/views/main_window_runtime_archive_helpers.rb +7 -1
- data/app/views/main_window_runtime_key_helpers.rb +24 -5
- data/app/views/main_window_runtime_render_helpers.rb +2 -1
- data/app/views/main_window_splitter_toggle_bootstrap_helpers.rb +1 -1
- data/app/views/main_window_splitter_toggle_helpers.rb +10 -10
- data/app/views/main_window_splitter_toggle_hover_helpers.rb +2 -2
- data/app/views/main_window_ui_helpers.rb +31 -3
- data/app/views/window_icon_loader.rb +1 -1
- data/lib/qtimetrap/version.rb +1 -1
- data/lib/rubocop/cop/qt/prefer_ruby_style_setter.rb +50 -0
- metadata +11 -5
|
@@ -2,79 +2,163 @@
|
|
|
2
2
|
|
|
3
3
|
module QTimetrap
|
|
4
4
|
module Entries
|
|
5
|
-
# Entry-leaf helpers for editable task
|
|
5
|
+
# Entry-leaf helpers for editable task controls.
|
|
6
6
|
module LeafTaskHelpers
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def build_entry_task_input(row, node)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
task_input.set_placeholder_text('task')
|
|
16
|
-
task_input.set_minimum_width(220)
|
|
17
|
-
populate_entry_task_options(task_input, node)
|
|
18
|
-
bind_entry_task_input_events(task_input, resolve_entry_id(node))
|
|
10
|
+
task_name = node.fetch(:task_name, '').to_s
|
|
11
|
+
QWidget.new(row).tap do |task_container|
|
|
12
|
+
initialize_entry_task_container(task_container, task_name, node)
|
|
13
|
+
bind_entry_task_input_events(task_container, resolve_entry_id(node))
|
|
14
|
+
show_entry_task_display(task_container)
|
|
19
15
|
end
|
|
20
16
|
end
|
|
21
17
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
def initialize_entry_task_container(task_container, task_name, node)
|
|
19
|
+
task_container.object_name = 'entry_node_entry_task_container'
|
|
20
|
+
build_entry_task_layout(task_container)
|
|
21
|
+
display_input = build_entry_task_display(task_container, task_name)
|
|
22
|
+
editor, line_edit = build_entry_task_editor(task_container, task_name, node)
|
|
23
|
+
register_task_widgets(task_container, display_input, editor, line_edit)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_entry_task_layout(task_container)
|
|
27
|
+
QHBoxLayout.new(task_container).tap do |layout|
|
|
28
|
+
layout.set_contents_margins(0, 0, 0, 0)
|
|
29
|
+
layout.spacing = 0
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
def build_entry_task_display(task_container, task_name)
|
|
34
|
+
QLabel.new(task_container).tap do |task_input|
|
|
35
|
+
task_input.object_name = 'entry_node_entry_task'
|
|
36
|
+
task_input.text = task_name
|
|
37
|
+
task_input.tool_tip = task_name
|
|
38
|
+
task_input.minimum_width = 220
|
|
39
|
+
task_input.fixed_height = 24
|
|
40
|
+
sync_task_display_field(task_input, task_name)
|
|
41
|
+
task_container.layout.add_widget(task_input)
|
|
42
|
+
end
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
input.on(:focus_out) { |_| handle_entry_task_focus_out(task_input) }
|
|
43
|
-
task_input.connect('textActivated') { |_| handle_entry_task_commit(task_input, entry_id, force: true) }
|
|
45
|
+
def build_entry_task_editor(task_container, task_name, node)
|
|
46
|
+
editor, line_edit = build_entry_task_editor_shell(task_container, task_name)
|
|
47
|
+
populate_entry_task_options(editor, task_name, node)
|
|
48
|
+
editor.current_text = task_name
|
|
49
|
+
[editor, line_edit]
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
def build_entry_task_editor_shell(task_container, task_name)
|
|
53
|
+
editor = QComboBox.new(task_container)
|
|
54
|
+
editor.object_name = 'entry_node_entry_task_editor'
|
|
55
|
+
editor.editable = true
|
|
56
|
+
editor.focus_policy = Qt::StrongFocus
|
|
57
|
+
editor.minimum_width = 220
|
|
58
|
+
editor.tool_tip = task_name
|
|
59
|
+
line_edit = build_entry_task_line_edit(task_container)
|
|
60
|
+
editor.instance_variable_set(:@qtimetrap_task_line_edit, line_edit)
|
|
61
|
+
editor.line_edit = line_edit
|
|
62
|
+
task_container.layout.add_widget(editor)
|
|
63
|
+
editor.hide
|
|
64
|
+
[editor, line_edit]
|
|
49
65
|
end
|
|
50
66
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
def configure_entry_task_editor(editor, task_container, task_name, node)
|
|
68
|
+
editor.object_name = 'entry_node_entry_task_editor'
|
|
69
|
+
editor.editable = true
|
|
70
|
+
editor.focus_policy = Qt::StrongFocus
|
|
71
|
+
editor.minimum_width = 220
|
|
72
|
+
editor.tool_tip = task_name
|
|
73
|
+
line_edit = build_entry_task_line_edit(task_container)
|
|
74
|
+
editor.instance_variable_set(:@qtimetrap_task_line_edit, line_edit)
|
|
75
|
+
editor.line_edit = line_edit
|
|
76
|
+
populate_entry_task_options(editor, task_name, node)
|
|
77
|
+
line_edit
|
|
54
78
|
end
|
|
55
79
|
|
|
56
|
-
def
|
|
57
|
-
|
|
80
|
+
def ensure_entry_task_editor(task_container)
|
|
81
|
+
editor = entry_task_editor(task_container)
|
|
82
|
+
return [editor, entry_task_editor_line_edit(editor)] if editor
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
config = task_editor_config_for(task_container)
|
|
85
|
+
task_name = config.fetch(:task_name)
|
|
86
|
+
editor, line_edit = build_entry_task_editor_shell(task_container, task_name)
|
|
87
|
+
apply_entry_widget_styles(editor, %w[entry_node_entry_task_editor])
|
|
88
|
+
populate_entry_task_options(editor, task_name, config.fetch(:node))
|
|
89
|
+
editor.current_text = task_name
|
|
90
|
+
register_task_editor_widgets(task_container, editor, line_edit)
|
|
91
|
+
bind_entry_task_editor_events(task_container, config.fetch(:node))
|
|
92
|
+
[editor, line_edit]
|
|
60
93
|
end
|
|
61
94
|
|
|
62
|
-
def
|
|
63
|
-
|
|
95
|
+
def bind_entry_task_editor_events(task_container, node)
|
|
96
|
+
entry_id = resolve_entry_id(node)
|
|
97
|
+
editor = entry_task_editor(task_container)
|
|
98
|
+
line_edit = entry_task_editor_line_edit(editor)
|
|
99
|
+
line_edit.on(:key_press) { |event| handle_entry_task_key_press(task_container, entry_id, event) }
|
|
100
|
+
line_edit.connect('returnPressed') { |_| handle_entry_task_commit(task_container, entry_id) }
|
|
101
|
+
line_edit.on(:focus_out) { |_| handle_entry_task_focus_out(task_container) }
|
|
102
|
+
editor.connect('textActivated') { |_| handle_entry_task_commit(task_container, entry_id, force: true) }
|
|
103
|
+
end
|
|
64
104
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
105
|
+
def build_entry_task_line_edit(task_container)
|
|
106
|
+
QLineEdit.new(task_container).tap do |line_edit|
|
|
107
|
+
line_edit.object_name = 'entry_node_entry_task_editor_input'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
68
110
|
|
|
69
|
-
|
|
111
|
+
def populate_entry_task_options(editor, task_name, node)
|
|
112
|
+
suggestions = Array(task_suggestions_for_project&.call(node.fetch(:project_name, '').to_s))
|
|
113
|
+
([task_name] + suggestions.map(&:to_s)).reject(&:empty?).uniq.each do |suggestion|
|
|
114
|
+
editor.add_item(suggestion)
|
|
115
|
+
end
|
|
116
|
+
editor.current_text = task_name
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def bind_entry_task_input_events(task_container, entry_id)
|
|
120
|
+
display_input = entry_task_display_input(task_container)
|
|
121
|
+
editor = entry_task_editor(task_container)
|
|
122
|
+
editor_input = entry_task_editor_line_edit(editor)
|
|
123
|
+
|
|
124
|
+
display_input.on(:mouse_button_press) { |_| activate_entry_task_input(task_container) }
|
|
125
|
+
editor_input.on(:key_press) { |event| handle_entry_task_key_press(task_container, entry_id, event) }
|
|
126
|
+
editor_input.connect('returnPressed') { |_| handle_entry_task_commit(task_container, entry_id) }
|
|
127
|
+
editor_input.on(:focus_out) { |_| handle_entry_task_focus_out(task_container) }
|
|
128
|
+
editor.connect('textActivated') { |_| handle_entry_task_commit(task_container, entry_id, force: true) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def entry_task_display_input(task_container)
|
|
132
|
+
task_display_input_for(task_container)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def entry_task_editor(task_container)
|
|
136
|
+
task_editor_for(task_container)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def entry_task_editor_line_edit(editor)
|
|
140
|
+
return unless editor
|
|
141
|
+
|
|
142
|
+
task_editor_line_edit_for(editor) || editor.instance_variable_get(:@qtimetrap_task_line_edit)
|
|
70
143
|
end
|
|
71
144
|
|
|
72
|
-
def
|
|
73
|
-
|
|
145
|
+
def handle_entry_task_key_press(task_container, entry_id, event)
|
|
146
|
+
return unless enter_key?(event)
|
|
147
|
+
|
|
148
|
+
handle_entry_task_commit(task_container, entry_id)
|
|
74
149
|
end
|
|
75
150
|
|
|
76
|
-
def
|
|
77
|
-
|
|
151
|
+
def handle_entry_task_commit(task_container, entry_id, force: false)
|
|
152
|
+
editor = entry_task_editor(task_container)
|
|
153
|
+
editor_input = entry_task_editor_line_edit(editor)
|
|
154
|
+
return if editor_input.is_read_only && !force
|
|
155
|
+
|
|
156
|
+
task_name = editor.current_text.to_s
|
|
157
|
+
sync_entry_task_display(task_container, task_name)
|
|
158
|
+
deactivate_entry_task_input(task_container)
|
|
159
|
+
return unless on_entry_task_change
|
|
160
|
+
|
|
161
|
+
on_entry_task_change.call(entry_id, task_name)
|
|
78
162
|
end
|
|
79
163
|
end
|
|
80
164
|
end
|
|
@@ -25,10 +25,10 @@ module QTimetrap
|
|
|
25
25
|
|
|
26
26
|
def build_entry_time_group(row, start_input, end_input)
|
|
27
27
|
QWidget.new(row).tap do |time_group|
|
|
28
|
-
time_group.
|
|
28
|
+
time_group.object_name = 'entry_node_entry_time_group'
|
|
29
29
|
QHBoxLayout.new(time_group).tap do |time_layout|
|
|
30
30
|
time_layout.set_contents_margins(0, 0, 0, 0)
|
|
31
|
-
time_layout.
|
|
31
|
+
time_layout.spacing = 4
|
|
32
32
|
time_layout.add_widget(start_input)
|
|
33
33
|
time_layout.add_widget(build_entry_time_separator(time_group))
|
|
34
34
|
time_layout.add_widget(end_input)
|
|
@@ -38,19 +38,19 @@ module QTimetrap
|
|
|
38
38
|
|
|
39
39
|
def build_entry_time_separator(row)
|
|
40
40
|
QLabel.new(row).tap do |separator|
|
|
41
|
-
separator.
|
|
42
|
-
separator.
|
|
41
|
+
separator.object_name = 'entry_node_entry_time_sep'
|
|
42
|
+
separator.text = '-'
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def build_entry_time_input(row, object_name:, value:, placeholder:)
|
|
47
47
|
QLineEdit.new(row).tap do |time_input|
|
|
48
|
-
time_input.
|
|
48
|
+
time_input.object_name = object_name
|
|
49
49
|
time_input.text = value.to_s
|
|
50
|
-
time_input.
|
|
51
|
-
time_input.
|
|
52
|
-
time_input.
|
|
53
|
-
time_input.
|
|
50
|
+
time_input.placeholder_text = placeholder
|
|
51
|
+
time_input.alignment = Qt::AlignCenter
|
|
52
|
+
time_input.read_only = true
|
|
53
|
+
time_input.fixed_width = 58
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -6,6 +6,8 @@ module QTimetrap
|
|
|
6
6
|
class ListComponent
|
|
7
7
|
include ListHostHelpers
|
|
8
8
|
include ListStateHelpers
|
|
9
|
+
include ListTaskEditorHelpers
|
|
10
|
+
include EntryRowRenderHelpers
|
|
9
11
|
include QtUiHelpers
|
|
10
12
|
include TreeHelpers
|
|
11
13
|
include RenderHelpers
|
|
@@ -13,6 +15,7 @@ module QTimetrap
|
|
|
13
15
|
HOST_HORIZONTAL_MARGINS = 28
|
|
14
16
|
WIDTH_PADDING = 24
|
|
15
17
|
TIME_FILTER_DEBOUNCE_MS = 220
|
|
18
|
+
RENDER_SLICE_MS = 4
|
|
16
19
|
|
|
17
20
|
attr_reader :widget
|
|
18
21
|
|
|
@@ -29,16 +32,89 @@ module QTimetrap
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def render(nodes)
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
time_filter_debounce.stop if time_filter_debounce&.is_active
|
|
36
|
+
render_timer.stop if render_timer.is_active
|
|
37
|
+
@theme_style_queue = []
|
|
38
|
+
@render_generation = render_generation.to_i + 1
|
|
34
39
|
@rendering = true
|
|
35
40
|
@current_nodes = Array(nodes)
|
|
36
41
|
@branch_bindings = {}
|
|
37
42
|
@leaf_labels = []
|
|
38
43
|
@entry_rows = []
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@
|
|
44
|
+
@entry_rows_by_id = {}
|
|
45
|
+
@entry_row_parent_branches = {}
|
|
46
|
+
@project_branch_entry_ids = {}
|
|
47
|
+
@container_branch_ids = {}
|
|
48
|
+
@row_filter_state = {}
|
|
49
|
+
@width_queue = []
|
|
50
|
+
@visibility_queue = []
|
|
51
|
+
@visible_branch_ids = nil
|
|
52
|
+
@visible_branch_labels = {}
|
|
53
|
+
@visible_entry_ids = nil
|
|
54
|
+
@task_suggestion_cache = {}
|
|
55
|
+
@render_needs_stretch = true
|
|
56
|
+
rebuild_host!
|
|
57
|
+
@empty_label = QLabel.new(host)
|
|
58
|
+
@empty_label.object_name = 'entry_node_empty'
|
|
59
|
+
@empty_label.fixed_height = 32
|
|
60
|
+
@empty_label.visible = false
|
|
61
|
+
host_layout.add_widget(@empty_label)
|
|
62
|
+
@render_queue = []
|
|
63
|
+
enqueue_render_nodes(current_nodes, level: 0, layout: host_layout, parent_widget: host)
|
|
64
|
+
render_timer.start
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def filter_to(nodes)
|
|
69
|
+
branch_nodes = {}
|
|
70
|
+
entry_ids = {}
|
|
71
|
+
project_entry_ids = Hash.new { |hash, key| hash[key] = [] }
|
|
72
|
+
empty_label = nil
|
|
73
|
+
visit = lambda do |collection, project_branch_id = nil|
|
|
74
|
+
Array(collection).each do |node|
|
|
75
|
+
if node.fetch(:type) == :entry
|
|
76
|
+
entry_id = node.fetch(:entry_id, node.fetch(:id))
|
|
77
|
+
entry_ids[entry_id] = true
|
|
78
|
+
project_entry_ids[project_branch_id] << entry_id if project_branch_id
|
|
79
|
+
elsif node.fetch(:type) == :empty
|
|
80
|
+
empty_label = node.fetch(:label)
|
|
81
|
+
elsif branch_node?(node)
|
|
82
|
+
branch_nodes[node.fetch(:id)] = node
|
|
83
|
+
next_project_id = node.fetch(:type) == :project ? node.fetch(:id) : project_branch_id
|
|
84
|
+
visit.call(node.fetch(:children), next_project_id)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
visit.call(nodes)
|
|
89
|
+
|
|
90
|
+
@visible_branch_ids = branch_nodes
|
|
91
|
+
@visible_branch_labels = branch_nodes.transform_values { |node| node.fetch(:label) }
|
|
92
|
+
@visible_entry_ids = entry_ids
|
|
93
|
+
@visible_project_entry_ids = project_entry_ids
|
|
94
|
+
@visibility_queue = branch_bindings.filter_map do |node_id, binding|
|
|
95
|
+
visible = branch_nodes.key?(node_id)
|
|
96
|
+
label = branch_nodes[node_id]&.fetch(:label)
|
|
97
|
+
next if binding[:filter_visible] == visible && (!visible || binding[:label] == label)
|
|
98
|
+
|
|
99
|
+
[:branch, node_id]
|
|
100
|
+
end
|
|
101
|
+
project_branch_entry_ids.each do |branch_id, entry_keys|
|
|
102
|
+
next unless project_entry_ids.key?(branch_id)
|
|
103
|
+
|
|
104
|
+
visible_ids = project_entry_ids.fetch(branch_id)
|
|
105
|
+
entry_keys.each do |entry_id|
|
|
106
|
+
visible = visible_ids.include?(entry_id)
|
|
107
|
+
next if row_filter_state[entry_id] == visible
|
|
108
|
+
|
|
109
|
+
@visibility_queue << [:entry, entry_id, entry_rows_by_id.fetch(entry_id), visible]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
@visibility_queue << [:empty, empty_label]
|
|
113
|
+
if visibility_queue.any?
|
|
114
|
+
@rendering = true
|
|
115
|
+
render_timer.start
|
|
116
|
+
end
|
|
117
|
+
nil
|
|
42
118
|
end
|
|
43
119
|
|
|
44
120
|
def update_time_range_inputs(from_at:, to_at:)
|
|
@@ -57,7 +133,34 @@ module QTimetrap
|
|
|
57
133
|
@syncing_time_filters = false
|
|
58
134
|
end
|
|
59
135
|
|
|
136
|
+
def apply_theme_stylesheet(stylesheet, external_targets: [], defer: false)
|
|
137
|
+
@entry_stylesheet = stylesheet
|
|
138
|
+
@entry_scroll_stylesheet = stylesheet_for_ids(stylesheet, %w[entries_scroll])
|
|
139
|
+
@entry_toolbar_stylesheet = stylesheet_for_ids(stylesheet, %w[
|
|
140
|
+
entries_toolbar entries_expand_all entries_collapse_all
|
|
141
|
+
entries_time_filter_from_toggle entries_time_filter_to_toggle
|
|
142
|
+
entries_time_filter_from entries_time_filter_to
|
|
143
|
+
])
|
|
144
|
+
@entry_host_stylesheet = stylesheet_for_ids(stylesheet, %w[entries_host])
|
|
145
|
+
@entry_row_stylesheet = stylesheet_for_ids(stylesheet, ENTRY_ROW_STYLE_IDS)
|
|
146
|
+
@entry_empty_stylesheet = stylesheet_for_ids(stylesheet, %w[entry_node_empty])
|
|
147
|
+
@entry_branch_stylesheets = %w[week day project].to_h do |type|
|
|
148
|
+
name = "entry_node_#{type}"
|
|
149
|
+
[name, stylesheet_for_ids(stylesheet, [name])]
|
|
150
|
+
end
|
|
151
|
+
@theme_style_queue = theme_style_targets(stylesheet).concat(external_targets)
|
|
152
|
+
@rendering = true if defer && theme_style_queue.any?
|
|
153
|
+
unless defer
|
|
154
|
+
scroll_area.style_sheet = @entry_scroll_stylesheet
|
|
155
|
+
toolbar.style_sheet = @entry_toolbar_stylesheet
|
|
156
|
+
@theme_style_queue.clear
|
|
157
|
+
end
|
|
158
|
+
render_timer.start if theme_style_queue.any?
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
60
162
|
def shutdown
|
|
163
|
+
render_timer.stop if render_timer&.is_active
|
|
61
164
|
return unless time_filter_debounce
|
|
62
165
|
|
|
63
166
|
time_filter_debounce.stop if time_filter_debounce.is_active
|
|
@@ -69,19 +172,201 @@ module QTimetrap
|
|
|
69
172
|
:rendering, :scroll_area, :on_entry_note_change, :on_entry_task_change, :task_suggestions_for_project,
|
|
70
173
|
:on_entry_time_change, :on_entry_archive, :on_time_range_change,
|
|
71
174
|
:time_filter_from_toggle, :time_filter_to_toggle, :time_filter_from_input, :time_filter_to_input,
|
|
72
|
-
:time_filter_debounce
|
|
175
|
+
:time_filter_debounce, :render_timer, :render_queue, :render_generation, :width_queue,
|
|
176
|
+
:task_suggestion_cache, :visibility_queue, :entry_rows_by_id, :visible_branch_ids,
|
|
177
|
+
:visible_branch_labels, :visible_entry_ids, :empty_label, :project_branch_entry_ids,
|
|
178
|
+
:container_branch_ids, :entry_row_parent_branches, :row_filter_state, :theme_style_queue, :toolbar
|
|
73
179
|
|
|
74
180
|
def build
|
|
75
181
|
@widget = QWidget.new(parent)
|
|
76
|
-
widget.
|
|
182
|
+
widget.object_name = 'entries_panel'
|
|
77
183
|
panel_layout = build_panel_layout
|
|
78
184
|
@time_filter_debounce = build_time_filter_debounce_timer
|
|
79
|
-
|
|
185
|
+
@toolbar = build_toolbar(parent_widget: widget)
|
|
186
|
+
panel_layout.add_widget(toolbar)
|
|
80
187
|
@scroll_area = build_scroll_area
|
|
81
188
|
bind_scroll_resize
|
|
82
189
|
panel_layout.add_widget(scroll_area)
|
|
83
190
|
panel_layout.set_stretch(1, 1)
|
|
84
191
|
rebuild_host!
|
|
192
|
+
@render_timer = QTimer.new(widget)
|
|
193
|
+
render_timer.interval = 1
|
|
194
|
+
render_timer.connect('timeout') { |_| process_render_slice(render_generation) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def enqueue_render_nodes(nodes, level:, layout:, parent_widget:)
|
|
198
|
+
nodes.reverse_each do |node|
|
|
199
|
+
render_queue << [node, level, layout, parent_widget]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def process_render_slice(generation)
|
|
204
|
+
return unless generation == render_generation
|
|
205
|
+
|
|
206
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (RENDER_SLICE_MS / 1000.0)
|
|
207
|
+
content_changed = false
|
|
208
|
+
while queues_pending? && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
209
|
+
if theme_style_queue.any?
|
|
210
|
+
target, stylesheet = theme_style_queue.shift
|
|
211
|
+
target.style_sheet = stylesheet
|
|
212
|
+
elsif visibility_queue.any?
|
|
213
|
+
content_changed = true
|
|
214
|
+
apply_filter_work(visibility_queue.pop)
|
|
215
|
+
elsif width_queue.any?
|
|
216
|
+
content_changed = true
|
|
217
|
+
width_queue.pop.fixed_width = branch_button_width
|
|
218
|
+
else
|
|
219
|
+
content_changed = true
|
|
220
|
+
work = render_queue.pop
|
|
221
|
+
if work.first == :entry_stage
|
|
222
|
+
process_entry_row_stage(work[2], work[1], work[3])
|
|
223
|
+
elsif work.first == :branch_complete
|
|
224
|
+
show_rendered_branch_children(work[1])
|
|
225
|
+
else
|
|
226
|
+
node, level, layout, parent_widget = work
|
|
227
|
+
render_one_node(node, level, layout: layout, parent_widget: parent_widget)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if render_queue.empty? && @render_needs_stretch
|
|
233
|
+
content_changed = true
|
|
234
|
+
host_layout.add_stretch(1)
|
|
235
|
+
@render_needs_stretch = false
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if !queues_pending?
|
|
239
|
+
@rendering = false
|
|
240
|
+
host.update if content_changed
|
|
241
|
+
render_timer.stop if render_timer.is_active
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def queues_pending?
|
|
246
|
+
render_queue.any? || width_queue.any? || visibility_queue.any? || theme_style_queue.any?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def theme_style_targets(stylesheet)
|
|
250
|
+
targets = [[toolbar, @entry_toolbar_stylesheet]]
|
|
251
|
+
unless rendering
|
|
252
|
+
branch_bindings.each do |_node_id, binding|
|
|
253
|
+
targets << [binding.fetch(:button), @entry_branch_stylesheets.fetch("entry_node_#{binding.fetch(:type)}")]
|
|
254
|
+
end
|
|
255
|
+
entry_rows.each do |row|
|
|
256
|
+
targets << [row, @entry_row_stylesheet]
|
|
257
|
+
end
|
|
258
|
+
targets << [empty_label, @entry_empty_stylesheet] if empty_label
|
|
259
|
+
end
|
|
260
|
+
targets.reject { |target, rules| target.nil? || rules.empty? }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
ENTRY_ROW_STYLE_IDS = %w[
|
|
264
|
+
entry_node_entry_row entry_node_entry_prefix entry_node_entry_time_sep
|
|
265
|
+
entry_node_entry_start entry_node_entry_end entry_node_entry_note
|
|
266
|
+
entry_node_entry_task entry_node_entry_task_editor entry_node_entry_archive
|
|
267
|
+
].freeze
|
|
268
|
+
|
|
269
|
+
def stylesheet_for_ids(stylesheet, ids)
|
|
270
|
+
id_pattern = ids.map { |id| Regexp.escape("##{id}") }.join('|')
|
|
271
|
+
stylesheet.scan(/([^{}]+)\{([^{}]*)\}/m).filter_map do |selectors, declarations|
|
|
272
|
+
selected = selectors.split(',').map(&:strip).select do |selector|
|
|
273
|
+
selector.match?(/(?:#{id_pattern})(?:\b|(?=[:\s.#>+~\[]))/)
|
|
274
|
+
end
|
|
275
|
+
next if selected.empty?
|
|
276
|
+
|
|
277
|
+
"#{selected.join(",\n")} {#{declarations}}"
|
|
278
|
+
end.join("\n")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def apply_entry_widget_styles(target, ids)
|
|
282
|
+
stylesheet = case ids
|
|
283
|
+
when %w[entries_host] then @entry_host_stylesheet
|
|
284
|
+
when ListComponent::ENTRY_ROW_STYLE_IDS then @entry_row_stylesheet
|
|
285
|
+
when %w[entry_node_empty] then @entry_empty_stylesheet
|
|
286
|
+
else @entry_branch_stylesheets&.fetch(ids.first, nil)
|
|
287
|
+
end
|
|
288
|
+
target.style_sheet = stylesheet if stylesheet && !stylesheet.empty?
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def apply_filter_work(work)
|
|
292
|
+
case work.first
|
|
293
|
+
when :branch then apply_branch_filter(work[1])
|
|
294
|
+
when :entry
|
|
295
|
+
work[2].visible = work[3]
|
|
296
|
+
row_filter_state[work[1]] = work[3]
|
|
297
|
+
when :empty
|
|
298
|
+
empty_label.text = work[1] || 'No entries for filter'
|
|
299
|
+
empty_label.visible = visible_entry_ids.empty?
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def apply_branch_filter(node_id)
|
|
304
|
+
binding = branch_bindings.fetch(node_id)
|
|
305
|
+
visible = visible_branch_ids.key?(node_id)
|
|
306
|
+
expanded_state = expanded.fetch(node_id, true)
|
|
307
|
+
binding.fetch(:button).visible = visible
|
|
308
|
+
binding.fetch(:children_container).visible = visible && expanded_state
|
|
309
|
+
label = visible_branch_labels[node_id] || binding.fetch(:label)
|
|
310
|
+
binding[:label] = label
|
|
311
|
+
binding[:filter_visible] = visible
|
|
312
|
+
binding.fetch(:button).text = branch_button_text(binding.fetch(:level), label, expanded_state)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def render_one_node(node, level, layout:, parent_widget:)
|
|
316
|
+
if node.fetch(:type) == :empty
|
|
317
|
+
apply_entry_widget_styles(empty_label, %w[entry_node_empty])
|
|
318
|
+
return render_empty_node(node)
|
|
319
|
+
end
|
|
320
|
+
return start_entry_row_render(node, level, layout: layout, parent_widget: parent_widget) if node.fetch(:type) == :entry
|
|
321
|
+
return render_node(node, level, layout: layout, parent_widget: parent_widget) unless branch_node?(node)
|
|
322
|
+
|
|
323
|
+
node_id = node.fetch(:id)
|
|
324
|
+
label = node.fetch(:label)
|
|
325
|
+
expanded_state = expanded.fetch(node_id, true)
|
|
326
|
+
button = build_branch_button(node, level, expanded_state, parent_widget: parent_widget)
|
|
327
|
+
apply_entry_widget_styles(button, [object_name_for(node)])
|
|
328
|
+
layout.add_widget(button)
|
|
329
|
+
children_container, children_layout = build_children_container(parent_widget)
|
|
330
|
+
layout.add_widget(children_container)
|
|
331
|
+
register_branch_binding(
|
|
332
|
+
node_id: node_id,
|
|
333
|
+
button: button,
|
|
334
|
+
children_container: children_container,
|
|
335
|
+
label: label,
|
|
336
|
+
level: level
|
|
337
|
+
)
|
|
338
|
+
branch_bindings[node_id][:type] = node.fetch(:type)
|
|
339
|
+
if node.fetch(:type) == :project
|
|
340
|
+
container_branch_ids[children_container.handle.address] = node_id
|
|
341
|
+
project_branch_entry_ids[node_id] = []
|
|
342
|
+
end
|
|
343
|
+
children_container.visible = false
|
|
344
|
+
button.text = branch_button_text(level, label, expanded_state)
|
|
345
|
+
button_visible = visible_branch_ids.nil? || visible_branch_ids.key?(node_id)
|
|
346
|
+
button.visible = button_visible
|
|
347
|
+
branch_bindings[node_id][:filter_visible] = button_visible
|
|
348
|
+
if expanded_state
|
|
349
|
+
render_queue << [:branch_complete, node_id]
|
|
350
|
+
enqueue_render_nodes(node.fetch(:children), level: level + 1, layout: children_layout,
|
|
351
|
+
parent_widget: children_container)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def show_rendered_branch_children(node_id)
|
|
356
|
+
binding = branch_bindings.fetch(node_id)
|
|
357
|
+
visible = visible_branch_ids.nil? || visible_branch_ids.key?(node_id)
|
|
358
|
+
expanded_state = expanded.fetch(node_id, true)
|
|
359
|
+
binding.fetch(:button).visible = visible
|
|
360
|
+
binding.fetch(:children_container).visible = visible && expanded_state
|
|
361
|
+
label = visible_branch_labels[node_id] || binding.fetch(:label)
|
|
362
|
+
binding[:label] = label
|
|
363
|
+
binding[:filter_visible] = visible
|
|
364
|
+
binding.fetch(:button).text = branch_button_text(binding.fetch(:level), label, expanded_state)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def render_empty_node(node)
|
|
368
|
+
empty_label.text = node.fetch(:label)
|
|
369
|
+
empty_label.visible = true
|
|
85
370
|
end
|
|
86
371
|
|
|
87
372
|
def branch_button_width
|
|
@@ -90,23 +375,25 @@ module QTimetrap
|
|
|
90
375
|
end
|
|
91
376
|
|
|
92
377
|
def adjust_node_widths
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
378
|
+
@width_queue = [
|
|
379
|
+
*branch_bindings.values.map { |binding| binding.fetch(:button) },
|
|
380
|
+
*leaf_labels,
|
|
381
|
+
*entry_rows
|
|
382
|
+
]
|
|
383
|
+
render_timer.start if width_queue.any? && !render_timer.is_active
|
|
97
384
|
end
|
|
98
385
|
|
|
99
386
|
def build_panel_layout
|
|
100
387
|
QVBoxLayout.new(widget).tap do |layout|
|
|
101
388
|
layout.set_contents_margins(0, 0, 0, 0)
|
|
102
|
-
layout.
|
|
389
|
+
layout.spacing = 6
|
|
103
390
|
end
|
|
104
391
|
end
|
|
105
392
|
|
|
106
393
|
def build_scroll_area
|
|
107
394
|
QScrollArea.new(widget).tap do |area|
|
|
108
|
-
area.
|
|
109
|
-
area.
|
|
395
|
+
area.object_name = 'entries_scroll'
|
|
396
|
+
area.widget_resizable = true
|
|
110
397
|
end
|
|
111
398
|
end
|
|
112
399
|
|
|
@@ -7,20 +7,23 @@ module QTimetrap
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def build_host
|
|
10
|
-
QWidget.new(scroll_area).tap { |container| container.
|
|
10
|
+
QWidget.new(scroll_area).tap { |container| container.object_name = 'entries_host' }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def build_host_layout
|
|
14
14
|
QVBoxLayout.new(host).tap do |layout|
|
|
15
15
|
layout.set_contents_margins(14, 10, 14, 10)
|
|
16
|
-
layout.
|
|
16
|
+
layout.spacing = 2
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def rebuild_host!
|
|
21
|
+
previous_host = scroll_area.take_widget
|
|
22
|
+
previous_host.delete_later if previous_host
|
|
21
23
|
@host = build_host
|
|
24
|
+
apply_entry_widget_styles(host, %w[entries_host])
|
|
22
25
|
@host_layout = build_host_layout
|
|
23
|
-
scroll_area.
|
|
26
|
+
scroll_area.widget = host
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|