pads 0.1.0 → 1.0.0
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/.rubocop.yml +32 -2
- data/.ruby-version +1 -1
- data/Gemfile +6 -4
- data/Gemfile.lock +11 -1
- data/README.md +4 -6
- data/Rakefile +3 -3
- data/lib/pads/client/local_resolver.rb +75 -0
- data/lib/pads/client/pad.rb +94 -0
- data/lib/pads/client.rb +74 -0
- data/lib/pads/events/base.rb +8 -0
- data/lib/pads/events/button_clicked.rb +24 -0
- data/lib/pads/events/dispatcher.rb +34 -0
- data/lib/pads/events/files_dropped.rb +25 -0
- data/lib/pads/events/publisher.rb +39 -0
- data/lib/pads/events/subscription.rb +26 -0
- data/lib/pads/live_array.rb +29 -0
- data/lib/pads/mapper.rb +78 -0
- data/lib/pads/mutable_pad_state.rb +22 -0
- data/lib/pads/observable_pad_state.rb +40 -0
- data/lib/pads/pad.rb +82 -0
- data/lib/pads/pad_group.rb +76 -0
- data/lib/pads/pad_or_group.rb +35 -0
- data/lib/pads/pad_state.rb +45 -0
- data/lib/pads/pad_view.rb +44 -0
- data/lib/pads/provider/group_binding.rb +11 -0
- data/lib/pads/provider/tracking.rb +76 -0
- data/lib/pads/provider.rb +140 -0
- data/lib/pads/version.rb +1 -1
- data/lib/pads.rb +14 -3
- data/pads.gemspec +15 -13
- metadata +41 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef83cd4cd517c8201f18b8208a6adb8ca42d5dadef3e810c5007822662768b1f
|
4
|
+
data.tar.gz: 77a457197b5c98ec963a95b2f62a6b9e1e59a2d69cdfd268112279e280248df5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48cd6de1372be26d55b682c4de3ad1119c6855b588190f05ea1c9ce4b34412a15ccc851c760e8e8e388a5dd786f596b93e21bac9975f0c9011bc9d33e166101d
|
7
|
+
data.tar.gz: 98939e84ce761264920b1a039ff62dd50fd31ee8a2c497b55467ede72a505f1a7ea7a163d6a181e6b2f005c956e83637c25d8ad890dc792ea42df1150cd72d20
|
data/.rubocop.yml
CHANGED
@@ -1,13 +1,43 @@
|
|
1
1
|
AllCops:
|
2
2
|
TargetRubyVersion: 2.6
|
3
|
+
NewCops: enable
|
4
|
+
|
5
|
+
Style/Alias:
|
6
|
+
EnforcedStyle: prefer_alias
|
7
|
+
|
8
|
+
Style/BlockDelimiters:
|
9
|
+
Exclude:
|
10
|
+
- spec/**/*_spec.rb
|
3
11
|
|
4
12
|
Style/StringLiterals:
|
5
13
|
Enabled: true
|
6
|
-
EnforcedStyle:
|
14
|
+
EnforcedStyle: single_quotes
|
7
15
|
|
8
16
|
Style/StringLiteralsInInterpolation:
|
9
17
|
Enabled: true
|
10
|
-
EnforcedStyle:
|
18
|
+
EnforcedStyle: single_quotes
|
11
19
|
|
12
20
|
Layout/LineLength:
|
13
21
|
Max: 120
|
22
|
+
|
23
|
+
Layout/HashAlignment:
|
24
|
+
EnforcedColonStyle: table
|
25
|
+
EnforcedHashRocketStyle: table
|
26
|
+
|
27
|
+
Lint/AmbiguousBlockAssociation:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Lint/AmbiguousOperator:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Metrics/BlockLength:
|
34
|
+
IgnoredMethods:
|
35
|
+
- describe
|
36
|
+
- it
|
37
|
+
|
38
|
+
Metrics/MethodLength:
|
39
|
+
Max: 12
|
40
|
+
|
41
|
+
Naming/MethodName:
|
42
|
+
IgnoredPatterns:
|
43
|
+
- !ruby/regexp /^([A-Z][a-z]*)+$/
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.6.8
|
data/Gemfile
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in pads.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
gem
|
8
|
+
gem 'rake', '~> 13.0'
|
9
9
|
|
10
|
-
gem
|
10
|
+
gem 'rspec', '~> 3.0'
|
11
11
|
|
12
|
-
gem
|
12
|
+
gem 'rubocop', '~> 1.21'
|
13
|
+
gem 'rubocop-rake'
|
14
|
+
gem 'rubocop-rspec'
|
data/Gemfile.lock
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pads (
|
4
|
+
pads (1.0.0)
|
5
|
+
interop (~> 0.3.1)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
8
9
|
specs:
|
9
10
|
ast (2.4.2)
|
10
11
|
diff-lcs (1.5.0)
|
12
|
+
interop (0.3.1)
|
11
13
|
parallel (1.21.0)
|
12
14
|
parser (3.1.1.0)
|
13
15
|
ast (~> 2.4.1)
|
@@ -39,18 +41,26 @@ GEM
|
|
39
41
|
unicode-display_width (>= 1.4.0, < 3.0)
|
40
42
|
rubocop-ast (1.16.0)
|
41
43
|
parser (>= 3.1.1.0)
|
44
|
+
rubocop-rake (0.6.0)
|
45
|
+
rubocop (~> 1.0)
|
46
|
+
rubocop-rspec (2.9.0)
|
47
|
+
rubocop (~> 1.19)
|
42
48
|
ruby-progressbar (1.11.0)
|
43
49
|
unicode-display_width (2.1.0)
|
44
50
|
|
45
51
|
PLATFORMS
|
52
|
+
x86_64-darwin-19
|
46
53
|
x86_64-darwin-20
|
47
54
|
x86_64-darwin-21
|
55
|
+
x86_64-linux
|
48
56
|
|
49
57
|
DEPENDENCIES
|
50
58
|
pads!
|
51
59
|
rake (~> 13.0)
|
52
60
|
rspec (~> 3.0)
|
53
61
|
rubocop (~> 1.21)
|
62
|
+
rubocop-rake
|
63
|
+
rubocop-rspec
|
54
64
|
|
55
65
|
BUNDLED WITH
|
56
66
|
2.3.5
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Pads
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
Provide pads to the Pads macOS app.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -22,7 +20,7 @@ Or install it yourself as:
|
|
22
20
|
|
23
21
|
## Usage
|
24
22
|
|
25
|
-
|
23
|
+
Check the examples.
|
26
24
|
|
27
25
|
## Development
|
28
26
|
|
@@ -32,7 +30,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
30
|
|
33
31
|
## Contributing
|
34
32
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hx/pads. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/hx/pads/blob/master/CODE_OF_CONDUCT.md).
|
36
34
|
|
37
35
|
## License
|
38
36
|
|
@@ -40,4 +38,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
40
38
|
|
41
39
|
## Code of Conduct
|
42
40
|
|
43
|
-
Everyone interacting in the Pads project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
41
|
+
Everyone interacting in the Pads project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/hx/pads/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
|
-
require
|
8
|
+
require 'rubocop/rake_task'
|
9
9
|
|
10
10
|
RuboCop::RakeTask.new
|
11
11
|
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'socket'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Pads
|
8
|
+
class Client
|
9
|
+
# Creates raw connections based on the macOS app's configuration.
|
10
|
+
class LocalResolver
|
11
|
+
APP_CONTAINER_SUFFIX = 'Library/Containers/hx.pads/Data'
|
12
|
+
LISTENERS_PATH = '.pads/listeners.json'
|
13
|
+
|
14
|
+
def initialize(home: Dir.home)
|
15
|
+
@home = Pathname(home)
|
16
|
+
end
|
17
|
+
|
18
|
+
def connect
|
19
|
+
errors = []
|
20
|
+
each_config do |type, config|
|
21
|
+
conn, error = __send__(:"try_#{type}", config)
|
22
|
+
return conn if conn
|
23
|
+
|
24
|
+
errors << error
|
25
|
+
end
|
26
|
+
raise errors.compact.first || 'Unable to connect to local server'
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def each_config
|
32
|
+
%w[unix tcp].each do |type|
|
33
|
+
parsed_listeners.each do |listeners|
|
34
|
+
listeners[type]&.each do |config|
|
35
|
+
yield type, config
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Array<Hash>]
|
42
|
+
def parsed_listeners
|
43
|
+
listeners_path_candidates
|
44
|
+
.map do |path|
|
45
|
+
next unless path.exist?
|
46
|
+
|
47
|
+
JSON.parse path.readlines.delete_if { |l| l.start_with? '//' }.join
|
48
|
+
end
|
49
|
+
.compact
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array<Pathname>]
|
53
|
+
def listeners_path_candidates
|
54
|
+
[
|
55
|
+
@home + LISTENERS_PATH, # Regular app version
|
56
|
+
@home + APP_CONTAINER_SUFFIX + LISTENERS_PATH # Sandboxed (app store) app version
|
57
|
+
]
|
58
|
+
end
|
59
|
+
|
60
|
+
def try_unix(config)
|
61
|
+
try { UNIXSocket.new Pathname(config.fetch('path')).to_s }
|
62
|
+
end
|
63
|
+
|
64
|
+
def try_tcp(config)
|
65
|
+
try { TCPSocket.new config.fetch('host', '127.0.0.1'), config.fetch('port') }
|
66
|
+
end
|
67
|
+
|
68
|
+
def try
|
69
|
+
[yield, nil]
|
70
|
+
rescue StandardError => e
|
71
|
+
[nil, e]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pads/events/dispatcher'
|
4
|
+
require 'pads/mutable_pad_state'
|
5
|
+
|
6
|
+
module Pads
|
7
|
+
class Client
|
8
|
+
# Proxy to a Client for a specific pad
|
9
|
+
class Pad < MutablePadState
|
10
|
+
attr_reader :events, :pad_id
|
11
|
+
|
12
|
+
def initialize(pad_id, client, &on_destroy)
|
13
|
+
@pad_id = pad_id
|
14
|
+
@client = client
|
15
|
+
@on_destroy = on_destroy
|
16
|
+
@events = Events::Dispatcher.new
|
17
|
+
|
18
|
+
super()
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy
|
22
|
+
call :destroy_pad
|
23
|
+
@client = nil
|
24
|
+
@on_destroy.call
|
25
|
+
@on_destroy = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def title=(new_title)
|
29
|
+
call :set_pad_title, new_title.to_s
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def subtitle=(new_subtitle)
|
34
|
+
call :set_pad_subtitle, new_subtitle.to_s
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def activity=(fraction)
|
39
|
+
call :set_pad_activity, transform_activity(fraction)
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def clickable=(clickable)
|
44
|
+
call :set_pad_clickable, clickable ? true : false
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def buttons=(buttons)
|
49
|
+
call :set_pad_buttons, buttons&.map { |i| { label: i.to_s } } || []
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_drop_target=(is_drop_target)
|
54
|
+
call :set_pad_is_drop_target, is_drop_target ? true : false
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_pad_clicked(&handler)
|
59
|
+
@events.on :pad_clicked, &handler
|
60
|
+
end
|
61
|
+
|
62
|
+
# @yieldparam [Pads::Events::ButtonClicked]
|
63
|
+
def on_button_clicked(&handler)
|
64
|
+
@events.on :button_clicked, &handler
|
65
|
+
end
|
66
|
+
|
67
|
+
# @yieldparam [Pads::Events::FilesDropped]
|
68
|
+
def on_files_dropped(&handler)
|
69
|
+
@events.on :files_dropped, &handler
|
70
|
+
end
|
71
|
+
|
72
|
+
alias == equal?
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def call(class_name, arg = nil)
|
77
|
+
raise 'Pad was destroyed' unless @client
|
78
|
+
|
79
|
+
@client.call class_name, Hx::Interop::ContentType::JSON.encode(arg), pad_id: @pad_id
|
80
|
+
end
|
81
|
+
|
82
|
+
def transform_activity(fraction_or_bool)
|
83
|
+
case fraction_or_bool
|
84
|
+
when true, false, nil
|
85
|
+
{ busy: fraction_or_bool == true, progress: nil }
|
86
|
+
when Numeric
|
87
|
+
{ busy: false, progress: fraction_or_bool.to_f.clamp(0, 1) }
|
88
|
+
else
|
89
|
+
raise ArgumentError, 'Expected nil, boolean, or a number'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/pads/client.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'interop'
|
4
|
+
|
5
|
+
require 'pads/client/pad'
|
6
|
+
require 'pads/client/local_resolver'
|
7
|
+
|
8
|
+
module Pads
|
9
|
+
# Low-level client for Pads servers
|
10
|
+
class Client
|
11
|
+
def initialize(reader = nil, writer = nil)
|
12
|
+
reader ||= LocalResolver.new.connect
|
13
|
+
|
14
|
+
@pads = {}
|
15
|
+
@rpc_client = Hx::Interop::RPC::Client.new(reader, writer || reader)
|
16
|
+
@rpc_client.on(//) do |message|
|
17
|
+
next unless (pad_id = message[:pad_id])
|
18
|
+
|
19
|
+
@pads[pad_id]&.events&.dispatch(
|
20
|
+
message[Hx::Interop::Headers::CLASS],
|
21
|
+
message[Hx::Interop::Headers::CONTENT_TYPE] ? message.decode : message.body
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_pad(before: nil, after: nil)
|
27
|
+
raise 'You cannot specify before and after' if before && after
|
28
|
+
|
29
|
+
headers = {
|
30
|
+
before_pad_id: before && pad_id(before),
|
31
|
+
after_pad_id: after && pad_id(after)
|
32
|
+
}.compact
|
33
|
+
|
34
|
+
pad_id = call(:create_pad, headers)[:pad_id]
|
35
|
+
@pads[pad_id] = Pad.new(pad_id, self) { @pads.delete pad_id }
|
36
|
+
end
|
37
|
+
|
38
|
+
def swap_pads(pad_or_range_a, pad_or_range_b)
|
39
|
+
call :swap_pads, Hx::Interop::ContentType::JSON.encode(
|
40
|
+
range_a: pad_range(*Array(pad_or_range_a)),
|
41
|
+
range_b: pad_range(*Array(pad_or_range_b))
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def wait
|
46
|
+
@rpc_client.wait
|
47
|
+
end
|
48
|
+
|
49
|
+
def call(*args, &block)
|
50
|
+
@rpc_client.call(*args, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def pad_id(pad_or_id)
|
56
|
+
case pad_or_id
|
57
|
+
when String
|
58
|
+
pad_or_id
|
59
|
+
when Pad
|
60
|
+
@pads.key(pad_or_id) or
|
61
|
+
raise 'The given pad does not belong to this client'
|
62
|
+
else
|
63
|
+
raise ArgumentError, "Unexpected #{pad_or_id.inspect}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def pad_range(first, last = first)
|
68
|
+
{
|
69
|
+
first_pad_id: pad_id(first),
|
70
|
+
last_pad_id: pad_id(last)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pads/events/base'
|
4
|
+
|
5
|
+
module Pads
|
6
|
+
module Events
|
7
|
+
# Occurs when one of a pad's buttons are clicked.
|
8
|
+
class ButtonClicked < Base
|
9
|
+
# Zero-based index of the button that was clicked.
|
10
|
+
# @return Integer
|
11
|
+
attr_reader :index
|
12
|
+
|
13
|
+
# Label of the button that was clicked.
|
14
|
+
# @return String
|
15
|
+
attr_reader :label
|
16
|
+
|
17
|
+
def initialize(event_data)
|
18
|
+
@index = event_data.fetch('index')
|
19
|
+
@label = event_data.fetch('label')
|
20
|
+
super()
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pads/events/publisher'
|
4
|
+
require 'pads/events/button_clicked'
|
5
|
+
require 'pads/events/files_dropped'
|
6
|
+
|
7
|
+
module Pads
|
8
|
+
module Events
|
9
|
+
# A central object for dispatch all events related to an event subject.
|
10
|
+
class Dispatcher
|
11
|
+
DEFAULT_PUBLISHERS = {
|
12
|
+
pad_clicked: nil,
|
13
|
+
button_clicked: Events::ButtonClicked,
|
14
|
+
files_dropped: Events::FilesDropped
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
attr_reader :publishers
|
18
|
+
|
19
|
+
def initialize(publishers = DEFAULT_PUBLISHERS)
|
20
|
+
@publishers = publishers.to_h do |event_name, event_class|
|
21
|
+
[event_name.to_s, Publisher.new(event_class)]
|
22
|
+
end.freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def on(event_name, &handler)
|
26
|
+
@publishers.fetch(event_name.to_s).subscribe(&handler)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dispatch(event_name, payload = nil)
|
30
|
+
@publishers[event_name.to_s]&.publish payload
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pads/events/base'
|
4
|
+
|
5
|
+
require 'pathname'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module Pads
|
9
|
+
module Events
|
10
|
+
# Occurs when files are dropped on a pad.
|
11
|
+
class FilesDropped < Base
|
12
|
+
File = Struct.new(:path)
|
13
|
+
|
14
|
+
# @return Array<File>
|
15
|
+
attr_reader :files
|
16
|
+
|
17
|
+
def initialize(event_data)
|
18
|
+
@files = event_data.map do |file|
|
19
|
+
File.new Pathname URI.decode_www_form_component URI(file.fetch('url')).path
|
20
|
+
end
|
21
|
+
super()
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'pads/events/subscription'
|
5
|
+
|
6
|
+
module Pads
|
7
|
+
module Events
|
8
|
+
# Manages event subscribers and distributes events to them.
|
9
|
+
class Publisher
|
10
|
+
def initialize(payload_class = nil)
|
11
|
+
@payload_class = payload_class
|
12
|
+
@handlers = Set.new.compare_by_identity
|
13
|
+
end
|
14
|
+
|
15
|
+
def subscribe(&block)
|
16
|
+
Subscription.new(self, block).tap { |h| @handlers << h }
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsubscribe_all
|
20
|
+
@handlers.clear
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def publish(payload = nil)
|
25
|
+
payload = @payload_class.new(payload) if @payload_class && !payload.is_a?(@payload_class)
|
26
|
+
@handlers.each do |handler|
|
27
|
+
handler.__send__ :call, payload
|
28
|
+
end
|
29
|
+
payload
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def unsubscribe(handler)
|
35
|
+
@handlers.delete handler
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Pads
|
6
|
+
module Events
|
7
|
+
# Represents an event subscription, and can be used to unsubscribe.
|
8
|
+
class Subscription
|
9
|
+
def initialize(publisher, proc)
|
10
|
+
@publisher = publisher
|
11
|
+
@proc = proc
|
12
|
+
end
|
13
|
+
|
14
|
+
def unsubscribe
|
15
|
+
@publisher.__send__ :unsubscribe, self
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def call(*args, &block)
|
22
|
+
@proc.call(*args, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'observer'
|
4
|
+
|
5
|
+
module Pads
|
6
|
+
# An array that can be mapped to pads, and will update a pad group when it changes.
|
7
|
+
class LiveArray < Array
|
8
|
+
include Observable
|
9
|
+
|
10
|
+
OBSERVED_METHODS = Set.new(
|
11
|
+
%i[
|
12
|
+
[]= << push append prepend pop shift unshift insert
|
13
|
+
delete delete_at delete_if keep_if fill clear replace
|
14
|
+
] + instance_methods.grep(/!$/)
|
15
|
+
)
|
16
|
+
|
17
|
+
OBSERVED_METHODS.each do |method|
|
18
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
def #{method}(*) # def push(*)
|
20
|
+
original = dup # original = dup
|
21
|
+
result = super # result = super
|
22
|
+
changed self != original # changed self != original
|
23
|
+
notify_observers # notify_observers
|
24
|
+
result # result
|
25
|
+
end # end
|
26
|
+
RUBY
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/pads/mapper.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pads/pad_group'
|
4
|
+
require 'pads/live_array'
|
5
|
+
|
6
|
+
module Pads
|
7
|
+
# A Mapper binds an ObservableArray to a PadGroup
|
8
|
+
class Mapper
|
9
|
+
def self.map(source, id: :object_id, &mapper)
|
10
|
+
if source.is_a? LiveArray
|
11
|
+
new(source, id: id, &mapper).target
|
12
|
+
else
|
13
|
+
PadGroup.new Array(source).map(&mapper)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [PadGroup]
|
18
|
+
attr_reader :target
|
19
|
+
|
20
|
+
def initialize(source, id: :object_id, &mapper)
|
21
|
+
raise ArgumentError, 'Expected a LiveArray' unless source.is_a? LiveArray
|
22
|
+
|
23
|
+
@id = id.to_proc
|
24
|
+
@mapper = mapper
|
25
|
+
@source = source
|
26
|
+
@target = PadGroup.new
|
27
|
+
@actual = []
|
28
|
+
source.add_observer self
|
29
|
+
update
|
30
|
+
end
|
31
|
+
|
32
|
+
def update
|
33
|
+
ids = @source.map(&@id)
|
34
|
+
@target.batch do
|
35
|
+
remove_old_members ids
|
36
|
+
sort_members ids
|
37
|
+
add_new_members ids
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def remove_old_members(ids)
|
44
|
+
expected = Set.new(ids)
|
45
|
+
@actual.each.with_index.reverse_each do |id, index|
|
46
|
+
next if expected.include? id
|
47
|
+
|
48
|
+
@actual.delete_at index
|
49
|
+
@target.delete_at index
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sort_members(ids)
|
54
|
+
return unless @actual.length > 1
|
55
|
+
|
56
|
+
expected = (ids & @actual)
|
57
|
+
|
58
|
+
(0..(@actual.length - 2)).each do |i|
|
59
|
+
next if expected[i] == @actual[i]
|
60
|
+
|
61
|
+
j = @actual.index(expected[i])
|
62
|
+
@actual[j], @actual[i] = @actual.values_at(i, j)
|
63
|
+
@target.swap i, j
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_new_members(ids)
|
68
|
+
skip = Set.new(@actual)
|
69
|
+
@source.each.with_index do |object, index|
|
70
|
+
id = ids[index]
|
71
|
+
next if skip.include? id
|
72
|
+
|
73
|
+
@actual.insert index, id
|
74
|
+
@target.insert index, @mapper.call(object)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|