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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +3 -4
  4. data/app/components/entries/branch_hierarchy_helpers.rb +4 -4
  5. data/app/components/entries/entry_row_render_helpers.rb +196 -0
  6. data/app/components/entries/leaf_archive_helpers.rb +6 -6
  7. data/app/components/entries/leaf_note_helpers.rb +18 -12
  8. data/app/components/entries/leaf_task_display_helpers.rb +15 -0
  9. data/app/components/entries/leaf_task_editor_state_helpers.rb +68 -0
  10. data/app/components/entries/leaf_task_helpers.rb +130 -46
  11. data/app/components/entries/leaf_time_helpers.rb +9 -9
  12. data/app/components/entries/list_component.rb +302 -15
  13. data/app/components/entries/list_host_helpers.rb +6 -3
  14. data/app/components/entries/list_state_helpers.rb +77 -4
  15. data/app/components/entries/list_task_editor_helpers.rb +31 -0
  16. data/app/components/entries/render_helpers.rb +2 -2
  17. data/app/components/entries/tree_helpers.rb +9 -7
  18. data/app/components/entries/tree_toolbar_helpers.rb +12 -12
  19. data/app/components/project_sidebar/archive_toggle_helpers.rb +6 -6
  20. data/app/components/project_sidebar/component.rb +18 -7
  21. data/app/components/project_sidebar/logo_helpers.rb +14 -14
  22. data/app/components/project_sidebar/project_button_helpers.rb +7 -7
  23. data/app/components/project_sidebar/task_helpers.rb +11 -11
  24. data/app/components/qt_ui_helpers.rb +9 -9
  25. data/app/components/tracker_controls/component.rb +19 -8
  26. data/app/components/tracker_controls/layout_builder.rb +35 -15
  27. data/app/components/tracker_controls/layout_helpers.rb +7 -6
  28. data/app/services/archived_entries_store.rb +11 -6
  29. data/app/styles/themes/dark/entries_list.qss +12 -4
  30. data/app/styles/themes/light/entries_list.qss +12 -4
  31. data/app/view_models/main_view_model.rb +4 -0
  32. data/app/views/main_window.rb +2 -3
  33. data/app/views/main_window_layout_builder.rb +6 -6
  34. data/app/views/main_window_runtime.rb +14 -8
  35. data/app/views/main_window_runtime_archive_helpers.rb +7 -1
  36. data/app/views/main_window_runtime_key_helpers.rb +24 -5
  37. data/app/views/main_window_runtime_render_helpers.rb +2 -1
  38. data/app/views/main_window_splitter_toggle_bootstrap_helpers.rb +1 -1
  39. data/app/views/main_window_splitter_toggle_helpers.rb +10 -10
  40. data/app/views/main_window_splitter_toggle_hover_helpers.rb +2 -2
  41. data/app/views/main_window_ui_helpers.rb +31 -3
  42. data/app/views/window_icon_loader.rb +1 -1
  43. data/lib/qtimetrap/version.rb +1 -1
  44. data/lib/rubocop/cop/qt/prefer_ruby_style_setter.rb +50 -0
  45. metadata +11 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c354f5a65349384bd932fceb8f62cb7b43927c789b67bcd57a5c064baf6754f
4
- data.tar.gz: a0b067c516cfc16874afcd1f98088e8e8dbf023c079bc26cdb390c2e6e0c6b58
3
+ metadata.gz: '018f44eb75f1594bcb9a78ac9f9cf8663901e2c3bf34d76f324f6e5a955da4d1'
4
+ data.tar.gz: b0c0f032ae783bb94d12061186c8430596ae7c5dbcef6497337113517bebc6b5
5
5
  SHA512:
