AndyObtiva/glimmer-dsl-swt

View on GitHub
samples/elaborate/meta_sample.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# Copyright (c) 2007-2024 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'glimmer-dsl-swt'
require 'fileutils'
require 'yaml'
require 'net/http'

class Sample
  UNEDITABLE = ['meta_sample.rb'] + (OS.windows? ? ['calculator.rb', 'weather.rb'] : [])  # Windows StyledText does not support unicode characters found in certain samples
  URL_TUTORIALS = 'https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-swt/master/samples/elaborate/meta_sample/tutorials.yml'
  FILE_TUTORIALS = File.expand_path(File.join('meta_sample', 'tutorials.yml'), __dir__)
  TUTORIALS = YAML.load_file(FILE_TUTORIALS)
  
  class << self
    def glimmer_directory
      File.expand_path('../../..', __FILE__)
    end
  
    def user_glimmer_directory
      File.join(File.expand_path('~'), '.glimmer-dsl-swt')
    end
    
    def ensure_user_glimmer_directory
      unless @ensured_glimmer_directory
        Thread.new do
          FileUtils.rm_rf(user_glimmer_directory)
          FileUtils.cp_r(glimmer_directory, user_glimmer_directory)
          @ensured_glimmer_directory = true
        end
      end
    end
    
    def tutorials
      if remote_tutorials && !remote_tutorials.empty? && remote_tutorials.size >= local_tutorials.size
        remote_tutorials
      else
        local_tutorials
      end
    end
    
    def remote_tutorials
      if @remote_tutorials.nil?
        remote_tutorials_response = Net::HTTP.get_response(URI(URL_TUTORIALS))
        raise "Error downloading remote tutorial list from #{URL_TUTORIALS} (defaulting to local list): HTTP status #{remote_tutorials_response.code} #{remote_tutorials_response.message}!" unless remote_tutorials_response.code.to_i.between?(200, 299)
        remote_tutorials_yaml = remote_tutorials_response.body
        @remote_tutorials = YAML.load(remote_tutorials_yaml)
      end
      @remote_tutorials
    rescue => e
      Glimmer::Config.logger.error e.full_message
      nil
    end
    
    def local_tutorials
      TUTORIALS
    end
  end
  
  include Glimmer::DataBinding::ObservableModel
    
  attr_accessor :sample_directory, :file, :selected
  
  def initialize(file, sample_directory: )
    self.file = file
    self.sample_directory = sample_directory
  end
  
  def name
    if @name.nil?
      @name = File.basename(file, '.rb').split('_').map(&:capitalize).join(' ')
      if @name.start_with?('Hello')
        name_parts = @name.split
        name_parts[0] = name_parts.first + ','
        name_parts[-1] = name_parts.last + '!'
        @name = name_parts.join(' ')
      end
    end
    @name
  end
  
  def code
    reset_code! if @code.nil?
    @code
  end
  
  def reset_code!
    @code = File.read(file)
    notify_observers('code')
  end
  
  def editable
    !UNEDITABLE.include?(File.basename(file))
  end
  alias editable? editable
  
  def launchable
    File.basename(file) != 'meta_sample.rb'
  end
    
  def teachable
    !!tutorial
  end
  
  def tutorial
    Sample.tutorials[name]
  end
    
  def file_relative_path
    file.sub(Sample.glimmer_directory, '')
  end
  
  def user_file
    File.join(Sample.user_glimmer_directory, file_relative_path)
  end
    
  def user_file_parent_directory
    File.dirname(user_file)
  end
  
  def directory
    file.sub(/\.rb/, '')
  end
    
  def launch(modified_code)
    launch_file = user_file
    begin
      raise 'Unsupported through editor!' unless editable?
      FileUtils.cp_r(file, user_file_parent_directory)
      FileUtils.cp_r(directory, user_file_parent_directory) if File.exist?(directory)
      File.write(user_file, modified_code)
    rescue => e
      puts 'Error writing sample modifications. Launching original sample.'
      puts e.full_message
      launch_file = file # load original file if failed to write changes
    end
    load(launch_file)
  end
end

