diff --git a/ext/wxruby3/include/wxruby-Config.h b/ext/wxruby3/include/wxruby-Config.h index cb3f9ec9..6ebc345a 100644 --- a/ext/wxruby3/include/wxruby-Config.h +++ b/ext/wxruby3/include/wxruby-Config.h @@ -53,6 +53,7 @@ static int wxrb_CountConfig(VALUE key, VALUE value, VALUE rbCounter) } static VALUE g_cConfigBase; +static VALUE g_cConfigWx; static VALUE g_cConfig; /* @@ -88,12 +89,18 @@ class wxRbHashConfig : public wxConfigBase virtual ~wxRbHashConfig() { - DATA_PTR(m_cfgInstance) = 0; // make sure it never get's deleted twice + if (!NIL_P(m_cfgInstance)) + { + DATA_PTR(m_cfgInstance) = 0; // make sure it never get's deleted twice + } } - // Get wrapped Ruby ConfigBase instance + // Get wrapped Ruby Config instance VALUE GetRubyConfig() const { return m_cfgInstance; } + // Reset wrapped Ruby Config instance + void ResetRubyConfig() { m_cfgInstance = Qnil; } + // implement inherited pure virtual functions virtual void SetPath(const wxString& strPath) override { DoSetPath(strPath, true /* create missing components */); } virtual const wxString& GetPath() const override { return m_strPath; } @@ -806,7 +813,7 @@ class wxRbHashConfig : public wxConfigBase void SetRootPath() { - m_strPath.Empty(); + m_strPath.Clear(); m_cfgGroup = m_cfgHash; m_cfgGroupKeys = Qnil; } @@ -815,7 +822,7 @@ class wxRbHashConfig : public wxConfigBase // if path doesn't exist and createMissingComponents == false bool DoSetPath(const wxString& strPath, bool createMissingComponents) { - if ( strPath.empty() ) + if ( strPath.IsEmpty() || strPath == cfgSepStr) { SetRootPath(); return true; @@ -913,7 +920,8 @@ static const char * __iv_Config_data = "@data"; WXRUBY_EXPORT bool wxRuby_IsRubyConfig(VALUE rbConfig) { - return rb_obj_is_kind_of(rbConfig, g_cConfig) == Qtrue; + return rb_obj_is_kind_of(rbConfig, g_cConfig) == Qtrue || + rb_obj_is_kind_of(rbConfig, g_cConfigWx) == Qtrue; } // Wrap a Ruby hash for input type mapping @@ -933,6 +941,12 @@ WXRUBY_EXPORT wxConfigBase* wxRuby_Ruby2ConfigBase(VALUE rbConfig) // return wrapper return config; } + else if (rb_obj_is_kind_of(rbConfig, g_cConfigWx) == Qtrue) + { + wxConfigBase* cfg; + Data_Get_Struct(rbConfig, wxConfigBase, cfg); + return cfg; + } return nullptr; } @@ -946,6 +960,10 @@ WXRUBY_EXPORT VALUE wxRuby_ConfigBase2Ruby(wxConfigBase* config) { return hsh_config->GetRubyConfig(); } + else + { + return Data_Wrap_Struct(g_cConfigWx, 0, 0, config); + } } return Qnil; } diff --git a/ext/wxruby3/include/wxruby-Persistence.h b/ext/wxruby3/include/wxruby-Persistence.h new file mode 100644 index 00000000..b1b3838a --- /dev/null +++ b/ext/wxruby3/include/wxruby-Persistence.h @@ -0,0 +1,79 @@ +// Copyright (c) 2023 M.J.N. Corino, The Netherlands +// +// This software is released under the MIT license. + +/* + * WxRuby3 persistence classes + */ + +#ifndef _WXRUBY_PERSISTENCE_HASH_H +#define _WXRUBY_PERSISTENCE_HASH_H + +#include +#include + +#include + +/* + This class serves as a base for any Ruby defined persistence manager in order to provide + customized save and restore methods for Ruby values but also as a replacement for the + default global persistence manager instance. + */ +class WxRubyPersistenceManager : public wxPersistenceManager +{ +private: + typedef std::map rb_object_to_rb_po_map_t; + rb_object_to_rb_po_map_t rb_object_po_map_; + +public: + WxRubyPersistenceManager() : wxPersistenceManager() {} + + bool SaveRubyValue(const wxPersistentObject& who, const wxString& name, VALUE value); + + VALUE RestoreRubyValue(const wxPersistentObject& who, const wxString& name); + + + bool DoSaveRubyValue(const wxPersistentObject& who, const wxString& name, VALUE value); + + VALUE DoRestoreRubyValue(const wxPersistentObject& who, const wxString& name); + + void RegisterRbPO(VALUE rb_obj, VALUE rb_po) + { + rb_object_po_map_[rb_obj] = rb_po; + } + + VALUE FindRbPO(VALUE rb_obj) + { + VALUE rb_po = Qnil; + if (rb_object_po_map_.count(rb_obj) > 0) + { + rb_po = rb_object_po_map_[rb_obj]; + } + return rb_po; + } + + VALUE UnregisterRbPO(VALUE rb_obj) + { + VALUE rb_po = Qnil; + if (rb_object_po_map_.count(rb_obj) > 0) + { + rb_po = rb_object_po_map_[rb_obj]; + rb_object_po_map_.erase(rb_obj); + } + return rb_po; + } + + void GC_markPO(); + + static void UnregisterPersistentObject(VALUE rb_obj); +}; + +class WxRubyPersistentObject : public wxPersistentObject +{ +public: + virtual ~WxRubyPersistentObject(); +protected: + WxRubyPersistentObject(VALUE rb_obj); +}; + +#endif /* _WXRUBY_PERSISTENCE_HASH_H */ diff --git a/ext/wxruby3/swig/memory_management.i b/ext/wxruby3/swig/memory_management.i index 6a86cb95..c103bd8a 100644 --- a/ext/wxruby3/swig/memory_management.i +++ b/ext/wxruby3/swig/memory_management.i @@ -106,6 +106,12 @@ GC_NEVER(kls); %define GC_MANAGE_AS_UNTRACKED(kls) %enddef +// Strategy for objects that are GC marked through customized, tailored, mechanisms outside +// of the standard SWIG object tracking option. +// The different naming is mostly to allow doc gen to properly recognize these objects. +%define GC_MANAGE_AS_MARKED(kls) +%enddef + // Sizers attached to windows are automatically destroyed by wxWidgets, // so they should not be deleted. // diff --git a/lib/wx/core/book_ctrl_base.rb b/lib/wx/core/book_ctrl_base.rb new file mode 100644 index 00000000..fce8dd84 --- /dev/null +++ b/lib/wx/core/book_ctrl_base.rb @@ -0,0 +1,16 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +module Wx + + class BookCtrlBase + + # create PersistentObject for toplevel windows (incl. Dialog and Frame) + def create_persistent_object + PersistentBookCtrl.new(self) + end + + end + +end diff --git a/lib/wx/core/config.rb b/lib/wx/core/config.rb index b9809bc7..c13fc989 100644 --- a/lib/wx/core/config.rb +++ b/lib/wx/core/config.rb @@ -8,25 +8,331 @@ module Wx - class Config < Wx::ConfigBase + class ConfigBase SEPARATOR = '/'.freeze module Interface - def each(&block) + # provide auto-magic accessor support for config objects + def method_missing(sym, *args, &block) + unless block_given? || args.size>1 + setter = false + key = sym.to_s.sub(/=\z/) { |s| setter = true; '' } + if (!setter && args.empty?) || (!has_group?(key) && setter && args.size==1) + if setter + return set(key, args.shift) + else + return get(key) + end + elsif setter && args.size == 1 && args.first.is_a?(::Hash) && has_group?(key) + return set(key, args.shift) + end + end + super + end + + end + + end + + class ConfigWx < ConfigBase + + include ConfigBase::Interface + + # add protection against exceptions raised in blocks + wx_for_path = instance_method :for_path + define_method :for_path do |path, &block| + if block + ex = nil + rc = wx_for_path.bind(self).call(path) do |cfg, key| + begin + block.call(cfg, key) + rescue Exception + ex = $! + nil + end + end + raise ex if ex + rc + else + nil + end + end + private :for_path # make this method private (internal use only) + + # add Enumerator support + + wx_each_entry = instance_method :each_entry + define_method :each_entry do |&block| + if block_given? + wx_each_entry.bind(self).call { |k| block.call(k, read(k)) } + else + ::Enumerator.new { |y| wx_each_entry.bind(self).call { |k| y << [k, read_entry(k)] } } + end + end + + wx_each_group = instance_method :each_group + define_method :each_group do |&block| + if block_given? + wx_each_group.bind(self).call { |k| block.call(k, Group.new(self, self.path.dup.push(k))) } + else + ::Enumerator.new { |y| wx_each_group.bind(self).call { |k| y << [k, Group.new(self, self.path.dup.push(k))] } } + end + end + + # make this return a path array + wx_path = instance_method :path + define_method :path do + wx_path.bind(self).call.split(ConfigBase::SEPARATOR) + end + + # protect against attempts to rename complete paths + wx_rename = instance_method :rename + define_method :rename do |old_key, new_key| + raise ArgumentError, 'No paths allowed' if old_key.index(ConfigBase::SEPARATOR) || new_key.index(ConfigBase::SEPARATOR) + wx_rename.bind(self).call(old_key, new_key) + end + + # fix recursive number_of_xxx methods as wxRegConfig does not support this currently + wx_number_of_entries = instance_method :number_of_entries + define_method :number_of_entries do |recurse=false| + if recurse + each_group.inject(wx_number_of_entries.bind(self).call) { |c, (_, g)| c + g.number_of_entries(true) } + else + wx_number_of_entries.bind(self).call + end + end + + wx_number_of_groups = instance_method :number_of_groups + define_method :number_of_groups do |recurse=false| + if recurse + each_group.inject(wx_number_of_groups.bind(self).call) { |c, (_, g)| c + g.number_of_groups(true) } + else + wx_number_of_groups.bind(self).call + end + end + + def root? + true + end + + def root + self + end + + def parent + nil + end + + def read(path_str, output=nil) + if has_group?(path_str) + raise TypeError, "Cannot convert group" unless output.nil? + Group.new(self, get_path(path_str)) + else + val = read_entry(path_str) + return val unless val && output + case + when ::String == output || ::String === output + val.to_s + when ::Integer == output || ::Integer === output + Kernel.Integer(val) + when ::Float == output || ::Float === output + Kernel.Float(val) + when ::TrueClass == output || ::FalseClass == output || output == true || output == false + val.is_a?(Integer) ? val != 0 : !!val + else + raise ArgumentError, "Unknown coercion type #{output.is_a?(::Class) ? output : output.class}" if output + val + end + end + end + alias :[] :read + + def write(path_str, val) + if val.nil? + delete(path_str) + nil + elsif val.is_a?(::Hash) + raise ArgumentError, 'Cannot change existing value entry to group.' if has_entry?(path_str) + group = Group.new(self, get_path(path_str)) + val.each_pair { |k, v| group.set(k, v) } + group + else + raise ArgumentError, 'Cannot change existing group to value entry.' if has_group?(path_str) + write_entry(path_str, val) + read_entry(path_str) + end + end + alias :[]= :write + + def get_path(path_str) + path_str = path_str.to_s + abs = path_str.start_with?(ConfigBase::SEPARATOR) + segs = path_str.split(ConfigBase::SEPARATOR) + segs.shift if abs + abs ? segs : (self.path+segs) + end + protected :get_path + + def get(key) + raise ArgumentError, 'No paths allowed' if key.index(ConfigBase::SEPARATOR) + if has_entry?(key) + read_entry(key) + elsif has_group?(key) + Group.new(self, self.path.dup.push(key)) + else + nil + end + end + + def set(key, val) + raise ArgumentError, 'No paths allowed' if key.index(ConfigBase::SEPARATOR) + if val.nil? + delete(key) + nil + else + if (!val.is_a?(::Hash) && !has_group?(key)) || has_entry?(key) + raise ArgumentError, 'Cannot change existing value entry to group.' if val.is_a?(::Hash) + write_entry(key, val) + read_entry(key) + else + raise ArgumentError, 'Cannot change existing group to value entry.' if has_group?(key) && !val.is_a?(::Hash) + delete(key) + group = Group.new(self, self.path.dup.push(key)) + val.each_pair { |k, v| group.set(k, v) } + group + end + end + end + + def to_s + ConfigBase::SEPARATOR + end + + def to_h + data = each_entry.inject({}) { |hash, pair| hash[pair.first] = pair.last; hash } + each_group.inject(data) { |hash, pair| hash[pair.first] = pair.last.to_h; hash } + end + + def replace(hash) + raise ArgumentError, 'Expected Hash' unless hash.is_a?(::Hash) + clear + hash.each_pair { |k,v| self.set(k, v) } + self + end + + class Group + + include ConfigBase::Interface + + def initialize(parent, path) + @parent = parent + @path = path.freeze + @path_str = ConfigBase::SEPARATOR + @path.join(ConfigBase::SEPARATOR) + ConfigBase::SEPARATOR + end + + def root? + false + end + + def root + @parent.root + end + + def path + @path + end + + def parent + @parent + end + + def each_entry(&block) + if block_given? + root.__send__(:for_path, @path_str) do |cfg, _| + cfg.each_entry(&block) + end + else + ::Enumerator.new { |y| root.__send__(:for_path, @path_str) { |cfg,_| cfg.each_entry { |k,v| y << [k, v] } } } + end + end + + def each_group(&block) if block_given? - @data.each_pair(&block) + root.__send__(:for_path, @path_str) do |cfg, _| + cfg.each_group(&block) + end else - @data.each_pair + ::Enumerator.new { |y| root.__send__(:for_path, @path_str) { |cfg,_| cfg.each_group { |k,g| y << [k, g] } } } end end + def number_of_entries(recurse=false) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.number_of_entries(recurse) } + end + + def number_of_groups(recurse=false) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.number_of_groups(recurse) } + end + + def get(key) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.get(key) } + end + + def set(key, val) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.set(key, val) } + end + + def delete(path_str) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.delete(path_str) } + end + + def rename(old_key, new_key) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.rename(old_key, new_key) } + end + + def has_entry?(path_str) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.has_entry?(path_str) } + end + + def has_group?(path_str) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.has_group?(path_str) } + end + + def read(path_str, output=nil) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.read(path_str, output) } + end + alias :[] :read + + def write(path_str, val) + root.__send__(:for_path, @path_str) { |cfg,_| cfg.write(path_str, val) } + end + alias :[]= :write + + def to_s + @path_str + end + + def to_h + root.__send__(:for_path, @path_str) { |cfg,_| cfg.to_h } + end + + end + + end + + class Config < ConfigBase + + include ConfigBase::Interface + + module Interface + def each_entry(&block) if block_given? - @data.keys.select { |k| !@data[k].is_a?(::Hash) }.each(&block) + @data.select { |_,v| !v.is_a?(::Hash) }.each(&block) else - ::Enumerator.new { |y| @data.keys.each { |k| y << k if !@data[k].is_a?(::Hash) } } + ::Enumerator.new { |y| @data.each_pair { |k,v| y << [k,v] if !v.is_a?(::Hash) } } end end @@ -34,21 +340,21 @@ def each_group(&block) if block_given? @data.select { |_,g| g.is_a?(::Hash) }.each { |k,g| block.call(k, Group.new(self, self.path.dup.push(k), g)) } else - ::Enumerator.new { |y| @data.each { |k,g| y << [k,Group.new(self, self.path.dup.push(k), g)] if g.is_a?(::Hash) } } + ::Enumerator.new { |y| @data.each_pair { |k,g| y << [k,Group.new(self, self.path.dup.push(k), g)] if g.is_a?(::Hash) } } end end - def number_of_entries(recurse: false) + def number_of_entries(recurse=false) if recurse - each_group.inject(each_entry.inject(0) { |c, _| c + 1 }) { |c, (_, g)| c + g.number_of_entries(recurse: true) } + each_group.inject(each_entry.inject(0) { |c, _| c + 1 }) { |c, (_, g)| c + g.number_of_entries(true) } else each_entry.inject(0) { |c, _| c + 1 } end end - def number_of_groups(recurse: false) + def number_of_groups(recurse=false) if recurse - each_group.inject(0) { |c, (_,g)| c + 1 + g.number_of_groups(recurse: true) } + each_group.inject(0) { |c, (_,g)| c + 1 + g.number_of_groups(true) } else each_group.inject(0) { |c, _| c + 1 } end @@ -80,6 +386,7 @@ def has_group?(path_str) def get(key) key = key.to_s + raise ArgumentError, 'No paths allowed' if key.index(ConfigBase::SEPARATOR) elem = @data[key] if elem.is_a?(::Hash) Group.new(self, self.path.dup.push(key), elem) @@ -90,6 +397,7 @@ def get(key) def set(key, val) key = key.to_s + raise ArgumentError, 'No paths allowed' if key.index(ConfigBase::SEPARATOR) exist = @data.has_key?(key) elem = exist ? @data[key] : nil if val.nil? @@ -107,13 +415,26 @@ def set(key, val) end end - def delete(key) - @data.delete(key.to_s) + def delete(path_str) + segments, abs = get_path(path_str) + return nil if segments.empty? + last = segments.pop + group_data = if segments.empty? + @data + else + unless abs || root? + segments = self.path + segments + end + get_group_at(segments, create_missing_groups: false) + end + raise ArgumentError, "Unable to resolve path #{segments+[last]}" unless group_data + group_data.delete(last) end def rename(old_key, new_key) old_key = old_key.to_s new_key = new_key.to_s + raise ArgumentError, 'No paths allowed' if old_key.index(ConfigBase::SEPARATOR) || new_key.index(ConfigBase::SEPARATOR) if @data.has_key?(old_key) && !@data.has_key?(new_key) @data[new_key] = @data.delete(old_key) true @@ -122,7 +443,7 @@ def rename(old_key, new_key) end end - def [](path_str) + def read(path_str, output=nil) segments, abs = get_path(path_str) return nil if segments.empty? last = segments.pop @@ -135,15 +456,29 @@ def [](path_str) get_group_at(segments, create_missing_groups: true) end raise ArgumentError, "Unable to resolve path #{segments+[last]}" unless group_data - elem = group_data[last] - if elem.is_a?(::Hash) - Group.new(self, segments.dup.push(last), elem) + val = group_data[last] + if val.is_a?(::Hash) + raise TypeError, "Cannot convert group" unless output.nil? + Group.new(self, segments.dup.push(last), val) else - elem + case + when ::String == output || ::String === output + val.to_s + when ::Integer == output || ::Integer === output + Kernel.Integer(val) + when ::Float == output || ::Float === output + Kernel.Float(val) + when ::TrueClass == output || ::FalseClass == output || output == true || output == false + val.is_a?(::Integer) ? val != 0 : !!val + else + raise ArgumentError, "Unknown coercion type #{output.is_a?(::Class) ? output : output.class}" if output + val + end end end + alias :[] :read - def []=(path_str, val) + def write(path_str, val) segments, abs = get_path(path_str) return false if segments.empty? last = segments.pop @@ -165,16 +500,17 @@ def []=(path_str, val) raise ArgumentError, 'Cannot change existing value entry to group.' if exist && !elem.is_a?(::Hash) elem = group_data[last] = {} unless elem group = Group.new(self, segments.dup.push(last), elem) - val.each_pair { |key, val| group.set(key, val) } + val.each_pair { |k, v| group.set(k, v) } group else raise ArgumentError, 'Cannot change existing group to value entry.' if exist && elem.is_a?(::Hash) group_data[last] = sanitize_value(val) end end + alias :[]= :write def to_s - SEPARATOR+self.path.join(SEPARATOR) + ConfigBase::SEPARATOR+self.path.join(ConfigBase::SEPARATOR) end def to_h @@ -183,8 +519,8 @@ def to_h def get_path(path_str) path_str = path_str.to_s - abs = path_str.start_with?(SEPARATOR) - segs = path_str.split(SEPARATOR) + abs = path_str.start_with?(ConfigBase::SEPARATOR) + segs = path_str.split(ConfigBase::SEPARATOR) segs.shift if abs [segs, abs] end @@ -210,6 +546,8 @@ def sanitize_value(val) class Group + include ConfigBase::Interface + include Interface def initialize(parent, path, data) @@ -264,6 +602,11 @@ def parent nil end + def clear + @data.clear + true + end + def replace(hash) raise ArgumentError, 'Expected Hash' unless hash.is_a?(::Hash) @data.clear diff --git a/lib/wx/core/notebook.rb b/lib/wx/core/notebook.rb index ef27ac3b..f64d3246 100644 --- a/lib/wx/core/notebook.rb +++ b/lib/wx/core/notebook.rb @@ -8,15 +8,17 @@ # Displays a set of pages in parallel using tabs -class Wx::Notebook - # Convenience method for iterating pages - def each_page - if block_given? - 0.upto(get_page_count - 1) do | i | - yield get_page(i) +module Wx + class Notebook + # Convenience method for iterating pages + def each_page + if block_given? + 0.upto(get_page_count - 1) do | i | + yield get_page(i) + end + else + ::Enumerator.new { |y| each_page { |pg| y << pg } } end - else - ::Enumerator.new { |y| each_page { |pg| y << pg } } end end end diff --git a/lib/wx/core/peristent_object.rb b/lib/wx/core/peristent_object.rb new file mode 100644 index 00000000..a7612014 --- /dev/null +++ b/lib/wx/core/peristent_object.rb @@ -0,0 +1,15 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +module Wx + + class PersistentObject + + # make these protected + protected :save_value + protected :restore_value + + end + +end diff --git a/lib/wx/core/persistence_manager.rb b/lib/wx/core/persistence_manager.rb new file mode 100644 index 00000000..c0d84266 --- /dev/null +++ b/lib/wx/core/persistence_manager.rb @@ -0,0 +1,39 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +module Wx + + # Function used to create the correct persistent adapter for the given object. + def self.create_persistent_object(obj) + obj.create_persistent_object + end + + # A shorter synonym for {Wx::PersistenceManager#register_and_restore}. + def self.persistent_register_and_restore(obj, name=nil) + obj.name = name if name && !name.empty? + PersistenceManager.get.register_and_restore(obj) + end + + class PersistenceManager + + class << self + + # Cache the global instance to keep it safe from GC + + wx_get = instance_method :get + define_method :get do + @the_manager ||= wx_get.bind(self).call + end + + wx_set = instance_method :set + define_method :set do |pman| + wx_set.bind(self).call(pman) + @the_manager = pman + end + + end + + end + +end diff --git a/lib/wx/core/persistent_window.rb b/lib/wx/core/persistent_window.rb new file mode 100644 index 00000000..3f00c8d5 --- /dev/null +++ b/lib/wx/core/persistent_window.rb @@ -0,0 +1,16 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +module Wx + + class PersistentWindowBase < PersistentObject + + alias :get :get_object + + end + + # class alias + PersistentWindow = PersistentWindowBase + +end diff --git a/lib/wx/core/top_level_window.rb b/lib/wx/core/top_level_window.rb new file mode 100644 index 00000000..e2488ac1 --- /dev/null +++ b/lib/wx/core/top_level_window.rb @@ -0,0 +1,16 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +module Wx + + class TopLevelWindow + + # create PersistentObject for toplevel windows (incl. Dialog and Frame) + def create_persistent_object + PersistentTLW.new(self) + end + + end + +end diff --git a/lib/wx/core/treebook.rb b/lib/wx/core/treebook.rb new file mode 100644 index 00000000..c2e2f72b --- /dev/null +++ b/lib/wx/core/treebook.rb @@ -0,0 +1,18 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +require_relative './book_ctrl_base' + +module Wx + + class Treebook + + # Creates PersistentObject for this treebook control instance. + def create_persistent_object + PersistentTreeBookCtrl.new(self) + end + + end + +end diff --git a/lib/wx/doc/book_ctrl_base.rb b/lib/wx/doc/book_ctrl_base.rb new file mode 100644 index 00000000..f8744405 --- /dev/null +++ b/lib/wx/doc/book_ctrl_base.rb @@ -0,0 +1,19 @@ +# :stopdoc: +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. +# :startdoc: + + +module Wx + + class BookCtrlBase < Control + + # Creates PersistentObject for this book control instance (incl. ChoiceBook, ListBook and NoteBook). + # @see Wx.create_persistent_object + # @return [Wx::PersistentBookCtrl] + def create_persistent_object; end + + end + +end diff --git a/lib/wx/doc/config.rb b/lib/wx/doc/config.rb index 3aeb4943..fa6961b5 100644 --- a/lib/wx/doc/config.rb +++ b/lib/wx/doc/config.rb @@ -14,53 +14,35 @@ module Wx # as in C++. class ConfigBase - # Create a new config object and sets it as the current one unless a global Ruby config was already created/installed. + # Create a new config instance and sets it as the current one unless a global config was already created/installed. If + # forced_create is true any existing global config will be replaced by a new config instance. # This function will create the most appropriate implementation of Wx::ConfigBase available for the current platform. - # In Ruby this means that a Wx::Config instance will created and appropriately wrapped in C++. - # @return [Wx::Config] the current configuration object - def self.create; end + # If use_hash_config is true this means that a Wx::Config instance will created and appropriately wrapped in C++ + # otherwise the default C++ config for the current/active platform will be used. + # @param [Boolean] forced_create specifies to force replacing any existing global config if true + # @param [Boolean] use_hash_config specifies to create a Ruby Hash based config when required if true + # @return [Wx::ConfigBase] the current configuration object + def self.create(forced_create=false, use_hash_config: false) end # Sets the config object as the current one, returns the previous current object (both the parameter and returned # value may be nil). - # @param [Wx::Config,nil] config the config object to install - # @return [Wx::Config] the previous current configuration object + # @param [Wx::ConfigBase,nil] config the config object to install + # @return [Wx::ConfigBase] the previous current configuration object def self.set(config) end # Get the current config object. - # If there is no current object and create_on_demand is true, this creates one. - # @param [Boolean] create_on_demand specifies whether to create a configuration object is none has been created/installed before - # @return [Wx::Config,nil] the current configuration object + # If there is no current object and create_on_demand is true, this creates a default config instance appropriate for + # the current/active platform (registry based for Windows and file based otherwise). + # @param [Boolean] create_on_demand specifies whether to create a configuration object if none has been created/installed before + # @return [Wx::ConfigBase,nil] the current configuration object def self.get(create_on_demand=true) end - end - - # Configuration class for wxRuby which stores it's settings in a (possibly nested) Hash. - # This way configurations can be easily persisted using any commonly used Ruby methods like - # YAML or JSON files. - # - # Wx::Config supports Boolean (true or false), Integer, Float and String values and nested groups - # (essentially nested hashes). Any entry values set will be sanitized to match the supported types, i.e. - # if the value matches a supported type the value is accepted unaltered otherwise Integer (`to_int`), Float (`to_f`) - # or String (`to_s`) coercion are applied (in that order). Hash values are installed as nested groups. - # - # Like the C++ wxConfigBase derivatives Wx::Config supports arbitrary access using path strings which support - # absolute paths ('/xxxx') and relative paths ('xxx/xxx', '../xxx', './xxxx'). Relative segments can also be - # embedded in the path strings ('/aaa/bbb/../ccc'). - class Config < ConfigBase - # Config path separator SEPARATOR = '/' # Common configuration access methods for either the root object or any nested group objects. module Interface - # Iterate all settings at the current object (no recursion). - # Passes key/value pairs to the given block or returns an Enumerator is no block given. - # @yieldparam [String] key setting key - # @yieldparam [Boolean,String,Integer,Float,Wx::Config::Group] value setting value - # @return [Object,Enumerator] either the last result of the executed block or an enumerator if no block given - def each(&block) end - # Iterate all value entries at the current object (no recursion). # Passes key/value pairs to the given block or returns an Enumerator is no block given. # @yieldparam [String] key entry key @@ -79,13 +61,13 @@ def each_group(&block) end # any nested groups (if recurse is true) # @param [Boolean] recurse # @return [Integer] count - def number_of_entries(recurse: false) end + def number_of_entries(recurse=false) end # Returns the total number of group entries at the current object only (if recurse is false) or including # any nested groups (if recurse is true) # @param [Boolean] recurse # @return [Integer] count - def number_of_groups(recurse: false) end + def number_of_groups(recurse=false) end # Returns if a value entry exists matching the given path string. # Path strings can be absolute (starting with {SEPARATOR}) or relative to the current object and can have @@ -114,10 +96,10 @@ def get(key) end # @return [Boolean,String,Integer,Float,Wx::Config::Group,nil] value entry value def set(key, val) end - # Removes the entry at the current object identified by `key` if it exists and returns it's value. - # @param [String] key entry key + # Removes the entry identified by `path_str` if it exists and returns it's value. + # @param [String] path_str entry path # @return [Boolean,String,Integer,Float,Hash,nil] entry value - def delete(key) end + def delete(path_str) end # Changes key for the entry at the current object identified by `old_key` to `new_key` if it exists. # @param [String] old_key current entry key @@ -126,7 +108,17 @@ def delete(key) end def rename(old_key, new_key) end # Returns a value for an entry from the configuration identified by `path_str`. - # Allows arbitrary access though the entire configuration using absolute or relative paths. + # Provides arbitrary access though the entire configuration using absolute or relative paths. + # Supports coercing configuration values to a specified output type (Integer,Float,String,TrueClass,FalseClass). + # By default returns un-coerced value. + # Raises exception if incompatible coercion is specified. + # @param [String] path_str + # @param [Class,nil] output output type to convert to + # @return [Boolean,String,Integer,Float,Wx::Config::Group,nil] value entry value + def read(path_str, output=nil) end + + # Returns a value for an entry from the configuration identified by `path_str`. + # Provides arbitrary access though the entire configuration using absolute or relative paths. # @param [String] path_str # @return [Boolean,String,Integer,Float,Wx::Config::Group,nil] value entry value def [](path_str) end @@ -137,7 +129,8 @@ def [](path_str) end # @param [String] path_str # @param [Boolean,String,Integer,Float,Hash,nil] val entry value # @return [Boolean,String,Integer,Float,Wx::Config::Group,nil] value entry value - def []=(path_str, val) end + def write(path_str, val) end + alias :[]= :write # Returns the path string for the current configuration object. # @return [String] @@ -166,19 +159,66 @@ def parent; end end + end + + # This is an abstract class wrapping the default C++ Config class for the active platform + # (on Windows this would be `wxRegConfig` and `wxFileConfig` otherwise). + # + # Unless {Wx::ConfigBase.create} or {Wx::ConfigBase.set} has been called this is what will be + # returned by {Wx::ConfigBase.get}. + class ConfigWx < ConfigBase + + class Group + + include ConfigBase::Interface + + end + + include ConfigBase::Interface + + # Deletes all configuration content and returns true if successful. + # Also deletes any persisted storage (files or registry entries). + # @return [Boolean] + def clear; end + + # Replaces the configuration content with the content of the provided Hash. + # @param [Hash] hash content to replace configuration + # @return [self] + def replace(hash) end + + end + + # Configuration class for wxRuby which stores it's settings in a (possibly nested) Hash. + # This way configurations can be easily persisted using any commonly used Ruby methods like + # YAML or JSON files. + # + # Wx::Config supports Boolean (true or false), Integer, Float and String values and nested groups + # (essentially nested hashes). Any entry values set will be sanitized to match the supported types, i.e. + # if the value matches a supported type the value is accepted unaltered otherwise Integer (`to_int`), Float (`to_f`) + # or String (`to_s`) coercion are applied (in that order). Hash values are installed as nested groups. + # + # Like the C++ wxConfigBase derivatives Wx::Config supports arbitrary access using path strings which support + # absolute paths ('/xxxx') and relative paths ('xxx/xxx', '../xxx', './xxxx'). Relative segments can also be + # embedded in the path strings ('/aaa/bbb/../ccc'). + class Config < ConfigBase + class Group - include Interface + include ConfigBase::Interface end - include Interface + include ConfigBase::Interface # Constructor. # @param [Hash] hash optional Hash initializing configuration object # @return [Wx::Config] def initialize(hash = nil)end + # Deletes all configuration content and returns if successful. + # @return [true] + def clear; end + # Replaces the configuration content with the content of the provided Hash. # Values will be sanitized (see {Wx::Config}). # @param [Hash] hash content to replace configuration diff --git a/lib/wx/doc/persistence_manager.rb b/lib/wx/doc/persistence_manager.rb new file mode 100644 index 00000000..2d6d7070 --- /dev/null +++ b/lib/wx/doc/persistence_manager.rb @@ -0,0 +1,36 @@ +# :stopdoc: +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. +# :startdoc: + + +module Wx + + # Function used to create the correct persistent adapter for the given object. + # + # This is a compatibility function that simply redirects the call to the object itself. Any object class + # supporting persistence should implement the #create_persistent_object method to return a Wx::PersistentObject + # instance for the object it is called for. + # This method raises a NoImplementError if the object class does not support persistence. + # @see Defining Custom Persistent Windows + # @param obj [Object] + # @return [Wx::PersistentObject] + def self.create_persistent_object(obj) end + + # A shorter synonym for {Wx::PersistenceManager#register_and_restore}. + # + # This function simply calls {Wx::PersistenceManager#register_and_restore} but using it results in slightly shorter + # code as it calls {Wx::PersistenceManager.get} internally. As an additional convenience, this function can also set the window name. + # + # Returns true if the settings were restored or false otherwise (this will always be the case when the program runs + # for the first time, for example). + # @param obj [Wx::Window] window to register with persistence manager and to try to restore the settings for. + # @param name [String] If specified non-empty, window name is changed to the provided value before registering it. + # @return [Boolean] + def self.persistent_register_and_restore(obj, name=nil) end + + # class alias + PersistentWindow = PersistentWindowBase + +end diff --git a/lib/wx/doc/persistent_object.rb b/lib/wx/doc/persistent_object.rb new file mode 100644 index 00000000..32da870c --- /dev/null +++ b/lib/wx/doc/persistent_object.rb @@ -0,0 +1,27 @@ +# :stopdoc: +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. +# :startdoc: + + +module Wx + + class PersistentObject < ::Object + + # Save the specified value using the given name. + # @param [String] name The name of the value in the configuration file. + # @param [Object] value The value to save, currently must be a type supported by wxConfig. + # @return [Boolean] true if the value was saved or false if an error occurred. + def save_value(name, value); end + protected :save_value + + # Restore a value saved by {#save_value}. + # @param [String] name The name of the value in the configuration file. + # @return [Object,nil] The value if successfully read, nil otherwise + def restore_value(name); end + protected :restore_value + + end + +end diff --git a/lib/wx/doc/top_level_window.rb b/lib/wx/doc/top_level_window.rb new file mode 100644 index 00000000..bd560a61 --- /dev/null +++ b/lib/wx/doc/top_level_window.rb @@ -0,0 +1,19 @@ +# :stopdoc: +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. +# :startdoc: + + +module Wx + + class TopLevelWindow < NonOwnedWindow + + # Creates PersistentObject for this toplevel window instance (incl. Dialog and Frame). + # @see Wx.create_persistent_object + # @return [Wx::PersistentTLW] + def create_persistent_object; end + + end + +end diff --git a/lib/wx/doc/treebook.rb b/lib/wx/doc/treebook.rb index 454fe556..23d289e8 100644 --- a/lib/wx/doc/treebook.rb +++ b/lib/wx/doc/treebook.rb @@ -7,13 +7,18 @@ module Wx - class Treebook + class Treebook < BookCtrlBase # Returns the Wx::TreeCtrl used for this Treebook # @return [Wx::TreeCtrl] the tree control def get_tree_ctrl; end alias :tree_ctrl :get_tree_ctrl + # Creates PersistentObject for this treebook control instance. + # @see Wx.create_persistent_object + # @return [Wx::PersistentTreeBookCtrl] + def create_persistent_object; end + end end diff --git a/rakelib/build.rb b/rakelib/build.rb index 3de3acb8..5edfabe6 100644 --- a/rakelib/build.rb +++ b/rakelib/build.rb @@ -48,7 +48,7 @@ def enum_list_cache end # The main source module - which needs to initialize all the other modules in the package - file pkg.initializer_src => pkg.all_swig_files do |t| + file pkg.initializer_src => (pkg.all_swig_files + (pkg.parent ? [pkg.parent.initializer_src] : [])) do |t| pkg.generate_initializer end diff --git a/rakelib/lib/core/package.rb b/rakelib/lib/core/package.rb index f587a33c..8318471b 100644 --- a/rakelib/lib/core/package.rb +++ b/rakelib/lib/core/package.rb @@ -6,6 +6,8 @@ # wxRuby3 extension library Package class ### +require 'monitor' + module WXRuby3 class Director @@ -434,9 +436,29 @@ class << self def generated_events @generated_events ||= ::Set.new end + + def event_list_packages + unless @event_list_packages + @event_list_packages = ::Set.new + @event_list_packages.extend(MonitorMixin) + end + @event_list_packages + end end def generate_event_list + # make sure parent package has already generated event list + Package.event_list_packages.synchronize do + if parent && !Package.event_list_packages.include?(parent) + # make sure all included director modules have been extracted + parent.included_directors.each do |dir| + dir.extract_interface(false) # no need to generate anything here + end + parent.generate_event_list + end + end + # list ourselves as generated + Package.event_list_packages << self # determine Ruby library events root for package rbevt_root = File.join(ruby_classes_path, 'events') # create event list file @@ -483,7 +505,6 @@ class Wx::EvtHandler fout.puts 'end' end end - private :generate_event_list def find_event_doc(evh_name) unless doc = event_docs[evh_name] diff --git a/rakelib/lib/core/spec.rb b/rakelib/lib/core/spec.rb index 0cdc7b68..7f53cfdf 100644 --- a/rakelib/lib/core/spec.rb +++ b/rakelib/lib/core/spec.rb @@ -272,6 +272,16 @@ def gc_as_untracked(*names) self end + def gc_as_marked(*names) + if names.empty? + @gc_type = :GC_MANAGE_AS_MARKED + else + @gc_type = ::Hash.new unless @gc_type.is_a?(::Hash) + names.flatten.each {|n| @gc_type[n] = :GC_MANAGE_AS_MARKED } + end + self + end + def gc_type(name) @gc_type.is_a?(::Hash) ? @gc_type[name] : @gc_type end diff --git a/rakelib/lib/core/spec_helper.rb b/rakelib/lib/core/spec_helper.rb index 98770d6a..43e77102 100644 --- a/rakelib/lib/core/spec_helper.rb +++ b/rakelib/lib/core/spec_helper.rb @@ -121,7 +121,7 @@ def base_list(classdef_or_name, do_not_fold: false) def get_base_module_list(hierarchy, foldedbases, list = ::Set.new) hierarchy.each_value do |super_def| - list << super_def.module unless foldedbases.include?(super_def.name) + list << super_def.module unless foldedbases.include?(super_def.name) || super_def.module == module_name get_base_module_list(super_def.supers, folded_bases(super_def.name), list) end list diff --git a/rakelib/lib/director/config_base.rb b/rakelib/lib/director/config_base.rb index 0fa7a98e..70780b09 100644 --- a/rakelib/lib/director/config_base.rb +++ b/rakelib/lib/director/config_base.rb @@ -17,9 +17,22 @@ def setup spec.items.clear spec.add_header_code <<~__HEREDOC #include "wxruby-Config.h" + #include static const char * __iv_ConfigBase_sc_config = "@config"; + static WxRuby_ID s_use_hash_config_id("use_hash_config"); + + static void + _free_config_wx(void* cfg) + { + if (cfg) + { + wxConfigBase* config = (wxConfigBase*)cfg; + delete config; + } + } + static VALUE config_base_get(int argc, VALUE *argv, VALUE self) { bool autoCreate = true; @@ -30,34 +43,110 @@ def setup rb_raise(rb_eArgError, "Expected a single boolean argument"); return Qnil; } - autoCreate = !(argv[0] == Qfalse || argv[0] == Qnil); + autoCreate = !(argv[0] == Qfalse || argv[0] == Qnil); // test truthy-ness } + // get global ConfigBase instance from Ruby instance variable of ConfigBase singleton class VALUE cConfigBase_Singleton = rb_funcall(g_cConfigBase, rb_intern("singleton_class"), 0, 0); VALUE curConfig = rb_iv_get(cConfigBase_Singleton, __iv_ConfigBase_sc_config); - // create new config instance if none exists and autoCreate is true - if (NIL_P(curConfig) && autoCreate) + + if (NIL_P(curConfig)) { - // create new ConfigBase instance - curConfig = rb_class_new_instance(0, 0, g_cConfig); - // set global wxConfigBase instance to a new Ruby Config wrapper - wxConfigBase::Set(wxRuby_Ruby2ConfigBase(curConfig)); - // store global ConfigBase instance as Ruby instance variable of ConfigBase singleton class - // (keeps it safe from GC) - VALUE cConfigBase_Singleton = rb_funcall(g_cConfigBase, rb_intern("singleton_class"), 0, 0); - rb_iv_set(cConfigBase_Singleton, __iv_ConfigBase_sc_config, curConfig); - } + wxConfigBase* cfg = wxConfigBase::Get(autoCreate); + if (cfg) + { + // wrap the C++ config object + curConfig = Data_Wrap_Struct(g_cConfigWx, 0, 0, cfg); + // store global ConfigBase instance as Ruby instance variable of ConfigBase singleton class + rb_iv_set(cConfigBase_Singleton, __iv_ConfigBase_sc_config, curConfig); + } + } return curConfig; } static VALUE config_base_create(int argc, VALUE *argv, VALUE self) { + bool forced_create = false; + bool use_hash = false; if (argc>0) { - rb_raise(rb_eArgError, "No arguments allowed."); - return Qnil; + if (argc>2) + { + rb_raise(rb_eArgError, "Unexpected number of arguments."); + return Qnil; + } + if (argc>1 && TYPE(argv[1]) != T_HASH) + { + rb_raise(rb_eArgError, "Expected kwargs for 2."); + return Qnil; + } + if ((argc==1 && TYPE(argv[0]) != T_HASH) || argc>1) + { + VALUE rb_forced_create = argc==1 ? argv[0] : argv[1]; + forced_create = !(rb_forced_create == Qfalse || rb_forced_create == Qnil); // test truthy-ness + } + if (TYPE(argv[argc-1]) == T_HASH) + { + VALUE rb_hash = argv[argc-1]; + int hsz = RHASH_SIZE(rb_hash); + if (hsz>1 || (hsz==1 &&!rb_hash_includes(rb_hash, ID2SYM(s_use_hash_config_id())))) + { + rb_raise(rb_eArgError, "Unexpected keyword argument. Only :use_hash_config allowed."); + return Qnil; + } + + VALUE rb_use_hash = rb_hash_aref(rb_hash, ID2SYM(s_use_hash_config_id())); + use_hash = !(rb_use_hash == Qfalse || rb_use_hash == Qnil); // test truthy-ness + } + } + + VALUE curConfig = Qnil; + + // get singleton class + VALUE cConfigBase_Singleton = rb_funcall(g_cConfigBase, rb_intern("singleton_class"), 0, 0); + + // Any existing C++ global instance known? (do not auto-create if not) + wxConfigBase* config = wxConfigBase::Get(false); + if (config == nullptr || forced_create) + { + if (use_hash) + { + // create new Wx::Config instance + curConfig = rb_class_new_instance(0, 0, g_cConfig); + // set global wxConfigBase instance to a new Ruby Config wrapper + wxConfigBase::Set(wxRuby_Ruby2ConfigBase(curConfig)); + } + else + { + if (config) wxConfigBase::Set(nullptr); // reset + wxConfigBase* new_cfg = wxConfigBase::Create(); // create new C++ instance + // wrap the C++ config object + curConfig = Data_Wrap_Struct(g_cConfigWx, 0, 0, new_cfg); + } + // store global ConfigBase instance as Ruby instance variable of ConfigBase singleton class + // (keeps it safe from GC) + rb_iv_set(cConfigBase_Singleton, __iv_ConfigBase_sc_config, curConfig); + if (config) + { + // clean up; destroy any previous config instance + delete config; + } + } + else + { + // check if this instance was already wrapped + curConfig = rb_iv_get(cConfigBase_Singleton, __iv_ConfigBase_sc_config); + if (NIL_P(curConfig)) + { + // no global Ruby instance known so can't be wrapped yet (must be C++ instance than) + // wrap the C++ config object + curConfig = Data_Wrap_Struct(g_cConfigWx, 0, 0, config); + // store global ConfigBase instance as Ruby instance variable of ConfigBase singleton class + // (keeps it safe from GC) + rb_iv_set(cConfigBase_Singleton, __iv_ConfigBase_sc_config, curConfig); + } } - return config_base_get(0, 0, self); + return curConfig; } static VALUE config_base_set(int argc, VALUE *argv, VALUE self) @@ -82,15 +171,350 @@ def setup VALUE cConfigBase_Singleton = rb_funcall(g_cConfigBase, rb_intern("singleton_class"), 0, 0); VALUE curConfig = rb_iv_get(cConfigBase_Singleton, __iv_ConfigBase_sc_config); // set new config instance (could be nil) - if (!NIL_P(newCfg)) + // set global wxConfigBase instance to a (new) Ruby Hash wrapper (or nullptr) + wxConfigBase::Set(wxRuby_Ruby2ConfigBase(newCfg)); + rb_iv_set(cConfigBase_Singleton, __iv_ConfigBase_sc_config, newCfg); + + // check curConfig type + if (!NIL_P(curConfig) && rb_obj_is_kind_of(curConfig, g_cConfigWx) != Qtrue) { - // set global wxConfigBase instance to a (new) Ruby Hash wrapper - wxConfigBase::Set(wxRuby_Ruby2ConfigBase(newCfg)); + // need to make config Ruby owned to it gets proper GC handling + // and the C++ allocated config instance gets destroyed + RDATA(curConfig)->dfree = _free_config_wx; } - rb_iv_set(cConfigBase_Singleton, __iv_ConfigBase_sc_config, newCfg); return curConfig; // return old config (if any) } + + static WxRuby_ID to_f_id("to_f"); + static WxRuby_ID to_i_id("to_i"); + static WxRuby_ID to_s_id("to_s"); + + #ifdef wxHAS_LONG_LONG_T_DIFFERENT_FROM_LONG + #define PO_LONG wxLongLong_t + #define PO_NUM2LONG(n) NUM2LL(n) + #define PO_LONG2NUM(l) LL2NUM(l) + #else + #define PO_LONG long + #define PO_NUM2LONG(n) NUM2LONG(n) + #define PO_LONG2NUM(l) LONG2NUM(l) + #endif + + static VALUE config_wx_read(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 1 || argc > 1) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + wxString key = RSTR_TO_WXSTR(argv[0]); + wxConfigBase::EntryType vtype = cfg->GetEntryType(key); + switch(vtype) + { + case wxConfigBase::Type_Boolean: + { + bool v = false; + if (cfg->Read(key, &v)) + { + return v ? Qtrue : Qfalse; + } + break; + } + case wxConfigBase::Type_Integer: + { + PO_LONG v = 0; + if (cfg->Read(key, &v)) + { + return PO_LONG2NUM(v); + } + break; + } + case wxConfigBase::Type_Float: + { + double v = 0.0; + if (cfg->Read(key, &v)) + { + return DBL2NUM(v); + } + break; + } + case wxConfigBase::Type_String: + { + wxString v; + if (cfg->Read(key, &v)) + { + return WXSTR_TO_RSTR(v); + } + break; + } + default: + break; + } + return Qnil; + } + + static VALUE config_wx_write(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 2 || argc > 2) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 2)", argc); + } + wxString key = RSTR_TO_WXSTR(argv[0]); + VALUE value = argv[1]; + bool rc = false; + switch(TYPE(value)) + { + case T_TRUE: + case T_FALSE: + { + int32_t v = (value == Qtrue ? 1 : 0); + rc = cfg->Write(key, v); + break; + } + + case T_FIXNUM: + { + PO_LONG v = PO_NUM2LONG(value); + if (v > std::numeric_limits::max()) + { rc = cfg->Write(key, PO_NUM2LONG(value)); } + else + { rc = cfg->Write(key, static_cast (v)); } + break; + } + + case T_BIGNUM: + { + VALUE sval = rb_funcall(value, to_s_id(), 0); + rc = cfg->Write(key, RSTR_TO_WXSTR(sval)); + } + break; + + case T_FLOAT: + rc = cfg->Write(key, NUM2DBL(value)); + break; + + case T_STRING: + rc = cfg->Write(key, RSTR_TO_WXSTR(value)); + break; + + default: + if (rb_respond_to(value, to_i_id())) + { + VALUE ival = rb_funcall(value, to_i_id(), 0); + rc = cfg->Write(key, PO_NUM2LONG(ival)); + } + else if (rb_respond_to(value, to_f_id())) + { + VALUE fval = rb_funcall(value, to_f_id(), 0); + rc = cfg->Write(key, NUM2DBL(fval)); + } + else + { + VALUE sval = rb_funcall(value, to_s_id(), 0); + rc = cfg->Write(key, RSTR_TO_WXSTR(sval)); + } + break; + } + return rc ? Qtrue : Qfalse; + } + + static VALUE config_wx_for_path(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 1 || argc > 1) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + wxString name = RSTR_TO_WXSTR(argv[0]); + wxConfigPathChanger path(cfg, name); + VALUE rc = Qnil; + if (rb_block_given_p ()) + { + VALUE key = WXSTR_TO_RSTR(path.Name()); + rc = rb_yield_values(2, self, key); + } + return rc; + } + + static VALUE config_wx_delete(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 1 || argc > 1) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + wxString key = RSTR_TO_WXSTR(argv[0]); + VALUE rc = Qfalse; + if (cfg->HasGroup(key)) + { + rc = cfg->DeleteGroup(key) ? Qtrue : Qfalse; + } + else if (cfg->HasEntry(key)) + { + rc = cfg->DeleteEntry(key) ? Qtrue : Qfalse; + } + return rc; + } + + static VALUE config_wx_rename(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 2 || argc > 2) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 2)", argc); + } + wxString key = RSTR_TO_WXSTR(argv[0]); + wxString newKey = RSTR_TO_WXSTR(argv[1]); + VALUE rc = Qfalse; + if (cfg->HasGroup(key)) + { + rc = cfg->RenameGroup(key, newKey) ? Qtrue : Qfalse; + } + else + { + rc = cfg->RenameEntry(key, newKey) ? Qtrue : Qfalse; + } + return rc; + } + + static VALUE config_wx_each_entry(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 0 || argc > 0) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 0)", argc); + } + wxString key; + long index = 0; + VALUE rc = Qnil; + if (rb_block_given_p()) + { + if (cfg->GetFirstEntry(key, index)) + { + do { + VALUE rb_key = WXSTR_TO_RSTR(key); + rc = rb_yield(rb_key); + } while (cfg->GetNextEntry(key, index)); + } + } + return rc; + } + + static VALUE config_wx_each_group(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 0 || argc > 0) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 0)", argc); + } + wxString key; + long index = 0; + VALUE rc = Qnil; + if (rb_block_given_p()) + { + if (cfg->GetFirstGroup(key, index)) + { + do { + VALUE rb_key = WXSTR_TO_RSTR(key); + rc = rb_yield(rb_key); + } while (cfg->GetNextGroup(key, index)); + } + } + return rc; + } + + static VALUE config_wx_number_of_entries(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 0 || argc > 2) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + bool recurse = argc>0 ? (argv[0] != Qfalse && argv[0] != Qnil) : false; + size_t n = cfg->GetNumberOfEntries(recurse); + return LONG2NUM(n); + } + + static VALUE config_wx_number_of_groups(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 0 || argc > 2) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + bool recurse = argc>0 ? (argv[0] != Qfalse && argv[0] != Qnil) : false; + size_t n = cfg->GetNumberOfGroups(recurse); + return LONG2NUM(n); + } + + static VALUE config_wx_has_entry(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 1 || argc > 1) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + wxString path = RSTR_TO_WXSTR(argv[0]); + return cfg->HasEntry(path) ? Qtrue : Qfalse; + } + + static VALUE config_wx_has_group(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 1 || argc > 1) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + wxString path = RSTR_TO_WXSTR(argv[0]); + return cfg->HasGroup(path) ? Qtrue : Qfalse; + } + + static VALUE config_wx_path(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 0 || argc > 2) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + return WXSTR_TO_RSTR(cfg->GetPath()); + } + + static VALUE config_wx_clear(int argc, VALUE *argv, VALUE self) + { + wxConfigBase *cfg; + Data_Get_Struct(self, wxConfigBase, cfg); + + if (argc < 0 || argc > 2) + { + rb_raise(rb_eArgError, "wrong # of arguments(%d for 1)", argc); + } + return cfg->DeleteAll() ? Qtrue : Qfalse; + } __HEREDOC spec.add_wrapper_code <<~__HEREDOC SWIGINTERN void @@ -99,6 +523,7 @@ def setup if (cfg) { wxRbHashConfig* config = (wxRbHashConfig*)cfg; + config->ResetRubyConfig(); delete config; } } @@ -123,6 +548,22 @@ def setup rb_define_module_function(g_cConfigBase, "get", VALUEFUNC(config_base_get), -1); rb_define_module_function(g_cConfigBase, "set", VALUEFUNC(config_base_set), -1); + g_cConfigWx = rb_define_class_under(mWxCore, "ConfigWx", g_cConfigBase); + rb_undef_alloc_func(g_cConfigWx); + rb_define_protected_method(g_cConfigWx, "read_entry", VALUEFUNC(config_wx_read), -1); + rb_define_protected_method(g_cConfigWx, "write_entry", VALUEFUNC(config_wx_write), -1); + rb_define_method(g_cConfigWx, "for_path", VALUEFUNC(config_wx_for_path), -1); + rb_define_method(g_cConfigWx, "each_entry", VALUEFUNC(config_wx_each_entry), -1); + rb_define_method(g_cConfigWx, "each_group", VALUEFUNC(config_wx_each_group), -1); + rb_define_method(g_cConfigWx, "number_of_entries", VALUEFUNC(config_wx_number_of_entries), -1); + rb_define_method(g_cConfigWx, "number_of_groups", VALUEFUNC(config_wx_number_of_groups), -1); + rb_define_method(g_cConfigWx, "has_entry?", VALUEFUNC(config_wx_has_entry), -1); + rb_define_method(g_cConfigWx, "has_group?", VALUEFUNC(config_wx_has_group), -1); + rb_define_method(g_cConfigWx, "delete", VALUEFUNC(config_wx_delete), -1); + rb_define_method(g_cConfigWx, "rename", VALUEFUNC(config_wx_rename), -1); + rb_define_method(g_cConfigWx, "path", VALUEFUNC(config_wx_path), -1); + rb_define_method(g_cConfigWx, "clear", VALUEFUNC(config_wx_clear), -1); + g_cConfig = rb_define_class_under(mWxCore, "Config", g_cConfigBase); rb_define_alloc_func(g_cConfig, config_allocate); __HEREDOC diff --git a/rakelib/lib/director/event_filter.rb b/rakelib/lib/director/event_filter.rb index 1860e984..6a276776 100644 --- a/rakelib/lib/director/event_filter.rb +++ b/rakelib/lib/director/event_filter.rb @@ -14,7 +14,7 @@ class EventFilter < Director def setup super - spec.gc_as_untracked # no tracking + spec.gc_as_marked # not tracked but cached in Ruby end end # class EventFilter diff --git a/rakelib/lib/director/event_loop.rb b/rakelib/lib/director/event_loop.rb index 0a61ce57..dc1c9253 100644 --- a/rakelib/lib/director/event_loop.rb +++ b/rakelib/lib/director/event_loop.rb @@ -15,7 +15,7 @@ class GUIEventLoop < Director def setup super spec.items << 'wxEventLoopBase' - spec.gc_as_untracked + spec.gc_as_marked # untracked but never disowned in Ruby spec.disable_proxies spec.make_concrete 'wxGUIEventLoop' spec.fold_bases 'wxGUIEventLoop' => 'wxEventLoopBase' diff --git a/rakelib/lib/director/file_dialog_customize_hook.rb b/rakelib/lib/director/file_dialog_customize_hook.rb index f3042e9b..c9e7d2a6 100644 --- a/rakelib/lib/director/file_dialog_customize_hook.rb +++ b/rakelib/lib/director/file_dialog_customize_hook.rb @@ -17,8 +17,8 @@ class FileDialogCustomizeHook < Director def setup super spec.items << 'wxFileDialogCustomize' - spec.gc_as_untracked 'wxFileDialogCustomizeHook' - spec.gc_never 'wxFileDialogCustomize' + spec.gc_as_marked 'wxFileDialogCustomizeHook' # not tracked but cached in Ruby + spec.gc_as_untracked 'wxFileDialogCustomize' spec.make_abstract 'wxFileDialogCustomize' end end # class FileDialogCustomizeHook diff --git a/rakelib/lib/director/grid_cell_attr.rb b/rakelib/lib/director/grid_cell_attr.rb index 3ae9da58..e342adf9 100644 --- a/rakelib/lib/director/grid_cell_attr.rb +++ b/rakelib/lib/director/grid_cell_attr.rb @@ -17,7 +17,7 @@ def setup # exposing the mixin wxClientDataContainer/wxSharedClientDataContainer has no real upside # for wxRuby; far easier to just use member variables in derived classes spec.override_inheritance_chain('wxGridCellAttr', []) - spec.gc_as_untracked('wxGridCellAttr') + spec.gc_as_marked('wxGridCellAttr') # tailored tracking # use custom free func to be able to account for more complex inheritance spec.add_header_code <<~__HEREDOC static void GC_free_GridCellAttr(void *ptr) diff --git a/rakelib/lib/director/grid_cell_editor.rb b/rakelib/lib/director/grid_cell_editor.rb index ea0de228..12a1849c 100644 --- a/rakelib/lib/director/grid_cell_editor.rb +++ b/rakelib/lib/director/grid_cell_editor.rb @@ -14,7 +14,7 @@ class GridCellEditor < Director def setup super - spec.gc_as_untracked + spec.gc_as_marked # tailored tracking method # use custom free func to be able to account for more complex inheritance spec.add_header_code 'extern void GC_free_GridCellEditor(void *ptr);' spec.add_swig_code '%feature("freefunc") wxGridCellEditor "GC_free_GridCellEditor";' diff --git a/rakelib/lib/director/grid_cell_renderer.rb b/rakelib/lib/director/grid_cell_renderer.rb index 0de1c838..d4137c9f 100644 --- a/rakelib/lib/director/grid_cell_renderer.rb +++ b/rakelib/lib/director/grid_cell_renderer.rb @@ -14,7 +14,7 @@ class GridCellRenderer < Director def setup super - spec.gc_as_untracked + spec.gc_as_marked # tailored tracking # use custom free func to be able to account for more complex inheritance spec.add_header_code 'extern void GC_free_GridCellRenderer(void *ptr);' spec.add_swig_code '%feature("freefunc") wxGridCellRenderer "GC_free_GridCellRenderer";' diff --git a/rakelib/lib/director/header_ctrl.rb b/rakelib/lib/director/header_ctrl.rb index 3644a387..3ef7ad56 100644 --- a/rakelib/lib/director/header_ctrl.rb +++ b/rakelib/lib/director/header_ctrl.rb @@ -17,6 +17,7 @@ class HeaderCtrl < Window def setup super spec.items << 'wxHeaderColumn' << 'wxHeaderCtrlSimple' << 'wxSettableHeaderColumn' << 'wxHeaderColumnSimple' + spec.gc_as_marked 'wxHeaderColumn', 'wxSettableHeaderColumn', 'wxHeaderColumnSimple' spec.regard 'wxHeaderCtrl::GetColumn', 'wxHeaderCtrl::UpdateColumnVisibility', 'wxHeaderCtrl::UpdateColumnsOrder', @@ -25,6 +26,8 @@ def setup spec.regard 'wxHeaderCtrlSimple::GetBestFittingWidth' spec.extend_interface 'wxHeaderCtrlSimple', 'virtual const wxHeaderColumn& GetColumn(unsigned int idx) const', + 'virtual void UpdateColumnVisibility(unsigned int idx, bool show)', + 'virtual void UpdateColumnsOrder(const wxArrayInt& order)', visibility: 'protected' # handled; can be suppressed spec.suppress_warning(473, 'wxHeaderCtrl::GetColumn', 'wxHeaderCtrlSimple::GetColumn') diff --git a/rakelib/lib/director/html_listbox.rb b/rakelib/lib/director/html_listbox.rb index 8884a5d9..8cb5a70c 100644 --- a/rakelib/lib/director/html_listbox.rb +++ b/rakelib/lib/director/html_listbox.rb @@ -95,7 +95,8 @@ class wxRubyHtmlListBox : public wxHtmlListBox 'wxItemContainer::SetClientData', 'wxItemContainer::HasClientUntypedData', 'wxItemContainer::Clear']) - spec.ignore([ 'wxItemContainer::Append(const wxArrayString &, wxClientData **)', + spec.ignore([ 'wxItemContainer::DetachClientObject', + 'wxItemContainer::Append(const wxArrayString &, wxClientData **)', 'wxItemContainer::Insert(const wxArrayString &, unsigned int, wxClientData **)', 'wxItemContainer::Set(const wxArrayString &, wxClientData **)'], ignore_doc: false) # for doc only diff --git a/rakelib/lib/director/persistence_manager.rb b/rakelib/lib/director/persistence_manager.rb new file mode 100644 index 00000000..7718acc2 --- /dev/null +++ b/rakelib/lib/director/persistence_manager.rb @@ -0,0 +1,410 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +### +# wxRuby3 wxWidgets interface director +### + +module WXRuby3 + + class Director + + class PersistenceManager < Director + + include Typemap::ConfigBase + + def setup + super + spec.gc_as_marked + spec.use_class_implementation 'wxPersistenceManager', 'WxRubyPersistenceManager' + spec.ignore 'wxPersistenceManager::Register', + 'wxPersistenceManager::Find', + 'wxPersistenceManager::Unregister', + 'wxPersistenceManager::Save', + 'wxPersistenceManager::Restore', + 'wxPersistenceManager::SaveAndUnregister', + 'wxPersistenceManager::RegisterAndRestore', + ignore_doc: false + # doc gen only + spec.map 'void *obj' => 'Object', swig: false do + map_in code: '' + end + spec.map 'T *obj' => 'Object', swig: false do + map_in code: '' + end + spec.regard 'wxPersistenceManager::wxPersistenceManager', + 'wxPersistenceManager::GetConfig', + 'wxPersistenceManager::GetKey' + spec.suppress_warning 473, 'wxPersistenceManager::GetConfig' + spec.ignore %w[wxCreatePersistentObject wxPersistentRegisterAndRestore] + spec.add_header_code <<~__HEREDOC + #include "wxruby-Persistence.h" + + // default global wxRuby persistence manager + static WxRubyPersistenceManager s_wxruby_persistence_manager {}; + + static WxRuby_ID to_f_id("to_f"); + static WxRuby_ID to_i_id("to_i"); + static WxRuby_ID to_s_id("to_s"); + static WxRuby_ID save_value_id("save_value"); + static WxRuby_ID restore_value_id("save_value"); + static WxRuby_ID create_po_id("create_persistent_object"); + + #ifdef wxHAS_LONG_LONG_T_DIFFERENT_FROM_LONG + #define PO_LONG wxLongLong_t + #define PO_NUM2LONG(n) NUM2LL(n) + #define PO_LONG2NUM(l) LL2NUM(l) + #else + #define PO_LONG long + #define PO_NUM2LONG(n) NUM2LONG(n) + #define PO_LONG2NUM(l) LONG2NUM(l) + #endif + + bool WxRubyPersistenceManager::SaveRubyValue(const wxPersistentObject& who, const wxString& name, VALUE value) + { + Swig::Director* dir = dynamic_cast (this); + // is this a user defined Ruby persistence manager with overridden #save_value? + if (dir && !wxRuby_IsNativeMethod(dir->swig_get_self(), save_value_id())) + { + VALUE rb_who = SWIG_NewPointerObj(SWIG_as_voidptr(&who), SWIGTYPE_p_wxPersistentObject, 0 ); + return wxRuby_Funcall(dir->swig_get_self(), save_value_id(), 3, rb_who, WXSTR_TO_RSTR(name), value); + } + else + { + // just call C++ base implementation + return DoSaveRubyValue(who, name, value); + } + } + + VALUE WxRubyPersistenceManager::RestoreRubyValue(const wxPersistentObject& who, const wxString& name) + { + Swig::Director* dir = dynamic_cast (this); + // is this a user defined Ruby persistence manager with overridden #restore_value? + if (dir && !wxRuby_IsNativeMethod(dir->swig_get_self(), restore_value_id())) + { + VALUE rb_who = SWIG_NewPointerObj(SWIG_as_voidptr(&who), SWIGTYPE_p_wxPersistentObject, 0 ); + return wxRuby_Funcall(dir->swig_get_self(), restore_value_id(), 3, rb_who, WXSTR_TO_RSTR(name)); + } + else + { + // just call C++ base implementation + return DoRestoreRubyValue(who, name); + } + } + + bool WxRubyPersistenceManager::DoSaveRubyValue(const wxPersistentObject& who, const wxString& name, VALUE value) + { + wxConfigBase* cfg = this->GetConfig(); + if (!cfg) + return false; + wxString key = this->GetKey(who, name); + switch(TYPE(value)) + { + case T_TRUE: + case T_FALSE: + return cfg->Write(key, value == Qtrue); + + case T_FIXNUM: + case T_BIGNUM: + return cfg->Write(key, PO_NUM2LONG(value)); + + case T_FLOAT: + return cfg->Write(key, NUM2DBL(value)); + + case T_STRING: + return cfg->Write(key, RSTR_TO_WXSTR(value)); + + default: + if (rb_respond_to(value, to_i_id())) + { + VALUE ival = rb_funcall(value, to_i_id(), 0); + return cfg->Write(key, PO_NUM2LONG(ival)); + } + else if (rb_respond_to(value, to_f_id())) + { + VALUE fval = rb_funcall(value, to_f_id(), 0); + return cfg->Write(key, NUM2DBL(fval)); + } + break; + } + VALUE sval = rb_funcall(value, to_s_id(), 0); + return cfg->Write(key, RSTR_TO_WXSTR(sval)); + } + + VALUE WxRubyPersistenceManager::DoRestoreRubyValue(const wxPersistentObject& who, const wxString& name) + { + wxConfigBase* cfg = this->GetConfig(); + if (!cfg) + return Qnil; + wxString key = this->GetKey(who, name); + wxConfigBase::EntryType vtype = cfg->GetEntryType(key); + switch(vtype) + { + case wxConfigBase::Type_Boolean: + { + bool v; + if (cfg->Read(key, &v)) + { + return v ? Qtrue : Qfalse; + } + break; + } + case wxConfigBase::Type_Integer: + { + PO_LONG v; + if (cfg->Read(key, &v)) + { + return PO_LONG2NUM(v); + } + break; + } + case wxConfigBase::Type_Float: + { + double v; + if (cfg->Read(key, &v)) + { + return DBL2NUM(v); + } + break; + } + case wxConfigBase::Type_String: + { + wxString v; + if (cfg->Read(key, &v)) + { + return WXSTR_TO_RSTR(v); + } + break; + } + default: + break; + } + return Qnil; + } + + void WxRubyPersistenceManager::UnregisterPersistentObject(VALUE rb_obj) + { + WxRubyPersistenceManager* wxrb_pm = + dynamic_cast (&wxPersistenceManager::Get()); + if (wxrb_pm) wxrb_pm->UnregisterRbPO(rb_obj); + } + + void WxRubyPersistenceManager::GC_markPO() + { + rb_object_to_rb_po_map_t::iterator it; + for( it = rb_object_po_map_.begin(); it != rb_object_po_map_.end(); ++it ) + { + rb_gc_mark(it->first); + rb_gc_mark(it->second); + } + } + + static void wxRuby_markPersistentObjects() + { + WxRubyPersistenceManager* wxrb_pm = + dynamic_cast (&wxPersistenceManager::Get()); + if (wxrb_pm) wxrb_pm->GC_markPO(); + } + __HEREDOC + spec.add_extend_code 'wxPersistenceManager', <<~__HEREDOC + VALUE Register(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = rb_funcall(obj, create_po_id(), 0); + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, SWIG_POINTER_DISOWN); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Unable to create Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + if ($self->Register(po->GetObject(), po)) + { + wxrb_pm->RegisterRbPO(obj, rb_po); + return rb_po; + } + } + return Qnil; + } + + VALUE Register(VALUE obj, VALUE rb_po) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, SWIG_POINTER_DISOWN); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Unable to create Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + if ($self->Register(po->GetObject(), po)) + { + wxrb_pm->RegisterRbPO(obj, rb_po); + return rb_po; + } + } + return Qnil; + } + + VALUE Find(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = wxrb_pm ? wxrb_pm->FindRbPO(obj) : Qnil; + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, 0); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Invalid Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + if ($self->Find(po->GetObject())) + return rb_po; + } + return Qnil; + } + + void Unregister(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = wxrb_pm ? wxrb_pm->FindRbPO(obj) : Qnil; + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, 0); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Invalid Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + $self->Unregister(po->GetObject()); + } + } + + void Save(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = wxrb_pm ? wxrb_pm->FindRbPO(obj) : Qnil; + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, 0); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Invalid Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + $self->Save(po->GetObject()); + } + } + + bool Restore(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = wxrb_pm ? wxrb_pm->FindRbPO(obj) : Qnil; + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, 0); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Invalid Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + return $self->Restore(po->GetObject()); + } + return false; + } + + void SaveAndUnregister(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = wxrb_pm ? wxrb_pm->FindRbPO(obj) : Qnil; + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, 0); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Invalid Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + $self->Save(po->GetObject()); + $self->Unregister(po->GetObject()); + } + } + + bool RegisterAndRestore(VALUE obj) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + VALUE rb_po = rb_funcall(obj, create_po_id(), 0); + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, SWIG_POINTER_DISOWN); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Unable to create Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + if ($self->Register(po->GetObject(), po)) + { + wxrb_pm->RegisterRbPO(obj, rb_po); + return $self->Restore(po->GetObject()); + } + } + return false; + } + + bool RegisterAndRestore(VALUE obj, VALUE rb_po) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + if (!NIL_P(rb_po) && wxrb_pm) + { + void* ptr; + int res = SWIG_ConvertPtr(rb_po, &ptr, SWIGTYPE_p_wxPersistentObject, SWIG_POINTER_DISOWN); + if (!SWIG_IsOK(res)) + { + rb_raise(rb_eRuntimeError, "Unable to create Wx::PersistentObject for object"); + } + wxPersistentObject* po = reinterpret_cast< wxPersistentObject * >(ptr); + if ($self->Register(po->GetObject(), po)) + { + wxrb_pm->RegisterRbPO(obj, rb_po); + return $self->Restore(po->GetObject()); + } + } + return false; + } + + bool SaveValue(const wxPersistentObject& who, const wxString& name, VALUE value) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + return wxrb_pm ? wxrb_pm->DoSaveRubyValue(who, name, value) : false; + } + + VALUE RestoreValue(const wxPersistentObject& who, const wxString& name) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast ($self); + return wxrb_pm ? wxrb_pm->DoRestoreRubyValue(who, name) : Qnil; + } + __HEREDOC + spec.add_init_code <<~__HEREDOC + // install the default global wxRuby persistence manager + wxPersistenceManager::Set(s_wxruby_persistence_manager); + // and the persistent object marker + wxRuby_AppendMarker(wxRuby_markPersistentObjects); + __HEREDOC + end + + end # class PersistenceManager + + end # class Director + +end # module WXRuby3 diff --git a/rakelib/lib/director/persistent_object.rb b/rakelib/lib/director/persistent_object.rb new file mode 100644 index 00000000..11e73c89 --- /dev/null +++ b/rakelib/lib/director/persistent_object.rb @@ -0,0 +1,70 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +### +# wxRuby3 wxWidgets interface director +### + +module WXRuby3 + + class Director + + class PersistentObject < Director + + def setup + super + spec.gc_as_marked + spec.add_header_code <<~__HEREDOC + #include "wxruby-Persistence.h" + + WxRubyPersistentObject::WxRubyPersistentObject(VALUE rb_obj) + : wxPersistentObject((void*)rb_obj) + {} + + WxRubyPersistentObject::~WxRubyPersistentObject() + { + if (this->GetObject()) + WxRubyPersistenceManager::UnregisterPersistentObject( + reinterpret_cast (this->GetObject())); + } + __HEREDOC + spec.use_class_implementation 'wxPersistentObject', 'WxRubyPersistentObject' + spec.ignore %w[wxPersistentObject::GetObject wxPersistentObject::wxPersistentObject] + spec.ignore %w[wxCreatePersistentObject wxPersistentRegisterAndRestore] + spec.extend_interface 'wxPersistentObject', + 'wxPersistentObject(VALUE rb_obj)', + visibility: 'protected' + spec.add_extend_code 'wxPersistentObject', <<~__HEREDOC + bool SaveValue(const wxString& name, VALUE value) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast (&wxPersistenceManager::Get()); + return wxrb_pm ? wxrb_pm->SaveRubyValue(*$self, name, value) : false; + } + VALUE RestoreValue(const wxString& name) + { + WxRubyPersistenceManager* wxrb_pm = dynamic_cast (&wxPersistenceManager::Get()); + return wxrb_pm ? wxrb_pm->RestoreRubyValue(*$self, name) : Qnil; + } + __HEREDOC + spec.add_extend_code 'wxPersistentObject', <<~__HEREDOC + VALUE GetObject() + { + WxRubyPersistentObject* rpo = dynamic_cast ($self); + if (rpo) + { + return reinterpret_cast (rpo->GetObject()); + } + else + { + return Qnil; + } + } + __HEREDOC + super + end + end # class PersistentObject + + end # class Director + +end # module WXRuby3 diff --git a/rakelib/lib/director/persistent_window.rb b/rakelib/lib/director/persistent_window.rb new file mode 100644 index 00000000..c5d9f595 --- /dev/null +++ b/rakelib/lib/director/persistent_window.rb @@ -0,0 +1,73 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +### +# wxRuby3 wxWidgets interface director +### + +module WXRuby3 + + class Director + + class PersistentWindow < Director + + def setup + spec.items << 'wxPersistentTLW' << 'wxPersistentBookCtrl' << 'wxPersistentTreeBookCtrl' + if Config.instance.wx_version >= '3.3.0' + # before 3.3.0 this was not properly available + spec.items << 'wxPersistentComboBox' + end + super + spec.gc_as_marked + spec.use_template_as_class('wxPersistentWindow', 'wxPersistentWindowBase') + spec.override_inheritance_chain('wxPersistentWindow', %w[wxPersistentObject]) + spec.ignore 'wxPersistentWindow::wxPersistentWindow', + 'wxPersistentWindow::Get', + ignore_doc: false + # make ctor protected because of pure virt methods + spec.extend_interface 'wxPersistentWindow', + 'wxPersistentWindowBase(wxWindow *win)', + visibility: 'protected' + spec.add_header_code 'typedef wxWindow WindowType;' + spec.add_swig_code 'typedef wxWindow WindowType;' + spec.map 'WindowType *' => 'Wx::Window', swig: false do + map_in code: '' + map_out code: '' + end + spec.add_extend_code 'wxPersistentWindowBase', <<~__HEREDOC + wxWindow * GetObject() + { + return reinterpret_cast ($self->GetObject()); + } + __HEREDOC + # wxPersistentTLW + spec.override_inheritance_chain('wxPersistentTLW', [{ 'wxPersistentWindowBase' => 'wxPersistentWindow' }, 'wxPersistentObject']) + # add method override missing from docs + spec.extend_interface 'wxPersistentTLW', + 'virtual wxString GetKind() const override' + # wxPersistentBookCtrl + spec.override_inheritance_chain('wxPersistentBookCtrl', [{ 'wxPersistentWindowBase' => 'wxPersistentWindow' }, 'wxPersistentObject']) + # add method override missing from docs + spec.extend_interface 'wxPersistentBookCtrl', + 'virtual wxString GetKind() const override' + # wxPersistentTreeBookCtrl + spec.override_inheritance_chain('wxPersistentTreeBookCtrl', ['wxPersistentBookCtrl', { 'wxPersistentWindowBase' => 'wxPersistentWindow' }, 'wxPersistentObject']) + # add method override missing from docs + spec.extend_interface 'wxPersistentTreeBookCtrl', + 'virtual wxString GetKind() const override' + spec.do_not_generate :functions, :defines, :typedefs, :variables, :enums + if Config.instance.wx_version >= '3.0.0' + # wxPersistentComboBox + spec.override_inheritance_chain('wxPersistentComboBox', [{ 'wxPersistentWindowBase' => 'wxPersistentWindow' }, 'wxPersistentObject']) + # add method override missing from docs + spec.extend_interface 'wxPersistentComboBox', + 'virtual wxString GetKind() const override' + end + end + + end + + end + +end diff --git a/rakelib/lib/director/static_bitmap.rb b/rakelib/lib/director/static_bitmap.rb index 3a199190..1494cdc4 100644 --- a/rakelib/lib/director/static_bitmap.rb +++ b/rakelib/lib/director/static_bitmap.rb @@ -45,6 +45,10 @@ def process(gendoc: false) spec.add_swig_code %Q{typedef wxStaticBitmap::ScaleMode ScaleMode; } spec.add_header_code %Q{typedef wxStaticBitmap::ScaleMode ScaleMode; } defmod.items << def_genstatbmp + # as we already called super before adding wxGenericStaticBitmap the no_proxy settings from the + # base Window director are missing; just copy all those set for wxStaticBitmap + list = spec.no_proxies.select { |name| name.start_with?('wxStaticBitmap::') } + spec.no_proxy(*list.collect { |name| name.sub(/\AwxStaticBitmap::/, 'wxGenericStaticBitmap::')}) defmod end diff --git a/rakelib/lib/director/text_entry.rb b/rakelib/lib/director/text_entry.rb index 09a6c7a9..2184f7bd 100644 --- a/rakelib/lib/director/text_entry.rb +++ b/rakelib/lib/director/text_entry.rb @@ -15,7 +15,7 @@ class TextEntry < Director def setup super spec.items << 'wxTextCompleter' << 'wxTextCompleterSimple' - spec.gc_as_untracked 'wxTextCompleter', 'wxTextCompleterSimple' + spec.gc_as_marked 'wxTextCompleter', 'wxTextCompleterSimple' # untracked but cached in Ruby spec.gc_as_untracked 'wxTextEntry' # actually no GC control necessary as this is a mixin only # turn wxTextEntry into a mixin module spec.make_mixin 'wxTextEntry' diff --git a/rakelib/lib/generate/analyzer.rb b/rakelib/lib/generate/analyzer.rb index 3157506a..c0cd493b 100644 --- a/rakelib/lib/generate/analyzer.rb +++ b/rakelib/lib/generate/analyzer.rb @@ -540,53 +540,53 @@ def check_interface_methods(director, doc_gen: false) # check the preprocessed definitions errors = [] warnings = [] - def_items.each do |item| - if Extractor::ClassDef === item && !item_ignored(item, doc_gen) && + # select eligible ClassDef + cls_items = def_items.select do |item| + Extractor::ClassDef === item && !item_ignored(item, doc_gen) && (!item.is_template? || template_as_class?(item.name)) && !is_folded_base?(item.name) - intf_class_name = if item.is_template? || template_as_class?(item.name) - template_class_name(item.name) - else - item.name - end - # this should not happen - raise "Missing preprocessed data for class #{intf_class_name}" unless has_class_interface(intf_class_name) - # get the class's method registry - cls_mtdreg = class_interface_methods(intf_class_name) - # check all directly inherited generated methods - mtdlist = ::Set.new # remember handled signatures - base_list(item).each do |base_name| - # get 'real' base name (i.e. take renames into account) - base_name = ifspec.classdef_name(base_name) - # make sure the base class has been preprocessed - get_class_interface(package, base_name, doc_gen) unless has_class_interface(base_name) - # generate any required enum typemaps for inherited virtuals - gen_base_class_enum_typemaps(base_name, enum_maps) - # iterate the base class's method registrations - class_interface_methods(base_name).each_pair do |mtdsig, mtdreg| - # only check on methods we have not handled yet - if !mtdlist.include?(mtdsig) - # did we inherit a virtual method that was not proxied in the base - if mtdreg[:virtual] && !mtdreg[:proxy] - # # if we did NOT generate a wrapper override and we do not have the proxy suppressed we're in trouble - # if !cls_mtdreg.has_key?(mtdsig) && has_method_proxy?(item.name, mtdreg[:method]) - # warnings << "* WARNING: disabling proxy for virtual method #{mtdreg[:method].signature} without wrapper implementation in class #{item.name} since it is NOT proxied in base class #{base_name}!" - # els - if cls_mtdreg.has_key?(mtdsig) && !cls_mtdreg[mtdsig][:extension] && !has_method_proxy?(item.name, cls_mtdreg[mtdsig][:method]) - # if this is not a custom extension and we do have an override wrapper and no proxy this is unnecessary code bloat - warnings << " * WARNING: Unnecessary override #{mtdreg[:method].signature} in class #{item.name} for non-proxied base in #{base_name}. Ignoring." - cls_mtdreg[mtdsig][:ignore] = true - end - # or did we inherit a virtual method that was proxied in the base - # for which we DO generate a wrapper override - elsif mtdreg[:virtual] && mtdreg[:proxy] && cls_mtdreg.has_key?(mtdsig) - # if we do not have a proxy as well we're in trouble - if !has_method_proxy?(item, mtdreg[:method]) - errors << "* ERROR: method #{mtdreg[:method].signature} is NOT proxied with an overriden wrapper implementation in class #{item.name} but is also implemented and proxied in base class #{base_name}!" - end + end + cls_items.each do |item| + intf_class_name = if item.is_template? || template_as_class?(item.name) + template_class_name(item.name) + else + item.name + end + # this should not happen + raise "Missing preprocessed data for class #{intf_class_name}" unless has_class_interface(intf_class_name) + # get the class's method registry + cls_mtdreg = class_interface_methods(intf_class_name) + # check all directly inherited generated methods + mtdlist = ::Set.new # remember handled signatures + base_list(item).each do |base_name| + # make sure the base class has been preprocessed + get_class_interface(package, base_name, doc_gen) unless has_class_interface(base_name) + # generate any required enum typemaps for inherited virtuals + gen_base_class_enum_typemaps(base_name, enum_maps) + # iterate the base class's method registrations + class_interface_methods(base_name).each_pair do |mtdsig, mtdreg| + # only check on methods we have not handled yet + if !mtdlist.include?(mtdsig) + # did we inherit a virtual method that was not proxied in the base + if mtdreg[:virtual] && !mtdreg[:proxy] + # # if we did NOT generate a wrapper override and we do not have the proxy suppressed we're in trouble + # if !cls_mtdreg.has_key?(mtdsig) && has_method_proxy?(item.name, mtdreg[:method]) + # warnings << "* WARNING: disabling proxy for virtual method #{mtdreg[:method].signature} without wrapper implementation in class #{item.name} since it is NOT proxied in base class #{base_name}!" + # els + if cls_mtdreg.has_key?(mtdsig) && !cls_mtdreg[mtdsig][:extension] && !has_method_proxy?(item.name, cls_mtdreg[mtdsig][:method]) + # if this is not a custom extension and we do have an override wrapper and no proxy this is unnecessary code bloat + warnings << " * WARNING: Unnecessary override #{mtdreg[:method].signature} in class #{item.name} for non-proxied base in #{base_name}. Ignoring." + cls_mtdreg[mtdsig][:ignore] = true + end + # or did we inherit a virtual method that was proxied in the base + # for which we DO generate a wrapper override + elsif mtdreg[:virtual] && mtdreg[:proxy] && cls_mtdreg.has_key?(mtdsig) + # if we do not have a proxy as well we're in trouble + if !has_method_proxy?(item, mtdreg[:method]) + errors << "* ERROR: method #{mtdreg[:method].signature} is NOT proxied with an overriden wrapper implementation in class #{item.name} but is also implemented and proxied in base class #{base_name}!" end - mtdlist << mtdsig end + mtdlist << mtdsig end end end diff --git a/rakelib/lib/generate/doc.rb b/rakelib/lib/generate/doc.rb index 416bfac1..1513f54e 100644 --- a/rakelib/lib/generate/doc.rb +++ b/rakelib/lib/generate/doc.rb @@ -480,7 +480,7 @@ def para_to_doc(node) /\A\s*Include\s+file:/ # Include file note '' else - para.sub!(/Include\s+file:\s+\#include\s+\<[^>]+\>\s*\Z/, '') + para.sub!(/Include\s+file:\s+\\?#include\s+<[^>]+> */, '') if event_section? case para when /The following event handler macros redirect.*(\{.*})/ diff --git a/rakelib/lib/specs/interfaces.rb b/rakelib/lib/specs/interfaces.rb index 0b9b8fa6..aa2e0335 100644 --- a/rakelib/lib/specs/interfaces.rb +++ b/rakelib/lib/specs/interfaces.rb @@ -230,6 +230,9 @@ module WXRuby3 Director.Spec(pkg, 'wxFileSystem', requirements: %w[USE_FILESYSTEM]) Director.Spec(pkg, 'wxDialUpManager', requirements: %w[USE_DIALUP_MANAGER]) Director.Spec(pkg, 'wxDialUpEvent', requirements: %w[USE_DIALUP_MANAGER]) + Director.Spec(pkg, 'wxPersistenceManager', requirements: %w[USE_CONFIG]) + Director.Spec(pkg, 'wxPersistentObject', requirements: %w[USE_CONFIG]) + Director.Spec(pkg, 'wxPersistentWindow', requirements: %w[USE_CONFIG]) } Director.Package('Wx::PRT', 'USE_PRINTING_ARCHITECTURE') do |pkg| diff --git a/rakelib/lib/typemap/config.rb b/rakelib/lib/typemap/config.rb index 44ca1f6e..15504073 100644 --- a/rakelib/lib/typemap/config.rb +++ b/rakelib/lib/typemap/config.rb @@ -44,6 +44,14 @@ module ConfigBase $input = wxRuby_ConfigBase2Ruby($1); __CODE + map_out code: <<~__CODE + $result = wxRuby_ConfigBase2Ruby($1); + __CODE + + map_directorout code: <<~__CODE + $result = wxRuby_Ruby2ConfigBase($1); + __CODE + end end diff --git a/samples/widgets/widgets.rb b/samples/widgets/widgets.rb index 1b8e7adc..c5e26394 100644 --- a/samples/widgets/widgets.rb +++ b/samples/widgets/widgets.rb @@ -456,12 +456,10 @@ def initialize(title) @panel.set_sizer(sizerTop) - # TODO - review wxPersistenceManager - # sizeSet = wxPersistentRegisterAndRestore(this, "Main") - + sizeSet = Wx.persistent_register_and_restore(self, "Main") + sizeMin = @panel.get_best_size - # if ( !sizeSet ) - set_client_size(sizeMin) + set_client_size(sizeMin) unless sizeSet set_min_client_size(sizeMin) # connect the event handlers @@ -1068,10 +1066,8 @@ def init_book evt_choicebook_page_changed(ID::Widgets_BookCtrl, :on_page_changed) end - # TODO - review wxPersistenceManager - # const bool pageSet = wxPersistentRegisterAndRestore(m_book) - pageSet = false - + pageSet = Wx.persistent_register_and_restore(@book) + if Wx.has_feature?(:USE_TREEBOOK) # for treebook page #0 is empty parent page only so select the first page # with some contents diff --git a/tests/test_config.rb b/tests/test_config.rb index 6be1d404..155ed817 100644 --- a/tests/test_config.rb +++ b/tests/test_config.rb @@ -13,21 +13,21 @@ class TestConfig < Test::Unit::TestCase 'RootEntry4' => 3.14, 'Group1' => { 'Group1Entry' => 'Group1 string', - 'Group1.1' => { - 'Group1.1Integer' => 999, - 'Group1.1Bignum' => 2**999, - 'Group1.1Float' => (2**999)-0.1 + 'Group1_1' => { + 'Group1_1Integer' => 999, + 'Group1_1Bignum' => 2**999, + 'Group1_1Float' => (2**999)-0.1 } }, 'Group2' => { - 'Group2.1' => { - 'Group2.1.1' => { + 'Group2_1' => { + 'Group2_1_1' => { 'String' => 'hello' }, - 'Group2.1.2' => { + 'Group2_1_2' => { 'String' => 'world' }, - 'Group2.1.3' => { + 'Group2_1_3' => { 'True' => true, 'False' => false } @@ -35,79 +35,176 @@ class TestConfig < Test::Unit::TestCase } } + def stringified_entry(val) + case val + when TrueClass,FalseClass + val ? '1' : '0' + when Float + '%g' % val + else + val.to_s + end + end + + def stringified(val) + val.is_a?(::Hash) ? val.inject({}) { |hash, pair| hash[pair.first] = stringified(pair.last); hash } : stringified_entry(val) + end + + def assert_true_cfg(val) + assert_block('expected "1" or true') do + val == '1' || val == 1 || val == true + end + end + + def assert_false_cfg(val) + assert_block("expected '0' or false") do + val == '0' || val == 0 || val == false + end + end + + def assert_equal_cfg(expected, val) + assert_block("expected #{expected.is_a?(::Hash) ? stringified(expected) : %Q['#{stringified(expected)}']} \nor #{expected}\nbut got #{val}") do + expected == val || stringified(expected) == stringified(val) + end + end + def run_config_tests(cfg) - assert_equal(DEMO_CONFIG, cfg.to_h) + assert_equal_cfg(DEMO_CONFIG, cfg.to_h) - assert_equal(4, cfg.number_of_entries) - assert_equal(2, cfg.number_of_groups) - assert_equal(12, cfg.number_of_entries(recurse: true)) - assert_equal(7, cfg.number_of_groups(recurse: true)) + assert_equal_cfg(4, cfg.number_of_entries) + assert_equal_cfg(2, cfg.number_of_groups) + assert_equal_cfg(12, cfg.number_of_entries(recurse: true)) + assert_equal_cfg(7, cfg.number_of_groups(recurse: true)) assert_true(cfg.has_entry?('/RootEntry2')) assert_true(cfg.has_entry?('/Group1/Group1Entry')) - assert_true(cfg.has_entry?('/Group2/Group2.1/Group2.1.2/String')) + assert_true(cfg.has_entry?('/Group2/Group2_1/Group2_1_2/String')) - assert_false(cfg.has_entry?('/Group2/Group2.2/Group2.1.2/String')) + assert_false(cfg.has_entry?('/Group2/Group2.2/Group2_1_2/String')) assert_true(cfg.has_group?('/Group2')) - assert_true(cfg.has_group?('/Group1/Group1.1')) - assert_true(cfg.has_group?('/Group2/Group2.1/Group2.1.2')) + assert_true(cfg.has_group?('/Group1/Group1_1')) + assert_true(cfg.has_group?('/Group2/Group2_1/Group2_1_2')) - assert_false(cfg.has_group?('/Group2/Group2.1/Group2.1.2/String')) + assert_false(cfg.has_group?('/Group2/Group2_1/Group2_1_2/String')) - grp = cfg['/Group1/Group1.1'] + grp = cfg['/Group1/Group1_1'] - assert_equal(DEMO_CONFIG['Group1']['Group1.1'], grp.to_h) + assert_equal_cfg(DEMO_CONFIG['Group1']['Group1_1'], grp.to_h) assert_equal(3, grp.number_of_entries) assert_equal(0, grp.number_of_groups) - assert_true(grp.has_entry?('Group1.1Integer')) + assert_true(grp.has_entry?('Group1_1Integer')) assert_false(grp.has_entry?('Group1Entry')) assert_true(grp.has_entry?('../Group1Entry')) - assert_true(grp.has_group?('/Group2/Group2.1/Group2.1.2')) + assert_true(grp.has_group?('/Group2/Group2_1/Group2_1_2')) assert_equal('This is a string value', cfg['/RootEntry1']) - assert_equal(true, cfg['/RootEntry2']) - assert_equal(101, cfg['/RootEntry3']) - assert_equal(3.14, cfg['/RootEntry4']) + assert_equal_cfg(true, cfg['/RootEntry2']) + assert_equal_cfg(101, cfg['/RootEntry3']) + assert_equal_cfg(3.14, cfg['/RootEntry4']) - grp = cfg['/Group2/Group2.1/Group2.1.3'] - assert_true(grp.get('True')) - assert_false(grp.get('False')) - assert_nil(grp.get('../Group2.1.2/String')) + grp = cfg['/Group2/Group2_1/Group2_1_3'] + assert_true_cfg(grp.get('True')) + assert_false_cfg(grp.get('False')) + assert_raise(ArgumentError) { grp.get('../Group2_1_2/String') } - assert_true(grp['True']) - assert_false(grp['False']) - assert_equal('world', grp['../Group2.1.2/String']) + assert_true_cfg(grp['True']) + assert_false_cfg(grp['False']) + assert_equal('world', grp['../Group2_1_2/String']) cfg.set('RootEntry1', 'Altered string value') assert_equal('Altered string value', cfg['RootEntry1']) assert_equal('Altered string value', cfg['/RootEntry1']) assert_equal('Altered string value', cfg.get('RootEntry1')) - cfg.set('RootEntry3', cfg.get('RootEntry3')+99) - assert_equal(200, cfg['/RootEntry3']) + cfg.set('RootEntry3', cfg.read('RootEntry3', ::Integer)+99) + assert_equal_cfg(200, cfg['/RootEntry3']) - cfg.set('Group1', { 'Group1.2' => { 'Integer' => 777 }}) - assert_equal(777, cfg['/Group1/Group1.2/Integer']) + cfg.set('Group1', { 'Group1_2' => { 'Integer' => 777 }}) + assert_equal_cfg(777, cfg['/Group1/Group1_2/Integer']) - cfg['/Group1/Group1.2/Integer'] = 666 - assert_equal(666, cfg['/Group1/Group1.2'].get('Integer')) + cfg['/Group1/Group1_2/Integer'] = 666 + assert_equal_cfg(666, cfg['/Group1/Group1_2'].get('Integer')) - cfg['/Group1/Group1.2'] = { 'Float' => 0.3330 } - assert_equal(0.3330, cfg['/Group1/Group1.2'].get('Float')) + cfg['/Group1/Group1_2'] = { 'Float' => 0.3330 } + assert_equal_cfg(0.3330, cfg['/Group1/Group1_2'].get('Float')) + + cfg.replace(DEMO_CONFIG) # reset + end + + def run_auto_accessor_tests(cfg) + assert_not_nil(cfg.RootEntry2) + assert_not_nil(cfg.Group1.Group1Entry) + assert_not_nil(cfg.Group2.Group2_1.Group2_1_2.String) + + assert_nil(cfg.Group2.Group2_1.Group2_1_2.AString) + + assert_kind_of(cfg.class::Group, cfg.Group2) + assert_kind_of(cfg.class::Group, cfg.Group1.Group1_1) + assert_kind_of(cfg.class::Group, cfg.Group2.Group2_1.Group2_1_2) + + assert_not_kind_of(cfg.class::Group, cfg.Group2.Group2_1.Group2_1_2.String) + + grp = cfg.Group1 + + assert_equal_cfg(DEMO_CONFIG['Group1'], grp.to_h) + + assert_not_nil(grp.Group1Entry) + assert_nil(grp.Group1_1Integer) + + assert_kind_of(grp.class, grp.Group1_1) + assert_not_nil(grp.Group1_1.Group1_1Integer) + + assert_true(grp.has_entry?('../RootEntry1')) + + assert_true(grp.has_group?('/Group2/Group2_1/Group2_1_2')) + + assert_equal_cfg('This is a string value', cfg.RootEntry1) + assert_equal_cfg(true, cfg.RootEntry2) + assert_equal_cfg(101, cfg.RootEntry3) + assert_equal_cfg(3.14, cfg.RootEntry4) + + grp = cfg.Group2.Group2_1.Group2_1_3 + assert_true_cfg(grp.True) + assert_false_cfg(grp.False) + + assert_true_cfg(grp['True']) + assert_false_cfg(grp['False']) + assert_equal_cfg('world', grp['../Group2_1_2/String']) + + cfg.RootEntry1 = 'Altered string value' + assert_equal_cfg('Altered string value', cfg['RootEntry1']) + assert_equal_cfg('Altered string value', cfg['/RootEntry1']) + assert_equal_cfg('Altered string value', cfg.get('RootEntry1')) + assert_equal_cfg('Altered string value', cfg.RootEntry1) + + cfg.RootEntry3 = (Kernel.Integer(cfg.RootEntry3) rescue 0)+99 + assert_equal_cfg(200, cfg.RootEntry3) + + cfg.Group1 = { 'Group1_2' => { 'Integer' => 777 }} + assert_equal_cfg(777, cfg.Group1.Group1_2.Integer) + + cfg.Group1.Group1_2.Integer = 666 + assert_equal_cfg(666, cfg.Group1.Group1_2.get('Integer')) + + cfg.Group1.Group1_2 = { 'Float' => 0.3330 } + assert_equal_cfg(0.3330, cfg.Group1.Group1_2.get('Float')) end def test_basic cfg = Wx::Config.new(DEMO_CONFIG) run_config_tests(cfg) + run_auto_accessor_tests(cfg) end def test_global - cfg = Wx::ConfigBase.create + cfg = Wx::ConfigBase.create(true, use_hash_config: true) + + assert_kind_of(Wx::Config, cfg) cfg.replace(DEMO_CONFIG) @@ -116,6 +213,7 @@ def test_global assert_equal(cfg, Wx::ConfigBase.get(false)) run_config_tests(cfg) + run_auto_accessor_tests(cfg) cfg_old = Wx::ConfigBase.set(nil) @@ -123,8 +221,27 @@ def test_global assert_nil(Wx::ConfigBase.get(false)) end + # default registry based config does not seem to do well in CI build env + unless is_ci_build? && Wx::PLATFORM == 'WXMSW' + + def test_default_wx + Wx::ConfigBase.set(nil) # reset global instance + cfg = Wx::ConfigBase.get # forced auto creation of default config + + assert_kind_of(Wx::ConfigWx, cfg) + + cfg.replace(DEMO_CONFIG) + + run_config_tests(cfg) + run_auto_accessor_tests(cfg) + + assert_true(cfg.clear) # cleanup + end + + end + def test_html_help - cfg = Wx::ConfigBase.create + cfg = Wx::ConfigBase.create(true, use_hash_config: true) assert_true(cfg.to_h.empty?) diff --git a/tests/test_persistence.rb b/tests/test_persistence.rb new file mode 100644 index 00000000..690a20d5 --- /dev/null +++ b/tests/test_persistence.rb @@ -0,0 +1,142 @@ +# Copyright (c) 2023 M.J.N. Corino, The Netherlands +# +# This software is released under the MIT license. + +require_relative './lib/wxframe_runner' + +class TopLevelPersistenceTests < WxRuby::Test::GUITests + + PERSIST_ROOT = 'Persistent_Options' + + def run_frame_props_tests + Wx.persistent_register_and_restore(frame_win, 'TestFrame') + + frame_win.size = [450, 350] + frame_win.position = [100, 150] + + Wx::PersistenceManager.get.save_and_unregister(frame_win) + + cfg = Wx::ConfigBase.get + assert_kind_of(Wx::ConfigBase, cfg) + grp = cfg.get(PERSIST_ROOT) + assert_kind_of(cfg.class::Group, grp) + grp = grp.get('Window') + assert_kind_of(cfg.class::Group, grp) + grp = grp.get('TestFrame') + assert_kind_of(cfg.class::Group, grp) + + assert_equal(100, Integer(grp['x'])) + assert_equal(150, Integer(grp['y'])) + assert_equal(450, Integer(grp.w)) + assert_equal(350, Integer(grp.h)) + + grp.x = 110 + grp.y = 140 + + assert_equal(110, Integer(grp['x'])) + assert_equal(140, Integer(grp['y'])) + + Wx.persistent_register_and_restore(frame_win, 'TestFrame') + + assert_equal(Wx::Point.new(110, 140), frame_win.position) + + Wx::PersistenceManager.get.unregister(frame_win) + end + + def test_frame_props_ruby_config + # force creation of hash based Wx::Config instance + Wx::ConfigBase.create(true, use_hash_config: true) + + run_frame_props_tests + + Wx::ConfigBase.get.clear + end + + # default registry based config does not seem to do well in CI build env + unless is_ci_build? && Wx::PLATFORM == 'WXMSW' + + def test_frame_props_default_config + # force creation of default C++ config instance + Wx::ConfigBase.create(true) + + run_frame_props_tests + + Wx::ConfigBase.get.clear + end + + end + + class PersistentButton < Wx::PersistentWindowBase + + def get_kind + 'Button' + end + + def save + save_value('w', get.size.width) + save_value('h', get.size.height) + save_value('label', get.label) + save_value('my_custom_value', get.my_custom_value) + end + + def restore + get.size = [Integer(restore_value('w')), Integer(restore_value('h'))] + get.label = restore_value('label') + get.my_custom_value = Float(restore_value('my_custom_value')) + true + end + + end + + class MyButton < Wx::Button + + def initialize(parent=nil, name) + super(parent, label: '', name: name) + @my_custom_value = '' + end + + attr_accessor :my_custom_value + + def create_persistent_object + PersistentButton.new(self) + end + + end + + def test_custom_persistent_object + # force creation of hash based Wx::Config instance + Wx::ConfigBase.create(true, use_hash_config: true) + + assert_false(Wx::ConfigBase.get.has_group?(PERSIST_ROOT)) + + btn = MyButton.new(frame_win, 'AButton') + btn.label = 'Hello world' + btn.my_custom_value = 3.14 + + Wx::PersistenceManager.get.register(btn) + + assert_false(Wx::ConfigBase.get.has_group?(PERSIST_ROOT)) + + # destroying window should save and unregister + btn.destroy + btn = nil + + + assert_true(Wx::ConfigBase.get.has_group?(PERSIST_ROOT)) + + cfg = Wx::ConfigBase.get[PERSIST_ROOT]['Button']['AButton'] + assert_true(cfg.has_entry?('w')) + assert_true(cfg.has_entry?('h')) + assert_true(cfg.has_entry?('label')) + assert_true(cfg.has_entry?('my_custom_value')) + + + btn = MyButton.new(frame_win, 'AButton') + + Wx::PersistenceManager.get.register_and_restore(btn) + + assert_equal('Hello world', btn.label) + assert_equal(3.14, btn.my_custom_value) + end + +end