lib/xcodeproj/project/object/group.rb
require 'xcodeproj/project/object/helpers/groupable_helper'
require 'xcodeproj/project/object/helpers/file_references_factory'
require 'xcodeproj/project/object/file_system_synchronized_root_group'
module Xcodeproj
class Project
module Object
# This class represents a group. A group can contain other groups
# (PBXGroup) and file references (PBXFileReference).
#
class PBXGroup < AbstractObject
# @!group Attributes
# @return [ObjectList<PBXGroup, PBXFileReference>]
# the objects contained by the group.
#
has_many :children, [PBXGroup, PBXFileReference, PBXReferenceProxy, PBXFileSystemSynchronizedRootGroup]
# @return [String] the directory to which the path is relative.
#
# @note The accepted values are:
# - `<absolute>` for absolute paths
# - `<group>` for paths relative to the group
# - `SOURCE_ROOT` for paths relative to the project
# - `DEVELOPER_DIR` for paths relative to the developer
# directory.
# - `BUILT_PRODUCTS_DIR` for paths relative to the build
# products directory.
# - `SDKROOT` for paths relative to the SDK directory.
#
attribute :source_tree, String, '<group>'
# @return [String] the path to a folder in the file system.
#
# @note This attribute is present for groups that are linked to a
# folder in the file system.
#
attribute :path, String
# @return [String] the name of the group.
#
# @note If path is specified this attribute is not present.
#
attribute :name, String
# @return [String] Whether Xcode should use tabs for text alignment.
#
# @example
# `1`
#
attribute :uses_tabs, String
# @return [String] The width of the indent.
#
# @example
# `2`
#
attribute :indent_width, String
# @return [String] The width of the tabs.
#
# @example
# `2`
#
attribute :tab_width, String
# @return [String] Whether Xcode should wrap lines.
#
# @example
# `1`
#
attribute :wraps_lines, String
# @return [String] Comments associated with this group.
#
# @note This is apparently no longer used by Xcode.
#
attribute :comments, String
public
# @!group Helpers
#---------------------------------------------------------------------#
# @return [PBXGroup, PBXProject] The parent of the group.
#
def parent
GroupableHelper.parent(self)
end
# @return [Array<PBXGroup, PBXProject>] The list of the parents of the
# group.
#
def parents
GroupableHelper.parents(self)
end
# @return [String] A representation of the group hierarchy.
#
def hierarchy_path
GroupableHelper.hierarchy_path(self)
end
# Moves the group to a new parent.
#
# @param [PBXGroup] new_parent
# The new parent.
#
# @return [void]
#
def move(new_parent)
GroupableHelper.move(self, new_parent)
end
# @return [Pathname] the absolute path of the group resolving the
# source tree.
#
def real_path
GroupableHelper.real_path(self)
end
# Sets the source tree of the group.
#
# @param [Symbol, String] source_tree
# The source tree, either a string or a symbol.
#
# @return [void]
#
def set_source_tree(source_tree)
GroupableHelper.set_source_tree(self, source_tree)
end
# Allows to set the path according to the source tree of the group.
#
# @param [#to_s] the path for the group.
#
# @return [void]
#
def set_path(path)
if path
source_tree
GroupableHelper.set_path_with_source_tree(self, path, source_tree)
else
self.path = nil
self.source_tree = '<group>'
end
end
# @return [Array<PBXFileReference>] the file references in the group
# children.
#
def files
children.grep(PBXFileReference)
end
# @return [PBXFileReference] The file references whose path (regardless
# of the source tree) matches the give path.
#
def find_file_by_path(path)
files.find { |ref| ref.path == path }
end
# @return [Array<PBXGroup>] the groups in the group children.
#
def groups
# Don't grep / is_a? as this would include child classes.
children.select { |obj| obj.class == PBXGroup }
end
# @return [Array<XCVersionGroup>] the version groups in the group
# children.
#
def version_groups
children.grep(XCVersionGroup)
end
# @return [Array<PBXGroup,PBXFileReference,PBXReferenceProxy>] the
# recursive children of the group.
#
def recursive_children_groups
result = []
groups.each do |child|
result << child
result.concat(child.recursive_children_groups)
end
result
end
# @return [Array<PBXGroup,PBXFileReference,PBXReferenceProxy>] the
# recursive list of the children of the group.
#
def recursive_children
result = []
children.each do |child|
result << child
if child.is_a?(PBXGroup)
result.concat(child.recursive_children)
end
end
result
end
# @return [Bool] Whether the group is empty.
#
def empty?
children.count.zero?
end
# Creates a new reference with the given path and adds it to the
# group. The reference is configured according to the extension
# of the path.
#
# @param [#to_s] path
# The, preferably absolute, path of the reference.
#
# @param [Symbol] source_tree
# The source tree key to use to configure the path (@see
# GroupableHelper::SOURCE_TREES_BY_KEY).
#
# @return [PBXFileReference, XCVersionGroup] The new reference.
#
def new_reference(path, source_tree = :group)
FileReferencesFactory.new_reference(self, path, source_tree)
end
alias_method :new_file, :new_reference
# Creates a file reference to a static library and adds it to the
# group.
#
# @param [#to_s] product_basename
# The name of the static library.
#
# @return [PBXFileReference] The new file reference.
#
def new_product_ref_for_target(product_basename, product_type)
FileReferencesFactory.new_product_ref_for_target(self, product_basename, product_type)
end
# Creates a file reference to a new bundle.
#
# @param [#to_s] product_basename
# The name of the bundle.
#
# @return [PBXFileReference] The new file reference.
#
def new_bundle(product_basename)
FileReferencesFactory.new_bundle(self, product_basename)
end
# Creates a file reference to a new bundle and adds it to the group.
#
# @note @see new_reference
#
# @param [#to_s] name
# the name of the new group.
#
# @return [PBXGroup] the new group.
#
def new_group(name, path = nil, source_tree = :group)
group = project.new(PBXGroup)
children << group
group.name = name
group.set_source_tree(source_tree)
group.set_path(path)
group
end
# Creates a new variant group and adds it to the group
#
# @note @see new_group
#
# @param [#to_s] name
# the name of the new group.
#
# @param [#to_s] path
# The, preferably absolute, path of the variant group.
# Pass the path of the folder containing all the .lproj bundles,
# that contain files for the variant group.
# Do not pass the path of a specific bundle (such as en.lproj)
#
# @param [Symbol] source_tree
# The source tree key to use to configure the path (@see
# GroupableHelper::SOURCE_TREES_BY_KEY).
#
# @return [PBXVariantGroup] the new variant group.
#
def new_variant_group(name, path = nil, source_tree = :group)
group = project.new(PBXVariantGroup)
children << group
group.name = name
group.set_source_tree(source_tree)
group.set_path(path)
group
end
# Traverses the children groups and finds the group with the given
# path, if exists.
#
# @see find_subpath
#
def [](path)
find_subpath(path, false)
end
# Removes children files and groups under this group.
#
def clear
children.objects.each(&:remove_from_project)
end
alias_method :remove_children_recursively, :clear
# Traverses the children groups and finds the children with the given
# path, optionally, creating any needed group. If the given path is
# `nil` it returns itself.
#
# @param [String] path
# a string with the names of the groups separated by a '`/`'.
#
# @param [Boolean] should_create
# whether the path should be created.
#
# @note The path is matched against the {#display_name} of the groups.
#
# @example
# g = main_group['Frameworks']
# g.name #=> 'Frameworks'
#
# @return [PBXGroup] the group if found.
# @return [Nil] if the path could not be found and should create is
# false.
#
def find_subpath(path, should_create = false)
return self unless path
path = path.split('/') unless path.is_a?(Array)
child_name = path.shift
child = children.find { |c| c.display_name == child_name }
if child.nil?
if should_create
child = new_group(child_name)
else
return nil
end
end
if path.empty?
child
else
child.find_subpath(path, should_create)
end
end
# Adds an object to the group.
#
# @return [ObjectList<AbstractObject>] the children list.
#
def <<(child)
children << child
end
# Sorts the children of the group by type and then by name.
#
# @note This is safe to call in an object list because it modifies it
# in C in Ruby MRI. In other Ruby implementation it can cause
# issues if there is one call to the notification enabled
# methods not compensated by the corespondent opposite (loss of
# UUIDs and objects from the project).
#
# @return [void]
#
def sort_by_type
children.sort! do |x, y|
if x.isa == 'PBXGroup' && !(y.isa == 'PBXGroup')
-1
elsif !(x.isa == 'PBXGroup') && y.isa == 'PBXGroup'
1
elsif x.display_name && y.display_name
extname_x = File.extname(x.display_name)
extname_y = File.extname(y.display_name)
if extname_x != extname_y
extname_x <=> extname_y
else
File.basename(x.display_name, '.*') <=> File.basename(y.display_name, '.*')
end
else
0
end
end
end
# Sorts the group by type recursively.
#
# @return [void]
#
def sort_recursively_by_type
groups.each(&:sort_recursively_by_type)
sort_by_type
end
public
# @!group AbstractObject Hooks
#---------------------------------------------------------------------#
# @return [String] the name of the group taking into account the path
# or other factors if needed.
#
def display_name
if name
name
elsif path
File.basename(path)
elsif self.equal?(project.main_group)
'Main Group'
end
end
def ascii_plist_annotation
super unless self.equal?(project.main_group)
end
# Sorts the to many attributes of the object according to the display
# name.
#
# @param [Hash] options
# the sorting options.
# @option options [Symbol] :groups_position
# the position of the groups can be either `:above` or
# `:below`.
#
# @return [void]
#
def sort(options = nil)
children.sort! do |x, y|
if options && groups_position = options[:groups_position]
raise ArgumentError unless [:above, :below].include?(groups_position)
if x.isa == 'PBXGroup' && !(y.isa == 'PBXGroup')
next groups_position == :above ? -1 : 1
elsif !(x.isa == 'PBXGroup') && y.isa == 'PBXGroup'
next groups_position == :above ? 1 : -1
end
end
result = File.basename(x.display_name.downcase, '.*') <=> File.basename(y.display_name.downcase, '.*')
if result.zero?
result = File.extname(x.display_name.downcase) <=> File.extname(y.display_name.downcase)
if result.zero? && !(x.path.nil? || y.path.nil?)
result = x.path.downcase <=> y.path.downcase
end
end
result
end
end
# @return [Array<PBXBuildFile>] the build files associated with the
# current reference proxy.
#
def build_files
referrers.grep(PBXBuildFile)
end
# In addition to removing the reference proxy, this will also remove any
# items related to this reference.
#
# @see AbstractObject#remove_from_project
#
# @return [void]
#
def remove_from_project
build_files.each(&:remove_from_project)
super
end
end
#-----------------------------------------------------------------------#
# This class is used to gather localized files into one entry.
#
class PBXVariantGroup < PBXGroup
# @!group Attributes
# @return [String] the file type guessed by Xcode.
#
attribute :last_known_file_type, String
end
#-----------------------------------------------------------------------#
# A group that contains multiple files references to the different
# versions of a resource.
#
# Used to contain the different versions of a `xcdatamodel`.
#
class XCVersionGroup < PBXGroup
# @!group Attributes
# @return [PBXFileReference] the reference to the current version.
#
has_one :current_version, PBXFileReference
# @return [String] the type of the versioned resource.
#
attribute :version_group_type, String, 'wrapper.xcdatamodel'
end
#-----------------------------------------------------------------------#
end
end
end