qtimetrap 0.1.2 → 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 +8 -0
- data/README.md +3 -4
- data/app/components/entries/entry_row_render_helpers.rb +196 -0
- data/app/components/entries/leaf_note_helpers.rb +7 -1
- data/app/components/entries/leaf_task_display_helpers.rb +0 -5
- data/app/components/entries/leaf_task_editor_state_helpers.rb +1 -2
- data/app/components/entries/leaf_task_helpers.rb +48 -9
- data/app/components/entries/list_component.rb +297 -11
- data/app/components/entries/list_host_helpers.rb +3 -0
- data/app/components/entries/list_state_helpers.rb +18 -1
- data/app/components/project_sidebar/component.rb +11 -0
- data/app/components/project_sidebar/logo_helpers.rb +2 -2
- data/app/components/tracker_controls/component.rb +11 -0
- data/app/components/tracker_controls/layout_builder.rb +16 -4
- data/app/components/tracker_controls/layout_helpers.rb +2 -1
- data/app/services/archived_entries_store.rb +11 -6
- data/app/styles/themes/dark/entries_list.qss +1 -13
- data/app/styles/themes/light/entries_list.qss +1 -13
- data/app/view_models/main_view_model.rb +4 -0
- data/app/views/main_window.rb +1 -1
- data/app/views/main_window_runtime.rb +12 -6
- data/app/views/main_window_runtime_archive_helpers.rb +7 -1
- data/app/views/main_window_runtime_render_helpers.rb +2 -1
- data/app/views/main_window_ui_helpers.rb +29 -1
- data/lib/qtimetrap/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '018f44eb75f1594bcb9a78ac9f9cf8663901e2c3bf34d76f324f6e5a955da4d1'
|
|
4
|
+
data.tar.gz: b0c0f032ae783bb94d12061186c8430596ae7c5dbcef6497337113517bebc6b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10055a894a818debb7f33541ca4146fe65eb46e0bb364f10d0d7d0cc8bee9d3ee0421658fd9848d186a264d4fa2bd9c1830c166c5e11cdf990ed39bdc5cb4bf7
|
|
7
|
+
data.tar.gz: c1b33688d08191603e914254c48a1405393dc21482041e8a9f7a5587dd104a012bd8e0ff3b0c5093be0a2743e0a2dd8982e9c0f32570fb599806ad454ee45e20
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.3] - 2026-11-10
|
|
6
|
+
|
|
7
|
+
### Performance
|
|
8
|
+
- Cache archived-entry IDs during refresh.
|
|
9
|
+
- Render the entry list incrementally and filter existing widgets instead of rebuilding the tree.
|
|
10
|
+
- Create task editors only when activated to reduce initial widget construction.
|
|
11
|
+
- Add a startup and interaction profiling script.
|
|
12
|
+
|
|
5
13
|
## [0.1.2] - 2026-03-17
|
|
6
14
|
|
|
7
15
|
### Added
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ QTimetrap follows an MVVM-style architecture with `Zeitwerk` autoloading and a R
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
21
|
- Ruby `>= 3.2`
|
|
22
|
-
- Qt bridge gem: `qt >= 0.1.
|
|
22
|
+
- Qt bridge gem: `qt >= 0.1.7`
|
|
23
23
|
- `timetrap` CLI available as `t` (or configured via env)
|
|
24
24
|
|
|
25
25
|
## Install
|
|
@@ -38,9 +38,8 @@ Install required system libraries first:
|
|
|
38
38
|
### Fedora COPR
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
sudo dnf copr enable cyjimmy264/ruby-qt
|
|
42
|
-
sudo dnf
|
|
43
|
-
sudo dnf install ruby-qt ruby-qtimetrap
|
|
41
|
+
sudo dnf copr enable cyjimmy264/ruby-qt-stack
|
|
42
|
+
sudo dnf install ruby-qtimetrap
|
|
44
43
|
gem install timetrap
|
|
45
44
|
qtimetrap
|
|
46
45
|
```
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Builds editable entry rows in small GUI-thread steps.
|
|
6
|
+
module EntryRowRenderHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def start_entry_row_render(node, level, layout:, parent_widget:)
|
|
10
|
+
row = build_entry_row(parent_widget)
|
|
11
|
+
apply_entry_widget_styles(row, ListComponent::ENTRY_ROW_STYLE_IDS)
|
|
12
|
+
row_layout = build_entry_row_layout(row)
|
|
13
|
+
layout.add_widget(row)
|
|
14
|
+
entry_rows << row
|
|
15
|
+
entry_id = node.fetch(:entry_id, node.fetch(:id))
|
|
16
|
+
entry_rows_by_id[entry_id] = row
|
|
17
|
+
project_branch_id = container_branch_ids[parent_widget.handle.address]
|
|
18
|
+
entry_row_parent_branches[entry_id] = project_branch_id
|
|
19
|
+
project_branch_entry_ids[project_branch_id] << entry_id if project_branch_id
|
|
20
|
+
row_visible = visible_entry_ids.nil? || visible_entry_ids.key?(entry_id)
|
|
21
|
+
row_filter_state[entry_id] = row_visible
|
|
22
|
+
row.visible = row_visible
|
|
23
|
+
|
|
24
|
+
context = {
|
|
25
|
+
node: node,
|
|
26
|
+
level: level,
|
|
27
|
+
row: row,
|
|
28
|
+
row_layout: row_layout,
|
|
29
|
+
parent_layout: layout,
|
|
30
|
+
parent_widget: parent_widget
|
|
31
|
+
}
|
|
32
|
+
enqueue_entry_row_stage(context, :time_group)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def enqueue_entry_row_stage(context, stage, index = nil)
|
|
36
|
+
render_queue << [:entry_stage, stage, context, index]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def process_entry_row_stage(context, stage, index)
|
|
40
|
+
case stage
|
|
41
|
+
when :time_group then create_entry_time_group(context)
|
|
42
|
+
when :start_time then create_entry_start_time(context)
|
|
43
|
+
when :time_separator then create_entry_time_separator(context)
|
|
44
|
+
when :end_time then create_entry_end_time(context)
|
|
45
|
+
when :bind_start_time then bind_entry_start_time(context)
|
|
46
|
+
when :bind_end_time then bind_entry_end_time(context)
|
|
47
|
+
when :prefix then create_entry_prefix(context)
|
|
48
|
+
when :task_container then create_task_container(context)
|
|
49
|
+
when :task_display then create_task_display(context)
|
|
50
|
+
when :note_input then create_entry_note(context)
|
|
51
|
+
when :note_bind_mouse then bind_entry_note_mouse(context)
|
|
52
|
+
when :note_bind_key then bind_entry_note_key(context)
|
|
53
|
+
when :note_bind_return then bind_entry_note_return(context)
|
|
54
|
+
when :note_bind_focus then bind_entry_note_focus(context)
|
|
55
|
+
when :archive_button then create_entry_archive_button(context)
|
|
56
|
+
when :finish then finish_entry_row(context)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_entry_time_group(context)
|
|
61
|
+
group = QWidget.new(context.fetch(:row))
|
|
62
|
+
group.object_name = 'entry_node_entry_time_group'
|
|
63
|
+
layout = QHBoxLayout.new(group)
|
|
64
|
+
layout.set_contents_margins(0, 0, 0, 0)
|
|
65
|
+
layout.spacing = 4
|
|
66
|
+
context[:time_group] = group
|
|
67
|
+
context[:time_layout] = layout
|
|
68
|
+
context.fetch(:row_layout).add_widget(group)
|
|
69
|
+
enqueue_entry_row_stage(context, :start_time)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_entry_start_time(context)
|
|
73
|
+
node = context.fetch(:node)
|
|
74
|
+
input = build_entry_time_input(
|
|
75
|
+
context.fetch(:row),
|
|
76
|
+
object_name: 'entry_node_entry_start',
|
|
77
|
+
value: node.fetch(:start_label, '--:--'),
|
|
78
|
+
placeholder: '--:--'
|
|
79
|
+
)
|
|
80
|
+
context[:start_input] = input
|
|
81
|
+
context.fetch(:time_layout).add_widget(input)
|
|
82
|
+
enqueue_entry_row_stage(context, :time_separator)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create_entry_time_separator(context)
|
|
86
|
+
separator = build_entry_time_separator(context.fetch(:time_group))
|
|
87
|
+
context[:time_layout].add_widget(separator)
|
|
88
|
+
enqueue_entry_row_stage(context, :end_time)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create_entry_end_time(context)
|
|
92
|
+
node = context.fetch(:node)
|
|
93
|
+
input = build_entry_time_input(
|
|
94
|
+
context.fetch(:row),
|
|
95
|
+
object_name: 'entry_node_entry_end',
|
|
96
|
+
value: node.fetch(:end_label, 'running'),
|
|
97
|
+
placeholder: 'running'
|
|
98
|
+
)
|
|
99
|
+
context[:end_input] = input
|
|
100
|
+
context.fetch(:time_layout).add_widget(input)
|
|
101
|
+
enqueue_entry_row_stage(context, :bind_start_time)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def bind_entry_start_time(context)
|
|
105
|
+
bind_single_time_input(
|
|
106
|
+
context.fetch(:start_input),
|
|
107
|
+
resolve_entry_id(context.fetch(:node)),
|
|
108
|
+
context.fetch(:start_input),
|
|
109
|
+
context.fetch(:end_input)
|
|
110
|
+
)
|
|
111
|
+
enqueue_entry_row_stage(context, :bind_end_time)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def bind_entry_end_time(context)
|
|
115
|
+
bind_single_time_input(
|
|
116
|
+
context.fetch(:end_input),
|
|
117
|
+
resolve_entry_id(context.fetch(:node)),
|
|
118
|
+
context.fetch(:start_input),
|
|
119
|
+
context.fetch(:end_input)
|
|
120
|
+
)
|
|
121
|
+
enqueue_entry_row_stage(context, :prefix)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def create_entry_prefix(context)
|
|
125
|
+
label = build_entry_prefix_label(context.fetch(:row), context.fetch(:node), context.fetch(:level))
|
|
126
|
+
context.fetch(:row_layout).add_widget(label)
|
|
127
|
+
enqueue_entry_row_stage(context, :task_container)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def create_task_container(context)
|
|
131
|
+
container = QWidget.new(context.fetch(:row))
|
|
132
|
+
container.object_name = 'entry_node_entry_task_container'
|
|
133
|
+
build_entry_task_layout(container)
|
|
134
|
+
context[:task_container] = container
|
|
135
|
+
context.fetch(:row_layout).add_widget(container)
|
|
136
|
+
enqueue_entry_row_stage(context, :task_display)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def create_task_display(context)
|
|
140
|
+
node = context.fetch(:node)
|
|
141
|
+
task_name = node.fetch(:task_name, '').to_s
|
|
142
|
+
display = build_entry_task_display(context.fetch(:task_container), task_name)
|
|
143
|
+
context[:task_name] = task_name
|
|
144
|
+
context[:task_display] = display
|
|
145
|
+
task_container = context.fetch(:task_container)
|
|
146
|
+
register_task_widgets(task_container, display, nil, nil)
|
|
147
|
+
register_task_editor_config(task_container, node: context.fetch(:node), task_name: task_name)
|
|
148
|
+
display.on(:mouse_button_press) { |_| activate_entry_task_input(task_container) }
|
|
149
|
+
enqueue_entry_row_stage(context, :note_input)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def create_entry_note(context)
|
|
153
|
+
input = build_entry_note_input_widget(context.fetch(:row), context.fetch(:node))
|
|
154
|
+
context[:note_input] = input
|
|
155
|
+
context.fetch(:row_layout).add_widget(input)
|
|
156
|
+
enqueue_entry_row_stage(context, :note_bind_mouse)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def bind_entry_note_mouse(context)
|
|
160
|
+
input = context.fetch(:note_input)
|
|
161
|
+
input.on(:mouse_button_press) { |_| activate_entry_note_input(input) }
|
|
162
|
+
enqueue_entry_row_stage(context, :note_bind_key)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def bind_entry_note_key(context)
|
|
166
|
+
input = context.fetch(:note_input)
|
|
167
|
+
entry_id = resolve_entry_id(context.fetch(:node))
|
|
168
|
+
input.on(:key_press) { |event| handle_entry_note_key_press(input, entry_id, event) }
|
|
169
|
+
enqueue_entry_row_stage(context, :note_bind_return)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def bind_entry_note_return(context)
|
|
173
|
+
input = context.fetch(:note_input)
|
|
174
|
+
entry_id = resolve_entry_id(context.fetch(:node))
|
|
175
|
+
input.connect('returnPressed') { |_| handle_entry_note_commit(input, entry_id) }
|
|
176
|
+
enqueue_entry_row_stage(context, :note_bind_focus)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def bind_entry_note_focus(context)
|
|
180
|
+
context.fetch(:note_input).on(:focus_out) { |_| handle_entry_note_focus_out(context.fetch(:note_input)) }
|
|
181
|
+
enqueue_entry_row_stage(context, :archive_button)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def create_entry_archive_button(context)
|
|
185
|
+
button = build_entry_archive_button(context.fetch(:row), context.fetch(:node))
|
|
186
|
+
context.fetch(:row_layout).add_widget(button)
|
|
187
|
+
enqueue_entry_row_stage(context, :finish)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def finish_entry_row(context)
|
|
191
|
+
context.fetch(:row_layout).set_stretch(2, 1)
|
|
192
|
+
context.fetch(:row_layout).set_stretch(3, 2)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -8,6 +8,7 @@ module QTimetrap
|
|
|
8
8
|
|
|
9
9
|
def render_entry_leaf_node(node, level, layout:, parent_widget:)
|
|
10
10
|
row = build_entry_row(parent_widget)
|
|
11
|
+
apply_entry_widget_styles(row, ListComponent::ENTRY_ROW_STYLE_IDS)
|
|
11
12
|
row_layout = build_entry_row_layout(row)
|
|
12
13
|
start_input, end_input = add_entry_time_widgets(row_layout, row, node)
|
|
13
14
|
add_entry_row_widgets(row_layout, row, node, level)
|
|
@@ -52,12 +53,17 @@ module QTimetrap
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def build_entry_note_input(row, node)
|
|
56
|
+
build_entry_note_input_widget(row, node).tap do |note_input|
|
|
57
|
+
bind_entry_note_input_events(note_input, resolve_entry_id(node))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_entry_note_input_widget(row, node)
|
|
55
62
|
QLineEdit.new(row).tap do |note_input|
|
|
56
63
|
note_input.object_name = 'entry_node_entry_note'
|
|
57
64
|
note_input.text = node.fetch(:note, '')
|
|
58
65
|
note_input.placeholder_text = '(no note)'
|
|
59
66
|
note_input.read_only = true
|
|
60
|
-
bind_entry_note_input_events(note_input, resolve_entry_id(node))
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
|
|
@@ -6,14 +6,9 @@ module QTimetrap
|
|
|
6
6
|
module LeafTaskDisplayHelpers
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def reset_task_display_viewport(task_input)
|
|
10
|
-
task_input.cursor_position = 0
|
|
11
|
-
end
|
|
12
|
-
|
|
13
9
|
def sync_task_display_field(task_input, task_name)
|
|
14
10
|
task_input.text = task_name
|
|
15
11
|
task_input.tool_tip = task_name
|
|
16
|
-
reset_task_display_viewport(task_input)
|
|
17
12
|
end
|
|
18
13
|
end
|
|
19
14
|
end
|
|
@@ -8,10 +8,9 @@ module QTimetrap
|
|
|
8
8
|
|
|
9
9
|
def activate_entry_task_input(task_container)
|
|
10
10
|
close_previous_task_editor(task_container)
|
|
11
|
+
editor, editor_input = ensure_entry_task_editor(task_container)
|
|
11
12
|
show_entry_task_editor(task_container)
|
|
12
13
|
self.active_task_container = task_container
|
|
13
|
-
editor = entry_task_editor(task_container)
|
|
14
|
-
editor_input = entry_task_editor_line_edit(editor)
|
|
15
14
|
editor_input.read_only = false
|
|
16
15
|
editor.set_focus
|
|
17
16
|
editor_input.set_focus
|
|
@@ -31,25 +31,37 @@ module QTimetrap
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def build_entry_task_display(task_container, task_name)
|
|
34
|
-
|
|
34
|
+
QLabel.new(task_container).tap do |task_input|
|
|
35
35
|
task_input.object_name = 'entry_node_entry_task'
|
|
36
36
|
task_input.text = task_name
|
|
37
37
|
task_input.tool_tip = task_name
|
|
38
|
-
task_input.placeholder_text = 'task'
|
|
39
38
|
task_input.minimum_width = 220
|
|
40
|
-
task_input.
|
|
39
|
+
task_input.fixed_height = 24
|
|
41
40
|
sync_task_display_field(task_input, task_name)
|
|
42
41
|
task_container.layout.add_widget(task_input)
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
def build_entry_task_editor(task_container, task_name, node)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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]
|
|
50
|
+
end
|
|
51
|
+
|
|
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]
|
|
53
65
|
end
|
|
54
66
|
|
|
55
67
|
def configure_entry_task_editor(editor, task_container, task_name, node)
|
|
@@ -65,6 +77,31 @@ module QTimetrap
|
|
|
65
77
|
line_edit
|
|
66
78
|
end
|
|
67
79
|
|
|
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
|
|
83
|
+
|
|
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]
|
|
93
|
+
end
|
|
94
|
+
|
|
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
|
|
104
|
+
|
|
68
105
|
def build_entry_task_line_edit(task_container)
|
|
69
106
|
QLineEdit.new(task_container).tap do |line_edit|
|
|
70
107
|
line_edit.object_name = 'entry_node_entry_task_editor_input'
|
|
@@ -100,6 +137,8 @@ module QTimetrap
|
|
|
100
137
|
end
|
|
101
138
|
|
|
102
139
|
def entry_task_editor_line_edit(editor)
|
|
140
|
+
return unless editor
|
|
141
|
+
|
|
103
142
|
task_editor_line_edit_for(editor) || editor.instance_variable_get(:@qtimetrap_task_line_edit)
|
|
104
143
|
end
|
|
105
144
|
|
|
@@ -7,6 +7,7 @@ module QTimetrap
|
|
|
7
7
|
include ListHostHelpers
|
|
8
8
|
include ListStateHelpers
|
|
9
9
|
include ListTaskEditorHelpers
|
|
10
|
+
include EntryRowRenderHelpers
|
|
10
11
|
include QtUiHelpers
|
|
11
12
|
include TreeHelpers
|
|
12
13
|
include RenderHelpers
|
|
@@ -14,6 +15,7 @@ module QTimetrap
|
|
|
14
15
|
HOST_HORIZONTAL_MARGINS = 28
|
|
15
16
|
WIDTH_PADDING = 24
|
|
16
17
|
TIME_FILTER_DEBOUNCE_MS = 220
|
|
18
|
+
RENDER_SLICE_MS = 4
|
|
17
19
|
|
|
18
20
|
attr_reader :widget
|
|
19
21
|
|
|
@@ -30,16 +32,89 @@ module QTimetrap
|
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def render(nodes)
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
39
|
@rendering = true
|
|
36
40
|
@current_nodes = Array(nodes)
|
|
37
41
|
@branch_bindings = {}
|
|
38
42
|
@leaf_labels = []
|
|
39
43
|
@entry_rows = []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@
|
|
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
|
|
43
118
|
end
|
|
44
119
|
|
|
45
120
|
def update_time_range_inputs(from_at:, to_at:)
|
|
@@ -58,7 +133,34 @@ module QTimetrap
|
|
|
58
133
|
@syncing_time_filters = false
|
|
59
134
|
end
|
|
60
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
|
+
|
|
61
162
|
def shutdown
|
|
163
|
+
render_timer.stop if render_timer&.is_active
|
|
62
164
|
return unless time_filter_debounce
|
|
63
165
|
|
|
64
166
|
time_filter_debounce.stop if time_filter_debounce.is_active
|
|
@@ -70,19 +172,201 @@ module QTimetrap
|
|
|
70
172
|
:rendering, :scroll_area, :on_entry_note_change, :on_entry_task_change, :task_suggestions_for_project,
|
|
71
173
|
:on_entry_time_change, :on_entry_archive, :on_time_range_change,
|
|
72
174
|
:time_filter_from_toggle, :time_filter_to_toggle, :time_filter_from_input, :time_filter_to_input,
|
|
73
|
-
: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
|
|
74
179
|
|
|
75
180
|
def build
|
|
76
181
|
@widget = QWidget.new(parent)
|
|
77
182
|
widget.object_name = 'entries_panel'
|
|
78
183
|
panel_layout = build_panel_layout
|
|
79
184
|
@time_filter_debounce = build_time_filter_debounce_timer
|
|
80
|
-
|
|
185
|
+
@toolbar = build_toolbar(parent_widget: widget)
|
|
186
|
+
panel_layout.add_widget(toolbar)
|
|
81
187
|
@scroll_area = build_scroll_area
|
|
82
188
|
bind_scroll_resize
|
|
83
189
|
panel_layout.add_widget(scroll_area)
|
|
84
190
|
panel_layout.set_stretch(1, 1)
|
|
85
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
|
|
86
370
|
end
|
|
87
371
|
|
|
88
372
|
def branch_button_width
|
|
@@ -91,10 +375,12 @@ module QTimetrap
|
|
|
91
375
|
end
|
|
92
376
|
|
|
93
377
|
def adjust_node_widths
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
384
|
end
|
|
99
385
|
|
|
100
386
|
def build_panel_layout
|
|
@@ -18,7 +18,10 @@ module QTimetrap
|
|
|
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
26
|
scroll_area.widget = host
|
|
24
27
|
end
|
|
@@ -19,6 +19,7 @@ module QTimetrap
|
|
|
19
19
|
@active_task_container_key = nil
|
|
20
20
|
@leaf_labels = []
|
|
21
21
|
@entry_rows = []
|
|
22
|
+
@theme_style_queue = []
|
|
22
23
|
@rendering = false
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -27,6 +28,7 @@ module QTimetrap
|
|
|
27
28
|
@task_display_inputs = {}
|
|
28
29
|
@task_editors = {}
|
|
29
30
|
@task_editor_line_edits = {}
|
|
31
|
+
@task_editor_configs = {}
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def initialize_time_filter_state!
|
|
@@ -68,10 +70,23 @@ module QTimetrap
|
|
|
68
70
|
container_key = task_container_key(task_container)
|
|
69
71
|
task_containers[container_key] = task_container
|
|
70
72
|
task_display_inputs[container_key] = display_input
|
|
73
|
+
register_task_editor_widgets(task_container, editor, line_edit) if editor
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def register_task_editor_config(task_container, node:, task_name:)
|
|
77
|
+
task_editor_configs[task_container_key(task_container)] = { node: node, task_name: task_name }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def register_task_editor_widgets(task_container, editor, line_edit)
|
|
81
|
+
container_key = task_container_key(task_container)
|
|
71
82
|
task_editors[container_key] = editor
|
|
72
83
|
task_editor_line_edits[task_editor_key(editor)] = line_edit
|
|
73
84
|
end
|
|
74
85
|
|
|
86
|
+
def task_editor_config_for(task_container)
|
|
87
|
+
task_editor_configs[task_container_key(task_container)]
|
|
88
|
+
end
|
|
89
|
+
|
|
75
90
|
def task_container_key(task_container)
|
|
76
91
|
task_container.handle.address
|
|
77
92
|
end
|
|
@@ -105,11 +120,13 @@ module QTimetrap
|
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
def task_editor_line_edit_for(editor)
|
|
123
|
+
return unless editor
|
|
124
|
+
|
|
108
125
|
task_editor_line_edits[task_editor_key(editor)]
|
|
109
126
|
end
|
|
110
127
|
|
|
111
128
|
attr_reader :task_containers, :task_display_inputs, :task_editors, :task_editor_line_edits,
|
|
112
|
-
:active_task_container_key
|
|
129
|
+
:active_task_container_key, :task_editor_configs
|
|
113
130
|
end
|
|
114
131
|
end
|
|
115
132
|
end
|
|
@@ -34,6 +34,17 @@ module QTimetrap
|
|
|
34
34
|
archive_toggle_button.checked = archive_mode
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def update_tasks(tasks:, selected_project:, selected_task:)
|
|
38
|
+
render_tasks(tasks: tasks, selected_project: selected_project, selected_task: selected_task)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def theme_style_targets(stylesheet)
|
|
42
|
+
widgets = [@logo_text, @heading, tasks_heading, archive_toggle_button]
|
|
43
|
+
widgets.concat(buttons.map { |slot| slot.fetch(:view) })
|
|
44
|
+
widgets.concat(task_buttons)
|
|
45
|
+
widgets.compact.map { |target| [target, stylesheet] }
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
private
|
|
38
49
|
|
|
39
50
|
attr_reader :parent, :on_project_selected, :on_task_selected, :buttons, :buttons_layout, :task_buttons,
|
|
@@ -30,7 +30,7 @@ module QTimetrap
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def build_logo_text(parent_widget)
|
|
33
|
-
QLabel.new(parent_widget).tap do |label|
|
|
33
|
+
@logo_text = QLabel.new(parent_widget).tap do |label|
|
|
34
34
|
label.object_name = 'sidebar_logo_text'
|
|
35
35
|
label.alignment = Qt::AlignCenter
|
|
36
36
|
label.text = 'QTimetrap'
|
|
@@ -42,7 +42,7 @@ module QTimetrap
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def build_heading
|
|
45
|
-
QLabel.new(widget).tap do |label|
|
|
45
|
+
@heading = QLabel.new(widget).tap do |label|
|
|
46
46
|
label.object_name = 'sidebar_heading'
|
|
47
47
|
label.alignment = Qt::AlignCenter
|
|
48
48
|
label.text = 'PROJECTS'
|
|
@@ -40,6 +40,10 @@ module QTimetrap
|
|
|
40
40
|
project_input.read_only = running
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def theme_style_targets(stylesheet)
|
|
44
|
+
theme_widgets.compact.map { |target| [target, stylesheet] }
|
|
45
|
+
end
|
|
46
|
+
|
|
43
47
|
private
|
|
44
48
|
|
|
45
49
|
attr_reader :summary_label, :theme_button, :start_button, :stop_button
|
|
@@ -69,9 +73,16 @@ module QTimetrap
|
|
|
69
73
|
@theme_button = ui_map.fetch(:theme_button)
|
|
70
74
|
@start_button = ui_map.fetch(:start_button)
|
|
71
75
|
@stop_button = ui_map.fetch(:stop_button)
|
|
76
|
+
@theme_widgets = [
|
|
77
|
+
ui_map.fetch(:topbar), ui_map.fetch(:tracker_row), ui_map.fetch(:actions_row),
|
|
78
|
+
ui_map.fetch(:title_label), clock_label, timer_label, task_input, project_input,
|
|
79
|
+
summary_label, theme_button, start_button, stop_button, ui_map.fetch(:refresh_button)
|
|
80
|
+
]
|
|
72
81
|
configure_timer_font_fit!
|
|
73
82
|
end
|
|
74
83
|
|
|
84
|
+
attr_reader :theme_widgets
|
|
85
|
+
|
|
75
86
|
def configure_timer_font_fit!
|
|
76
87
|
apply_timer_font_fit(TIMER_FIT_SAMPLE)
|
|
77
88
|
end
|
|
@@ -15,11 +15,13 @@ module QTimetrap
|
|
|
15
15
|
def build
|
|
16
16
|
@widget = QWidget.new(parent)
|
|
17
17
|
root = build_root_layout
|
|
18
|
-
|
|
18
|
+
@topbar = build_topbar
|
|
19
|
+
root.add_widget(@topbar)
|
|
19
20
|
start_button, stop_button = build_tracker_row(root)
|
|
20
21
|
refresh_button = build_actions_row(root)
|
|
22
|
+
@refresh_button = refresh_button
|
|
21
23
|
connect_actions(start_button, stop_button, refresh_button)
|
|
22
|
-
ui_payload
|
|
24
|
+
ui_payload.merge(theme_ui_payload)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
private
|
|
@@ -35,7 +37,7 @@ module QTimetrap
|
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def build_tracker_row(root_layout)
|
|
38
|
-
row = QWidget.new(widget)
|
|
40
|
+
row = @tracker_row = QWidget.new(widget)
|
|
39
41
|
row.object_name = 'tracker_row'
|
|
40
42
|
layout = QHBoxLayout.new(row)
|
|
41
43
|
layout.set_contents_margins(14, 12, 14, 12)
|
|
@@ -46,7 +48,7 @@ module QTimetrap
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def build_actions_row(root_layout)
|
|
49
|
-
row = QWidget.new(widget)
|
|
51
|
+
row = @actions_row = QWidget.new(widget)
|
|
50
52
|
layout = QHBoxLayout.new(row)
|
|
51
53
|
layout.set_contents_margins(0, 0, 0, 0)
|
|
52
54
|
layout.spacing = 8
|
|
@@ -82,6 +84,16 @@ module QTimetrap
|
|
|
82
84
|
connect_project_input
|
|
83
85
|
end
|
|
84
86
|
|
|
87
|
+
def theme_ui_payload
|
|
88
|
+
{
|
|
89
|
+
topbar: @topbar,
|
|
90
|
+
tracker_row: @tracker_row,
|
|
91
|
+
actions_row: @actions_row,
|
|
92
|
+
title_label: @title_label,
|
|
93
|
+
refresh_button: @refresh_button
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
85
97
|
def connect_start_stop(start_button, stop_button)
|
|
86
98
|
start_button.connect('clicked') do |_|
|
|
87
99
|
callbacks.fetch(:on_start).call(@task_input.text.to_s, @project_input.text.to_s)
|
|
@@ -26,7 +26,8 @@ module QTimetrap
|
|
|
26
26
|
def configure_topbar_layout(layout, topbar)
|
|
27
27
|
layout.set_contents_margins(16, 8, 16, 8)
|
|
28
28
|
layout.spacing = 8
|
|
29
|
-
|
|
29
|
+
@title_label = build_label(topbar, 'title_label', random_title_slogan)
|
|
30
|
+
layout.add_widget(@title_label)
|
|
30
31
|
layout.add_stretch(1)
|
|
31
32
|
@clock_label = build_label(topbar, 'clock_label', nil, width: 220)
|
|
32
33
|
@clock_label.alignment = Qt::AlignCenter
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'set'
|
|
4
5
|
require 'yaml'
|
|
5
6
|
|
|
6
7
|
module QTimetrap
|
|
@@ -12,7 +13,7 @@ module QTimetrap
|
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def archived_ids
|
|
15
|
-
read_ids
|
|
16
|
+
read_ids.to_a.sort
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def archived?(entry_id)
|
|
@@ -47,19 +48,23 @@ module QTimetrap
|
|
|
47
48
|
attr_reader :path
|
|
48
49
|
|
|
49
50
|
def read_ids
|
|
50
|
-
return
|
|
51
|
+
return @ids_cache if @ids_cache
|
|
52
|
+
return @ids_cache = Set.new unless File.exist?(path)
|
|
51
53
|
|
|
52
54
|
payload = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
|
|
53
|
-
return
|
|
55
|
+
return @ids_cache = Set.new unless payload.is_a?(Hash)
|
|
54
56
|
|
|
55
|
-
Array(payload['archived_entry_ids']).filter_map { |value| Integer(value) }.uniq
|
|
57
|
+
values = Array(payload['archived_entry_ids']).filter_map { |value| Integer(value) }.uniq
|
|
58
|
+
@ids_cache = Set.new(values)
|
|
56
59
|
rescue Psych::SyntaxError, ArgumentError, TypeError
|
|
57
|
-
|
|
60
|
+
@ids_cache = Set.new
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
def write_ids(ids)
|
|
61
64
|
FileUtils.mkdir_p(File.dirname(path))
|
|
62
|
-
|
|
65
|
+
normalized_ids = ids.uniq.sort
|
|
66
|
+
File.write(path, { 'archived_entry_ids' => normalized_ids }.to_yaml)
|
|
67
|
+
@ids_cache = Set.new(normalized_ids)
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
def default_path
|
|
@@ -190,25 +190,13 @@ QLineEdit#entry_node_entry_note:focus {
|
|
|
190
190
|
padding-left: 6px;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
QLabel#entry_node_entry_task {
|
|
194
194
|
color: #94a3b8;
|
|
195
195
|
font-size: 12px;
|
|
196
196
|
background: transparent;
|
|
197
|
-
border: 0px;
|
|
198
197
|
padding-left: 2px;
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
QLineEdit#entry_node_entry_task:read-only {
|
|
202
|
-
background: transparent;
|
|
203
|
-
border: 0px;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
QLineEdit#entry_node_entry_task:focus {
|
|
207
|
-
background-color: #111827;
|
|
208
|
-
border: 1px solid #334155;
|
|
209
|
-
border-radius: 6px;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
200
|
QComboBox#entry_node_entry_task_editor {
|
|
213
201
|
color: #94a3b8;
|
|
214
202
|
font-size: 12px;
|
|
@@ -190,25 +190,13 @@ QLineEdit#entry_node_entry_note:focus {
|
|
|
190
190
|
padding-left: 6px;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
QLabel#entry_node_entry_task {
|
|
194
194
|
color: #334155;
|
|
195
195
|
font-size: 12px;
|
|
196
196
|
background: transparent;
|
|
197
|
-
border: 0px;
|
|
198
197
|
padding-left: 2px;
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
QLineEdit#entry_node_entry_task:read-only {
|
|
202
|
-
background: transparent;
|
|
203
|
-
border: 0px;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
QLineEdit#entry_node_entry_task:focus {
|
|
207
|
-
background-color: #ffffff;
|
|
208
|
-
border: 1px solid #d8e0ea;
|
|
209
|
-
border-radius: 6px;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
200
|
QComboBox#entry_node_entry_task_editor {
|
|
213
201
|
color: #334155;
|
|
214
202
|
font-size: 12px;
|
|
@@ -93,6 +93,10 @@ module QTimetrap
|
|
|
93
93
|
EntryNodesBuilder.new(entries: filtered_entries, selected_project: selected_project).build
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
def all_entry_nodes
|
|
97
|
+
EntryNodesBuilder.new(entries: entries, selected_project: '* ALL').build
|
|
98
|
+
end
|
|
99
|
+
|
|
96
100
|
private
|
|
97
101
|
|
|
98
102
|
attr_reader :gateway, :archived_entries_store
|
data/app/views/main_window.rb
CHANGED
|
@@ -46,7 +46,6 @@ module QTimetrap
|
|
|
46
46
|
@window = build_base_window
|
|
47
47
|
restore_window_geometry
|
|
48
48
|
set_window_icon
|
|
49
|
-
window.style_sheet = theme.application_stylesheet
|
|
50
49
|
ui = MainWindowLayoutBuilder.new(
|
|
51
50
|
window: window,
|
|
52
51
|
callbacks: layout_callbacks
|
|
@@ -54,6 +53,7 @@ module QTimetrap
|
|
|
54
53
|
@sidebar = ui.fetch(:sidebar)
|
|
55
54
|
@controls = ui.fetch(:controls)
|
|
56
55
|
@entries = ui.fetch(:entries)
|
|
56
|
+
apply_theme_styles
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def connect_heartbeat
|
|
@@ -56,7 +56,13 @@ module QTimetrap
|
|
|
56
56
|
update_current_fields = !view_model.running_current_sheet?
|
|
57
57
|
view_model.select_projects(projects, primary_project: project, sync_current_fields: update_current_fields)
|
|
58
58
|
view_model.current_project_name = project if update_current_fields && project != '* ALL'
|
|
59
|
-
|
|
59
|
+
sidebar.update_tasks(
|
|
60
|
+
tasks: view_model.task_names_for_selected_project,
|
|
61
|
+
selected_project: view_model.selected_project,
|
|
62
|
+
selected_task: view_model.selected_tasks.first
|
|
63
|
+
)
|
|
64
|
+
controls.update_summary(view_model.summary_line)
|
|
65
|
+
entries.filter_to(view_model.entry_nodes)
|
|
60
66
|
return unless update_current_fields
|
|
61
67
|
|
|
62
68
|
controls.update_task_input(view_model.current_sheet_input)
|
|
@@ -69,8 +75,8 @@ module QTimetrap
|
|
|
69
75
|
view_model.current_task_input = task.to_s
|
|
70
76
|
controls.update_task_input(view_model.current_sheet_input)
|
|
71
77
|
end
|
|
72
|
-
|
|
73
|
-
entries.
|
|
78
|
+
controls.update_summary(view_model.summary_line)
|
|
79
|
+
entries.filter_to(view_model.entry_nodes)
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
def handle_project_input(project_name)
|
|
@@ -81,12 +87,12 @@ module QTimetrap
|
|
|
81
87
|
|
|
82
88
|
def handle_time_range_changed(from_at, to_at)
|
|
83
89
|
view_model.update_time_range_filter(from_at: from_at, to_at: to_at)
|
|
84
|
-
|
|
90
|
+
controls.update_summary(view_model.summary_line)
|
|
85
91
|
entries.update_time_range_inputs(
|
|
86
92
|
from_at: view_model.time_filter_from_at,
|
|
87
93
|
to_at: view_model.time_filter_to_at
|
|
88
94
|
)
|
|
89
|
-
entries.
|
|
95
|
+
entries.filter_to(view_model.entry_nodes)
|
|
90
96
|
rescue StandardError => e
|
|
91
97
|
warn("[qtimetrap] update time-range failed: #{e.class}: #{e.message}")
|
|
92
98
|
end
|
|
@@ -114,7 +120,7 @@ module QTimetrap
|
|
|
114
120
|
def switch_theme!
|
|
115
121
|
@theme = theme.with_name(next_theme_name)
|
|
116
122
|
apply_theme
|
|
117
|
-
|
|
123
|
+
controls.update_theme_label(theme.name)
|
|
118
124
|
rescue StandardError => e
|
|
119
125
|
warn("[qtimetrap] save theme failed: #{e.class}: #{e.message}")
|
|
120
126
|
end
|
|
@@ -8,7 +8,13 @@ module QTimetrap
|
|
|
8
8
|
|
|
9
9
|
def handle_archive_mode_toggled(enabled)
|
|
10
10
|
view_model.archive_mode = enabled
|
|
11
|
-
|
|
11
|
+
sidebar.update_tasks(
|
|
12
|
+
tasks: view_model.task_names_for_selected_project,
|
|
13
|
+
selected_project: view_model.selected_project,
|
|
14
|
+
selected_task: view_model.selected_tasks.first
|
|
15
|
+
)
|
|
16
|
+
controls.update_summary(view_model.summary_line)
|
|
17
|
+
entries.filter_to(view_model.entry_nodes)
|
|
12
18
|
end
|
|
13
19
|
|
|
14
20
|
def handle_entry_archived(entry_id)
|
|
@@ -30,7 +30,8 @@ module QTimetrap
|
|
|
30
30
|
from_at: view_model.time_filter_from_at,
|
|
31
31
|
to_at: view_model.time_filter_to_at
|
|
32
32
|
)
|
|
33
|
-
entries.render(view_model.
|
|
33
|
+
entries.render(view_model.all_entry_nodes)
|
|
34
|
+
entries.filter_to(view_model.entry_nodes)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def render_controls(sync_sheet:)
|
|
@@ -37,9 +37,37 @@ module QTimetrap
|
|
|
37
37
|
|
|
38
38
|
def apply_theme
|
|
39
39
|
Application.configuration.theme_name = theme.name
|
|
40
|
-
|
|
40
|
+
apply_theme_styles(update_window_background: false, incremental: true)
|
|
41
41
|
settings_store.write_theme_name(theme.name)
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
def apply_theme_styles(update_window_background: true, incremental: false)
|
|
45
|
+
window.style_sheet = theme.stylesheet('application') if update_window_background
|
|
46
|
+
sidebar_stylesheet = theme.stylesheet('project_sidebar')
|
|
47
|
+
controls_stylesheet = theme.stylesheet('tracker_controls')
|
|
48
|
+
entries_stylesheet = theme.stylesheet('entries_list')
|
|
49
|
+
if incremental
|
|
50
|
+
sidebar.widget.style_sheet = stylesheet_rule(sidebar_stylesheet, 'QWidget#sidebar_panel')
|
|
51
|
+
entries.apply_theme_stylesheet(
|
|
52
|
+
entries_stylesheet,
|
|
53
|
+
external_targets: sidebar.theme_style_targets(sidebar_stylesheet) +
|
|
54
|
+
controls.theme_style_targets(controls_stylesheet),
|
|
55
|
+
defer: true
|
|
56
|
+
)
|
|
57
|
+
else
|
|
58
|
+
sidebar.widget.style_sheet = sidebar_stylesheet
|
|
59
|
+
controls.widget.style_sheet = controls_stylesheet
|
|
60
|
+
entries.apply_theme_stylesheet(entries_stylesheet)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stylesheet_rule(stylesheet, selector)
|
|
65
|
+
stylesheet.scan(/([^{}]+)\{([^{}]*)\}/m).find do |selectors, _declarations|
|
|
66
|
+
selectors.split(',').map(&:strip).include?(selector)
|
|
67
|
+
end&.then do |selectors, declarations|
|
|
68
|
+
"#{selectors.strip} {#{declarations}}"
|
|
69
|
+
end.to_s
|
|
70
|
+
end
|
|
43
71
|
end
|
|
44
72
|
end
|
|
45
73
|
end
|
data/lib/qtimetrap/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: qtimetrap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maksim Veynberg
|
|
@@ -60,6 +60,7 @@ files:
|
|
|
60
60
|
- app/assets/icons/qtimetrap-icon-512.png
|
|
61
61
|
- app/assets/icons/qtimetrap-icon.svg
|
|
62
62
|
- app/components/entries/branch_hierarchy_helpers.rb
|
|
63
|
+
- app/components/entries/entry_row_render_helpers.rb
|
|
63
64
|
- app/components/entries/leaf_archive_helpers.rb
|
|
64
65
|
- app/components/entries/leaf_note_helpers.rb
|
|
65
66
|
- app/components/entries/leaf_task_display_helpers.rb
|
|
@@ -206,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
206
207
|
- !ruby/object:Gem::Version
|
|
207
208
|
version: '0'
|
|
208
209
|
requirements: []
|
|
209
|
-
rubygems_version:
|
|
210
|
+
rubygems_version: 4.0.10
|
|
210
211
|
specification_version: 4
|
|
211
212
|
summary: Desktop Timetrap UI on Qt
|
|
212
213
|
test_files: []
|