6
- metadata.gz: b8a21804d830a6b48bedd2bcd3bf53b7ddc00db6d9b38174a3c6ff0a7fc63dc7b41e61ba2ac301a4aeef5c9b9d432c096bd1c1bce614ada07fb7c50a19e4d840
7
- data.tar.gz: ba9ba9ff36f90346913699562b13c0842281e5c27bab6e22f5e326638b9c49bfceca7f51039345d84a994831065c1a73bfe8891ebed147ddfea2abb0a534a11d
6
+ metadata.gz: 10055a894a818debb7f33541ca4146fe65eb46e0bb364f10d0d7d0cc8bee9d3ee0421658fd9848d186a264d4fa2bd9c1830c166c5e11cdf990ed39bdc5cb4bf7
7
+ data.tar.gz: c1b33688d08191603e914254c48a1405393dc21482041e8a9f7a5587dd104a012bd8e0ff3b0c5093be0a2743e0a2dd8982e9c0f32570fb599806ad454ee45e20
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
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
+
13
+ ## [0.1.2] - 2026-03-17
14
+
15
+ ### Added
16
+ - Editable task reassignment for individual entries.
17
+ - Archive mode with archive/restore flow for entry rows.
18
+ - Multi-select project filtering in the sidebar with recent-activity ordering for projects and tasks.
19
+ - Date/time range filtering for rendered entry nodes.
20
+ - Custom RuboCop enforcement for Ruby-style Qt setter usage in `app/**`.
21
+
22
+ ### Changed
23
+ - Switched project license to BSD-2-Clause.
24
+ - Moved specs under `spec/qtimetrap/**` and removed the RuboCop todo file.
25
+ - Improved README, packaging metadata, and release preparation.
26
+ - Tracker inputs now take focus on direct click when editing is allowed.
27
+
28
+ ### Fixed
29
+ - Preserved running tracker inputs when selecting sidebar tasks.
30
+ - Closed inline editors on outside click without relying on application-level hit-testing workarounds.
31
+ - Restored entry task editor styling and display-field behavior.
32
+ - Reduced UI regressions around inline task editing, scrolling, and blur handling.
33
+
34
+ ## [0.1.1] - 2026-03-14
35
+
36
+ ### Added
37
+ - Fedora/COPR packaging, desktop entry installation, and application icons.
38
+ - Sidebar task filtering, multi-project filtering, and archive support groundwork.
39
+ - Editable note and time updates for entry leaves.
40
+
41
+ ### Changed
42
+ - Updated runtime dependency to newer `qt` releases in the 0.1 series.
43
+
44
+ ## [0.1.0] - 2026-03-05
45
+
46
+ ### Added
47
+ - Initial release of QTimetrap as a Ruby gem with a Qt desktop UI.
48
+ - MVVM application structure with Zeitwerk autoloading, themed QSS styling, and Timetrap integration.
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.4`
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 copr enable cyjimmy264/ruby-qtimetrap
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
  ```
@@ -8,10 +8,10 @@ module QTimetrap
8
8
 
9
9
  def build_children_container(parent_widget)
10
10
  container = QWidget.new(parent_widget)
11
- container.set_object_name('entry_node_children')
11
+ container.object_name = 'entry_node_children'
12
12
  layout = QVBoxLayout.new(container)
13
13
  layout.set_contents_margins(0, 0, 0, 0)
14
- layout.set_spacing(2)
14
+ layout.spacing = 2
15
15
  [container, layout]
16
16
  end
17
17
 
@@ -32,9 +32,9 @@ module QTimetrap
32
32
  binding = branch_bindings[node_id]
33
33
  return unless binding
34
34
 
35
- binding.fetch(:children_container).set_visible(visible)
35
+ binding.fetch(:children_container).visible = visible
36
36
  text = branch_button_text(binding.fetch(:level), binding.fetch(:label), visible)
37
- binding.fetch(:button).set_text(text)
37
+ binding.fetch(:button).text = text
38
38
  end
39
39
  end
40
40
  end
@@ -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,12 +8,12 @@ module QTimetrap
8
8
 
9
9
  def build_entry_archive_button(row, node)
10
10
  QPushButton.new(row).tap do |button|
11
- button.set_object_name('entry_node_entry_archive')
12
- button.set_text('🗃')
13
- button.set_tool_tip('Toggle archive state')
14
- button.set_focus_policy(Qt::NoFocus)
15
- button.set_fixed_width(28)
16
- button.set_fixed_height(24)
11
+ button.object_name = 'entry_node_entry_archive'
12
+ button.text = '🗃'
13
+ button.tool_tip = 'Toggle archive state'
14
+ button.focus_policy = Qt::NoFocus
15
+ button.fixed_width = 28
16
+ button.fixed_height = 24
17
17
  entry_id = resolve_entry_id(node)
18
18
  button.connect('clicked') { |_| on_entry_archive&.call(entry_id) }
19
19
  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)
@@ -31,33 +32,38 @@ module QTimetrap
31
32
 
32
33
  def build_entry_row(parent_widget)
33
34
  QWidget.new(parent_widget).tap do |row|
34
- row.set_object_name('entry_node_entry_row')
35
- row.set_fixed_width(branch_button_width)
36
- row.set_fixed_height(32)
35
+ row.object_name = 'entry_node_entry_row'
36
+ row.fixed_width = branch_button_width
37
+ row.fixed_height = 32
37
38
  end