class SampleDirectory
  class << self
    attr_accessor :selected_sample
    
    def sample_directories
      if @sample_directories.nil?
        @sample_directories = Dir.glob(File.join(File.expand_path('..', __FILE__), '*')).
            select { |file| File.directory?(file) }.
            map { |file| SampleDirectory.new(file) }
        glimmer_gems = Gem.find_latest_files("glimmer-*-*")
        sample_directories = glimmer_gems.map do |lib|
          File.dirname(File.dirname(lib))
        end.select do |gem|
          Dir.exist?(File.join(gem, 'samples'))
        end.map do |gem|
          Dir.glob(File.join(gem, 'samples', '*')).select {|file_or_dir| Dir.exist?(file_or_dir)}
        end.flatten.uniq.reverse
        if Dir.exist?('samples')
          Dir.glob(File.join('samples', '*')).to_a.reverse.each do |dir|
            sample_directories << dir if Dir.exist?(dir)
          end
        end
        sample_directories = sample_directories.uniq {|dir| File.basename(dir)}
        @sample_directories = sample_directories.map { |file| SampleDirectory.new(file) }
      end
      @sample_directories
    end
    
    def all_samples
      @all_samples ||= sample_directories.map(&:samples).reduce(:+)
    end
  end
  
  include Glimmer # used for observe syntax
  
  attr_accessor :file, :selected_sample_name
  
  def initialize(file)
    self.file = file
  end
  
  def name
    File.basename(file).split('_').map(&:capitalize).join(' ')
  end
  
  def samples
    if @samples.nil?
      @samples = Dir.glob(File.join(file, '*')).
          select { |file| File.file?(file) }.
          map { |sample_file| Sample.new(sample_file, sample_directory: self) }.
          sort_by(&:name)
      
      @samples.each do |sample|
        observe(sample, :selected) do |new_selected_value|
          if new_selected_value
            SampleDirectory.all_samples.reject {|a_sample| a_sample.name == sample.name}.each do |other_sample|
              other_sample.selected = false
            end
            SampleDirectory.selected_sample = sample
          end
        end
      end
    end
    @samples
  end
  
  def selected_sample_name_options
    samples.map(&:name)
  end
  
  def selected_sample_name=(selected_name)
    @selected_sample_name = selected_name
    unless selected_name.nil?
      (SampleDirectory.sample_directories - [self]).each { |sample_dir| sample_dir.selected_sample_name = nil }
      SampleDirectory.selected_sample = samples.detect { |sample| sample.name == @selected_sample_name }
    end
  end
  
end

class MetaSampleApplication
  include Glimmer::UI::CustomShell
  
  before_body do
    Sample.ensure_user_glimmer_directory
    selected_sample_directory = SampleDirectory.sample_directories.first
    selected_sample = selected_sample_directory.samples.first
    selected_sample_directory.selected_sample_name = selected_sample.name
    Display.app_name = 'Glimmer Meta-Sample'
  end
  
  body {
    shell(:fill_screen) {
      minimum_size 640, 384
      text 'Glimmer Meta-Sample (The Sample of Samples)'
      image File.expand_path('../../icons/scaffold_app.png', __dir__)
      
      sash_form {
        weights 1, 3
      
        composite {
          grid_layout(1, false) {
            margin_width 0
            margin_height 0
          }
            
          expand_bar {
            layout_data(:fill, :fill, true, true)
            font height: 25
                        
            SampleDirectory.sample_directories.each { |sample_directory|
              expand_item {
                layout_data(:fill, :fill, true, true)
                text " #{sample_directory.name} Samples (#{sample_directory.samples.count})"
                
                radio_group { |radio_group_proxy|
                  row_layout(:vertical) {
                    fill true
                  }
                  selection <=> [sample_directory, :selected_sample_name]
                  font height: 20
                }
              }
            }
          }
          
          composite {
            fill_layout
            
            layout_data(:fill, :center, true, false) {
              height_hint 96
            }
            
            button {
              text 'Tutorial'
              font height: 25
              enabled <= [SampleDirectory, 'selected_sample.teachable']
              
              on_widget_selected do
                shell(:fill_screen) {
                  text "Glimmer DSL for SWT Video Tutorial - #{SampleDirectory.selected_sample.name}"
                  
                  browser {
                    text "<iframe src='https://www.youtube.com/embed/#{SampleDirectory.selected_sample.tutorial}?autoplay=1' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen style='width: 100%; height: 100%;'></iframe>"
                  }
                }.open
              end
            }
            
            button {
              text 'Launch'
              font height: 25
              enabled <= [SampleDirectory, 'selected_sample.launchable']
              
              on_widget_selected do
                begin
                  SampleDirectory.selected_sample.launch(@code_text.text)
                rescue LoadError, StandardError, SyntaxError => launch_error
                  error_dialog(message: launch_error.full_message).open
                end
              end
            }
            
            button {
              text 'Reset'
              font height: 25
              enabled <= [SampleDirectory, 'selected_sample.editable']
              
              on_widget_selected do
                SampleDirectory.selected_sample.reset_code!
              end
            }
          }
        }
            
        @code_text = code_text(lines: {width: 3}) {
          root {
            grid_layout(2, false) {
              horizontal_spacing 0
              margin_left 0
              margin_right 0
              margin_top 0
              margin_bottom 0
            }
          }
          line_numbers {
            background Display.system_dark_theme? ? :black : :white
          }
          text <=> [SampleDirectory, 'selected_sample.code']
          editable <= [SampleDirectory, 'selected_sample.editable']
          left_margin 7
          right_margin 7
        }
      }
    }
  }
  
  # Method-based error_dialog custom widget
  def error_dialog(message:)
    return if message.nil?
    dialog(body_root) { |dialog_proxy|
      row_layout(:vertical) {
        center true
      }
      
      text 'Error Launching'
        
      styled_text(:border, :h_scroll, :v_scroll) {
        layout_data {
          width body_root.bounds.width*0.75
          height body_root.bounds.height*0.75
        }
        
        text message
        editable false
        caret nil
      }
      
      button {
        text 'Close'
        
        on_widget_selected do
          dialog_proxy.close
        end
      }
    }
  end
end

MetaSampleApplication.launch