glimmer-dsl-libui 0.9.6 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,258 @@
1
+ # Copyright (c) 2021-2023 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'super_module'
23
+ require 'glimmer'
24
+ require 'glimmer/error'
25
+ require 'glimmer/proc_tracker'
26
+ require 'glimmer/data_binding/observer'
27
+ require 'glimmer/data_binding/observable_model'
28
+
29
+ module Glimmer
30
+ module LibUI
31
+ module CustomShape
32
+ include SuperModule
33
+ include DataBinding::ObservableModel
34
+
35
+ # This module was only created to prevent Glimmer from checking method_missing first
36
+ module GlimmerSupersedable
37
+ def method_missing(method_name, *args, &block)
38
+ # TODO Consider supporting a glimmer error silencing option for methods defined here
39
+ # but fail the glimmer DSL for the right reason to avoid seeing noise in the log output
40
+ if block && can_handle_listener?(method_name)
41
+ handle_listener(method_name, &block)
42
+ elsif @body_root.respond_to?(method_name, true)
43
+ @body_root.send(method_name, *args, &block)
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ def respond_to?(method_name, *args, &block)
50
+ result = false
51
+ result ||= super
52
+ result ||= can_handle_listener?(method_name)
53
+ result ||= @body_root.respond_to?(method_name, *args, &block)
54
+ end
55
+ end
56
+
57
+ super_module_included do |klass|
58
+ # TODO clear memoization of Shape.libui_class_for for a keyword if a custom shape was defined with that keyword
59
+ klass.include(Glimmer)
60
+ klass.include(GlimmerSupersedable) # prevent Glimmer from running method_missing first
61
+ Glimmer::LibUI::CustomShape.add_custom_shape_namespaces_for(klass)
62
+ end
63
+
64
+ class << self
65
+ def for(keyword)
66
+ unless flyweight_custom_shape_classes.keys.include?(keyword)
67
+ begin
68
+ extracted_namespaces = keyword.
69
+ to_s.
70
+ split(/__/).map do |namespace|
71
+ namespace.camelcase(:upper)
72
+ end
73
+ custom_shape_namespaces.each do |base|
74
+ extracted_namespaces.reduce(base) do |result, namespace|
75
+ if !result.constants.include?(namespace)
76
+ namespace = result.constants.detect {|c| c.to_s.upcase == namespace.to_s.upcase } || namespace
77
+ end
78
+ begin
79
+ flyweight_custom_shape_classes[keyword] = constant = result.const_get(namespace)
80
+ return constant if constant.ancestors.include?(Glimmer::LibUI::CustomShape)
81
+ flyweight_custom_shape_classes[keyword] = constant
82
+ rescue => e
83
+ # Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
84
+ flyweight_custom_shape_classes[keyword] = result
85
+ end
86
+ end
87
+ end
88
+ raise "#{keyword} has no custom shape class!"
89
+ rescue => e
90
+ Glimmer::Config.logger.debug {e.message}
91
+ Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
92
+ flyweight_custom_shape_classes[keyword] = nil
93
+ end
94
+ end
95
+ flyweight_custom_shape_classes[keyword]
96
+ end
97
+
98
+ # Flyweight Design Pattern memoization cache. Can be cleared if memory is needed.
99
+ def flyweight_custom_shape_classes
100
+ @flyweight_custom_shape_classes ||= {}
101
+ end
102
+
103
+ # Returns keyword to use for this custom shape
104
+ def keyword
105
+ self.name.underscore.gsub('::', '__')
106
+ end
107
+
108
+ # Returns shortcut keyword to use for this custom shape (keyword minus namespace)
109
+ def shortcut_keyword
110
+ self.name.underscore.gsub('::', '__').split('__').last
111
+ end
112
+
113
+ def add_custom_shape_namespaces_for(klass)
114
+ Glimmer::LibUI::CustomShape.namespaces_for_class(klass).drop(1).each do |namespace|
115
+ Glimmer::LibUI::CustomShape.custom_shape_namespaces << namespace
116
+ end
117
+ end
118
+
119
+ def namespaces_for_class(m)
120
+ return [m] if m.name.nil?
121
+ namespace_constants = m.name.split(/::/).map(&:to_sym)
122
+ namespace_constants.reduce([Object]) do |output, namespace_constant|
123
+ output += [output.last.const_get(namespace_constant)]
124
+ end[1..-1].uniq.reverse
125
+ end
126
+
127
+ def custom_shape_namespaces
128
+ @custom_shape_namespaces ||= reset_custom_shape_namespaces
129
+ end
130
+
131
+ def reset_custom_shape_namespaces
132
+ @custom_shape_namespaces = Set[Object, Glimmer::LibUI]
133
+ end
134
+
135
+ # Allows defining convenience option accessors for an array of option names
136
+ # Example: `options :color1, :color2` defines `#color1` and `#color2`
137
+ # where they return the instance values `options[:color1]` and `options[:color2]`
138
+ # respectively.
139
+ # Can be called multiple times to set more options additively.
140
+ # When passed no arguments, it returns list of all option names captured so far
141
+ def options(*new_options)
142
+ new_options = new_options.compact.map(&:to_s).map(&:to_sym)
143
+ if new_options.empty?
144
+ @options ||= {} # maps options to defaults
145
+ else
146
+ new_options = new_options.reduce({}) {|new_options_hash, new_option| new_options_hash.merge(new_option => nil)}
147
+ @options = options.merge(new_options)
148
+ def_option_attr_accessors(new_options)
149
+ end
150
+ end
151
+
152
+ def option(new_option, default: nil)
153
+ new_option = new_option.to_s.to_sym
154
+ new_options = {new_option => default}
155
+ @options = options.merge(new_options)
156
+ def_option_attr_accessors(new_options)
157
+ end
158
+
159
+ def def_option_attr_accessors(new_options)
160
+ new_options.each do |option, default|
161
+ class_eval <<-end_eval, __FILE__, __LINE__
162
+ def #{option}
163
+ options[:#{option}]
164
+ end
165
+
166
+ def #{option}=(option_value)
167
+ self.options[:#{option}] = option_value
168
+ end
169
+ end_eval
170
+ end
171
+ end
172
+
173
+ def before_body(&block)
174
+ @before_body_block = block
175
+ end
176
+
177
+ def body(&block)
178
+ @body_block = block
179
+ end
180
+
181
+ def after_body(&block)
182
+ @after_body_block = block
183
+ end
184
+ end
185
+
186
+ attr_reader :body_root, :parent, :parent_proxy, :args, :keyword, :content, :options
187
+
188
+ def initialize(keyword, parent, args, options, &content)
189
+ @parent_proxy = @parent = parent
190
+ options ||= {}
191
+ @options = self.class.options.merge(options)
192
+ @content = ProcTracker.new(content) if content
193
+ execute_hook('before_body')
194
+ body_block = self.class.instance_variable_get("@body_block")
195
+ raise Glimmer::Error, 'Invalid custom shape for having no body! Please define body block!' if body_block.nil?
196
+ @body_root = instance_exec(&body_block)
197
+ raise Glimmer::Error, 'Invalid custom shape for having an empty body! Please fill body block!' if @body_root.nil?
198
+ execute_hook('after_body')
199
+ # TODO deregister all observer_registrations on destroy of the shape once that listener is supported
200
+ # (on_destroy) unless it is the last window closing, in which case exit faster
201
+ post_add_content if content.nil?
202
+ end
203
+
204
+ # Subclasses may override to perform post initialization work on an added child
205
+ def post_initialize_child(child)
206
+ # No Op by default
207
+ end
208
+
209
+ def post_add_content
210
+ # No Op by default
211
+ end
212
+
213
+ def observer_registrations
214
+ @observer_registrations ||= []
215
+ end
216
+
217
+ def can_handle_listener?(listener)
218
+ body_root&.can_handle_listener?(listener.to_s)
219
+ end
220
+
221
+ def handle_listener(listener, &block)
222
+ body_root.handle_listener(listener.to_s, &block)
223
+ end
224
+
225
+ # This method ensures it has an instance method not coming from Glimmer DSL
226
+ def has_instance_method?(method_name)
227
+ respond_to?(method_name) and
228
+ !@body_root.respond_to_libui?(method_name) and
229
+ (method(method_name) rescue nil) and
230
+ !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') and
231
+ !method(method_name)&.source_location&.first&.include?('glimmer/libui/shape.rb')
232
+ end
233
+
234
+ # Returns content block if used as an attribute reader (no args)
235
+ # Otherwise, if a block is passed, it adds it as content to this custom shape
236
+ def content(&block)
237
+ if block_given?
238
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Libui::CustomShapeExpression.new, self.class.keyword, &block)
239
+ else
240
+ @content
241
+ end
242
+ end
243
+
244
+ private
245
+
246
+ def execute_hook(hook_name)
247
+ hook_block = self.class.instance_variable_get("@#{hook_name}_block")
248
+ return if hook_block.nil?
249
+ temp_method_name = "#{hook_name}_block_#{hook_block.hash.abs}_#{(Time.now.to_f * 1_000_000).to_i}"
250
+ singleton_class.define_method(temp_method_name, &hook_block)
251
+ send(temp_method_name)
252
+ singleton_class.send(:remove_method, temp_method_name)
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ Dir[File.expand_path("./#{File.basename(__FILE__, '.rb')}/*.rb", __dir__)].each {|f| require f}
@@ -219,7 +219,7 @@ module Glimmer
219
219
  end
220
220
 
221
221
  def custom_window_gem(custom_window_name, namespace)
222
- gem_name = "glimmer-libui-cw-#{compact_name(custom_window_name)}"
222
+ gem_name = "glimmer-libui-cw-#{custom_window_name.underscore}"
223
223
  gem_summary = "#{human_name(custom_window_name)} - Glimmer Custom Window"
224
224
  begin
225
225
  custom_window_keyword = dsl_control_name(custom_window_name)
@@ -229,7 +229,7 @@ module Glimmer
229
229
  # No Op (keyword is not taken by a built in Ruby method)
230
230
  end
231
231
  if namespace
232
- gem_name += "-#{compact_name(namespace)}"
232
+ gem_name += "-#{namespace.underscore}"
233
233
  gem_summary += " (#{human_name(namespace)})"
234
234
  else
235
235
  return puts('Namespace is required! Usage: glimmer scaffold:gem:customwindow[name,namespace]') unless `git config --get github.user`.to_s.strip == 'AndyObtiva'
@@ -292,10 +292,10 @@ module Glimmer
292
292
  end
293
293
 
294
294
  def custom_control_gem(custom_control_name, namespace)
295
- gem_name = "glimmer-libui-cc-#{compact_name(custom_control_name)}"
295
+ gem_name = "glimmer-libui-cc-#{custom_control_name.underscore}"
296
296
  gem_summary = "#{human_name(custom_control_name)} - Glimmer Custom Control"
297
297
  if namespace
298
- gem_name += "-#{compact_name(namespace)}"
298
+ gem_name += "-#{namespace.underscore}"
299
299
  gem_summary += " (#{human_name(namespace)})"
300
300
  else
301
301
  return puts('Namespace is required! Usage: glimmer scaffold:custom_control_gem[custom_control_name,namespace]') unless `git config --get github.user`.to_s.strip == 'AndyObtiva'
@@ -331,10 +331,10 @@ module Glimmer
331
331
  end
332
332
 
333
333
  def custom_shape_gem(custom_shape_name, namespace)
334
- gem_name = "glimmer-libui-cs-#{compact_name(custom_shape_name)}"
334
+ gem_name = "glimmer-libui-cs-#{custom_shape_name.underscore}"
335
335
  gem_summary = "#{human_name(custom_shape_name)} - Glimmer Custom Shape"
336
336
  if namespace
337
- gem_name += "-#{compact_name(namespace)}"
337
+ gem_name += "-#{namespace.underscore}"
338
338
  gem_summary += " (#{human_name(namespace)})"
339
339
  else
340
340
  return puts('Namespace is required! Usage: glimmer scaffold:custom_shape_gem[custom_shape_name,namespace]') unless `git config --get github.user`.to_s.strip == 'AndyObtiva'
@@ -414,10 +414,6 @@ module Glimmer
414
414
  app_name.underscore.titlecase
415
415
  end
416
416
 
417
- def compact_name(gem_name)
418
- gem_name.underscore.camelcase.downcase
419
- end
420
-
421
417
  def gemfile(window_type)
422
418
  APP_GEMFILE
423
419
  end
@@ -436,8 +432,6 @@ module Glimmer
436
432
  end
437
433
 
438
434
  class #{class_name(app_name)}
439
- include Glimmer
440
-
441
435
  APP_ROOT = File.expand_path('../..', __FILE__)
442
436
  VERSION = File.read(File.join(APP_ROOT, 'VERSION'))
443
437
  LICENSE = File.read(File.join(APP_ROOT, 'LICENSE.txt'))
@@ -645,12 +639,11 @@ require '#{window_type == :app ? current_dir_name : namespace}/model/greeting'
645
639
 
646
640
  margined true
647
641
 
648
- vertical_box {
649
642
  MULTI_LINE_STRING
650
643
 
651
644
  if window_type == :gem
652
645
  custom_window_file_content += <<-MULTI_LINE_STRING
653
-
646
+ vertical_box {
654
647
  button('Preferences...') {
655
648
  stretchy false
656
649
 
@@ -658,14 +651,21 @@ require '#{window_type == :app ? current_dir_name : namespace}/model/greeting'
658
651
  display_preferences_dialog
659
652
  end
660
653
  }
661
- MULTI_LINE_STRING
662
- end
663
-
664
- custom_window_file_content += <<-MULTI_LINE_STRING
654
+
665
655
  label {
666
656
  #{%i[gem app].include?(window_type) ? "text <= [@greeting, :text]" : "text '#{human_name(custom_window_name)}'"}
667
657
  }
668
658
  }
659
+ MULTI_LINE_STRING
660
+ else
661
+ custom_window_file_content += <<-MULTI_LINE_STRING
662
+ label {
663
+ #{%i[gem app].include?(window_type) ? "text <= [@greeting, :text]" : "text '#{human_name(custom_window_name)}'"}
664
+ }
665
+ MULTI_LINE_STRING
666
+ end
667
+
668
+ custom_window_file_content += <<-MULTI_LINE_STRING
669
669
  }
670
670
  }