38
39
  end
39
40
 
40
41
  def build_entry_row_layout(row)
41
42
  QHBoxLayout.new(row).tap do |layout|
42
43
  layout.set_contents_margins(8, 0, 8, 0)
43
- layout.set_spacing(6)
44
+ layout.spacing = 6
44
45
  end
45
46
  end
46
47
 
47
48
  def build_entry_prefix_label(row, node, level)
48
49
  QLabel.new(row).tap do |prefix_label|
49
- prefix_label.set_object_name('entry_node_entry_prefix')
50
- prefix_label.set_text("#{indent(level)}#{node.fetch(:prefix, node.fetch(:label))}")
50
+ prefix_label.object_name = 'entry_node_entry_prefix'
51
+ prefix_label.text = "#{indent(level)}#{node.fetch(:prefix, node.fetch(:label))}"
51
52
  end
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
- note_input.set_object_name('entry_node_entry_note')
63
+ note_input.object_name = 'entry_node_entry_note'
57
64
  note_input.text = node.fetch(:note, '')
58
- note_input.set_placeholder_text('(no note)')
59
- note_input.set_read_only(true)
60
- bind_entry_note_input_events(note_input, resolve_entry_id(node))
65
+ note_input.placeholder_text = '(no note)'
66
+ note_input.read_only = true
61
67
  end
62
68
  end
63
69
 
@@ -83,12 +89,12 @@ module QTimetrap
83
89
  end
84
90
 
85
91
  def activate_entry_note_input(note_input)
86
- note_input.set_read_only(false)
92
+ note_input.read_only = false
87
93
  note_input.set_focus
88
94
  end
89
95
 
90
96
  def deactivate_entry_note_input(note_input)
91
- note_input.set_read_only(true)
97
+ note_input.read_only = true
92
98
  note_input.clear_focus
93
99
  end
94
100
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QTimetrap
4
+ module Entries
5
+ # Display-field helpers for entry task controls.
6
+ module LeafTaskDisplayHelpers
7
+ private
8
+
9
+ def sync_task_display_field(task_input, task_name)
10
+ task_input.text = task_name
11
+ task_input.tool_tip = task_name
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QTimetrap
4
+ module Entries
5
+ # State transitions for entry task display/editor widgets.
6
+ module LeafTaskEditorStateHelpers
7
+ private
8
+
9
+ def activate_entry_task_input(task_container)
10
+ close_previous_task_editor(task_container)
11
+ editor, editor_input = ensure_entry_task_editor(task_container)
12
+ show_entry_task_editor(task_container)
13
+ self.active_task_container = task_container
14
+ editor_input.read_only = false
15
+ editor.set_focus
16
+ editor_input.set_focus
17
+ editor.show_popup
18
+ end
19
+
20
+ def deactivate_entry_task_input(task_container)
21
+ editor = entry_task_editor(task_container)
22
+ editor_input = entry_task_editor_line_edit(editor)
23
+ editor_input.read_only = true
24
+ editor.hide_popup
25
+ editor.clear_focus
26
+ show_entry_task_display(task_container)
27
+ end
28
+
29
+ def handle_entry_task_focus_out(task_container)
30
+ return unless task_container
31
+
32
+ reset_entry_task_editor(task_container)
33
+ deactivate_entry_task_input(task_container)
34
+ return unless active_task_container
35
+ return unless task_container_matches?(active_task_container, task_container)
36
+
37
+ self.active_task_container = nil
38
+ end
39
+
40
+ def close_previous_task_editor(task_container)
41
+ return unless active_task_container
42
+ return if task_container_matches?(active_task_container, task_container)
43
+
44
+ handle_entry_task_focus_out(active_task_container)
45
+ end
46
+
47
+ def reset_entry_task_editor(task_container)
48
+ entry_task_editor(task_container).current_text = entry_task_display_input(task_container).text.to_s
49
+ end
50
+
51
+ def show_entry_task_editor(task_container)
52
+ entry_task_display_input(task_container).hide
53
+ entry_task_editor(task_container).show
54
+ end
55
+
56
+ def show_entry_task_display(task_container)
57
+ entry_task_editor(task_container).hide
58
+ entry_task_display_input(task_container).show
59
+ end
60
+
61
+ def sync_entry_task_display(task_container, task_name)
62
+ sync_task_display_field(entry_task_display_input(task_container), task_name)
63
+ entry_task_editor(task_container).current_text = task_name
64
+ entry_task_editor(task_container).tool_tip = task_name
65
+ end
66
+ end
67
+ end
68
+ end