qtimetrap 0.1.1
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 +7 -0
- data/LICENSE +25 -0
- data/README.md +92 -0
- data/Rakefile +105 -0
- data/app/assets/icons/qtimetrap-icon-128.png +0 -0
- data/app/assets/icons/qtimetrap-icon-256.png +0 -0
- data/app/assets/icons/qtimetrap-icon-512.png +0 -0
- data/app/assets/icons/qtimetrap-icon.svg +123 -0
- data/app/components/entries/branch_hierarchy_helpers.rb +41 -0
- data/app/components/entries/leaf_archive_helpers.rb +23 -0
- data/app/components/entries/leaf_note_helpers.rb +118 -0
- data/app/components/entries/leaf_task_helpers.rb +81 -0
- data/app/components/entries/leaf_time_helpers.rb +105 -0
- data/app/components/entries/list_component.rb +118 -0
- data/app/components/entries/list_host_helpers.rb +27 -0
- data/app/components/entries/list_state_helpers.rb +59 -0
- data/app/components/entries/node_presentation_helpers.rb +29 -0
- data/app/components/entries/render_helpers.rb +25 -0
- data/app/components/entries/tree_helpers.rb +133 -0
- data/app/components/entries/tree_toolbar_helpers.rb +98 -0
- data/app/components/project_sidebar/archive_toggle_helpers.rb +22 -0
- data/app/components/project_sidebar/component.rb +125 -0
- data/app/components/project_sidebar/logo_helpers.rb +53 -0
- data/app/components/project_sidebar/project_button_helpers.rb +72 -0
- data/app/components/project_sidebar/project_selection_helpers.rb +94 -0
- data/app/components/project_sidebar/task_helpers.rb +94 -0
- data/app/components/project_sidebar/task_selection_helpers.rb +72 -0
- data/app/components/qt_ui_helpers.rb +27 -0
- data/app/components/tracker_controls/component.rb +80 -0
- data/app/components/tracker_controls/layout_builder.rb +100 -0
- data/app/components/tracker_controls/layout_helpers.rb +88 -0
- data/app/models/null_settings_store.rb +12 -0
- data/app/models/time_entry.rb +59 -0
- data/app/services/archived_entries_store.rb +71 -0
- data/app/services/formatters.rb +29 -0
- data/app/services/settings_store.rb +83 -0
- data/app/services/timetrap_gateway.rb +89 -0
- data/app/services/timetrap_gateway_logger.rb +54 -0
- data/app/services/timetrap_gateway_query_helpers.rb +102 -0
- data/app/services/timetrap_gateway_start_helpers.rb +28 -0
- data/app/services/timetrap_gateway_update_note_helpers.rb +48 -0
- data/app/services/timetrap_gateway_update_task_helpers.rb +47 -0
- data/app/services/timetrap_gateway_update_time_helpers.rb +50 -0
- data/app/styles/theme.rb +61 -0
- data/app/styles/themes/dark/application.qss +8 -0
- data/app/styles/themes/dark/entries_list.qss +235 -0
- data/app/styles/themes/dark/project_sidebar.qss +99 -0
- data/app/styles/themes/dark/snippets/app_background.qss +2 -0
- data/app/styles/themes/dark/snippets/button_ghost.qss +5 -0
- data/app/styles/themes/dark/snippets/button_start.qss +5 -0
- data/app/styles/themes/dark/snippets/button_stop.qss +5 -0
- data/app/styles/themes/dark/snippets/entries_host.qss +2 -0
- data/app/styles/themes/dark/snippets/entries_scroll.qss +16 -0
- data/app/styles/themes/dark/snippets/entry_row_day.qss +6 -0
- data/app/styles/themes/dark/snippets/entry_row_detail.qss +5 -0
- data/app/styles/themes/dark/snippets/entry_row_project.qss +6 -0
- data/app/styles/themes/dark/snippets/project_button.qss +5 -0
- data/app/styles/themes/dark/snippets/project_button_active.qss +5 -0
- data/app/styles/themes/dark/snippets/project_sidebar_heading.qss +2 -0
- data/app/styles/themes/dark/snippets/project_sidebar_logo.qss +3 -0
- data/app/styles/themes/dark/snippets/project_sidebar_panel.qss +2 -0
- data/app/styles/themes/dark/snippets/tracker_clock.qss +2 -0
- data/app/styles/themes/dark/snippets/tracker_input.qss +5 -0
- data/app/styles/themes/dark/snippets/tracker_project_label.qss +5 -0
- data/app/styles/themes/dark/snippets/tracker_row.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_summary.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_timer.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_title.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_topbar.qss +2 -0
- data/app/styles/themes/dark/tracker_controls.qss +84 -0
- data/app/styles/themes/light/application.qss +8 -0
- data/app/styles/themes/light/entries_list.qss +235 -0
- data/app/styles/themes/light/project_sidebar.qss +99 -0
- data/app/styles/themes/light/snippets/app_background.qss +2 -0
- data/app/styles/themes/light/snippets/button_ghost.qss +5 -0
- data/app/styles/themes/light/snippets/button_start.qss +5 -0
- data/app/styles/themes/light/snippets/button_stop.qss +5 -0
- data/app/styles/themes/light/snippets/entries_host.qss +2 -0
- data/app/styles/themes/light/snippets/entries_scroll.qss +16 -0
- data/app/styles/themes/light/snippets/entry_row_day.qss +6 -0
- data/app/styles/themes/light/snippets/entry_row_detail.qss +5 -0
- data/app/styles/themes/light/snippets/entry_row_project.qss +6 -0
- data/app/styles/themes/light/snippets/project_button.qss +5 -0
- data/app/styles/themes/light/snippets/project_button_active.qss +5 -0
- data/app/styles/themes/light/snippets/project_sidebar_heading.qss +2 -0
- data/app/styles/themes/light/snippets/project_sidebar_logo.qss +3 -0
- data/app/styles/themes/light/snippets/project_sidebar_panel.qss +2 -0
- data/app/styles/themes/light/snippets/tracker_clock.qss +2 -0
- data/app/styles/themes/light/snippets/tracker_input.qss +5 -0
- data/app/styles/themes/light/snippets/tracker_project_label.qss +5 -0
- data/app/styles/themes/light/snippets/tracker_row.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_summary.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_timer.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_title.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_topbar.qss +2 -0
- data/app/styles/themes/light/tracker_controls.qss +84 -0
- data/app/view_models/entry_nodes_builder.rb +109 -0
- data/app/view_models/main_view_model.rb +135 -0
- data/app/view_models/main_view_model_archive_mode_helpers.rb +102 -0
- data/app/view_models/main_view_model_entry_note_helpers.rb +30 -0
- data/app/view_models/main_view_model_entry_task_helpers.rb +39 -0
- data/app/view_models/main_view_model_entry_time_helpers.rb +45 -0
- data/app/view_models/main_view_model_sheet_helpers.rb +74 -0
- data/app/view_models/main_view_model_task_filter_helpers.rb +58 -0
- data/app/view_models/main_view_model_time_range_filter_helpers.rb +23 -0
- data/app/views/main_window.rb +126 -0
- data/app/views/main_window_layout_builder.rb +114 -0
- data/app/views/main_window_runtime.rb +123 -0
- data/app/views/main_window_runtime_archive_helpers.rb +26 -0
- data/app/views/main_window_runtime_entry_task_helpers.rb +21 -0
- data/app/views/main_window_runtime_entry_time_helpers.rb +17 -0
- data/app/views/main_window_runtime_key_helpers.rb +55 -0
- data/app/views/main_window_runtime_render_helpers.rb +50 -0
- data/app/views/main_window_splitter_toggle_bootstrap_helpers.rb +34 -0
- data/app/views/main_window_splitter_toggle_helpers.rb +103 -0
- data/app/views/main_window_splitter_toggle_hover_helpers.rb +71 -0
- data/app/views/main_window_splitter_toggle_layout_helpers.rb +24 -0
- data/app/views/main_window_ui_helpers.rb +45 -0
- data/app/views/window_icon_loader.rb +37 -0
- data/bin/qtimetrap +7 -0
- data/config/application.rb +87 -0
- data/config/environments/development.rb +5 -0
- data/config/environments/production.rb +5 -0
- data/config/initializers/theme.rb +5 -0
- data/config/initializers/timetrap.rb +5 -0
- data/lib/qtimetrap/configuration.rb +15 -0
- data/lib/qtimetrap/container.rb +57 -0
- data/lib/qtimetrap/version.rb +5 -0
- data/lib/qtimetrap.rb +25 -0
- metadata +207 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1c354f5a65349384bd932fceb8f62cb7b43927c789b67bcd57a5c064baf6754f
|
|
4
|
+
data.tar.gz: a0b067c516cfc16874afcd1f98088e8e8dbf023c079bc26cdb390c2e6e0c6b58
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b8a21804d830a6b48bedd2bcd3bf53b7ddc00db6d9b38174a3c6ff0a7fc63dc7b41e61ba2ac301a4aeef5c9b9d432c096bd1c1bce614ada07fb7c50a19e4d840
|
|
7
|
+
data.tar.gz: ba9ba9ff36f90346913699562b13c0842281e5c27bab6e22f5e326638b9c49bfceca7f51039345d84a994831065c1a73bfe8891ebed147ddfea2abb0a534a11d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Maksim Veynberg
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# QTimetrap
|
|
2
|
+
|
|
3
|
+
Desktop UI for [Timetrap](https://github.com/samg/timetrap), built with Ruby + Qt.
|
|
4
|
+
|
|
5
|
+
QTimetrap follows an MVVM-style architecture with `Zeitwerk` autoloading and a Rails-like project structure.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Expandable entries tree: `week -> day -> project/task -> time entry`.
|
|
12
|
+
- Start/stop tracking from UI (single button based on running state).
|
|
13
|
+
- Edit entry `note`, `start`, and `end` directly in the list.
|
|
14
|
+
- Project/task sidebar with multi-select task filtering.
|
|
15
|
+
- Date-time interval filtering (`FROM`/`TO`) with live apply.
|
|
16
|
+
- Archive mode (soft-hide entries via local archive store, no destructive Timetrap delete).
|
|
17
|
+
- Theme switch (`light`/`dark`) with persistent selection.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Ruby `>= 3.2`
|
|
22
|
+
- Qt bridge gem: `qt >= 0.1.4`
|
|
23
|
+
- `timetrap` CLI available as `t` (or configured via env)
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
### RubyGems
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install qtimetrap timetrap
|
|
31
|
+
qtimetrap
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Note: `qtimetrap` depends on the `qt` gem with native extensions.
|
|
35
|
+
Install required system libraries first:
|
|
36
|
+
<https://github.com/CyJimmy264/qt?tab=readme-ov-file#system-requirements>.
|
|
37
|
+
|
|
38
|
+
### Fedora COPR
|
|
39
|
+
|
|
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
|
|
44
|
+
gem install timetrap
|
|
45
|
+
qtimetrap
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Run
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle install
|
|
52
|
+
bundle exec bin/qtimetrap
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If `qt` is already installed in your current `rbenv` shell, you can also run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bin/qtimetrap
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
- `QTIMETRAP_ENV`: app environment (`development` by default)
|
|
64
|
+
- `TIMETRAP_BIN`: timetrap CLI command (`t` by default)
|
|
65
|
+
- `QTIMETRAP_THEME`: initial theme (`light` by default)
|
|
66
|
+
- `QTIMETRAP_RELOAD=1`: enable Zeitwerk reloading in development
|
|
67
|
+
|
|
68
|
+
Persisted settings:
|
|
69
|
+
|
|
70
|
+
- Theme: `~/.config/qtimetrap/config.yml`
|
|
71
|
+
- Archived entry ids: `~/.local/share/qtimetrap/archived_entries.yml`
|
|
72
|
+
|
|
73
|
+
(`$XDG_CONFIG_HOME` / `$XDG_DATA_HOME` are respected.)
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
rspec
|
|
79
|
+
gem build qtimetrap.gemspec
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Main directories:
|
|
83
|
+
|
|
84
|
+
- `app/models`, `app/services`, `app/view_models`
|
|
85
|
+
- `app/views`, `app/components`
|
|
86
|
+
- `app/styles/themes/{light,dark}/*.qss`
|
|
87
|
+
- `config/`, `lib/`, `spec/`
|
|
88
|
+
|
|
89
|
+
## Packaging
|
|
90
|
+
|
|
91
|
+
- Fedora/COPR assets: `packaging/rpm`
|
|
92
|
+
- Debian/Launchpad assets: `packaging/deb`
|
data/Rakefile
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
# Installs QTimetrap desktop entry and icon theme assets under ~/.local/share.
|
|
7
|
+
module DesktopInstall
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def root
|
|
11
|
+
File.expand_path(__dir__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def home
|
|
15
|
+
Dir.home
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def applications_dir
|
|
19
|
+
File.join(home, '.local', 'share', 'applications')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def icons_root
|
|
23
|
+
File.join(home, '.local', 'share', 'icons', 'hicolor')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def desktop_file
|
|
27
|
+
File.join(applications_dir, 'qtimetrap.desktop')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def icon_svg_src
|
|
31
|
+
File.join(root, 'app', 'assets', 'icons', 'qtimetrap-icon.svg')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def icon_png_sizes
|
|
35
|
+
{
|
|
36
|
+
'128x128' => File.join(root, 'app', 'assets', 'icons', 'qtimetrap-icon-128.png'),
|
|
37
|
+
'256x256' => File.join(root, 'app', 'assets', 'icons', 'qtimetrap-icon-256.png'),
|
|
38
|
+
'512x512' => File.join(root, 'app', 'assets', 'icons', 'qtimetrap-icon-512.png')
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exec_path
|
|
43
|
+
rbenv_shim = File.join(home, '.rbenv', 'shims', 'qtimetrap')
|
|
44
|
+
ENV.fetch('QTIMETRAP_DESKTOP_EXEC', File.executable?(rbenv_shim) ? rbenv_shim : 'qtimetrap')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def install!
|
|
48
|
+
FileUtils.mkdir_p(applications_dir)
|
|
49
|
+
install_icons!
|
|
50
|
+
File.write(desktop_file, desktop_entry)
|
|
51
|
+
validate_desktop_file!
|
|
52
|
+
refresh_icon_cache!
|
|
53
|
+
puts "Installed desktop entry: #{desktop_file}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def install_icons!
|
|
57
|
+
FileUtils.mkdir_p(File.join(icons_root, 'scalable', 'apps'))
|
|
58
|
+
FileUtils.cp(icon_svg_src, File.join(icons_root, 'scalable', 'apps', 'qtimetrap.svg'))
|
|
59
|
+
icon_png_sizes.each do |size, src|
|
|
60
|
+
next unless File.file?(src)
|
|
61
|
+
|
|
62
|
+
target_dir = File.join(icons_root, size, 'apps')
|
|
63
|
+
FileUtils.mkdir_p(target_dir)
|
|
64
|
+
FileUtils.cp(src, File.join(target_dir, 'qtimetrap.png'))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def desktop_entry
|
|
69
|
+
<<~DESKTOP
|
|
70
|
+
[Desktop Entry]
|
|
71
|
+
Type=Application
|
|
72
|
+
Version=1.0
|
|
73
|
+
Name=QTimetrap
|
|
74
|
+
Comment=Desktop Timetrap UI on Qt
|
|
75
|
+
Exec=#{exec_path}
|
|
76
|
+
Icon=qtimetrap
|
|
77
|
+
Terminal=false
|
|
78
|
+
Categories=Office;
|
|
79
|
+
StartupNotify=true
|
|
80
|
+
StartupWMClass=qtimetrap
|
|
81
|
+
X-KDE-StartupNotify=true
|
|
82
|
+
DESKTOP
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_desktop_file!
|
|
86
|
+
return unless system('sh', '-lc', 'command -v desktop-file-validate >/dev/null 2>&1')
|
|
87
|
+
return if system('desktop-file-validate', desktop_file)
|
|
88
|
+
|
|
89
|
+
abort("desktop-file-validate failed for #{desktop_file}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def refresh_icon_cache!
|
|
93
|
+
return unless system('sh', '-lc', 'command -v gtk-update-icon-cache >/dev/null 2>&1')
|
|
94
|
+
return unless File.file?(File.join(icons_root, 'index.theme'))
|
|
95
|
+
|
|
96
|
+
system('gtk-update-icon-cache', '-q', '-t', icons_root)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
namespace :desktop do
|
|
101
|
+
desc 'Install QTimetrap desktop entry into ~/.local/share/applications'
|
|
102
|
+
task :install do
|
|
103
|
+
DesktopInstall.install!
|
|
104
|
+
end
|
|
105
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<svg
|
|
3
|
+
width="512"
|
|
4
|
+
height="512"
|
|
5
|
+
viewBox="0 0 512 512"
|
|
6
|
+
fill="none"
|
|
7
|
+
version="1.1"
|
|
8
|
+
id="svg8"
|
|
9
|
+
sodipodi:docname="qtimetrap-icon.svg"
|
|
10
|
+
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
|
11
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
12
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
14
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
15
|
+
<sodipodi:namedview
|
|
16
|
+
id="namedview8"
|
|
17
|
+
pagecolor="#ffffff"
|
|
18
|
+
bordercolor="#000000"
|
|
19
|
+
borderopacity="0.25"
|
|
20
|
+
inkscape:showpageshadow="2"
|
|
21
|
+
inkscape:pageopacity="0.0"
|
|
22
|
+
inkscape:pagecheckerboard="0"
|
|
23
|
+
inkscape:deskcolor="#d1d1d1"
|
|
24
|
+
inkscape:zoom="0.21728516"
|
|
25
|
+
inkscape:cx="-775.47865"
|
|
26
|
+
inkscape:cy="1320.8449"
|
|
27
|
+
inkscape:window-width="1920"
|
|
28
|
+
inkscape:window-height="1136"
|
|
29
|
+
inkscape:window-x="0"
|
|
30
|
+
inkscape:window-y="0"
|
|
31
|
+
inkscape:window-maximized="1"
|
|
32
|
+
inkscape:current-layer="svg8" />
|
|
33
|
+
<defs
|
|
34
|
+
id="defs4">
|
|
35
|
+
<linearGradient
|
|
36
|
+
id="bg"
|
|
37
|
+
x1="64"
|
|
38
|
+
y1="48"
|
|
39
|
+
x2="448"
|
|
40
|
+
y2="464"
|
|
41
|
+
gradientUnits="userSpaceOnUse">
|
|
42
|
+
<stop
|
|
43
|
+
stop-color="#0EA5E9"
|
|
44
|
+
id="stop1" />
|
|
45
|
+
<stop
|
|
46
|
+
offset="1"
|
|
47
|
+
stop-color="#2563EB"
|
|
48
|
+
id="stop2" />
|
|
49
|
+
</linearGradient>
|
|
50
|
+
<linearGradient
|
|
51
|
+
id="ring"
|
|
52
|
+
x1="132"
|
|
53
|
+
y1="118"
|
|
54
|
+
x2="386"
|
|
55
|
+
y2="394"
|
|
56
|
+
gradientUnits="userSpaceOnUse">
|
|
57
|
+
<stop
|
|
58
|
+
stop-color="#E0F2FE"
|
|
59
|
+
id="stop3" />
|
|
60
|
+
<stop
|
|
61
|
+
offset="1"
|
|
62
|
+
stop-color="#BAE6FD"
|
|
63
|
+
id="stop4" />
|
|
64
|
+
</linearGradient>
|
|
65
|
+
</defs>
|
|
66
|
+
<rect
|
|
67
|
+
x="40"
|
|
68
|
+
y="40"
|
|
69
|
+
width="432"
|
|
70
|
+
height="432"
|
|
71
|
+
rx="108"
|
|
72
|
+
fill="url(#bg)"
|
|
73
|
+
id="rect4" />
|
|
74
|
+
<!-- Q ring -->
|
|
75
|
+
<circle
|
|
76
|
+
cx="252"
|
|
77
|
+
cy="248"
|
|
78
|
+
r="128"
|
|
79
|
+
stroke="url(#ring)"
|
|
80
|
+
stroke-width="36"
|
|
81
|
+
id="circle4" />
|
|
82
|
+
<!-- Q tail -->
|
|
83
|
+
<path
|
|
84
|
+
d="m 333.45169,346.65618 60,58"
|
|
85
|
+
stroke="#e0f2fe"
|
|
86
|
+
stroke-width="34"
|
|
87
|
+
stroke-linecap="round"
|
|
88
|
+
id="path4" />
|
|
89
|
+
<!-- clock hands -->
|
|
90
|
+
<path
|
|
91
|
+
d="m 256.08439,253.93795 59.98108,-59.12082"
|
|
92
|
+
stroke="#e0f2fe"
|
|
93
|
+
stroke-width="22"
|
|
94
|
+
stroke-linecap="round"
|
|
95
|
+
id="path5" />
|
|
96
|
+
<path
|
|
97
|
+
d="m 245.67191,250.30113 -47,-30"
|
|
98
|
+
stroke="#e0f2fe"
|
|
99
|
+
stroke-width="22"
|
|
100
|
+
stroke-linecap="round"
|
|
101
|
+
id="path6" />
|
|
102
|
+
<circle
|
|
103
|
+
cx="253.72585"
|
|
104
|
+
cy="252.60225"
|
|
105
|
+
r="14"
|
|
106
|
+
fill="#e0f2fe"
|
|
107
|
+
id="circle6" />
|
|
108
|
+
<!-- extra-thick check with dark-green outline -->
|
|
109
|
+
<path
|
|
110
|
+
d="m 53.12809,385.47416 74,68 106,-114"
|
|
111
|
+
stroke="#14532d"
|
|
112
|
+
stroke-width="88"
|
|
113
|
+
stroke-linecap="round"
|
|
114
|
+
stroke-linejoin="round"
|
|
115
|
+
id="path7" />
|
|
116
|
+
<path
|
|
117
|
+
d="m 53.12809,385.47416 74,68 106,-114"
|
|
118
|
+
stroke="#22c55e"
|
|
119
|
+
stroke-width="60"
|
|
120
|
+
stroke-linecap="round"
|
|
121
|
+
stroke-linejoin="round"
|
|
122
|
+
id="path8" />
|
|
123
|
+
</svg>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Hierarchical branch container/show-hide helpers for entries tree UI.
|
|
6
|
+
module BranchHierarchyHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_children_container(parent_widget)
|
|
10
|
+
container = QWidget.new(parent_widget)
|
|
11
|
+
container.set_object_name('entry_node_children')
|
|
12
|
+
layout = QVBoxLayout.new(container)
|
|
13
|
+
layout.set_contents_margins(0, 0, 0, 0)
|
|
14
|
+
layout.set_spacing(2)
|
|
15
|
+
[container, layout]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def register_branch_binding(node_id:, button:, children_container:, label:, level:)
|
|
19
|
+
branch_bindings[node_id] = {
|
|
20
|
+
button: button,
|
|
21
|
+
children_container: children_container,
|
|
22
|
+
label: label,
|
|
23
|
+
level: level
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply_all_branch_states
|
|
28
|
+
branch_bindings.each_key { |node_id| apply_branch_state(node_id, expanded.fetch(node_id, true)) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def apply_branch_state(node_id, visible)
|
|
32
|
+
binding = branch_bindings[node_id]
|
|
33
|
+
return unless binding
|
|
34
|
+
|
|
35
|
+
binding.fetch(:children_container).set_visible(visible)
|
|
36
|
+
text = branch_button_text(binding.fetch(:level), binding.fetch(:label), visible)
|
|
37
|
+
binding.fetch(:button).set_text(text)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Entry-leaf helpers for archive action button.
|
|
6
|
+
module LeafArchiveHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_entry_archive_button(row, node)
|
|
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)
|
|
17
|
+
entry_id = resolve_entry_id(node)
|
|
18
|
+
button.connect('clicked') { |_| on_entry_archive&.call(entry_id) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Entry-leaf row builders for editable note nodes.
|
|
6
|
+
module LeafNoteHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def render_entry_leaf_node(node, level, layout:, parent_widget:)
|
|
10
|
+
row = build_entry_row(parent_widget)
|
|
11
|
+
row_layout = build_entry_row_layout(row)
|
|
12
|
+
start_input, end_input = add_entry_time_widgets(row_layout, row, node)
|
|
13
|
+
add_entry_row_widgets(row_layout, row, node, level)
|
|
14
|
+
bind_entry_time_input_events(start_input, end_input, resolve_entry_id(node))
|
|
15
|
+
apply_entry_row_stretch(row_layout)
|
|
16
|
+
entry_rows << row
|
|
17
|
+
layout.add_widget(row)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_entry_row_widgets(row_layout, row, node, level)
|
|
21
|
+
row_layout.add_widget(build_entry_prefix_label(row, node, level))
|
|
22
|
+
row_layout.add_widget(build_entry_task_input(row, node))
|
|
23
|
+
row_layout.add_widget(build_entry_note_input(row, node))
|
|
24
|
+
row_layout.add_widget(build_entry_archive_button(row, node))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply_entry_row_stretch(row_layout)
|
|
28
|
+
row_layout.set_stretch(2, 1)
|
|
29
|
+
row_layout.set_stretch(3, 2)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_entry_row(parent_widget)
|
|
33
|
+
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)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_entry_row_layout(row)
|
|
41
|
+
QHBoxLayout.new(row).tap do |layout|
|
|
42
|
+
layout.set_contents_margins(8, 0, 8, 0)
|
|
43
|
+
layout.set_spacing(6)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_entry_prefix_label(row, node, level)
|
|
48
|
+
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))}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_entry_note_input(row, node)
|
|
55
|
+
QLineEdit.new(row).tap do |note_input|
|
|
56
|
+
note_input.set_object_name('entry_node_entry_note')
|
|
57
|
+
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))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bind_entry_note_input_events(note_input, entry_id)
|
|
65
|
+
note_input.on(:mouse_button_press) { |_| activate_entry_note_input(note_input) }
|
|
66
|
+
note_input.on(:key_press) { |event| handle_entry_note_key_press(note_input, entry_id, event) }
|
|
67
|
+
note_input.connect('returnPressed') { |_| handle_entry_note_commit(note_input, entry_id) }
|
|
68
|
+
note_input.on(:focus_out) { |_| handle_entry_note_focus_out(note_input) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resolve_entry_id(node)
|
|
72
|
+
node.fetch(:entry_id, node.fetch(:id).to_s.sub('entry:', ''))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_entry_note_commit(note_input, entry_id)
|
|
76
|
+
return if note_input.is_read_only
|
|
77
|
+
|
|
78
|
+
note = note_input.text.to_s
|
|
79
|
+
deactivate_entry_note_input(note_input)
|
|
80
|
+
return unless on_entry_note_change
|
|
81
|
+
|
|
82
|
+
on_entry_note_change.call(entry_id, note)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def activate_entry_note_input(note_input)
|
|
86
|
+
note_input.set_read_only(false)
|
|
87
|
+
note_input.set_focus
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def deactivate_entry_note_input(note_input)
|
|
91
|
+
note_input.set_read_only(true)
|
|
92
|
+
note_input.clear_focus
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_entry_note_key_press(note_input, entry_id, event)
|
|
96
|
+
return unless enter_key?(event)
|
|
97
|
+
|
|
98
|
+
handle_entry_note_commit(note_input, entry_id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_entry_note_focus_out(note_input)
|
|
102
|
+
deactivate_entry_note_input(note_input)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def enter_key?(event)
|
|
106
|
+
key = event_key_code(event)
|
|
107
|
+
[Qt::Key_Return, Qt::Key_Enter].include?(key)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def event_key_code(event)
|
|
111
|
+
return event[:a] if event.is_a?(Hash) && event.key?(:a)
|
|
112
|
+
return event['a'] if event.is_a?(Hash) && event.key?('a')
|
|
113
|
+
|
|
114
|
+
event.respond_to?(:key) ? event.key : nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Entry-leaf helpers for editable task combo-box controls.
|
|
6
|
+
module LeafTaskHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_entry_task_input(row, node)
|
|
10
|
+
QComboBox.new(row).tap do |task_input|
|
|
11
|
+
task_input.set_object_name('entry_node_entry_task')
|
|
12
|
+
task_input.set_editable(true)
|
|
13
|
+
task_input.instance_variable_set(:@qtimetrap_task_line_edit, build_entry_task_line_edit(row))
|
|
14
|
+
task_input.set_line_edit(entry_task_line_edit(task_input))
|
|
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))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_entry_task_line_edit(row)
|
|
23
|
+
QLineEdit.new(row).tap do |line_edit|
|
|
24
|
+
line_edit.set_object_name('entry_node_entry_task_input')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def populate_entry_task_options(task_input, node)
|
|
29
|
+
current_task = node.fetch(:task_name, '').to_s
|
|
30
|
+
suggestions = Array(task_suggestions_for_project&.call(node.fetch(:project_name, '').to_s))
|
|
31
|
+
options = ([current_task] + suggestions.map(&:to_s)).reject(&:empty?).uniq
|
|
32
|
+
options.each { |task_name| task_input.add_item(task_name) }
|
|
33
|
+
task_input.set_current_text(current_task)
|
|
34
|
+
deactivate_entry_task_input(task_input)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def bind_entry_task_input_events(task_input, entry_id)
|
|
38
|
+
input = entry_task_line_edit(task_input)
|
|
39
|
+
input.on(:mouse_button_press) { |_| activate_entry_task_input(task_input) }
|
|
40
|
+
input.on(:key_press) { |event| handle_entry_task_key_press(task_input, entry_id, event) }
|
|
41
|
+
input.connect('returnPressed') { |_| handle_entry_task_commit(task_input, entry_id) }
|
|
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) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def activate_entry_task_input(task_input)
|
|
47
|
+
entry_task_line_edit(task_input).set_read_only(false)
|
|
48
|
+
task_input.set_focus
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def deactivate_entry_task_input(task_input)
|
|
52
|
+
entry_task_line_edit(task_input).set_read_only(true)
|
|
53
|
+
task_input.clear_focus
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_entry_task_key_press(task_input, entry_id, event)
|
|
57
|
+
return unless enter_key?(event)
|
|
58
|
+
|
|
59
|
+
handle_entry_task_commit(task_input, entry_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_entry_task_commit(task_input, entry_id, force: false)
|
|
63
|
+
return if entry_task_line_edit(task_input).is_read_only && !force
|
|
64
|
+
|
|
65
|
+
task_name = task_input.current_text.to_s
|
|
66
|
+
deactivate_entry_task_input(task_input)
|
|
67
|
+
return unless on_entry_task_change
|
|
68
|
+
|
|
69
|
+
on_entry_task_change.call(entry_id, task_name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def handle_entry_task_focus_out(task_input)
|
|
73
|
+
deactivate_entry_task_input(task_input)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def entry_task_line_edit(task_input)
|
|
77
|
+
task_input.instance_variable_get(:@qtimetrap_task_line_edit)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|