671
671
  MULTI_LINE_STRING
@@ -833,6 +833,7 @@ end
833
833
  namespace_type = class_name(namespace) == class_name(current_dir_name) ? 'class' : 'module'
834
834
 
835
835
  <<-MULTI_LINE_STRING
836
+ # Delete this example model and replace with your own model
836
837
  #{namespace_type} #{class_name(namespace)}
837
838
  module Model
838
839
  class #{class_name(model_name)}
@@ -113,7 +113,7 @@ namespace :glimmer do
113
113
  task :cc, [:name, :namespace] => :customcontrol
114
114
  task :custom_control, [:name, :namespace] => :customcontrol
115
115
  task :"custom-control", [:name, :namespace] => :customcontrol
116
- #
116
+
117
117
  # desc 'Scaffold Glimmer::UI::CustomShape subclass (part of a view) under app/views (namespace is optional) [alt: scaffold:cs]'
118
118
  # task :customshape, [:name, :namespace] do |t, args|
119
119
  # require_relative 'rake_task/scaffold'
@@ -123,7 +123,7 @@ namespace :glimmer do
123
123
  # task :cs, [:name, :namespace] => :customshape
124
124
  # task :custom_shape, [:name, :namespace] => :customshape
125
125
  # task :"custom-shape", [:name, :namespace] => :customshape
