mittens_ui 0.0.14 → 0.0.16
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/Gemfile +1 -1
- data/README.md +4 -17
- data/examples/contacts.rb +33 -56
- data/examples/hn.rb +73 -0
- data/examples/keyboard_shortcut_demo.rb +71 -0
- data/examples/mig.rb +13 -0
- data/lib/mittens_ui/button.rb +4 -2
- data/lib/mittens_ui/checkbox.rb +1 -1
- data/lib/mittens_ui/colorpicker.rb +3 -3
- data/lib/mittens_ui/core.rb +110 -2
- data/lib/mittens_ui/header_bar.rb +3 -4
- data/lib/mittens_ui/knob.rb +1 -1
- data/lib/mittens_ui/label.rb +10 -0
- data/lib/mittens_ui/table_view.rb +214 -122
- data/lib/mittens_ui/textbox.rb +9 -0
- data/lib/mittens_ui/version.rb +1 -1
- data/lib/mittens_ui/web_link.rb +8 -0
- data/mittens_ui.gemspec +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce8a7d0aa1723a2134b6364e0e6fda2c2b9cd4e91b4e05c4f4ca6e948428c643
|
|
4
|
+
data.tar.gz: 34d9f15cb0cae6dedbc33bd62a8e2a137c06323d196fb607295ddd47b66196a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3528b40c3376faf23d93874d84247cfa6bcfa4b59f932a3e6000090619aa26c7dba5a2ff617f8530ca1cb62096f97ac533a4235da5ba91479e2dc9fa2601bb9d
|
|
7
|
+
data.tar.gz: b1c75473975e722ca380dda49dba9566128bc8becae1e6937a7e02a543361b367f5eb57a7b9c6837e980c95e7fa837583ce85a20c55c076ff4c3b664789ab932
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -243,27 +243,14 @@ img = MittensUi::Image.new("./assets/animation.gif")
|
|
|
243
243
|
### TableView
|
|
244
244
|
```ruby
|
|
245
245
|
table = MittensUi::TableView.new(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
["John", "john@example.com"],
|
|
249
|
-
["Jane", "jane@example.com"]
|
|
250
|
-
],
|
|
246
|
+
['Name', 'Email'],
|
|
247
|
+
[['John', 'john@example.com']]
|
|
251
248
|
)
|
|
252
249
|
|
|
253
|
-
table.add([
|
|
254
|
-
table.add(["First", "first@example.com"], :prepend)
|
|
250
|
+
table.add(['Jane', 'jane@example.com'])
|
|
255
251
|
|
|
256
|
-
table.remove_selected
|
|
257
|
-
|
|
258
|
-
puts table.row_count
|
|
259
|
-
|
|
260
|
-
puts table.selected_row.inspect
|
|
261
|
-
|
|
262
|
-
# Single click on the row
|
|
263
252
|
table.row_clicked { |row| puts row.inspect }
|
|
264
|
-
|
|
265
|
-
# Double click on the row
|
|
266
|
-
table.row_double_clicked { |row| puts row.inspect }
|
|
253
|
+
table.row_double_clicked { |row| puts "Double: #{row.inspect}" }
|
|
267
254
|
```
|
|
268
255
|
|
|
269
256
|
### Alert
|
data/examples/contacts.rb
CHANGED
|
@@ -2,6 +2,7 @@ require '../lib/mittens_ui'
|
|
|
2
2
|
|
|
3
3
|
app_options = { name: 'contacts', title: 'Contacts', height: 680, width: 600, can_resize: true }.freeze
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
MittensUi::Application.Window(app_options) do
|
|
6
7
|
puts MittensUi::Application.store.get(:last_selected_contact)
|
|
7
8
|
|
|
@@ -25,63 +26,39 @@ MittensUi::Application.Window(app_options) do
|
|
|
25
26
|
# --- Contacts Table ---
|
|
26
27
|
MittensUi::Separator.new(:horizontal, top: 2, bottom: 2)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
['Lucas Jackson', 'lucas.j@email.com', '555-1011'],
|
|
44
|
-
['Charlotte White', 'charlotte.w@email.com', '555-1012'],
|
|
45
|
-
['Henry Harris', 'henry.h@email.com', '555-1013'],
|
|
46
|
-
['Amelia Martin', 'amelia.m@email.com', '555-1014'],
|
|
47
|
-
['Alexander Thompson', 'alex.t@email.com', '555-1015'],
|
|
48
|
-
['Evelyn Garcia', 'evelyn.g@email.com', '555-1016'],
|
|
49
|
-
['William Martinez', 'will.m@email.com', '555-1017'],
|
|
50
|
-
['Harper Robinson', 'harper.r@email.com', '555-1018'],
|
|
51
|
-
['Daniel Clark', 'dan.clark@email.com', '555-1019'],
|
|
52
|
-
['Abigail Rodriguez', 'abigail.r@email.com', '555-1020'],
|
|
53
|
-
['Matthew Lewis', 'matt.lewis@email.com', '555-1021'],
|
|
54
|
-
['Ella Lee', 'ella.lee@email.com', '555-1022'],
|
|
55
|
-
['David Walker', 'd.walker@email.com', '555-1023'],
|
|
56
|
-
['Scarlett Hall', 'scarlett.h@email.com', '555-1024'],
|
|
57
|
-
['Joseph Allen', 'j.allen@email.com', '555-1025'],
|
|
58
|
-
['Grace Young', 'grace.y@email.com', '555-1026'],
|
|
59
|
-
['Samuel King', 'sam.king@email.com', '555-1027'],
|
|
60
|
-
['Chloe Wright', 'chloe.w@email.com', '555-1028'],
|
|
61
|
-
['Andrew Scott', 'andrew.s@email.com', '555-1029'],
|
|
62
|
-
['Victoria Green', 'victoria.g@email.com', '555-1030'],
|
|
63
|
-
['Joshua Adams', 'josh.adams@email.com', '555-1031'],
|
|
64
|
-
['Lily Baker', 'lily.b@email.com', '555-1032'],
|
|
65
|
-
['Ryan Nelson', 'ryan.n@email.com', '555-1033'],
|
|
66
|
-
['Zoey Carter', 'zoey.c@email.com', '555-1034'],
|
|
67
|
-
['Nathan Mitchell', 'nathan.m@email.com', '555-1035'],
|
|
68
|
-
['Hannah Perez', 'hannah.p@email.com', '555-1036'],
|
|
69
|
-
['Aaron Roberts', 'aaron.r@email.com', '555-1037'],
|
|
70
|
-
['Sofia Turner', 'sofia.t@email.com', '555-1038'],
|
|
71
|
-
['Caleb Phillips', 'caleb.p@email.com', '555-1039'],
|
|
72
|
-
['Avery Campbell', 'avery.c@email.com', '555-1040'],
|
|
73
|
-
['Ethan Parker', 'ethan.p@email.com', '555-1041'],
|
|
74
|
-
['Madison Evans', 'madison.e@email.com', '555-1042'],
|
|
75
|
-
['Logan Edwards', 'logan.e@email.com', '555-1043'],
|
|
76
|
-
['Layla Collins', 'layla.c@email.com', '555-1044'],
|
|
77
|
-
['Noah Stewart', 'noah.s@email.com', '555-1045'],
|
|
78
|
-
['Riley Sanchez', 'riley.s@email.com', '555-1046'],
|
|
79
|
-
['Jack Morris', 'jack.m@email.com', '555-1047'],
|
|
80
|
-
['Aria Rogers', 'aria.r@email.com', '555-1048'],
|
|
81
|
-
['Sebastian Reed', 'sebastian.r@email.com', '555-1049'],
|
|
82
|
-
['Nora Cook', 'nora.c@email.com', '555-1050']
|
|
29
|
+
first_names = %w[
|
|
30
|
+
John Jane Michael Emily Chris Olivia Daniel Sophia James Isabella
|
|
31
|
+
Benjamin Mia Lucas Charlotte Henry Amelia Alexander Evelyn William Harper
|
|
32
|
+
Daniel Abigail Matthew Ella David Scarlett Joseph Grace Samuel Chloe
|
|
33
|
+
Andrew Victoria Joshua Lily Ryan Zoey Nathan Hannah Aaron Sofia
|
|
34
|
+
Caleb Avery Ethan Madison Logan Layla Noah Riley Jack Aria
|
|
35
|
+
Sebastian Nora Liam Ava Mason Ella Ethan Harper Logan Grace
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
last_names = %w[
|
|
39
|
+
Smith Johnson Brown Taylor Anderson Thomas Jackson White Harris Martin
|
|
40
|
+
Thompson Garcia Martinez Robinson Clark Rodriguez Lewis Lee Walker Hall
|
|
41
|
+
Allen Young King Wright Scott Green Adams Baker Nelson Carter Mitchell
|
|
42
|
+
Perez Roberts Turner Phillips Campbell Parker Evans Edwards Collins Stewart
|
|
43
|
+
Sanchez Morris Rogers Reed Cook Morgan Bell Murphy Bailey Rivera Cooper
|
|
83
44
|
]
|
|
84
|
-
|
|
45
|
+
|
|
46
|
+
contacts_data = 1000.times.map do |i|
|
|
47
|
+
first = first_names.sample
|
|
48
|
+
last = last_names.sample
|
|
49
|
+
|
|
50
|
+
name = "#{first} #{last}"
|
|
51
|
+
email = "#{first.downcase}.#{last.downcase}#{i}@example.com"
|
|
52
|
+
phone = "555-#{1000 + i}"
|
|
53
|
+
|
|
54
|
+
[name, email, phone]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
contacts_table = MittensUi::TableView.new(
|
|
58
|
+
['Name', 'Email', 'Phone'],
|
|
59
|
+
contacts_data
|
|
60
|
+
)
|
|
61
|
+
|
|
85
62
|
contacts_table.row_double_clicked do |row|
|
|
86
63
|
puts "Double clicked: #{row.inspect}"
|
|
87
64
|
end
|
data/examples/hn.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require '../lib/mittens_ui'
|
|
7
|
+
|
|
8
|
+
class HackerNewsClient
|
|
9
|
+
BASE_URL = 'https://hacker-news.firebaseio.com/v0'
|
|
10
|
+
|
|
11
|
+
def self.get_json(path)
|
|
12
|
+
uri = URI("#{BASE_URL}/#{path}")
|
|
13
|
+
res = Net::HTTP.get_response(uri)
|
|
14
|
+
JSON.parse(res.body)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.top_story_ids
|
|
18
|
+
get_json('topstories.json')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.get_item(id)
|
|
22
|
+
get_json("item/#{id}.json")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.top_stories(limit = 20)
|
|
26
|
+
top_story_ids.first(limit).map do |id|
|
|
27
|
+
get_item(id)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.map_stories_to_rows(stories_data)
|
|
32
|
+
stories_data.map do |story|
|
|
33
|
+
url = story['url'] || "https://news.ycombinator.com/item?id=#{story['id']}"
|
|
34
|
+
|
|
35
|
+
[
|
|
36
|
+
story['title'] || 'N/A',
|
|
37
|
+
story['by'] || 'N/A',
|
|
38
|
+
story['score'] || 0,
|
|
39
|
+
url
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
MittensUi::Application.Window(
|
|
46
|
+
name: 'hn_viewer',
|
|
47
|
+
title: 'HN - Top Hacker News Stories',
|
|
48
|
+
width: 900,
|
|
49
|
+
height: 600
|
|
50
|
+
) do
|
|
51
|
+
|
|
52
|
+
stories = HackerNewsClient.top_stories(100)
|
|
53
|
+
rows = HackerNewsClient.map_stories_to_rows(stories)
|
|
54
|
+
|
|
55
|
+
top_stories_table = MittensUi::TableView.new(
|
|
56
|
+
%w[Title Author Score URL].freeze,
|
|
57
|
+
rows,
|
|
58
|
+
{ page_threshold: 20, page_size: 10 }.freeze
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
top_stories_table.row_double_clicked do |row|
|
|
62
|
+
link = MittensUi::WebLink.new('', row[3])
|
|
63
|
+
link.open_url
|
|
64
|
+
link.remove
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
MittensUi::Button.new(title: 'Refresh Stories').click do |btn|
|
|
68
|
+
btn.loading do
|
|
69
|
+
stories = HackerNewsClient.top_stories(5)
|
|
70
|
+
top_stories_table.update_data(HackerNewsClient.map_stories_to_rows(stories))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require '../lib/mittens_ui'
|
|
2
|
+
|
|
3
|
+
app_options = {
|
|
4
|
+
name: 'shortcut_demo',
|
|
5
|
+
title: 'Keyboard Shortcut Demo',
|
|
6
|
+
height: 400,
|
|
7
|
+
width: 500,
|
|
8
|
+
can_resize: false,
|
|
9
|
+
theme: :light
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
MittensUi::Application.Window(app_options) do
|
|
13
|
+
|
|
14
|
+
press_counter = 0
|
|
15
|
+
status = MittensUi::Label.new('Press a shortcut key...', expand: true, margin: 12)
|
|
16
|
+
|
|
17
|
+
MittensUi::Separator.new(:horizontal, top: 8, bottom: 8)
|
|
18
|
+
|
|
19
|
+
d_btn = MittensUi::Button.new(title: 'Focus me → ctrl+d (instant)')
|
|
20
|
+
d_btn.keyboard_shortcut("ctrl", "d") do
|
|
21
|
+
press_counter += 1
|
|
22
|
+
status.text = "ctrl+d fired! x#{press_counter.to_s}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
MittensUi::Separator.new(:horizontal, top: 8, bottom: 8)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
s_btn = MittensUi::Button.new(title: 'Focus me → ctrl+s (2s delay)')
|
|
29
|
+
s_btn.keyboard_shortcut("ctrl", "s", timer: 2) do
|
|
30
|
+
press_counter += 1
|
|
31
|
+
status.text = "ctrl+s fired after 2s! x#{press_counter}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
MittensUi::Separator.new(:horizontal, top: 8, bottom: 8)
|
|
35
|
+
|
|
36
|
+
tb = MittensUi::Textbox.new(multiline: false, width: :full, placeholder: "write text, press alt+r to clear it!")
|
|
37
|
+
tb.keyboard_shortcut("alt", "r") do
|
|
38
|
+
tb.text = ""
|
|
39
|
+
press_counter += 1
|
|
40
|
+
status.text = "alt+r fired — text field cleared! x#{press_counter}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# can register multiple keyboard_shortcuts for the same Widget.
|
|
44
|
+
tb.keyboard_shortcut("alt", "u") do
|
|
45
|
+
tb.text = tb.text.upcase
|
|
46
|
+
press_counter += 1
|
|
47
|
+
status.text = "alt+u fired - text uppercased! x#{press_counter}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
tb.keyboard_shortcut("alt", "d") do
|
|
51
|
+
tb.text = tb.text.downcase
|
|
52
|
+
press_counter += 1
|
|
53
|
+
status.text = "alt+d fired - text downcased! x#{press_counter}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
MittensUi::Separator.new(:horizontal, top: 8, bottom: 8)
|
|
57
|
+
|
|
58
|
+
all = []
|
|
59
|
+
inspection_button = MittensUi::Button.new(title: 'Inspect Shortcuts')
|
|
60
|
+
|
|
61
|
+
inspection_button.click do
|
|
62
|
+
all = [d_btn.shortcuts, s_btn.shortcuts, tb.shortcuts].flatten
|
|
63
|
+
status.text = "Registered: #{all.join(', ')}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
inspection_button.keyboard_shortcut("ctrl", "s") do
|
|
67
|
+
status.text = "Registered: #{ all.map{ |cmd| cmd.upcase }.join(', ') }"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
MittensUi::Separator.new(:horizontal, top: 8, bottom: 8)
|
|
71
|
+
end
|
data/examples/mig.rb
CHANGED
|
@@ -109,4 +109,17 @@ MittensUi::Application.Window(app_options) do
|
|
|
109
109
|
MittensUi::Separator.new(:horizontal, top: 10, bottom: 100)
|
|
110
110
|
MittensUi::RadioButton.new({ options: %w[Red Green Blue], layout: :horizontal, bottom: 50 })
|
|
111
111
|
|
|
112
|
+
MittensUi::Separator.new(:horizontal, top: 10, bottom: 10)
|
|
113
|
+
|
|
114
|
+
status = MittensUi::Label.new('Press a shortcut key...', expand: true, margin: 12)
|
|
115
|
+
|
|
116
|
+
MittensUi::Separator.new(:horizontal, top: 8, bottom: 8)
|
|
117
|
+
|
|
118
|
+
# Ctrl+D fires instantly on the button
|
|
119
|
+
d_btn = MittensUi::Button.new(title: 'Focus me → Ctrl+D (instant)')
|
|
120
|
+
d_btn.keyboard_shortcut("ctrl", "d") do
|
|
121
|
+
status.text = "Ctrl+D fired!"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
|
|
112
125
|
end
|
data/lib/mittens_ui/button.rb
CHANGED
|
@@ -38,8 +38,8 @@ module MittensUi
|
|
|
38
38
|
# @option options [Symbol] :width (:full) column width in the layout grid
|
|
39
39
|
# @option options [Boolean] :defer_render (false) skip auto-rendering into layout
|
|
40
40
|
def initialize(options = {})
|
|
41
|
-
button_title = options
|
|
42
|
-
icon_type = options
|
|
41
|
+
button_title = options.fetch(:title, 'Button')
|
|
42
|
+
icon_type = options.fetch(:icon, nil)
|
|
43
43
|
|
|
44
44
|
@loading = false
|
|
45
45
|
@button = Gtk::Button.new
|
|
@@ -53,7 +53,9 @@ module MittensUi
|
|
|
53
53
|
|
|
54
54
|
@label = Gtk::Label.new(button_title)
|
|
55
55
|
@box.append(@label)
|
|
56
|
+
@box.set_halign(:center)
|
|
56
57
|
@box.append(@spinner)
|
|
58
|
+
|
|
57
59
|
@button.set_child(@box)
|
|
58
60
|
@spinner.hide
|
|
59
61
|
|
data/lib/mittens_ui/checkbox.rb
CHANGED
|
@@ -24,7 +24,7 @@ module MittensUi
|
|
|
24
24
|
# @option options [Symbol] :width (:full) column width in the layout grid
|
|
25
25
|
# @option options [Boolean] :defer_render (false) skip auto-rendering into layout
|
|
26
26
|
def initialize(options = {})
|
|
27
|
-
label = options
|
|
27
|
+
label = options.fetch(:label, 'Checkbox')
|
|
28
28
|
@value = nil
|
|
29
29
|
@checkbox = Gtk::CheckButton.new
|
|
30
30
|
@checkbox.set_label(label.to_s)
|
|
@@ -38,9 +38,9 @@ module MittensUi
|
|
|
38
38
|
# @yield [picker] called only if the user selected a color
|
|
39
39
|
# @yieldparam picker [MittensUi::ColorPicker] the picker with color data
|
|
40
40
|
def initialize(options = {}, &block)
|
|
41
|
-
@title = options
|
|
42
|
-
@default = options
|
|
43
|
-
@alpha = options
|
|
41
|
+
@title = options.fetch(:title, 'Pick a Color')
|
|
42
|
+
@default = options.fetch(:default, nil)
|
|
43
|
+
@alpha = options.fetch(:alpha, false)
|
|
44
44
|
@selected = false
|
|
45
45
|
@color = nil
|
|
46
46
|
|
data/lib/mittens_ui/core.rb
CHANGED
|
@@ -49,11 +49,13 @@ module MittensUi
|
|
|
49
49
|
# @option options [Integer] :bottom bottom margin in pixels
|
|
50
50
|
# @option options [Integer] :right right margin in pixels
|
|
51
51
|
def initialize(widget, options = {})
|
|
52
|
-
@core_widget = widget
|
|
52
|
+
@core_widget = widget # This represents the GTK widget.
|
|
53
|
+
@core_widget.focusable = true
|
|
53
54
|
@width = options[:width] || :full
|
|
54
55
|
@defer_render = options[:defer_render] || false
|
|
55
56
|
set_margin_from_opts_for(@core_widget, options)
|
|
56
57
|
render unless @defer_render
|
|
58
|
+
enable_hover_focus
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
# Shows the widget if it is hidden.
|
|
@@ -108,7 +110,7 @@ module MittensUi
|
|
|
108
110
|
# Adds the widget to the application layout grid.
|
|
109
111
|
# Called automatically during initialization unless +:defer_render+ is true.
|
|
110
112
|
# Can be overridden in subclasses that require special placement
|
|
111
|
-
# (
|
|
113
|
+
# ({HeaderBar}, {Notify}).
|
|
112
114
|
#
|
|
113
115
|
# @return [void]
|
|
114
116
|
def render
|
|
@@ -119,5 +121,111 @@ module MittensUi
|
|
|
119
121
|
MittensUi::Application.layout.add(@core_widget, width: @width)
|
|
120
122
|
end
|
|
121
123
|
end
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Registers a simple keyboard shortcut on +@core_wiget+ using a modifier + key combination.
|
|
127
|
+
#
|
|
128
|
+
# Attaches a +Gtk::EventControllerKey+ to the widget that listens for the given
|
|
129
|
+
# key combination. The widget must have focus for the shortcut to fire. If a
|
|
130
|
+
# +timer+ is provided, the block is executed after the specified delay via
|
|
131
|
+
# +GLib::Timeout+.
|
|
132
|
+
#
|
|
133
|
+
# @param modifier [String] The modifier key. Accepts "ctrl", "shift", "alt", or "super".
|
|
134
|
+
# @param key [String] The key name as a GTK key string ("d", "s", "Return", "Escape").
|
|
135
|
+
# @param timer [Integer] Seconds to wait before invoking the block. Defaults to 0 (immediate).
|
|
136
|
+
# @param block [Proc] The block to execute when the shortcut is triggered.
|
|
137
|
+
#
|
|
138
|
+
# @example Instant shortcut
|
|
139
|
+
# btn.keyboard_shortcut("ctrl", "d") { puts "Ctrl+D fired" }
|
|
140
|
+
#
|
|
141
|
+
# @example Delayed shortcut
|
|
142
|
+
# btn.keyboard_shortcut("ctrl", "s", timer: 2) { puts "fired after 2s" }
|
|
143
|
+
#
|
|
144
|
+
# @return [void]
|
|
145
|
+
def keyboard_shortcut(modifier, key, timer: 0, &block)
|
|
146
|
+
@shortcut_controllers ||= []
|
|
147
|
+
|
|
148
|
+
controller = Gtk::EventControllerKey.new
|
|
149
|
+
|
|
150
|
+
controller.signal_connect("key-pressed") do |_ctrl, keyval, _keycode, state|
|
|
151
|
+
mod_match = case modifier.to_s.downcase
|
|
152
|
+
when "ctrl" then (state & Gdk::ModifierType::CONTROL_MASK).nonzero?
|
|
153
|
+
when "shift" then (state & Gdk::ModifierType::SHIFT_MASK).nonzero?
|
|
154
|
+
when "alt" then (state & Gdk::ModifierType::ALT_MASK).nonzero?
|
|
155
|
+
when "super" then (state & Gdk::ModifierType::SUPER_MASK).nonzero?
|
|
156
|
+
else false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
key_match = (keyval == Gdk::Keyval.from_name(key))
|
|
160
|
+
|
|
161
|
+
if mod_match && key_match
|
|
162
|
+
if timer > 0
|
|
163
|
+
GLib::Timeout.add(timer * 1000) do
|
|
164
|
+
block.call
|
|
165
|
+
GLib::Source::REMOVE
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
block.call
|
|
169
|
+
end
|
|
170
|
+
true # consume the event
|
|
171
|
+
else
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
@shortcut_controllers << { modifier: modifier, key: key, controller: controller }
|
|
177
|
+
@core_widget.add_controller(controller)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Removes a previously registered keyboard shortcut from +@core_widget+.
|
|
181
|
+
#
|
|
182
|
+
# Finds the shortcut matching the given +modifier+ and +key+ combination,
|
|
183
|
+
# detaches its +Gtk::EventControllerKey+ from the underlying GTK widget.
|
|
184
|
+
#
|
|
185
|
+
# If no matching shortcut is found, this is a no-op.
|
|
186
|
+
#
|
|
187
|
+
# @param modifier [String] The modifier key used when registering the shortcut ("ctrl").
|
|
188
|
+
# @param key [String] The key name used when registering the shortcut ("d").
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# btn.remove_shortcut("ctrl", "d")
|
|
192
|
+
#
|
|
193
|
+
# @return [void]
|
|
194
|
+
def remove_keyboard_shortcut(modifier, key)
|
|
195
|
+
@shortcut_controllers&.reject! do |entry|
|
|
196
|
+
if entry[:modifier] == modifier && entry[:key] == key
|
|
197
|
+
@gtk_widget.remove_controller(entry[:controller])
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Returns a list of all keyboard shortcuts registered on +@core_wiget+.
|
|
204
|
+
#
|
|
205
|
+
# Each shortcut is represented as a human-readable string in the format
|
|
206
|
+
# "modifier+key" ("ctrl+d", "alt+r").
|
|
207
|
+
#
|
|
208
|
+
# @example
|
|
209
|
+
# btn.shortcuts # => ["ctrl+d", "ctrl+s"]
|
|
210
|
+
#
|
|
211
|
+
# @return [Array<String>] Registered shortcuts, or an empty array if none.
|
|
212
|
+
def shortcuts
|
|
213
|
+
@shortcut_controllers&.map { |e| "#{e[:modifier]}+#{e[:key]}" } || []
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
# Grants focus to the +@core_wiget+ when the mouse pointer enters it.
|
|
219
|
+
#
|
|
220
|
+
# Attaches a +Gtk::EventControllerMotion+ and sets +focusable+ to +true+,
|
|
221
|
+
# which is required for +grab_focus+ to work with GTK4 wigets.
|
|
222
|
+
#
|
|
223
|
+
# @return [void]
|
|
224
|
+
def enable_hover_focus
|
|
225
|
+
@core_widget.focusable = true
|
|
226
|
+
motion = Gtk::EventControllerMotion.new
|
|
227
|
+
motion.signal_connect("enter") { @core_widget.grab_focus }
|
|
228
|
+
@core_widget.add_controller(motion)
|
|
229
|
+
end
|
|
122
230
|
end
|
|
123
231
|
end
|
|
@@ -25,8 +25,8 @@ module MittensUi
|
|
|
25
25
|
# Accepted values are +:left+ and +:right+
|
|
26
26
|
# @option options [Boolean] :defer_render (false) skip auto-rendering into layout
|
|
27
27
|
def initialize(widgets, options = {})
|
|
28
|
-
title = options
|
|
29
|
-
position = options
|
|
28
|
+
title = options.fetch(:title, '')
|
|
29
|
+
position = options.fetch(:position, :left)
|
|
30
30
|
|
|
31
31
|
@header = Gtk::HeaderBar.new
|
|
32
32
|
|
|
@@ -36,9 +36,8 @@ module MittensUi
|
|
|
36
36
|
title_label = Gtk::Label.new(title)
|
|
37
37
|
@header.title_widget = title_label
|
|
38
38
|
|
|
39
|
-
|
|
40
39
|
box = Gtk::Box.new(:horizontal, 0)
|
|
41
|
-
box.add_css_class(
|
|
40
|
+
box.add_css_class('linked')
|
|
42
41
|
|
|
43
42
|
widgets.each do |w|
|
|
44
43
|
w.remove
|
data/lib/mittens_ui/knob.rb
CHANGED
data/lib/mittens_ui/label.rb
CHANGED
|
@@ -32,5 +32,15 @@ module MittensUi
|
|
|
32
32
|
gtk_label = Gtk::Label.new(text)
|
|
33
33
|
super(gtk_label, options)
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# @return [String] returns the current label text.
|
|
37
|
+
def text
|
|
38
|
+
@core_widget.label
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param value [String] A String value that the label gets set to
|
|
42
|
+
def text=(value)
|
|
43
|
+
@core_widget.set_label(value.to_s)
|
|
44
|
+
end
|
|
35
45
|
end
|
|
36
46
|
end
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
require 'mittens_ui/core'
|
|
4
4
|
|
|
5
5
|
module MittensUi
|
|
6
|
-
# A simple,
|
|
6
|
+
# A simple, custom table widget built on Gtk::Grid.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
# -
|
|
10
|
-
# - Row selection
|
|
11
|
-
# - Single
|
|
12
|
-
# -
|
|
8
|
+
# Features:
|
|
9
|
+
# - Sorting with ▲ ▼ indicators
|
|
10
|
+
# - Row selection (mouse + keyboard)
|
|
11
|
+
# - Single & double click callbacks
|
|
12
|
+
# - Pagination for large datasets (auto-enabled > 500 rows)
|
|
13
|
+
# - Built-in pagination UI (Prev / Next buttons + page indicator)
|
|
13
14
|
# - Dark mode friendly styling
|
|
14
15
|
#
|
|
15
16
|
# @example Basic usage
|
|
@@ -21,23 +22,31 @@ module MittensUi
|
|
|
21
22
|
# @example Add row
|
|
22
23
|
# table.add(['Jane', 'jane@example.com'])
|
|
23
24
|
#
|
|
24
|
-
# @example
|
|
25
|
-
#
|
|
26
|
-
# table.row_double_clicked { |row| puts "Double: #{row.inspect}" }
|
|
25
|
+
# @example Pagination (automatic)
|
|
26
|
+
# # Pagination UI appears automatically when data > 500 rows, unless you change it by using options hash.
|
|
27
27
|
#
|
|
28
28
|
class TableView < Core
|
|
29
29
|
attr_reader :data, :headers, :selected_row_idx
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
PAGE_THRESHOLD = 500
|
|
32
|
+
PAGE_SIZE = 100
|
|
33
|
+
|
|
34
34
|
def initialize(headers = [], data = [], options = {})
|
|
35
35
|
@headers = headers
|
|
36
36
|
@data = data
|
|
37
|
+
|
|
37
38
|
@row_widgets = []
|
|
38
39
|
@header_labels = []
|
|
39
40
|
@selected_row_idx = nil
|
|
40
41
|
@sort_directions = {}
|
|
42
|
+
@current_page = 0
|
|
43
|
+
|
|
44
|
+
@page_threshold = options.fetch(:page_threshold, PAGE_THRESHOLD)
|
|
45
|
+
@page_size = options.fetch(:page_size, PAGE_SIZE)
|
|
46
|
+
|
|
47
|
+
# ---------------------------
|
|
48
|
+
# GTK Structure
|
|
49
|
+
# ---------------------------
|
|
41
50
|
|
|
42
51
|
@grid = Gtk::Grid.new
|
|
43
52
|
@grid.set_column_spacing(0)
|
|
@@ -46,102 +55,147 @@ module MittensUi
|
|
|
46
55
|
@scroller = Gtk::ScrolledWindow.new
|
|
47
56
|
@scroller.set_policy(:automatic, :automatic)
|
|
48
57
|
@scroller.set_child(@grid)
|
|
58
|
+
@scroller.set_vexpand(true)
|
|
59
|
+
@scroller.set_min_content_height(300)
|
|
60
|
+
@scroller.set_max_content_height(300)
|
|
61
|
+
|
|
62
|
+
# Pagination UI
|
|
63
|
+
@pagination_box = Gtk::Box.new(:horizontal, 10)
|
|
64
|
+
@pagination_box.set_margin_top(10)
|
|
65
|
+
@pagination_box.set_halign(:center)
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
max_height = 300
|
|
54
|
-
desired_height = [(@data.size * row_height) + header_height, max_height].min
|
|
67
|
+
@prev_btn = Gtk::Button.new(label: '← Prev')
|
|
68
|
+
@next_btn = Gtk::Button.new(label: 'Next →')
|
|
69
|
+
@page_label = Gtk::Label.new('')
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
@
|
|
58
|
-
@scroller.set_max_content_height(desired_height)
|
|
71
|
+
@prev_btn.signal_connect('clicked') { prev_page }
|
|
72
|
+
@next_btn.signal_connect('clicked') { next_page }
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
@pagination_box.append(@prev_btn)
|
|
75
|
+
@pagination_box.append(@page_label)
|
|
76
|
+
@pagination_box.append(@next_btn)
|
|
61
77
|
|
|
62
|
-
#
|
|
78
|
+
# Root container (important for Core)
|
|
79
|
+
@container = Gtk::Box.new(:vertical, 0)
|
|
80
|
+
@container.append(@scroller)
|
|
81
|
+
@container.append(@pagination_box)
|
|
82
|
+
|
|
83
|
+
super(@container, options)
|
|
84
|
+
|
|
85
|
+
# Events
|
|
63
86
|
@on_row_clicked = nil
|
|
64
87
|
@on_row_double_clicked = nil
|
|
65
88
|
@last_click_time = nil
|
|
66
89
|
@last_clicked_row = nil
|
|
67
90
|
|
|
68
91
|
setup_css
|
|
92
|
+
setup_keyboard
|
|
93
|
+
|
|
69
94
|
render_headers
|
|
70
95
|
render_rows
|
|
96
|
+
update_pagination_ui
|
|
71
97
|
end
|
|
72
98
|
|
|
73
99
|
# ---------------------------
|
|
74
100
|
# Public API
|
|
75
101
|
# ---------------------------
|
|
76
102
|
|
|
77
|
-
# Add a row to the table
|
|
78
|
-
#
|
|
79
|
-
# @param row [Array<String>] row data
|
|
80
|
-
# @param direction [Symbol] :append or :prepend
|
|
81
|
-
# @return [void]
|
|
82
103
|
def add(row, direction = :append)
|
|
83
104
|
return if row.nil? || row.empty?
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
@data.unshift(row)
|
|
87
|
-
else
|
|
88
|
-
@data << row
|
|
89
|
-
end
|
|
106
|
+
direction == :prepend ? @data.unshift(row) : @data << row
|
|
90
107
|
|
|
108
|
+
adjust_page_after_insert
|
|
91
109
|
render_rows
|
|
110
|
+
update_pagination_ui
|
|
92
111
|
end
|
|
93
112
|
|
|
94
|
-
# Replace table data
|
|
95
|
-
#
|
|
96
|
-
# @param new_data [Array<Array<String>>]
|
|
97
113
|
def update_data(new_data)
|
|
98
114
|
@data = new_data
|
|
115
|
+
@current_page = 0
|
|
99
116
|
render_rows
|
|
117
|
+
update_pagination_ui
|
|
100
118
|
end
|
|
101
119
|
|
|
102
|
-
# Check if a row is selected
|
|
103
|
-
#
|
|
104
|
-
# @param idx [Integer, nil]
|
|
105
|
-
# @return [Boolean]
|
|
106
120
|
def row_selected?(idx = nil)
|
|
107
121
|
return !@selected_row_idx.nil? if idx.nil?
|
|
122
|
+
|
|
108
123
|
@selected_row_idx == idx
|
|
109
124
|
end
|
|
110
125
|
|
|
111
|
-
# Get selected row data
|
|
112
|
-
#
|
|
113
|
-
# @return [Array<String>, nil]
|
|
114
126
|
def selected_row
|
|
115
127
|
return nil unless @selected_row_idx
|
|
128
|
+
|
|
116
129
|
@data[@selected_row_idx]
|
|
117
130
|
end
|
|
118
131
|
|
|
119
|
-
# Remove selected row
|
|
120
|
-
#
|
|
121
|
-
# @return [Array<String>, nil]
|
|
122
132
|
def remove_selected
|
|
123
133
|
return nil unless @selected_row_idx
|
|
124
134
|
|
|
125
135
|
removed = @data.delete_at(@selected_row_idx)
|
|
126
136
|
@selected_row_idx = nil
|
|
127
137
|
render_rows
|
|
138
|
+
update_pagination_ui
|
|
128
139
|
removed
|
|
129
140
|
end
|
|
130
141
|
|
|
131
|
-
# Register single-click handler
|
|
132
|
-
#
|
|
133
|
-
# @yield [row] row data
|
|
134
142
|
def row_clicked(&block)
|
|
135
143
|
@on_row_clicked = block
|
|
136
144
|
end
|
|
137
145
|
|
|
138
|
-
# Register double-click handler
|
|
139
|
-
#
|
|
140
|
-
# @yield [row] row data
|
|
141
146
|
def row_double_clicked(&block)
|
|
142
147
|
@on_row_double_clicked = block
|
|
143
148
|
end
|
|
144
149
|
|
|
150
|
+
# ---------------------------
|
|
151
|
+
# Pagination
|
|
152
|
+
# ---------------------------
|
|
153
|
+
|
|
154
|
+
def paginated_data
|
|
155
|
+
return @data if @data.size <= @page_threshold
|
|
156
|
+
|
|
157
|
+
start = @current_page * @page_size
|
|
158
|
+
@data.slice(start, @page_size) || []
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def next_page
|
|
162
|
+
return if @data.size <= @page_threshold
|
|
163
|
+
|
|
164
|
+
max_page = (@data.size / @page_size.to_f).ceil - 1
|
|
165
|
+
@current_page = [@current_page + 1, max_page].min
|
|
166
|
+
render_rows
|
|
167
|
+
update_pagination_ui
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def prev_page
|
|
171
|
+
return if @data.size <= @page_threshold
|
|
172
|
+
|
|
173
|
+
@current_page = [@current_page - 1, 0].max
|
|
174
|
+
render_rows
|
|
175
|
+
update_pagination_ui
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def adjust_page_after_insert
|
|
179
|
+
return if @data.size <= @page_threshold
|
|
180
|
+
|
|
181
|
+
@current_page = (@data.size / @page_size.to_f).floor
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def update_pagination_ui
|
|
185
|
+
if @data.size <= @page_threshold
|
|
186
|
+
@pagination_box.hide
|
|
187
|
+
return
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
total_pages = (@data.size / @page_size.to_f).ceil
|
|
191
|
+
@page_label.set_label("#{@current_page + 1} / #{total_pages}")
|
|
192
|
+
|
|
193
|
+
@prev_btn.set_sensitive(@current_page > 0)
|
|
194
|
+
@next_btn.set_sensitive(@current_page < total_pages - 1)
|
|
195
|
+
|
|
196
|
+
@pagination_box.show
|
|
197
|
+
end
|
|
198
|
+
|
|
145
199
|
# ---------------------------
|
|
146
200
|
# Rendering
|
|
147
201
|
# ---------------------------
|
|
@@ -151,37 +205,35 @@ module MittensUi
|
|
|
151
205
|
def setup_css
|
|
152
206
|
css = Gtk::CssProvider.new
|
|
153
207
|
css.load(data: <<~CSS)
|
|
154
|
-
|
|
155
|
-
border-radius: 0;
|
|
156
|
-
box-shadow: none;
|
|
157
|
-
border: none;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.table-cell {
|
|
208
|
+
box.table-cell {
|
|
161
209
|
padding: 6px 10px;
|
|
162
210
|
border-bottom: 1px solid @borders;
|
|
211
|
+
background-color: @theme_base_color;
|
|
212
|
+
color: @theme_text_color;
|
|
163
213
|
}
|
|
164
214
|
|
|
165
|
-
.header-cell {
|
|
215
|
+
box.header-cell {
|
|
166
216
|
font-weight: bold;
|
|
167
217
|
padding: 8px 10px;
|
|
168
218
|
border-bottom: 2px solid @borders;
|
|
169
|
-
background-color: @theme_base_color;
|
|
219
|
+
background-color: shade(@theme_base_color, 0.95);
|
|
220
|
+
color: @theme_text_color;
|
|
170
221
|
}
|
|
171
222
|
|
|
172
|
-
.row-even {
|
|
173
|
-
background-color:
|
|
223
|
+
box.row-even {
|
|
224
|
+
background-color: shade(@theme_base_color, 1.00);
|
|
174
225
|
}
|
|
175
226
|
|
|
176
|
-
.row-odd {
|
|
177
|
-
background-color:
|
|
227
|
+
box.row-odd {
|
|
228
|
+
background-color: shade(@theme_base_color, 0.97);
|
|
178
229
|
}
|
|
179
230
|
|
|
180
|
-
.table-cell:hover {
|
|
181
|
-
background-color:
|
|
231
|
+
box.table-cell:hover {
|
|
232
|
+
background-color: @theme_selected_bg_color;
|
|
233
|
+
color: @theme_selected_fg_color;
|
|
182
234
|
}
|
|
183
235
|
|
|
184
|
-
.row-selected {
|
|
236
|
+
box.row-selected {
|
|
185
237
|
background-color: @theme_selected_bg_color;
|
|
186
238
|
color: @theme_selected_fg_color;
|
|
187
239
|
}
|
|
@@ -190,7 +242,7 @@ module MittensUi
|
|
|
190
242
|
Gtk::StyleContext.add_provider_for_display(
|
|
191
243
|
Gdk::Display.default,
|
|
192
244
|
css,
|
|
193
|
-
Gtk::StyleProvider::
|
|
245
|
+
Gtk::StyleProvider::PRIORITY_APPLICATION
|
|
194
246
|
)
|
|
195
247
|
end
|
|
196
248
|
|
|
@@ -202,19 +254,15 @@ module MittensUi
|
|
|
202
254
|
|
|
203
255
|
@header_labels[col_idx] = label
|
|
204
256
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
frame.style_context.add_class('header-cell')
|
|
257
|
+
box = Gtk::Box.new(:horizontal, 0)
|
|
258
|
+
box.append(label)
|
|
259
|
+
box.style_context.add_class('header-cell')
|
|
209
260
|
|
|
210
261
|
gesture = Gtk::GestureClick.new
|
|
211
|
-
gesture.
|
|
212
|
-
|
|
213
|
-
sort_column(col_idx)
|
|
214
|
-
end
|
|
215
|
-
frame.add_controller(gesture)
|
|
262
|
+
gesture.signal_connect('pressed') { sort_column(col_idx) }
|
|
263
|
+
box.add_controller(gesture)
|
|
216
264
|
|
|
217
|
-
@grid.attach(
|
|
265
|
+
@grid.attach(box, col_idx, 0, 1, 1)
|
|
218
266
|
end
|
|
219
267
|
end
|
|
220
268
|
|
|
@@ -222,8 +270,11 @@ module MittensUi
|
|
|
222
270
|
@row_widgets.each { |row| row.each { |w| @grid.remove(w) } }
|
|
223
271
|
@row_widgets.clear
|
|
224
272
|
|
|
225
|
-
|
|
226
|
-
|
|
273
|
+
rows = paginated_data
|
|
274
|
+
|
|
275
|
+
rows.each_with_index do |row, visible_idx|
|
|
276
|
+
actual_idx = visible_idx + (@current_page * PAGE_SIZE)
|
|
277
|
+
base_class = visible_idx.even? ? 'row-even' : 'row-odd'
|
|
227
278
|
widget_row = []
|
|
228
279
|
|
|
229
280
|
row.each_with_index do |cell, col_idx|
|
|
@@ -231,46 +282,17 @@ module MittensUi
|
|
|
231
282
|
label.set_xalign(0.0)
|
|
232
283
|
label.set_hexpand(true)
|
|
233
284
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
frame.set_hexpand(true)
|
|
237
|
-
frame.style_context.add_class('table-cell')
|
|
238
|
-
frame.style_context.add_class(base_class)
|
|
239
|
-
frame.style_context.add_class('row-selected') if row_selected?(row_idx)
|
|
240
|
-
|
|
241
|
-
gesture = Gtk::GestureClick.new
|
|
242
|
-
gesture.set_button(0)
|
|
243
|
-
|
|
244
|
-
gesture.signal_connect("pressed") do |_g, _n, _x, _y|
|
|
245
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
246
|
-
|
|
247
|
-
if @last_click_time &&
|
|
248
|
-
@last_clicked_row == row_idx &&
|
|
249
|
-
(now - @last_click_time) < 0.3
|
|
250
|
-
|
|
251
|
-
# DOUBLE CLICK
|
|
252
|
-
@on_row_double_clicked&.call(@data[row_idx])
|
|
253
|
-
else
|
|
254
|
-
# SINGLE CLICK (delayed slightly to avoid conflict)
|
|
255
|
-
GLib::Timeout.add(250) do
|
|
256
|
-
if @last_clicked_row == row_idx
|
|
257
|
-
@on_row_clicked&.call(@data[row_idx])
|
|
258
|
-
end
|
|
259
|
-
false
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
@last_click_time = now
|
|
264
|
-
@last_clicked_row = row_idx
|
|
265
|
-
@selected_row_idx = row_idx
|
|
266
|
-
|
|
267
|
-
render_rows
|
|
268
|
-
end
|
|
285
|
+
box = Gtk::Box.new(:horizontal, 0)
|
|
286
|
+
box.append(label)
|
|
269
287
|
|
|
270
|
-
|
|
288
|
+
box.style_context.add_class('table-cell')
|
|
289
|
+
box.style_context.add_class(base_class)
|
|
290
|
+
box.style_context.add_class('row-selected') if row_selected?(actual_idx)
|
|
271
291
|
|
|
272
|
-
|
|
273
|
-
|
|
292
|
+
attach_click_handlers(box, actual_idx)
|
|
293
|
+
|
|
294
|
+
@grid.attach(box, col_idx, visible_idx + 1, 1, 1)
|
|
295
|
+
widget_row << box
|
|
274
296
|
end
|
|
275
297
|
|
|
276
298
|
@row_widgets << widget_row
|
|
@@ -280,21 +302,91 @@ module MittensUi
|
|
|
280
302
|
@row_widgets.flatten.each(&:show)
|
|
281
303
|
end
|
|
282
304
|
|
|
305
|
+
def attach_click_handlers(widget, row_idx)
|
|
306
|
+
gesture = Gtk::GestureClick.new
|
|
307
|
+
gesture.set_button(0)
|
|
308
|
+
|
|
309
|
+
gesture.signal_connect('pressed') do |_g, _n, _x, _y|
|
|
310
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
311
|
+
|
|
312
|
+
if @last_click_time &&
|
|
313
|
+
@last_clicked_row == row_idx &&
|
|
314
|
+
(now - @last_click_time) < 0.3
|
|
315
|
+
|
|
316
|
+
@on_row_double_clicked&.call(@data[row_idx])
|
|
317
|
+
else
|
|
318
|
+
GLib::Timeout.add(200) do
|
|
319
|
+
@last_clicked_row == row_idx ? @on_row_clicked&.call(@data[row_idx]) : false
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
@last_click_time = now
|
|
324
|
+
@last_clicked_row = row_idx
|
|
325
|
+
@selected_row_idx = row_idx
|
|
326
|
+
|
|
327
|
+
render_rows
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
widget.add_controller(gesture)
|
|
331
|
+
end
|
|
332
|
+
|
|
283
333
|
def sort_column(col_idx)
|
|
284
334
|
dir = @sort_directions[col_idx] ? :desc : :asc
|
|
285
335
|
@sort_directions[col_idx] = !@sort_directions[col_idx]
|
|
286
336
|
|
|
287
337
|
@header_labels.each_with_index do |lbl, i|
|
|
288
|
-
lbl.set_label(@headers[i]
|
|
338
|
+
lbl.set_label(@headers[i])
|
|
289
339
|
end
|
|
290
340
|
|
|
291
|
-
arrow = dir == :asc ?
|
|
341
|
+
arrow = dir == :asc ? ' ▲' : ' ▼'
|
|
292
342
|
@header_labels[col_idx].set_label(@headers[col_idx] + arrow)
|
|
293
343
|
|
|
294
|
-
@data.sort_by!
|
|
344
|
+
@data.sort_by! do |row|
|
|
345
|
+
val = row[col_idx]
|
|
346
|
+
|
|
347
|
+
case val
|
|
348
|
+
when Integer
|
|
349
|
+
val
|
|
350
|
+
when Float
|
|
351
|
+
val
|
|
352
|
+
when String
|
|
353
|
+
# try numeric string first
|
|
354
|
+
Integer(val) rescue Float(val) rescue val.downcase
|
|
355
|
+
else
|
|
356
|
+
val.to_s
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
295
360
|
@data.reverse! if dir == :desc
|
|
296
361
|
|
|
362
|
+
render_rows
|
|
363
|
+
update_pagination_ui
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def setup_keyboard
|
|
367
|
+
controller = Gtk::EventControllerKey.new
|
|
368
|
+
|
|
369
|
+
controller.signal_connect('key-pressed') do |_ctrl, key, _, _|
|
|
370
|
+
case key
|
|
371
|
+
when Gdk::Keyval::KEY_Up
|
|
372
|
+
move_selection(-1)
|
|
373
|
+
when Gdk::Keyval::KEY_Down
|
|
374
|
+
move_selection(1)
|
|
375
|
+
when Gdk::Keyval::KEY_Return
|
|
376
|
+
@on_row_double_clicked&.call(selected_row)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
@scroller.add_controller(controller)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def move_selection(delta)
|
|
384
|
+
return if @data.empty?
|
|
385
|
+
|
|
386
|
+
@selected_row_idx ||= 0
|
|
387
|
+
@selected_row_idx = [[@selected_row_idx + delta, 0].max, @data.size - 1].min
|
|
388
|
+
|
|
297
389
|
render_rows
|
|
298
390
|
end
|
|
299
391
|
end
|
|
300
|
-
end
|
|
392
|
+
end
|
data/lib/mittens_ui/textbox.rb
CHANGED
|
@@ -69,6 +69,15 @@ module MittensUi
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# @param value [String] A String value that the label gets set to
|
|
73
|
+
def text=(value)
|
|
74
|
+
if @multiline
|
|
75
|
+
@text_buffer.text = value.to_s
|
|
76
|
+
else
|
|
77
|
+
@textbox.text = value.to_s
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
# Clears all text from the widget.
|
|
73
82
|
# Works in both single-line and multiline mode.
|
|
74
83
|
#
|
data/lib/mittens_ui/version.rb
CHANGED
data/lib/mittens_ui/web_link.rb
CHANGED
|
@@ -43,5 +43,13 @@ module MittensUi
|
|
|
43
43
|
@web_link = Gtk::LinkButton.new(@url, @name)
|
|
44
44
|
super(@web_link, options)
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
# Opens the set @url link in a web browser.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def open_url
|
|
51
|
+
launcher = Gtk::UriLauncher.new(@url)
|
|
52
|
+
launcher.launch
|
|
53
|
+
end
|
|
46
54
|
end
|
|
47
55
|
end
|
data/mittens_ui.gemspec
CHANGED
|
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
|
14
14
|
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
|
15
15
|
|
|
16
16
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
-
spec.metadata["source_code_uri"] = "https://
|
|
17
|
+
spec.metadata["source_code_uri"] = "https://codeberg.org/tuttza/mittens_ui"
|
|
18
18
|
#spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
|
19
19
|
|
|
20
20
|
# Specify which files should be added to the gem when it is released.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mittens_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zach Tuttle
|
|
@@ -112,6 +112,8 @@ files:
|
|
|
112
112
|
- examples/assets/gnome_logo.png
|
|
113
113
|
- examples/assets/mittens_ui_preview.gif
|
|
114
114
|
- examples/contacts.rb
|
|
115
|
+
- examples/hn.rb
|
|
116
|
+
- examples/keyboard_shortcut_demo.rb
|
|
115
117
|
- examples/mig.rb
|
|
116
118
|
- lib/mittens_ui.rb
|
|
117
119
|
- lib/mittens_ui/alert.rb
|
|
@@ -150,7 +152,7 @@ licenses:
|
|
|
150
152
|
- MIT
|
|
151
153
|
metadata:
|
|
152
154
|
homepage_uri: https://github.com/tuttza/mittens_ui
|
|
153
|
-
source_code_uri: https://
|
|
155
|
+
source_code_uri: https://codeberg.org/tuttza/mittens_ui
|
|
154
156
|
rdoc_options: []
|
|
155
157
|
require_paths:
|
|
156
158
|
- lib
|
|
@@ -165,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
165
167
|
- !ruby/object:Gem::Version
|
|
166
168
|
version: '0'
|
|
167
169
|
requirements: []
|
|
168
|
-
rubygems_version: 4.0.
|
|
170
|
+
rubygems_version: 4.0.6
|
|
169
171
|
specification_version: 4
|
|
170
172
|
summary: A tiny GUI toolkit written on top of GTK
|
|
171
173
|
test_files: []
|