126
- #
126
+
127
127
  namespace :gem do
128
128
  desc 'Scaffold Glimmer::UI::CustomWindow subclass (full window view) under its own Ruby gem + app project (namespace is required) [alt: scaffold:gem:cw]'
129
129
  task :customwindow, [:name, :namespace] do |t, args|
@@ -134,17 +134,17 @@ namespace :glimmer do
134
134
  task :cw, [:name, :namespace] => :customwindow
135
135
  task :custom_window, [:name, :namespace] => :customwindow
136
136
  task :"custom-window", [:name, :namespace] => :customwindow
137
- #
138
- # desc 'Scaffold Glimmer::UI::CustomControl subclass (part of a view) under its own Ruby gem project (namespace is required) [alt: scaffold:gem:cc]'
139
- # task :customcontrol, [:name, :namespace] do |t, args|
140
- # require_relative 'rake_task/scaffold'
141
- # Glimmer::RakeTask::Scaffold.custom_control_gem(args[:name], args[:namespace])
142
- # end
143
- #
144
- # task :cc, [:name, :namespace] => :customcontrol
145
- # task :custom_control, [:name, :namespace] => :customcontrol
146
- # task :"custom-control", [:name, :namespace] => :customcontrol
147
- #
137
+
138
+ desc 'Scaffold Glimmer::UI::CustomControl subclass (part of a view) under its own Ruby gem project (namespace is required) [alt: scaffold:gem:cc]'
139
+ task :customcontrol, [:name, :namespace] do |t, args|
140
+ require_relative 'rake_task/scaffold'
141
+ Glimmer::RakeTask::Scaffold.custom_control_gem(args[:name], args[:namespace])
142
+ end
143
+
144
+ task :cc, [:name, :namespace] => :customcontrol
145
+ task :custom_control, [:name, :namespace] => :customcontrol
146
+ task :"custom-control", [:name, :namespace] => :customcontrol
147
+
148
148
  # desc 'Scaffold Glimmer::UI::CustomShape subclass (part of a view) under its own Ruby gem project (namespace is required) [alt: scaffold:gem:cs]'
149
149
  # task :customshape, [:name, :namespace] do |t, args|
150
150
  # require_relative 'rake_task/scaffold'
@@ -25,7 +25,7 @@ $LOAD_PATH.unshift(File.expand_path('..', __FILE__))
25
25
  require 'glimmer'
26
26
  require 'perfect-shape'
27
27
  # require 'logging'
28
- # require 'puts_debuggerer' if ENV['pd'].to_s.downcase == 'true'
28
+ require 'puts_debuggerer' if (ENV['PD'] || ENV['pd']).to_s.downcase == 'true'
29
29
  # require 'super_module'
30
30
  require 'color'
31
31
  require 'os'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glimmer-dsl-libui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.6
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-21 00:00:00.000000000 Z
11
+ date: 2023-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer
@@ -353,8 +353,9 @@ description: Glimmer DSL for LibUI (Fukuoka Award Winning Prerequisite-Free Ruby
353
353
  GUI that just works on Mac, Windows, and Linux! Glimmer DSL for LibUI aims to provide
354
354
  declarative DSL syntax that visually maps to GUI control hierarchy, convention over
355
355
  configuration via smart defaults, automation of low-level details, requiring the
356
- least amount of syntax possible to build GUI, bidirectional data-binding, and custom
357
- keyword support. If you liked Shoes, You'll love Glimmer!
356
+ least amount of syntax possible to build GUI, bidirectional data-binding, custom
357
+ control/window support, and application/gem/window/control scaffolding. If you liked
358
+ Shoes, You'll love Glimmer!
358
359
  email: andy.am@gmail.com
359
360
  executables:
360
361
  - glimmer
@@ -388,6 +389,7 @@ files:
388
389
  - examples/basic_child_window.rb
389
390
  - examples/basic_code_area.rb
390
391
  - examples/basic_composite_shape.rb
392
+ - examples/basic_custom_shape.rb
391
393
  - examples/basic_draw_text.rb
392
394
  - examples/basic_draw_text2.rb
393
395
  - examples/basic_entry.rb
@@ -500,6 +502,7 @@ files:
500
502
  - lib/glimmer/dsl/libui/bind_expression.rb
501
503
  - lib/glimmer/dsl/libui/control_expression.rb
502
504
  - lib/glimmer/dsl/libui/custom_control_expression.rb
505
+ - lib/glimmer/dsl/libui/custom_shape_expression.rb
503
506
  - lib/glimmer/dsl/libui/data_binding_expression.rb
504
507
  - lib/glimmer/dsl/libui/dsl.rb
505
508
  - lib/glimmer/dsl/libui/file_expression.rb
@@ -586,6 +589,7 @@ files:
586
589
  - lib/glimmer/libui/custom_control.rb
587
590
  - lib/glimmer/libui/custom_control/code_area.rb
588
591
  - lib/glimmer/libui/custom_control/refined_table.rb
592
+ - lib/glimmer/libui/custom_shape.rb
589
593
  - lib/glimmer/libui/custom_window.rb
590
594
  - lib/glimmer/libui/data_bindable.rb
591
595
  - lib/glimmer/libui/image_path_renderer.rb