diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
commit | 55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch) | |
tree | ecd18b995aea8eeeb8b3823266280d41245bf0f7 /eclipse | |
parent | 82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff) | |
download | sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'eclipse')
421 files changed, 71805 insertions, 0 deletions
diff --git a/eclipse/README_WINDOWS.txt b/eclipse/README_WINDOWS.txt new file mode 100644 index 0000000..1480f5d --- /dev/null +++ b/eclipse/README_WINDOWS.txt @@ -0,0 +1,32 @@ +[RM 20080623] + +1- To build the Eclipse plugin: +Under Linux: +$ cd your-device-directory +$ tools/eclipse/scripts/build_server.sh destination-directory + +This will create an "android-eclipse.zip" in the selected destination directory. +Then in Eclipse, you can use Help > Software Updates > Find and Install > Search for new Features > Next > New Archived Site > select the new android-eclipse.zip. Then with the new archive checked, click Finish/Next. + + +2- To build a Windows SDK, you need two steps: +a- First you need to create a Linux SDK: + +Under Linux: +$ cd your-device-directory +$ make sdk +Note: if you get an error when building the javadoc, make sure you use a Java SDK 1.5 +Note: if you get an error when building layoutlib, make sure you use a Java SDK 1.5.0-b13. + +b- Once you have a Linux SDK, you can create a Windows SDK: + +You need a Windows machine with XP or Vista and Cygwin. +- Installer at http://sources.redhat.com/cygwin/ +- Set Default Text File Type to DOS/text, not Unix/binary. +- Select packages autoconf, gcc, g++, bison, python, zip, unzip, mingw-zlib +- Suggested extra packages: emacs, wget, openssh, rsync + +Then under Cygwin: +$ cd your-device-directory +$ tools/buildbot/_make_windows_sdk.sh path-to-the-linux-sdk.zip destination-directory + diff --git a/eclipse/buildConfig/allElements.xml b/eclipse/buildConfig/allElements.xml new file mode 100644 index 0000000..2c8229c --- /dev/null +++ b/eclipse/buildConfig/allElements.xml @@ -0,0 +1,60 @@ +<!-- ========================================================================= --> +<!-- Feature build ant targets --> +<!-- Template obtained from org.eclipse.pde.build/templates/headless-build --> +<!-- ========================================================================= --> +<project name="allElements Delegator"> + + <!-- ===================================================================== --> + <!-- Run a given ${target} on all elements being built --> + <!-- Replace element.id with the id of the top level element being built. --> + <!-- If element.id does not exist in ${buildDirectory}/features/element.id --> + <!-- or ${baseLocation}/features/element.id, then you must provide the --> + <!-- location by setting the property "pluginPath" --> + <!-- Add on <ant> task for each top level element being built. --> + <!-- ===================================================================== --> + <target name="allElementsDelegator"> + + <ant antfile="${genericTargets}" target="${target}"> + <property name="type" value="feature" /> + <property name="id" value="com.android.ide.eclipse.ddms" /> + </ant> + + <ant antfile="${genericTargets}" target="${target}"> + <property name="type" value="feature" /> + <property name="id" value="com.android.ide.eclipse.adt" /> + </ant> + + <antcall target="buildInternalFeatures"/> + + </target> + + <!-- ===================================================================== --> + <!-- Conditional target for building the internal features --> + <!-- Builds if property internalSite is set --> + <!-- ===================================================================== --> + <target name="buildInternalFeatures" if="internalSite"> + <ant antfile="${genericTargets}" target="${target}"> + <property name="type" value="feature" /> + <property name="id" value="com.android.ide.eclipse.tests" /> + </ant> + </target> + + <!-- ===================================================================== --> + <!-- Targets to assemble the built elements for particular configurations --> + <!-- These generally call the generated assemble scripts (named in --> + <!-- ${assembleScriptName}) but may also add pre and post processing --> + <!-- Add one target for each root element and each configuration --> + <!-- Replace element.id with the id of the top level element being built --> + <!-- ===================================================================== --> + <target name="assemble.com.android.ide.eclipse.adt"> + <ant antfile="${assembleScriptName}" dir="${buildDirectory}"/> + </target> + + <target name="assemble.com.android.ide.eclipse.ddms"> + <ant antfile="${assembleScriptName}" dir="${buildDirectory}"/> + </target> + + <target name="assemble.com.android.ide.eclipse.tests"> + <ant antfile="${assembleScriptName}" dir="${buildDirectory}"/> + </target> +</project> diff --git a/eclipse/buildConfig/build.properties b/eclipse/buildConfig/build.properties new file mode 100644 index 0000000..cd477d8 --- /dev/null +++ b/eclipse/buildConfig/build.properties @@ -0,0 +1,238 @@ +############################################################################### +# Copyright (c) 2003, 2006 IBM Corporation and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# Contributors: +# IBM Corporation - initial API and implementation +############################################################################### + + +# This file was generated per the instructions located in Eclipse Help>Plug-in Development +# Environment > Guide > Tasks > Building features and customized for building the +# Android Eclipse plugins. + +##################### +# Parameters describing how and where to execute the build. +# Typical users need only update the following properties: +# baseLocation - where things you are building against are installed +# bootclasspath - The base jars to compile against (typicaly rt.jar) +# configs - the list of {os, ws, arch} configurations to build. +# +# Of course any of the settings here can be overridden by spec'ing +# them on the command line (e.g., -DbaseLocation=d:/eclipse + +############# PRODUCT/PACKAGING CONTROL ############# +product=/plugin or feature id/path/to/.product +runPackager=true + +#Set the name of the archive that will result from the product build. +#archiveNamePrefix= + +# The prefix that will be used in the generated archive. +# override default of "eclipse" to aid for external site generation +archivePrefix=android-eclipse + +# The location underwhich all of the build output will be collected. +collectingFolder=${archivePrefix} + +# The list of {os, ws, arch} configurations to build. This +# value is a '&' separated list of ',' separate triples. For example, +# configs=win32,win32,x86 & linux,motif,x86 +# By default the value is *,*,* +configs = *, *, * +#configs=win32, win32, x86 & \ +# linux, gtk, ppc &\ +# linux, gtk, x86 & \ +# linux, gtk, x86_64 & \ +# linux, motif, x86 & \ +# solaris, motif, sparc & \ +# solaris, gtk, sparc & \ +# aix, motif, ppc & \ +# hpux, motif, PA_RISC & \ +# macosx, carbon, ppc + +# By default PDE creates one archive (result) per entry listed in the configs property. +# Setting this value to try will cause PDE to only create one output containing all +# artifacts for all the platforms listed in the configs property. +#groupConfigurations=true + +#The format of the archive. By default a zip is created using antZip. +#The list can only contain the configuration for which the desired format is different than zip. +#archivesFormat=win32, win32, x86 - antZip& \ +# linux, gtk, ppc - antZip &\ +# linux, gtk, x86 - antZip& \ +# linux, gtk, x86_64 - antZip& \ +# linux, motif, x86 - antZip& \ +# solaris, motif, sparc - antZip& \ +# solaris, gtk, sparc - antZip& \ +# aix, motif, ppc - antZip& \ +# hpux, motif, PA_RISC - antZip& \ +# macosx, carbon, ppc - antZip + +#Set to true if you want the output to be ready for an update jar (no site.xml generated) +outputUpdateJars = true + +#Set to true for Jnlp generation +#codebase should be a URL that will be used as the root of all relative URLs in the output. +#generateJnlp=false +#jnlp.codebase=<codebase url> +#jnlp.j2se=<j2se version> +#jnlp.locale=<a locale> +#jnlp.generateOfflineAllowed=true or false generate <offlineAllowed/> attribute in the generated features +#jnlp.configs=${configs} #uncomment to filter the content of the generated jnlp files based on the configuration being built + +#Set to true if you want to sign jars +#signJars=false +#sign.alias=<alias> +#sign.keystore=<keystore location> +#sign.storepass=<keystore password> + +#Arguments to send to the zip executable +zipargs= + +#Arguments to send to the tar executable +tarargs= + +#Control the creation of a file containing the version included in each configuration - on by default +#generateVersionsLists=false + +############## BUILD NAMING CONTROL ################ +# The directory into which the build elements are fetched and where +# the build takes place. +buildDirectory=. + +# Type of build. Used in naming the build output. Typically this value is +# one of I, N, M, S, ... +buildType=build + +# ID of the build. Used in naming the build output. +# forceContextQualifer = build label +buildId=${forceContextQualifier} + +# Label for the build. Used in naming the build output +buildLabel=${buildId} + +# Timestamp for the build. Used in naming the build output +timestamp=007 + +#The value to be used for the qualifier of a plugin or feature when you want to override the value computed by pde. +#The value will only be applied to plugin or features indicating build.properties, qualifier = context +#forceContextQualifier=<the value for the qualifier> + +#Enable / disable the generation of a suffix for the features that use .qualifier. +#The generated suffix is computed according to the content of the feature +#generateFeatureVersionSuffix=true + +############# BASE CONTROL ############# +# Settings for the base Eclipse components and Java class libraries +# against which you are building. +# Base location for anything the build needs to compile against. For example, +# in most RCP app or a plug-in, the baseLocation should be the location of a previously +# installed Eclipse against which the application or plug-in code will be compiled and the RCP delta pack. + +baseLocation=${ECLIPSE_HOME} +#Os/Ws/Arch/nl of the eclipse specified by baseLocation +baseos=linux +basews=gtk +basearch=x86 + +#this property indicates whether you want the set of plug-ins and features to be considered during the build to be limited to the ones reachable from the features / plugins being built +filteredDependencyCheck=false + +#this property indicates whether the resolution should be done in development mode (i.e. ignore multiple bundles with singletons) +resolution.devMode=false + +#pluginPath is a list of locations in which to find plugins and features. This list is separated by the platform file separator (; or :) +#a location is one of: +#- the location of the jar or folder that is the plugin or feature : /path/to/foo.jar or /path/to/foo +#- a directory that contains a /plugins or /features subdirectory +#- the location of a feature.xml, or for 2.1 style plugins, the plugin.xml or fragment.xml +#pluginPath= + +skipBase=true +eclipseURL=<url for eclipse download site> +eclipseBuildId=<Id of Eclipse build to get> +eclipseBaseURL=${eclipseURL}/eclipse-platform-${eclipseBuildId}-win32.zip + + +############# MAP FILE CONTROL ################ +# This section defines CVS tags to use when fetching the map files from the repository. +# If you want to fetch the map file from repository / location, change the getMapFiles target in the customTargets.xml + +skipMaps=true +mapsRepo=:pserver:anonymous@example.com/path/to/repo +mapsRoot=path/to/maps +mapsCheckoutTag=HEAD + +#tagMaps=true +mapsTagTag=v${buildId} + + +############ REPOSITORY CONTROL ############### +# This section defines properties parameterizing the repositories where plugins, fragments +# bundles and features are being obtained from. + +# The tags to use when fetching elements to build. +# By default thebuilder will use whatever is in the maps. +# This value takes the form of a comma separated list of repository identifier (like used in the map files) and the +# overriding value +# For example fetchTag=CVS=HEAD, SVN=v20050101 +# fetchTag=HEAD +skipFetch=true + + +############# JAVA COMPILER OPTIONS ############## +# The location of the Java jars to compile against. Typically the rt.jar for your JDK/JRE +#bootclasspath=${java.home}/lib/rt.jar + +# specific JRE locations to compile against. These values are used to compile bundles specifying a +# Bundle-RequiredExecutionEnvironment. Uncomment and set values for environments that you support +#CDC-1.0/Foundation-1.0= /path/to/rt.jar +#CDC-1.1/Foundation-1.1= +#OSGi/Minimum-1.0= +#OSGi/Minimum-1.1= +#JRE-1.1= +#J2SE-1.2= +#J2SE-1.3= +#J2SE-1.4= +#J2SE-1.5= +#JavaSE-1.6= +#PersonalJava-1.1= +#PersonalJava-1.2= +#CDC-1.0/PersonalBasis-1.0= +#CDC-1.0/PersonalJava-1.0= +#CDC-1.1/PersonalBasis-1.1= +#CDC-1.1/PersonalJava-1.1= + +# Specify the output format of the compiler log when eclipse jdt is used +logExtension=.log + +# Whether or not to include debug info in the output jars +javacDebugInfo=false + +# Whether or not to fail the build if there are compiler errors +javacFailOnError=true + +# Enable or disable verbose mode of the compiler +javacVerbose=true + +# Extra arguments for the compiler. These are specific to the java compiler being used. +#compilerArg= + +# Default value for the version of the source code. This value is used when compiling plug-ins that do not set the Bundle-RequiredExecutionEnvironment or set javacSource in build.properties +javacSource=1.5 + +# Default value for the version of the byte code targeted. This value is used when compiling plug-ins that do not set the Bundle-RequiredExecutionEnvironment or set javacTarget in build.properties. +javacTarget=1.5 + +################### CUSTOM PROPERTIES ####################################### +# repository location for update site +# comment out - this is passed in from command line +#updateSiteSource=${buildDirectory}/sites/external +# where to place update site build +updateSiteRoot=${user.home}/www/no_crawl/ +updateSiteFolder=${archivePrefix} +updateSiteDestination=${updateSiteRoot}/${updateSiteFolder} diff --git a/eclipse/buildConfig/buildUpdateSite.xml b/eclipse/buildConfig/buildUpdateSite.xml new file mode 100644 index 0000000..1ab7c99 --- /dev/null +++ b/eclipse/buildConfig/buildUpdateSite.xml @@ -0,0 +1,13 @@ +<project name="update site"> + <!-- ========================================================================= --> + <!-- Extracts feature zip for update site --> + <!-- expected properties: --> + <!-- id - feature id to extract --> + <!-- buildDirectory - base directgory where build takes place --> + <!-- buildLabel - build id label --> + <!-- updateSiteRoot - where to extract feature zip --> + <!-- ========================================================================= --> + <target name="extractFeature"> + <unzip src="${buildDirectory}/${buildLabel}/${id}-${buildLabel}.zip" dest="${updateSiteRoot}"/> + </target> +</project> diff --git a/eclipse/buildConfig/customTargets.xml b/eclipse/buildConfig/customTargets.xml new file mode 100644 index 0000000..5a46bfc --- /dev/null +++ b/eclipse/buildConfig/customTargets.xml @@ -0,0 +1,195 @@ +<!-- ========================================================================= --> +<!-- Eclipse template file for PDE builds --> +<!-- template originally obtained from org.eclipse.pde.build/templates/headless-build --> +<!-- ========================================================================= --> +<project name="Build specific targets and properties" default="noDefault"> + + <!-- ===================================================================== --> + <!-- Run a given ${target} on all elements being built --> + <!-- Add on <ant> task for each top level element being built. --> + <!-- ===================================================================== --> + <property name="allElementsFile" value="${builder}/allElements.xml"/> + <import file="${allElementsFile}" /> + <target name="allElements"> + <antcall target="allElementsDelegator" /> + </target> + + <!-- ===================================================================== --> + <!-- ===================================================================== --> + <target name="getBaseComponents" depends="checkLocalBase" unless="skipBase"> + <get src="${eclipseBaseURL}" dest="${buildDirectory}/../temp-base.zip" /> + <unzip dest="${base}" overwrite="true" src="${buildDirectory}/../temp-base.zip" /> + </target> + + <target name="checkLocalBase"> + <available file="${base}" property="skipBase" /> + </target> + + <!-- ===================================================================== --> + <!-- Check out map files from correct repository --> + <!-- Replace values for mapsCheckoutTag as desired. --> + <!-- ===================================================================== --> + <target name="getMapFiles" depends="checkLocalMaps" unless="skipMaps"> + <property name="mapsCheckoutTag" value="HEAD" /> + <cvs cvsRoot="${mapsRepo}" package="${mapsRoot}" dest="${buildDirectory}/maps" tag="${mapsCheckoutTag}" /> + </target> + + <target name="checkLocalMaps"> + <available property="skipMaps" file="${buildDirectory}/maps" /> + </target> + + <target name="tagMapFiles" if="tagMaps"> + <cvs dest="${buildDirectory}/maps/${mapsRoot}" command="tag ${mapsTagTag}" /> + </target> + + <!-- ===================================================================== --> + + <target name="clean" unless="noclean"> + <antcall target="allElements"> + <param name="target" value="cleanElement" /> + </antcall> + </target> + + <target name="gatherLogs"> + <mkdir dir="${buildDirectory}/${buildLabel}/compilelogs" /> + <antcall target="allElements"> + <param name="target" value="gatherLogs" /> + </antcall> + <unzip dest="${buildDirectory}/${buildLabel}/compilelogs" overwrite="true"> + <fileset dir="${buildDirectory}/features"> + <include name="**/*.log.zip" /> + </fileset> + </unzip> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do before setup --> + <!-- ===================================================================== --> + <target name="preSetup"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after setup but before starting the build proper --> + <!-- ===================================================================== --> + <target name="postSetup"> + <antcall target="getBaseComponents" /> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do before fetching the build elements --> + <!-- ===================================================================== --> + <target name="preFetch"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after fetching the build elements --> + <!-- ===================================================================== --> + <target name="postFetch"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do before generating the build scripts. --> + <!-- ===================================================================== --> + <target name="preGenerate"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after generating the build scripts. --> + <!-- ===================================================================== --> + <target name="postGenerate"> + <antcall target="clean" /> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do before running the build.xmls for the elements being built. --> + <!-- ===================================================================== --> + <target name="preProcess"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after running the build.xmls for the elements being built. --> + <!-- ===================================================================== --> + <target name="postProcess"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do before running assemble. --> + <!-- ===================================================================== --> + <target name="preAssemble"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after running assemble. --> + <!-- ===================================================================== --> + <target name="postAssemble"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do before running package. --> + <!-- ===================================================================== --> + <target name="prePackage"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after running package. --> + <!-- ===================================================================== --> + <target name="postPackage"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do after the build is done. --> + <!-- ===================================================================== --> + <target name="postBuild"> + <antcall target="gatherLogs" /> + <!-- Added this custom target ! --> + <antcall target="generateUpdateSite" /> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do to test the build results --> + <!-- ===================================================================== --> + <target name="test"> + </target> + + <!-- ===================================================================== --> + <!-- Steps to do to publish the build results --> + <!-- ===================================================================== --> + <target name="publish"> + + </target> + + <!-- ===================================================================== --> + <!-- Default target --> + <!-- ===================================================================== --> + <target name="noDefault"> + <echo message="You must specify a target when invoking this file" /> + </target> + + <!-- ===================================================================== --> + <!-- Custom target: --> + <!-- Steps to do to generate the update site --> + <!-- ===================================================================== --> + <target name="generateUpdateSite"> + <echo message="Copying update site source ${updateSiteSource} to destination"/> + + <copy file="${updateSiteSource}/site.xml" overwrite="true" todir="${updateSiteDestination}"/> + <copy file="${updateSiteSource}/index.html" overwrite="true" todir="${updateSiteDestination}"/> + <copy file="${updateSiteSource}/web/site.css" overwrite="true" todir="${updateSiteDestination}/web"/> + <copy file="${updateSiteSource}/web/site.xsl" overwrite="true" todir="${updateSiteDestination}/web"/> + + <!-- replace qualifier version references with build label --> + <replace file="${updateSiteDestination}/site.xml" token="qualifier" value="${buildId}"/> + + <!-- now extract each features zip to update site --> + <antcall target="allElements"> + <param name="genericTargets" value="${builder}/buildUpdateSite.xml"/> + <param name="target" value="extractFeature" /> + </antcall> + + <chmod perm="755" type="both"> + <fileset dir="${updateSiteDestination}"> + </fileset> + </chmod> + </target> + +</project> diff --git a/eclipse/changes.txt b/eclipse/changes.txt new file mode 100644 index 0000000..781930c --- /dev/null +++ b/eclipse/changes.txt @@ -0,0 +1,214 @@ +0.9.0 (work in progress) +- Support for the new Android SDK with support for multiple versions of the Android platform and for vendor supplied add-ons. + * New Project Wizard lets you choose which platform/add-on to target. + * Project properties (right click project in Package Explorer, then "Properties"), lets you edit project target. + * New Launch configuration option to choose debug deployment target. +- Ability to export multiple apk from one project, using resource filters. See the 'android' property for Android projects. + +0.8.1: + +- Alternate Layout wizard. In the layout editor, the "create" button is now enabled to easily create alternate versions of the current layout. +- Fixed issue with custom themes/styles in the layout editor. +- Export Wizard: To export an application for release, and sign it with a non debug key. Accessible from the export menu, from the Android Tools contextual menu, or from the overview page of the manifest editor. +- New XML File Wizard: To easily create new XML resources file in the /res directory. +- New checks on launch when attempting to debug on a device. +- Basic support for drag'n'drop in Graphical layout editor. You can add new items by drag'n'drop from the palette. There is no support for moving/resizing yet. +- Undo/redo support in all XML form editors and Graphical layout editor. + +0.8.0: + +- Fixed issue with using custom classes implementing Parcelable in aidl files. Right click the project and choose Android Tools > Create aidl preprocess file for Parcelable Classes. +- Added Custom Themes to theme drop down in the layout editor. +- Customizable debug signing keystore path in preferences +- Customizable HOME package name. + +0.7.1: + +- Layout Editor. + +0.6.1: + +- Fixed install issue when project name contains spaces (requires new emulator image) +- Fixed setup of the New class wizard in the manifest (when clicking on "name" for a class attribute) in the cases where the class and some of its parent packages were missing. +- Properly kill the application that is about to be reinstalled. +- Create missing android folder automatically when building application (caused a signing error) +- Manifest editor: support for uses-library node +- Fixed NPE in editors.xml.descriptors.XmlDescriptors.createPreference +- Fixed assert in MultiEditorPart.setActivePage +- Fixed "connect to debugger" button in DeviceView. Also fixed support for custom process names. + +0.6.0: + +- new launch option for activity. Can choose to launch default activity (finds an activity configured to show up in the home screen), or specific activity, or none. +- normal java resources (non java files placed in package folders) are now properly packaged in the final package, and can be accessed through normal java API such as ClassLoader.getResourceAsStream() +- launch configuration now has an option to wipe emulator data on launch. This always asks for confirmation. +- launch configuration now has an option to disable the boot animation. This will let the emulator start faster on older computers. +- Applications are now signed with a debug key (stored in debug.keystore in ~/.android). +- Installation of application is now more robust and will notify of installation failure. Also installation is blocking, removing issues where ADT tried to launch the activity before the app was installed. +- Tree-based resource editor + content assist in XML editor for layout, menu, preferences, values xml files. Work in progress... + + +0.4.0 (adt 0.4.0, ddms 0.3.0, editors 0.2.0, common 0.1.0) + +- New AndroidManifest editor. +- True multiple device support allowing debugging apps on several device at the same time +- New launch modes for device selection: automatic will launch an emulator if no device are present, automatically target the device if only one exists, and prompt the user if 2+ are connected. Manual mode always prompt the user. +- New classpath container remove the dependencies on the location of android.jar making it easier to share a project through dsvn, cvs, etc... You should fix your project (right click project, choose Android > Fix Project properties) +- Fixed a case where pm would fail and would up end outputting the "usage" text, which would in turn confuse the plugin during parsing. +- Fixed an issue with compiling aidl file when they import project local files. + +0.3.4 (adt 0.3.4, ddms 0.2.3, editors 0.1.0) + +Internal release only. +- Enabled device support. + +0.3.3 (adt 0.3.3, ddms 0.2.3, editors 0.1.0) + +- Support for referenced projects. +- During launch, display if a package conflict occurs when the new application is pushed onto the device. +- You can now change the font of the logcat view. Also indentation is now properly displayed. +- Plugin generated files are now properly marked as derived. This will make Team plugins ignore them. + +0.3.2 + +- XML Highlighting for AndroidManifest.xml (requires WebTools WST plugin) +- Custom java editor for R.java/Manifest.java to make those files non editable. This is to replace the current locking mechanism which causes issues on Mac OS. +- Fixed some issue in the "Restart adb" feature in the device view of ddms. +- Better handling of aidl files and the java files generated from them. +- Plugin now retries to launch the app on the emulator if it fails due to timing issues. +- Skin dropdown in the Emulator/Target tabs is now build from the content of the skin directory, to support developer made skins. +- Emulator control panel. This is a UI on top of the emulator console. it allows you to change the state of the network and gsm connection, and to initiate incoming voice call. + +0.3.1 + +- Fixed issue on winXP/Eclipse 3.2 where errors in the New Project Wizard would not display. +- Added missing intent definition in the AndroidManifest.xml file created by the New Project Wizard. +- Fixed possible NPE in the debug action from the Process View +- Support for Eclipse 3.4 + +0.2.6 / 0.3.0 + +- New Project Wizard now makes it easy to open Android sample code +- Plugin will output a warning if the build id of the device/emulator does not match the sdk build id. +- Java/Debug/ddms perspective now contains direct menus to open some of the ddms views, and to create a new android project. This will require you to reset your perspectives. +- Error during builds now put an error marker on the project instead of displaying an (annoying) dialog box. +- Custom builders now remember their build state when restarting eclipse. +- Properly parse some aapt warnings and don't abort the build when they happen. +- Abort launch and prompt the user if the project contains errors. +- New silent/normal/verbose build output. + +0.2.5 + +- Check compiler compliance level before compilation and abort if different from 1.5 +- Fix Project Properties will fix the project compiler compliance if needed. +- Fixed an issue with multiple source folders. +- Added support for new Manifest.java class (it is automatically generated with R.java if the content of the AndroidManifest.xml requires it) +- Fixed an issue that could result in not packaging code changes. +- Automatic fix of the Launch Configurations when the java package in the manifest is changed. Also improved Launch Config dialog and error alert for erroneous activity names in the Launch Configuration. +- Support for external jars that are not under the project root directory. +- New projects have a default layout. +- Misc fixes for Windows support. + +0.2.4 + +- fixed large resource corruption issue. + +0.2.3 + +- fixed issue related to the integration of dx. +- fixed issue related to the package generation that was modified for windows support. + +0.2.2 + +- Changing the SDK location in the Preferences does not require to restart Eclipse anymore. +- New SDK-Project sync mode in Android preference pane. Default value set to true. If true, all android projects are automatically sync'ed to the SDK defined in the preferences. +- Cases where no emulator is running but a dialog still says "An emulator is running..." should be less frequent. +- Projects do not reference the standard desktop JRE anymore, as android.zip contains the core java library. This will solve the case where using a core class non present on the platform would not generate a compilation error. +- Changing the package defined in the manifest now deletes the R.java class from its previous location. This will require 1 build after upgrading the plugin, before it works. +- Project selection in the Launch Config Dialog now only shows Android projects. +- Launching a debug/run session now checks that the project uses the SDK set in the preferences (This is for the non automatic sync mode). +- Removed obsolete wallpaper mode in the New Project Creation Wizard. +- dx (dalvik code conversion tool) now embedded instead of calling the external version. +- improvements in the parsing of the aapt errors. +- some fixes for windows support. + +0.2.1 + +- fixed bug in logcat search where invalid regexp would cause a crash +- minor improvements to the build/launch process. + +0.2.0 + +- Logcat view. +- File Explorer view. +- Custom options for emulator. In the Launch configuration dialog you can specify custom command line emulator options. See "emulator -help" for available options. +- Android Tools > Export Application Package is now implemented. +- Misc incremental builder fixes. +- Including static .jar files as library in your project will automatically include them in the final APK. Warning: only the .class content is included. + +0.1.10 + +- res and assets folders now fully refresh before the build, ensuring R.java and packaged resources are always up to date. This can be disabled in the preferences under "Android" if this becomes slow due to too many files. + +0.1.9 + +- New Action in the "Processes" view to debug an application that is already running. The source project for this application MUST be opened in the current workspace. +- Building the project now force refreshes the res folder. This should help rebuilding the resources when only binary files were changed from outside eclipse. +- Clean/full builds now compile all aidl files found in the build path (previously only incremental builds would compile them). Also, misc improvements to the incremental builders. +- Starting a run/debug session now asks to save the files and forces a new build to ensure that the latest package is pushed on the device. +- Plugin should be less aggressive when waiting for the emulator to be ready. This should translate in fewer failed launches. + +0.1.8 + +- Fixed Debugger issue introduced in 0.1.6 +- Added Log level preferences for DDMS. Look under Android > DDMS > Advanced. Default error level is Error. + +0.1.7 + +- Fixed issue where java warnings wouldn't trigger a new package. Now only errors stop the packaging like it should be. +- Added more error output in the console during launch. + +0.1.6 + +- New "Android" Console. It receives the error output from external tools such and aidl, dx, and aapt (only when they can't be parsed). Any error force the console to be displayed. +- The Activity Manager on the device/emulator now outputs some messages in the "Android" console when asked to start an activity. This should help you figure out what is wrong if the application doesn't start. +- Fixed a case where the .apk file would be updated with broken code. Now if there are java compile error, the .apk is not touched. +- Added support for manifest with non fully qualified activity java name, yet not starting with a dot. +- Fixed creation of manifest files (through New Project wizard) to use proper namespace for attributes. +- Better error reporting for namespace issue in the manifest. +- "Reset Adb" action from the device view. Use this is the plugin tells you an emulator is running when there are none. +- New "ddms" Console which receives the standard output of ddms. + +0.1.5 + +- Support for new activity declaration inside AndroidManifest.xml +- fixed issue that prevented bin/ to be removed from the buildpath when converting project. + +0.1.4 + +- Changes in the Manifest, now properly trigger a new package of the resources. + +0.1.3 + +- Fixed the "fix project properties" action to remove old framework libraries, just not add new ones. + +0.1.2 + +- aidl builder. The Android Resources PreBuilder now also converts aidl files into java files. +- New Project wizard now allows to make Wallpaper activities instead of gadgets (which are obsolete.) +- Launch shortcuts. Right click in the package explorer allow you to launch the application in debug or run mode directly without creating launch configurations. +- New project wizard and Project conversion now sets up the java doc path for android.zip +- Package builder now supports custom application assets placed in assets/ (which is now created automatically by the New Project Wizard). +- New action: Android Tools > Fix Project Properties, in the package explorer contextual menu. This allows you to fix the framework path (and its javadoc path) in case you change the sdk location. + +0.1.1 + +- Fixed project convertor to add the framework library if missing. + +0.1.0 + +- New project wizard. +- Python script-generated project convertor. +- Incremental builders. +- XML validation for resource files. +- Android Launch Configuration. diff --git a/eclipse/features/com.android.ide.eclipse.adt/.project b/eclipse/features/com.android.ide.eclipse.adt/.project new file mode 100644 index 0000000..beca599 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.adt/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>adt-feature</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.pde.FeatureBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.FeatureNature</nature> + </natures> +</projectDescription> diff --git a/eclipse/features/com.android.ide.eclipse.adt/build.properties b/eclipse/features/com.android.ide.eclipse.adt/build.properties new file mode 100644 index 0000000..64f93a9 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.adt/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/eclipse/features/com.android.ide.eclipse.adt/feature.xml b/eclipse/features/com.android.ide.eclipse.adt/feature.xml new file mode 100644 index 0000000..b9e2c7f --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.adt/feature.xml @@ -0,0 +1,146 @@ +<?xml version="1.0" encoding="UTF-8"?> +<feature + id="com.android.ide.eclipse.adt" + label="Android Development Tools" + version="0.9.0.qualifier" + provider-name="The Android Open Source Project" + plugin="com.android.ide.eclipse.adt"> + + <description> + Android Developer Tools. + </description> + + <copyright> + Copyright (C) 2007 The Android Open Source Project + </copyright> + + <license url="http://www.eclipse.org/org/documents/epl-v10.php"> + Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and + +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the Program. + +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. + </license> + + <url> + <update label="Android Update Site" url="https://dl-ssl.google.com/android/eclipse/"/> + </url> + + <requires> + <import plugin="com.android.ide.eclipse.ddms"/> + <import plugin="org.eclipse.core.runtime"/> + <import plugin="org.eclipse.core.resources"/> + <import plugin="org.eclipse.debug.core"/> + <import plugin="org.eclipse.debug.ui"/> + <import plugin="org.eclipse.jdt"/> + <import plugin="org.eclipse.ant.core"/> + <import plugin="org.eclipse.jdt.core"/> + <import plugin="org.eclipse.jdt.ui"/> + <import plugin="org.eclipse.jdt.launching"/> + <import plugin="org.eclipse.jface.text"/> + <import plugin="org.eclipse.ui.editors"/> + <import plugin="org.eclipse.ui.workbench.texteditor"/> + <import plugin="org.eclipse.ui.console"/> + <import plugin="org.eclipse.core.filesystem"/> + <import plugin="org.eclipse.ui"/> + <import plugin="org.eclipse.ui.ide"/> + <import plugin="org.eclipse.ui.forms"/> + <import plugin="org.eclipse.gef"/> + <import plugin="org.eclipse.ui.browser"/> + <import plugin="org.eclipse.ui.views"/> + <import plugin="org.eclipse.wst.sse.core"/> + <import plugin="org.eclipse.wst.sse.ui"/> + <import plugin="org.eclipse.wst.xml.core"/> + <import plugin="org.eclipse.wst.xml.ui"/> + </requires> + + <plugin + id="com.android.ide.eclipse.adt" + download-size="0" + install-size="0" + version="0.0.0" + unpack="false"/> + +</feature> diff --git a/eclipse/features/com.android.ide.eclipse.ddms/.project b/eclipse/features/com.android.ide.eclipse.ddms/.project new file mode 100644 index 0000000..f80ff60 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.ddms/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddms-feature</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.pde.FeatureBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.FeatureNature</nature> + </natures> +</projectDescription> diff --git a/eclipse/features/com.android.ide.eclipse.ddms/build.properties b/eclipse/features/com.android.ide.eclipse.ddms/build.properties new file mode 100644 index 0000000..64f93a9 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.ddms/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/eclipse/features/com.android.ide.eclipse.ddms/feature.xml b/eclipse/features/com.android.ide.eclipse.ddms/feature.xml new file mode 100644 index 0000000..dfdf985 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.ddms/feature.xml @@ -0,0 +1,237 @@ +<?xml version="1.0" encoding="UTF-8"?> +<feature + id="com.android.ide.eclipse.ddms" + label="Android DDMS" + version="0.9.0.qualifier" + provider-name="The Android Open Source Project"> + + <description> + Android Dalvik Debug Monitor Service + </description> + + <copyright> + Copyright (C) 2007 The Android Open Source Project + </copyright> + + <license url="http://www.apache.org/licenses/LICENSE-2.0"> + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + </license> + + <url> + <update label="Android Update Site" url="https://dl-ssl.google.com/android/eclipse/"/> + </url> + + <requires> + <import plugin="org.eclipse.ui"/> + <import plugin="org.eclipse.core.runtime"/> + <import plugin="org.eclipse.ui.console"/> + </requires> + + <plugin + id="com.android.ide.eclipse.ddms" + download-size="0" + install-size="0" + version="0.0.0" + unpack="false"/> + +</feature> diff --git a/eclipse/features/com.android.ide.eclipse.tests/.project b/eclipse/features/com.android.ide.eclipse.tests/.project new file mode 100644 index 0000000..6a16276 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.tests/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>adt-tests-feature</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.pde.FeatureBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.FeatureNature</nature> + </natures> +</projectDescription> diff --git a/eclipse/features/com.android.ide.eclipse.tests/build.properties b/eclipse/features/com.android.ide.eclipse.tests/build.properties new file mode 100644 index 0000000..64f93a9 --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.tests/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/eclipse/features/com.android.ide.eclipse.tests/feature.xml b/eclipse/features/com.android.ide.eclipse.tests/feature.xml new file mode 100644 index 0000000..2a3a74f --- /dev/null +++ b/eclipse/features/com.android.ide.eclipse.tests/feature.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<feature + id="com.android.ide.eclipse.tests" + label="ADT Tests" + version="0.9.0.qualifier" + provider-name="The Android Open Source Project"> + + <copyright> + Copyright (C) 2007 The Android Open Source Project + </copyright> + + <requires> + <import plugin="org.eclipse.ui"/> + <import plugin="org.eclipse.core.runtime"/> + <import plugin="org.eclipse.core.resources"/> + <import plugin="com.android.ide.eclipse.adt"/> + <import plugin="org.junit"/> + <import plugin="org.eclipse.jdt.core"/> + <import plugin="org.eclipse.jdt.launching"/> + <import plugin="org.eclipse.ui.views"/> + </requires> + + <plugin + id="com.android.ide.eclipse.tests" + download-size="0" + install-size="0" + version="0.0.0" + unpack="false"/> + +</feature> diff --git a/eclipse/plugins/README.txt b/eclipse/plugins/README.txt new file mode 100644 index 0000000..184d731 --- /dev/null +++ b/eclipse/plugins/README.txt @@ -0,0 +1,114 @@ +Compiling and deploying the Android Development Toolkit (ADT) feature. + +The ADT feature is composed of four plugins: +- com.android.ide.eclipse.adt: + The ADT plugin, which provides support for compiling and debugging android + applications. +- com.android.ide.eclipse.common: + A common plugin providing utility services to the other plugins. +- com.android.ide.eclipse.editors: + A plugin providing optional XML editors. +- com.android.ide.eclipse.ddms: + A plugin version of the tool DDMS + +Because the DDMS plugin source code is not yet released, compiling the +ADT/Common/Editors plugins requires to install the DDMS plugin in eclipse. + +Basic requirements: +- Eclipse 3.3 or 3.4 with JDT and PDE. +- DDMS plugin installed and running. + + +-------------------------- +1- Install the DDMS plugin +-------------------------- + +The easiest way to setup the DDMS plugin in your Eclipse environment is to +install the ADT features (see SDK documentation for details) and then remove +the following features and plugins: + +- <eclipse-directory>/features/com.android.ide.eclipse.adt_x.x.x.jar +- <eclipse-directory>/plugins/com.android.ide.eclipse.adt_x.x.x.jar +- <eclipse-directory>/plugins/com.android.ide.eclipse.common_x.x.x.jar +- <eclipse-directory>/plugins/com.android.ide.eclipse.editors_x.x.x.jar + +This will leave you with only the DDMS plugin installed in your Eclipse +distribution. + + +------------------------------------- +2- Setting up the ADT/Common project +------------------------------------- + +- Download the ADT/Common/Editors source. + +- From the SDK, copy the following jars: + * androidprefs.jar => com.android.ide.eclipse.adt folder. + * jarutils.jar => com.android.ide.eclipse.adt folder. + * ping.jar => com.android.ide.eclipse.common folder. + * androidprefs.jar => com.android.ide.eclipse.common folder. + +- Create a java project from existing source for both the ADT plugin and the + common plugin. + +- In the Package Explorer, right click the projects and choose + PDE Tools > Convert Projects to Plug-in Project... + +- Select your projects in the dialog box and click OK. + +- In the Package Explorer, for ADT and common, right click the jar files mentioned above + and choose Build Path > Add to Build Path + +At this point the projects will compile. + +To launch the projects, open the Run/Debug Dialog and create an "Eclipse +Application" launch configuration. + +Additionnaly, another feature containing the Android Editors Plugin +(com.android.ide.eclipse.editors) is available. + +- Make sure the common project is present in your workspace as the Editors + plugin depends on this plugin. Alternatively, you can have the offical ADT + feature installed in your Eclipse distribution. +- Create a java project from existing source for the Editors project. +- In the Package Explorer, right click the project and choose + PDE Tools > Convert Projects to Plug-in Project... +- Select your project in the dialog box and click OK. + +Create an "Eclipse Application" launch configuration to test the plugin. + +------------------------------------- +3- Setting up the Editors project +------------------------------------- + +The "editors" plugin is optional. You can use ADT to develop Android +applications without the XML editor support. When this plugin is present, it +offers several customized form-based XML editors and one graphical layout +editor. + +At the time of this release (Android 0.9 SDK), some of the supporting libraries +still need some cleanup and are currently only provided as JAR files. + +- Download the ADT/Common/Editors source. + +- From the source archives, copy the following jars: + * ninepatch.jar => com.android.ide.eclipse.editors folder. + * layoutlib_utils.jar => com.android.ide.eclipse.editors folder. + * layoutlib_api.jar => com.android.ide.eclipse.editors folder. + +- From http://kxml.sourceforge.net/ download: + * kXML2-2.3.0.jar => com.android.ide.eclipse.editors folder. + +- Create a java project from existing source for both the editors plugin. + +- In the Package Explorer, right click the project and choose + PDE Tools > Convert Projects to Plug-in Project... + +- Select your project in the dialog box and click OK. + +- In the Package Explorer for editors, right click the jar files mentioned + above and choose Build Path > Add to Build Path + +To launch the projects, reuse the "Eclipse Application" launch configuration +created for ADT. + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath new file mode 100644 index 0000000..c3c8c10 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="Makefile|resources/" kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="lib" path="jarutils.jar"/> + <classpathentry kind="lib" path="androidprefs.jar"/> + <classpathentry kind="lib" path="sdkstats.jar"/> + <classpathentry kind="lib" path="kxml2-2.3.0.jar"/> + <classpathentry kind="lib" path="layoutlib_api.jar"/> + <classpathentry kind="lib" path="layoutlib_utils.jar"/> + <classpathentry kind="lib" path="ninepatch.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkUiLib"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.project b/eclipse/plugins/com.android.ide.eclipse.adt/.project new file mode 100644 index 0000000..c7b1ad4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/.project @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>adt</name> + <comment></comment> + <projects> + <project>SdkLib</project> + <project>SdkUiLib</project> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF new file mode 100644 index 0000000..a464d5c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -0,0 +1,79 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Android Development Toolkit +Bundle-SymbolicName: com.android.ide.eclipse.adt;singleton:=true +Bundle-Version: 0.9.0.qualifier +Bundle-ClassPath: ., + jarutils.jar, + androidprefs.jar, + sdkstats.jar, + kxml2-2.3.0.jar, + layoutlib_api.jar, + ninepatch.jar, + layoutlib_utils.jar, + sdklib.jar, + sdkuilib.jar +Bundle-Activator: com.android.ide.eclipse.adt.AdtPlugin +Bundle-Vendor: The Android Open Source Project +Require-Bundle: com.android.ide.eclipse.ddms, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.debug.core, + org.eclipse.debug.ui, + org.eclipse.jdt, + org.eclipse.ant.core, + org.eclipse.jdt.core, + org.eclipse.jdt.ui, + org.eclipse.jdt.launching, + org.eclipse.jface.text, + org.eclipse.ui.editors, + org.eclipse.ui.workbench.texteditor, + org.eclipse.ui.console, + org.eclipse.core.filesystem, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.ui.forms, + org.eclipse.gef, + org.eclipse.ui.browser, + org.eclipse.ui.views, + org.eclipse.wst.sse.core, + org.eclipse.wst.sse.ui, + org.eclipse.wst.xml.core, + org.eclipse.wst.xml.ui +Eclipse-LazyStart: true +Export-Package: com.android.ide.eclipse.adt, + com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.project;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.project.internal;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.sdk;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.wizards.newproject;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.common, + com.android.ide.eclipse.common.project, + com.android.ide.eclipse.common.resources, + com.android.ide.eclipse.editors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.layout;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.layout.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.layout.parts;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.layout.uimodel;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.manifest;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.manifest.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.manifest.model;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.manifest.pages;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.menu;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.menu.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources.configurations;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources.descriptors;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources.explorer;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources.manager;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources.manager.files;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.resources.uimodel;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.ui;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.ui.tree;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.uimodel;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.wizards;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.xml;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.editors.xml.descriptors;x-friends:="com.android.ide.eclipse.tests" + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL b/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE b/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE new file mode 100644 index 0000000..49c101d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE @@ -0,0 +1,224 @@ +*Eclipse Public License - v 1.0* + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +*1. DEFINITIONS* + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and +documentation distributed under this Agreement, and +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and +are distributed by that particular Contributor. A Contribution +'originates' from a Contributor if it was added to the Program by such +Contributor itself or anyone acting on such Contributor's behalf. +Contributions do not include additions to the Program which: (i) are +separate modules of software distributed in conjunction with the Program +under their own license agreement, and (ii) are not derivative works of +the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +*2. GRANT OF RIGHTS* + +a) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free copyright +license to reproduce, prepare derivative works of, publicly display, +publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and +object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free patent license +under Licensed Patents to make, use, sell, offer to sell, import and +otherwise transfer the Contribution of such Contributor, if any, in +source code and object code form. This patent license shall apply to the +combination of the Contribution and the Program if, at the time the +Contribution is added by the Contributor, such addition of the +Contribution causes such combination to be covered by the Licensed +Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are +provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility to +secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow Recipient +to distribute the Program, it is Recipient's responsibility to acquire +that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright +license set forth in this Agreement. + +*3. REQUIREMENTS* + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties +and conditions, express and implied, including warranties or conditions +of title and non-infringement, and implied warranties or conditions of +merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and +consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable +manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained +within the Program. + +Each Contributor must identify itself as the originator of its +Contribution, if any, in a manner that reasonably allows subsequent +Recipients to identify the originator of the Contribution. + +*4. COMMERCIAL DISTRIBUTION* + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, the +Contributor who includes the Program in a commercial product offering +should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in +a commercial product offering, such Contributor ("Commercial +Contributor") hereby agrees to defend and indemnify every other +Contributor ("Indemnified Contributor") against any losses, damages and +costs (collectively "Losses") arising from claims, lawsuits and other +legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the +Program in a commercial product offering. The obligations in this +section do not apply to any claims or Losses relating to any actual or +alleged intellectual property infringement. In order to qualify, an +Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial +Contributor to control, and cooperate with the Commercial Contributor +in, the defense and any related settlement negotiations. The Indemnified +Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + +*5. NO WARRANTY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED +ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES +OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR +A PARTICULAR PURPOSE. Each Recipient is solely responsible for +determining the appropriateness of using and distributing the Program +and assumes all risks associated with its exercise of rights under this +Agreement , including but not limited to the risks and costs of program +errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +*6. DISCLAIMER OF LIABILITY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +*7. GENERAL* + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including +a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails +to comply with any of the material terms or conditions of this Agreement +and does not cure such failure in a reasonable period of time after +becoming aware of such noncompliance. If all Recipient's rights under +this Agreement terminate, Recipient agrees to cease use and distribution +of the Program as soon as reasonably practicable. However, Recipient's +obligations under this Agreement and any licenses granted by Recipient +relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and may +only be modified in the following manner. The Agreement Steward reserves +the right to publish new versions (including revisions) of this +Agreement from time to time. No one other than the Agreement Steward has +the right to modify this Agreement. The Eclipse Foundation is the +initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is +published, Contributor may elect to distribute the Program (including +its Contributions) under the new version. Except as expressly stated in +Sections 2(a) and 2(b) above, Recipient receives no rights or licenses +to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in +the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to +this Agreement will bring a legal action under this Agreement more than +one year after the cause of action arose. Each party waives its rights +to a jury trial in any resulting litigation. + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/about.ini b/eclipse/plugins/com.android.ide.eclipse.adt/about.ini new file mode 100644 index 0000000..fddaef0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/about.ini @@ -0,0 +1 @@ +featureImage=icons/android_32X32.jpg diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/build.properties b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties new file mode 100644 index 0000000..c7eb749 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties @@ -0,0 +1,17 @@ +bin.includes = plugin.xml,\ + META-INF/,\ + icons/,\ + .,\ + templates/,\ + about.ini,\ + jarutils.jar,\ + androidprefs.jar,\ + sdkstats.jar,\ + kxml2-2.3.0.jar,\ + layoutlib_api.jar,\ + layoutlib_utils.jar,\ + ninepatch.jar,\ + sdklib.jar,\ + sdkuilib.jar +source.. = src/ +output.. = bin/ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png Binary files differnew file mode 100644 index 0000000..eefc2ca --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png Binary files differnew file mode 100644 index 0000000..3779d4d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpg b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpg Binary files differnew file mode 100644 index 0000000..823670b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpg diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png Binary files differnew file mode 100644 index 0000000..64e3601 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png Binary files differnew file mode 100644 index 0000000..5334568 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png Binary files differnew file mode 100644 index 0000000..5d92f76 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png Binary files differnew file mode 100644 index 0000000..db5fab8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png Binary files differnew file mode 100644 index 0000000..10057c8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png Binary files differnew file mode 100644 index 0000000..36cd223 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png Binary files differnew file mode 100644 index 0000000..fae5e96 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png Binary files differnew file mode 100644 index 0000000..1eecf2c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png Binary files differnew file mode 100644 index 0000000..7911a85 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png Binary files differnew file mode 100644 index 0000000..a727dd5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png Binary files differnew file mode 100644 index 0000000..2540fbb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png Binary files differnew file mode 100644 index 0000000..7e939c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png Binary files differnew file mode 100644 index 0000000..4dc95d7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png Binary files differnew file mode 100644 index 0000000..aefffe4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png Binary files differnew file mode 100644 index 0000000..c2bb79a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png Binary files differnew file mode 100644 index 0000000..0f0e883 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png Binary files differnew file mode 100644 index 0000000..8273185 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png Binary files differnew file mode 100644 index 0000000..423c3cd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png Binary files differnew file mode 100644 index 0000000..9608cd6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png Binary files differnew file mode 100644 index 0000000..b4ddc87 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png Binary files differnew file mode 100644 index 0000000..6536576 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png Binary files differnew file mode 100644 index 0000000..35b9a46 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png Binary files differnew file mode 100644 index 0000000..ca3b6ed --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml new file mode 100644 index 0000000..d6c9ac1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -0,0 +1,502 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.2"?> +<plugin> + <extension + id="com.android.ide.eclipse.common.xmlProblem" + name="Android XML Format Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <super type="org.eclipse.core.resources.textmarker"/> + <persistent value="true"/> + </extension> + <extension + id="com.android.ide.eclipse.common.aaptProblem" + name="Android AAPT Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <super type="org.eclipse.core.resources.textmarker"/> + <persistent value="true"/> + </extension> + <extension + id="com.android.ide.eclipse.common.aapt2Problem" + name="Android AAPT Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <super type="org.eclipse.core.resources.textmarker"/> + <persistent value="true"/> + </extension> + <extension + id="com.android.ide.eclipse.common.aidlProblem" + name="Android AIDL Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <super type="org.eclipse.core.resources.textmarker"/> + <persistent value="true"/> + </extension> + <extension + id="com.android.ide.eclipse.common.androidProblem" + name="Android XML Content Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <super type="org.eclipse.core.resources.textmarker"/> + <persistent value="true"/> + </extension> + <extension + id="ResourceManagerBuilder" + name="Android Resource Manager" + point="org.eclipse.core.resources.builders"> + <builder + hasNature="true"> + <run class="com.android.ide.eclipse.adt.build.ResourceManagerBuilder"/> + </builder> + </extension> + <extension + id="PreCompilerBuilder" + name="Android Pre Compiler" + point="org.eclipse.core.resources.builders"> + <builder + hasNature="true"> + <run class="com.android.ide.eclipse.adt.build.PreCompilerBuilder"/> + </builder> + </extension> + <extension + id="ApkBuilder" + name="Android Package Builder" + point="org.eclipse.core.resources.builders"> + <builder + hasNature="true"> + <run class="com.android.ide.eclipse.adt.build.ApkBuilder"/> + </builder> + </extension> + <extension + id="AndroidNature" + name="AndroidNature" + point="org.eclipse.core.resources.natures"> + <runtime> + <run class="com.android.ide.eclipse.adt.project.AndroidNature"/> + </runtime> + <builder id="com.android.ide.eclipse.adt.ResourceManagerBuilder"/> + <builder id="com.android.ide.eclipse.adt.PreCompilerBuilder"/> + <builder id="com.android.ide.eclipse.adt.ApkBuilder"/> + </extension> + <extension + point="org.eclipse.ui.newWizards"> + <category + id="com.android.ide.eclipse.wizards.category" + name="Android"/> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.wizards.newproject.NewProjectWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/android.png" + id="com.android.ide.eclipse.adt.project.NewProjectWizard" + name="Android Project" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="true"/> + <wizard + canFinishEarly="false" + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard" + finalPerspective="org.eclipse.jdt.ui.JavaPerspective" + hasPages="true" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard" + name="Android XML File" + preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective" + project="false"> + </wizard> + </extension> + <extension + point="org.eclipse.debug.core.launchConfigurationTypes"> + <launchConfigurationType + delegate="com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate" + delegateDescription="The Android Application Launcher supports running and debugging remote Android applications on devices or emulators." + delegateName="Android Launcher" + id="com.android.ide.eclipse.adt.debug.LaunchConfigType" + modes="debug, run" + name="Android Application" + public="true" + sourceLocatorId="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector" + sourcePathComputerId="org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer"> + </launchConfigurationType> + </extension> + <extension + point="org.eclipse.debug.ui.launchConfigurationTypeImages"> + <launchConfigurationTypeImage + configTypeID="com.android.ide.eclipse.adt.debug.LaunchConfigType" + icon="icons/android.png" + id="com.android.ide.eclipse.adt.debug.LaunchConfigTypeImage"/> + </extension> + <extension + point="org.eclipse.debug.ui.launchConfigurationTabGroups"> + <launchConfigurationTabGroup + class="com.android.ide.eclipse.adt.debug.ui.LaunchConfigTabGroup" + description="Android Application" + id="com.android.ide.eclipse.adt.debug.LaunchConfigTabGroup" + type="com.android.ide.eclipse.adt.debug.LaunchConfigType"/> + </extension> + <extension + point="org.eclipse.debug.ui.launchShortcuts"> + <shortcut + category="com.android.ide.eclipse.adt.debug.LaunchConfigType" + class="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut" + icon="icons/android.png" + id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut" + label="Android Application" + modes="debug, run"> + <contextualLaunch> + <enablement> + <with variable="selection"> + <count value="1"/> + <iterate> + <and> + <test property="org.eclipse.jdt.launching.isContainer"/> + <test property="org.eclipse.jdt.launching.hasProjectNature" args="com.android.ide.eclipse.adt.AndroidNature"/> + </and> + </iterate> + </with> + </enablement> + </contextualLaunch> + <perspective id="org.eclipse.jdt.ui.JavaPerspective"/> + <perspective id="org.eclipse.debug.ui.DebugPerspective"/> + <description + description="Runs an Android Application" + mode="run"> + </description> + <description + description="Debugs an Android Application" + mode="debug"> + </description> + </shortcut> + </extension> + <extension + point="org.eclipse.ui.popupMenus"> + <objectContribution + id="com.android.ide.eclipse.adt.contribution1" + nameFilter="*" + objectClass="org.eclipse.core.resources.IProject" + adaptable="true"> + <menu + id="com.android.ide.eclipse.adt.AndroidTools" + label="Android Tools" + path="additions"> + <separator name="group1"/> + </menu> + <visibility> + <not> + <or> + <objectState + name="projectNature" + value="com.android.ide.eclipse.adt.AndroidNature"/> + <objectState + name="open" + value="false"/> + </or> + </not> + </visibility> + <action + class="com.android.ide.eclipse.adt.project.ConvertToAndroidAction" + enablesFor="1" + id="com.android.ide.eclipse.adt.ConvertToAndroidAction" + label="Convert To Android Project" + menubarPath="com.android.ide.eclipse.adt.AndroidTools/group1"/> + </objectContribution> + <objectContribution + id="com.android.ide.eclipse.adt.contribution2" + nameFilter="*" + objectClass="org.eclipse.core.resources.IProject" + adaptable="true"> + <menu + id="com.android.ide.eclipse.adt.AndroidTools" + label="Android Tools" + path="additions"> + <separator name="group1"/> + <separator name="group2"/> + </menu> + <filter + name="projectNature" + value="com.android.ide.eclipse.adt.AndroidNature"> + </filter> + <action + class="com.android.ide.eclipse.adt.project.CreateAidlImportAction" + enablesFor="1" + id="com.android.ide.eclipse.adt.project.CreateAidlImportAction" + label="Create Aidl preprocess file for Parcelable classes" + menubarPath="com.android.ide.eclipse.adt.AndroidTools/group1"/> + <action + class="com.android.ide.eclipse.adt.project.NewXmlFileWizardAction" + enablesFor="1" + id="com.android.ide.eclipse.adt.project.NewXmlFileWizardAction" + label="New Resource File..." + menubarPath="com.android.ide.eclipse.adt.AndroidTools/group1"> + </action> + <action + class="com.android.ide.eclipse.adt.project.ExportAction" + enablesFor="1" + id="com.android.ide.eclipse.adt.project.ExportAction" + label="Export Unsigned Application Package..." + menubarPath="com.android.ide.eclipse.adt.AndroidTools/group2"/> + <action + class="com.android.ide.eclipse.adt.project.ExportWizardAction" + enablesFor="1" + id="com.android.ide.eclipse.adt.project.ExportWizardAction" + label="Export Signed Application Package..." + menubarPath="com.android.ide.eclipse.adt.AndroidTools/group2"/> + <action + class="com.android.ide.eclipse.adt.project.FixProjectAction" + enablesFor="1" + id="com.android.ide.eclipse.adt.project.FixProjectAction" + label="Fix Project Properties" + menubarPath="com.android.ide.eclipse.adt.AndroidTools/group3"/> + </objectContribution> + </extension> + <extension + point="org.eclipse.ui.preferencePages"> + <page + class="com.android.ide.eclipse.adt.preferences.AndroidPreferencePage" + id="com.android.ide.eclipse.preferences.main" + name="Android"/> + <page + category="com.android.ide.eclipse.preferences.main" + class="com.android.ide.eclipse.adt.preferences.BuildPreferencePage" + id="com.android.ide.eclipse.adt.preferences.BuildPreferencePage" + name="Build"/> + <page + category="com.android.ide.eclipse.preferences.main" + class="com.android.ide.eclipse.adt.preferences.LaunchPreferencePage" + id="com.android.ide.eclipse.adt.preferences.LaunchPreferencePage" + name="Launch"/> + <page + category="com.android.ide.eclipse.preferences.main" + class="com.android.ide.eclipse.common.preferences.UsagePreferencePage" + id="com.android.ide.eclipse.common.preferences.UsagePreferencePage" + name="Usage Stats"> + </page> + </extension> + <extension + point="org.eclipse.core.runtime.preferences"> + <initializer class="com.android.ide.eclipse.adt.preferences.PreferenceInitializer"/> + </extension> + <extension + id="com.android.ide.eclipse.adt.adtProblem" + name="Android ADT Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <persistent value="true"/> + </extension> + <extension + id="com.android.ide.eclipse.adt.targetProblem" + name="Android Target Problem" + point="org.eclipse.core.resources.markers"> + <super type="org.eclipse.core.resources.problemmarker"/> + <persistent value="false"/> + </extension> + <extension + point="org.eclipse.ui.perspectiveExtensions"> + <perspectiveExtension targetID="org.eclipse.jdt.ui.JavaPerspective"> + <newWizardShortcut id="com.android.ide.eclipse.adt.project.NewProjectWizard" /> + <newWizardShortcut + id="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard"> + </newWizardShortcut> + </perspectiveExtension> + <perspectiveExtension targetID="org.eclipse.debug.ui.DebugPerspective"> + <viewShortcut id="com.android.ide.eclipse.ddms.views.LogCatView"/> + <viewShortcut id="com.android.ide.eclipse.ddms.views.DeviceView"/> + </perspectiveExtension> + </extension> + <extension + point="org.eclipse.ui.ide.projectNatureImages"> + <image + icon="icons/android_project.png" + id="com.android.ide.eclipse.adt.AndroidNature.image" + natureId="com.android.ide.eclipse.adt.AndroidNature"> + </image> + </extension> + <extension + point="org.eclipse.jdt.core.classpathContainerInitializer"> + <classpathContainerInitializer + class="com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer" + id="com.android.ide.eclipse.adt.project.AndroidClasspathContainerInitializer"> + </classpathContainerInitializer> + <classpathContainerInitializer + class="com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer" + id="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"> + </classpathContainerInitializer> + </extension> + <extension + point="org.eclipse.ui.exportWizards"> + <category + id="com.android.ide.eclipse.wizards.category" + name="Android"> + </category> + <wizard + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.project.export.ExportWizard" + icon="icons/android.png" + id="com.android.ide.eclipse.adt.project.ExportWizard" + name="Export Android Application"> + </wizard> + </extension> + <extension + point="org.eclipse.ui.commands"> + <command + name="Debug Android Application" + description="Debug Android Application" + categoryId="org.eclipse.debug.ui.category.run" + id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.debug"> + </command> + <command + name="Run Android Application" + description="Run Android Application" + categoryId="org.eclipse.debug.ui.category.run" + id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.run"> + </command> + <keyBinding + keySequence="M3+M2+A D" + contextId="org.eclipse.ui.globalScope" + commandId="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.debug" + keyConfigurationId="org.eclipse.ui.defaultAcceleratorConfiguration"> + </keyBinding> + <keyBinding + keySequence="M3+M2+A R" + contextId="org.eclipse.ui.globalScope" + commandId="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.run" + keyConfigurationId="org.eclipse.ui.defaultAcceleratorConfiguration"> + </keyBinding> + </extension> + <extension + point="org.eclipse.ui.decorators"> + <decorator + adaptable="true" + class="com.android.ide.eclipse.adt.project.FolderDecorator" + id="com.android.ide.eclipse.adt.project.FolderDecorator" + label="Android Decorator" + lightweight="true" + location="TOP_RIGHT" + objectClass="org.eclipse.core.resources.IFolder" + state="true"> + </decorator> + </extension> + <extension + point="org.eclipse.ui.editors"> + <editor + class="com.android.ide.eclipse.editors.manifest.ManifestEditor" + default="true" + filenames="AndroidManifest.xml" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.manifest.ManifestEditor" + name="Android Manifest Editor"> + </editor> + <editor + class="com.android.ide.eclipse.editors.resources.ResourcesEditor" + default="false" + extensions="xml" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.resources.ResourcesEditor" + name="Android Resource Editor"> + </editor> + <editor + class="com.android.ide.eclipse.editors.layout.LayoutEditor" + default="false" + extensions="xml" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.layout.LayoutEditor" + matchingStrategy="com.android.ide.eclipse.editors.layout.MatchingStrategy" + name="Android Layout Editor"> + </editor> + <editor + class="com.android.ide.eclipse.editors.menu.MenuEditor" + default="false" + extensions="xml" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.menu.MenuEditor" + name="Android Menu Editor"> + </editor> + <editor + class="com.android.ide.eclipse.editors.xml.XmlEditor" + default="false" + extensions="xml" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.xml.XmlEditor" + name="Android Xml Resources Editor"> + </editor> + </extension> + <extension + point="org.eclipse.ui.views"> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.editors.resources.explorer.ResourceExplorerView" + icon="icons/android.png" + id="com.android.ide.eclipse.editors.resources.explorer.ResourceExplorerView" + name="Resource Explorer"> + </view> + </extension> + <extension + point="org.eclipse.wst.sse.ui.editorConfiguration"> + <sourceViewerConfiguration + class="com.android.ide.eclipse.editors.manifest.ManifestSourceViewerConfig" + target="com.android.ide.eclipse.editors.manifest.ManifestEditor"> + </sourceViewerConfiguration> + <sourceViewerConfiguration + class="com.android.ide.eclipse.editors.resources.ResourcesSourceViewerConfig" + target="com.android.ide.eclipse.editors.resources.ResourcesEditor"> + </sourceViewerConfiguration> + <sourceViewerConfiguration + class="com.android.ide.eclipse.editors.layout.LayoutSourceViewerConfig" + target="com.android.ide.eclipse.editors.layout.LayoutEditor"> + </sourceViewerConfiguration> + <sourceViewerConfiguration + class="com.android.ide.eclipse.editors.menu.MenuSourceViewerConfig" + target="com.android.ide.eclipse.editors.menu.MenuEditor"> + </sourceViewerConfiguration> + <sourceViewerConfiguration + class="com.android.ide.eclipse.editors.xml.XmlSourceViewerConfig" + target="com.android.ide.eclipse.editors.xml.XmlEditor"> + </sourceViewerConfiguration> + </extension> + <extension + point="org.eclipse.ui.propertyPages"> + <page + adaptable="true" + class="com.android.ide.eclipse.adt.project.properties.AndroidPropertyPage" + id="com.android.ide.eclipse.adt.project.properties.AndroidPropertyPage" + name="Android" + nameFilter="*" + objectClass="org.eclipse.core.resources.IProject"> + <enabledWhen> + <test property="org.eclipse.jdt.launching.hasProjectNature" + args="com.android.ide.eclipse.adt.AndroidNature"/> + </enabledWhen> + </page> + </extension> + <extension + point="org.eclipse.ui.actionSets"> + <actionSet + description="Android Wizards" + id="adt.actionSet1" + label="Android Wizards" + visible="true"> + <action + class="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction" + icon="icons/new_adt_project.png" + id="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction" + label="New Android Project" + style="push" + toolbarPath="android_project" + tooltip="Opens a wizard to help create a new Android project"> + </action> + <action + class="com.android.ide.eclipse.adt.wizards.actions.NewXmlFileAction" + icon="icons/new_xml.png" + id="com.android.ide.eclipse.adt.wizards.actions.NewXmlFileAction" + label="New Android XML File" + style="push" + toolbarPath="android_project" + tooltip="Opens a wizard to help create a new Android XML file"> + </action> + </actionSet> + </extension> +</plugin> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java new file mode 100644 index 0000000..7304e5e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt; + +import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; + + +/** + * Constant definition class.<br> + * <br> + * Most constants have a prefix defining the content. + * <ul> + * <li><code>WS_</code> Workspace path constant. Those are absolute paths, + * from the project root.</li> + * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li> + * <li><code>FN_</code> File name constant.</li> + * <li><code>FD_</code> Folder name constant.</li> + * <li><code>MARKER_</code> Resource Marker Ids constant.</li> + * <li><code>EXT_</code> File extension constant. This does NOT include a dot.</li> + * <li><code>DOT_</code> File extension constant. This start with a dot.</li> + * <li><code>RE_</code> Regexp constant.</li> + * <li><code>BUILD_</code> Build verbosity level constant. To use with + * <code>AdtPlugin.printBuildToConsole()</code></li> + * </ul> + */ +public class AdtConstants { + /** Generic marker for ADT errors. */ + public final static String MARKER_ADT = AdtPlugin.PLUGIN_ID + ".adtProblem"; //$NON-NLS-1$ + + /** Marker for Android Target errors. + * This is not cleared on each like other markers. Instead, it's cleared + * when an {@link AndroidClasspathContainerInitializer} has succeeded in creating an + * AndroidClasspathContainer */ + public final static String MARKER_TARGET = AdtPlugin.PLUGIN_ID + ".targetProblem"; //$NON-NLS-1$ + + /** Build verbosity "Always". Those messages are always displayed. */ + public final static int BUILD_ALWAYS = 0; + + /** Build verbosity level "Normal" */ + public final static int BUILD_NORMAL = 1; + + /** Build verbosity level "Verbose". Those messages are only displayed in verbose mode */ + public final static int BUILD_VERBOSE = 2; + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java new file mode 100644 index 0000000..61be3e5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -0,0 +1,1372 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt; + +import com.android.ddmuilib.StackTracePanel; +import com.android.ddmuilib.StackTracePanel.ISourceRevealer; +import com.android.ddmuilib.console.DdmConsole; +import com.android.ddmuilib.console.IDdmConsole; +import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController; +import com.android.ide.eclipse.adt.preferences.BuildPreferencePage; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.project.export.ExportWizard; +import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.sdk.AndroidTargetParser; +import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.EclipseUiHelper; +import com.android.ide.eclipse.common.SdkStatsHelper; +import com.android.ide.eclipse.common.StreamHelper; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.common.project.ExportHelper; +import com.android.ide.eclipse.common.project.ExportHelper.IExportCallback; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.ImageLoader; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.layout.LayoutEditor; +import com.android.ide.eclipse.editors.menu.MenuEditor; +import com.android.ide.eclipse.editors.resources.ResourcesEditor; +import com.android.ide.eclipse.editors.resources.manager.ProjectResources; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolder; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener; +import com.android.ide.eclipse.editors.xml.XmlEditor; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Preferences; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.Preferences.IPropertyChangeListener; +import org.eclipse.core.runtime.Preferences.PropertyChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleConstants; +import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The activator class controls the plug-in life cycle + */ +public class AdtPlugin extends AbstractUIPlugin { + /** The plug-in ID */ + public static final String PLUGIN_ID = "com.android.ide.eclipse.adt"; //$NON-NLS-1$ + + public final static String PREFS_SDK_DIR = PLUGIN_ID + ".sdk"; //$NON-NLS-1$ + + public final static String PREFS_RES_AUTO_REFRESH = PLUGIN_ID + ".resAutoRefresh"; //$NON-NLS-1$ + + public final static String PREFS_BUILD_VERBOSITY = PLUGIN_ID + ".buildVerbosity"; //$NON-NLS-1$ + + public final static String PREFS_DEFAULT_DEBUG_KEYSTORE = PLUGIN_ID + ".defaultDebugKeyStore"; //$NON-NLS-1$ + + public final static String PREFS_CUSTOM_DEBUG_KEYSTORE = PLUGIN_ID + ".customDebugKeyStore"; //$NON-NLS-1$ + + public final static String PREFS_HOME_PACKAGE = PLUGIN_ID + ".homePackage"; //$NON-NLS-1$ + + public final static String PREFS_EMU_OPTIONS = PLUGIN_ID + ".emuOptions"; //$NON-NLS-1$ + + /** singleton instance */ + private static AdtPlugin sPlugin; + + private static Image sAndroidLogo; + private static ImageDescriptor sAndroidLogoDesc; + + /** default store, provided by eclipse */ + private IPreferenceStore mStore; + + /** cached location for the sdk folder */ + private String mOsSdkLocation; + + /** The global android console */ + private MessageConsole mAndroidConsole; + + /** Stream to write in the android console */ + private MessageConsoleStream mAndroidConsoleStream; + + /** Stream to write error messages to the android console */ + private MessageConsoleStream mAndroidConsoleErrorStream; + + /** Image loader object */ + private ImageLoader mLoader; + + /** Verbosity of the build */ + private int mBuildVerbosity = AdtConstants.BUILD_NORMAL; + + /** Color used in the error console */ + private Color mRed; + + /** Load status of the SDK. Any access MUST be in a synchronized(mPostLoadProjects) block */ + private LoadStatus mSdkIsLoaded = LoadStatus.LOADING; + /** Project to update once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList<IJavaProject> mPostLoadProjectsToResolve = + new ArrayList<IJavaProject>(); + /** Project to check validity of cache vs actual once the SDK is loaded. + * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */ + private final ArrayList<IJavaProject> mPostLoadProjectsToCheck = new ArrayList<IJavaProject>(); + + private ResourceMonitor mResourceMonitor; + private ArrayList<ITargetChangeListener> mTargetChangeListeners = + new ArrayList<ITargetChangeListener>(); + + /** + * Custom PrintStream for Dx output. This class overrides the method + * <code>println()</code> and adds the standard output tag with the + * date and the project name in front of every messages. + */ + private static final class AndroidPrintStream extends PrintStream { + private IProject mProject; + private String mPrefix; + + /** + * Default constructor with project and output stream. + * The project is used to get the project name for the output tag. + * + * @param project The Project + * @param prefix A prefix to be printed before the actual message. Can be null + * @param stream The Stream + */ + public AndroidPrintStream(IProject project, String prefix, OutputStream stream) { + super(stream); + mProject = project; + } + + @Override + public void println(String message) { + // write the date/project tag first. + String tag = StreamHelper.getMessageTag(mProject != null ? mProject.getName() : null); + + print(tag); + if (mPrefix != null) { + print(mPrefix); + } + + // then write the regular message + super.println(message); + } + } + + /** + * An error handler for checkSdkLocationAndId() that will handle the generated error + * or warning message. Each method must return a boolean that will in turn be returned by + * checkSdkLocationAndId. + */ + public static abstract class CheckSdkErrorHandler { + /** Handle an error message during sdk location check. Returns whatever + * checkSdkLocationAndId() should returns. + */ + public abstract boolean handleError(String message); + + /** Handle a warning message during sdk location check. Returns whatever + * checkSdkLocationAndId() should returns. + */ + public abstract boolean handleWarning(String message); + } + + /** + * The constructor + */ + public AdtPlugin() { + sPlugin = this; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + Display display = getDisplay(); + + // set the default android console. + mAndroidConsole = new MessageConsole("Android", null); //$NON-NLS-1$ + ConsolePlugin.getDefault().getConsoleManager().addConsoles( + new IConsole[] { mAndroidConsole }); + + // get the stream to write in the android console. + mAndroidConsoleStream = mAndroidConsole.newMessageStream(); + mAndroidConsoleErrorStream = mAndroidConsole.newMessageStream(); + mRed = new Color(display, 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non ui thread, and beccause + // changing the console properties update the ui, we need to make this change + // in the ui thread. + display.asyncExec(new Runnable() { + public void run() { + mAndroidConsoleErrorStream.setColor(mRed); + } + }); + + // set up the ddms console to use this objects + DdmConsole.setConsole(new IDdmConsole() { + public void printErrorToConsole(String message) { + AdtPlugin.printErrorToConsole((String)null, message); + } + public void printErrorToConsole(String[] messages) { + AdtPlugin.printErrorToConsole((String)null, (Object[])messages); + } + public void printToConsole(String message) { + AdtPlugin.printToConsole((String)null, message); + } + public void printToConsole(String[] messages) { + AdtPlugin.printToConsole((String)null, (Object[])messages); + } + }); + + // get the eclipse store + mStore = getPreferenceStore(); + + // set the listener for the preference change + Preferences prefs = getPluginPreferences(); + prefs.addPropertyChangeListener(new IPropertyChangeListener() { + public void propertyChange(PropertyChangeEvent event) { + // get the name of the property that changed. + String property = event.getProperty(); + + // if the SDK changed, we update the cached version + if (PREFS_SDK_DIR.equals(property)) { + + // get the new one from the preferences + mOsSdkLocation = (String)event.getNewValue(); + + // make sure it ends with a separator + if (mOsSdkLocation.endsWith(File.separator) == false) { + mOsSdkLocation = mOsSdkLocation + File.separator; + } + + // finally restart adb, in case it's a different version + DdmsPlugin.setAdb(getOsAbsoluteAdb(), true /* startAdb */); + + // get the SDK location and build id. + if (checkSdkLocationAndId()) { + // if sdk if valid, reparse it + + // add all the opened Android projects to the list of projects to be updated + // after the SDK is reloaded + synchronized (getSdkLockObject()) { + // get the project to refresh. + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(); + mPostLoadProjectsToResolve.addAll(Arrays.asList(androidProjects)); + } + + // parse the SDK resources at the new location + parseSdkContent(); + } + } else if (PREFS_BUILD_VERBOSITY.equals(property)) { + mBuildVerbosity = BuildPreferencePage.getBuildLevel( + mStore.getString(PREFS_BUILD_VERBOSITY)); + } + } + }); + + mOsSdkLocation = mStore.getString(PREFS_SDK_DIR); + + // make sure it ends with a separator. Normally this is done when the preference + // is set. But to make sure older version still work, we fix it here as well. + if (mOsSdkLocation.length() > 0 && mOsSdkLocation.endsWith(File.separator) == false) { + mOsSdkLocation = mOsSdkLocation + File.separator; + } + + // check the location of SDK + final boolean isSdkLocationValid = checkSdkLocationAndId(); + + mBuildVerbosity = BuildPreferencePage.getBuildLevel( + mStore.getString(PREFS_BUILD_VERBOSITY)); + + // create the loader that's able to load the images + mLoader = new ImageLoader(this); + + // start the DdmsPlugin by setting the adb location, only if it is set already. + if (mOsSdkLocation.length() > 0) { + DdmsPlugin.setAdb(getOsAbsoluteAdb(), true); + } + + // and give it the debug launcher for android projects + DdmsPlugin.setRunningAppDebugLauncher(new DdmsPlugin.IDebugLauncher() { + public boolean debug(String appName, int port) { + // search for an android project matching the process name + IProject project = ProjectHelper.findAndroidProjectByAppName(appName); + if (project != null) { + AndroidLaunchController.debugRunningApp(project, port); + return true; + } else { + return false; + } + } + }); + + StackTracePanel.setSourceRevealer(new ISourceRevealer() { + public void reveal(String applicationName, String className, int line) { + IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); + if (project != null) { + BaseProjectHelper.revealSource(project, className, line); + } + } + }); + + // setup export callback for editors + ExportHelper.setCallback(new IExportCallback() { + public void startExportWizard(IProject project) { + StructuredSelection selection = new StructuredSelection(project); + + ExportWizard wizard = new ExportWizard(); + wizard.init(PlatformUI.getWorkbench(), selection); + WizardDialog dialog = new WizardDialog(getDisplay().getActiveShell(), + wizard); + dialog.open(); + } + }); + + // initialize editors + startEditors(); + + // Ping the usage server and parse the SDK content. + // This is deferred in separate jobs to avoid blocking the bundle start. + // We also serialize them to avoid too many parallel jobs when Eclipse starts. + Job pingJob = createPingUsageServerJob(); + pingJob.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + super.done(event); + + // Once the ping job is finished, start the SDK parser + if (isSdkLocationValid) { + // parse the SDK resources. + parseSdkContent(); + } + } + }); + // build jobs are run after other interactive jobs + pingJob.setPriority(Job.BUILD); + // Wait 2 seconds before starting the ping job. This leaves some time to the + // other bundles to initialize. + pingJob.schedule(2000 /*milliseconds*/); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + @Override + public void stop(BundleContext context) throws Exception { + super.stop(context); + + stopEditors(); + + mRed.dispose(); + synchronized (AdtPlugin.class) { + sPlugin = null; + } + } + + /** Return the image loader for the plugin */ + public static synchronized ImageLoader getImageLoader() { + if (sPlugin != null) { + return sPlugin.mLoader; + } + return null; + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static synchronized AdtPlugin getDefault() { + return sPlugin; + } + + public static Display getDisplay() { + IWorkbench bench = null; + synchronized (AdtPlugin.class) { + bench = sPlugin.getWorkbench(); + } + + if (bench != null) { + return bench.getDisplay(); + } + return null; + } + + /** Returns the adb path relative to the sdk folder */ + public static String getOsRelativeAdb() { + return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_ADB; + } + + /** Returns the emulator path relative to the sdk folder */ + public static String getOsRelativeEmulator() { + return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_EMULATOR; + } + + /** Returns the absolute adb path */ + public static String getOsAbsoluteAdb() { + return getOsSdkFolder() + getOsRelativeAdb(); + } + + /** Returns the absolute traceview path */ + public static String getOsAbsoluteTraceview() { + return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER + + AndroidConstants.FN_TRACEVIEW; + } + + /** Returns the absolute emulator path */ + public static String getOsAbsoluteEmulator() { + return getOsSdkFolder() + getOsRelativeEmulator(); + } + + /** + * Returns a Url file path to the javaDoc folder. + */ + public static String getUrlDoc() { + return ProjectHelper.getJavaDocPath( + getOsSdkFolder() + AndroidConstants.WS_JAVADOC_FOLDER_LEAF); + } + + /** + * Returns the SDK folder. + * Guaranteed to be terminated by a platform-specific path separator. + */ + public static synchronized String getOsSdkFolder() { + if (sPlugin == null) { + return null; + } + + if (sPlugin.mOsSdkLocation == null) { + sPlugin.mOsSdkLocation = sPlugin.mStore.getString(PREFS_SDK_DIR); + } + return sPlugin.mOsSdkLocation; + } + + public static String getOsSdkToolsFolder() { + return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER; + } + + public static synchronized boolean getAutoResRefresh() { + if (sPlugin == null) { + return false; + } + return sPlugin.mStore.getBoolean(PREFS_RES_AUTO_REFRESH); + } + + public static synchronized int getBuildVerbosity() { + if (sPlugin != null) { + return sPlugin.mBuildVerbosity; + } + + return 0; + } + + /** + * Returns an image descriptor for the image file at the given + * plug-in relative path + * + * @param path the path + * @return the image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + + /** + * Reads and returns the content of a text file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static String readEmbeddedTextFile(String filepath) { + Bundle bundle = null; + synchronized (AdtPlugin.class) { + if (sPlugin != null) { + bundle = sPlugin.getBundle(); + } else { + return null; + } + } + + // attempt to get a file to one of the template. + try { + URL url = bundle.getEntry(AndroidConstants.WS_SEP + filepath); + if (url != null) { + BufferedReader reader = new BufferedReader( + new InputStreamReader(url.openStream())); + + String line; + StringBuilder total = new StringBuilder(reader.readLine()); + while ((line = reader.readLine()) != null) { + total.append('\n'); + total.append(line); + } + + return total.toString(); + } + } catch (MalformedURLException e) { + // we'll just return null. + } catch (IOException e) { + // we'll just return null. + } + + return null; + } + + /** + * Reads and returns the content of a binary file embedded in the plugin jar + * file. + * @param filepath the file path to the text file + * @return null if the file could not be read + */ + public static byte[] readEmbeddedFile(String filepath) { + Bundle bundle = null; + synchronized (AdtPlugin.class) { + if (sPlugin != null) { + bundle = sPlugin.getBundle(); + } else { + return null; + } + } + + // attempt to get a file to one of the template. + try { + URL url = bundle.getEntry(AndroidConstants.WS_SEP + filepath); + if (url != null) { + // create a buffered reader to facilitate reading. + BufferedInputStream stream = new BufferedInputStream( + url.openStream()); + + // get the size to read. + int avail = stream.available(); + + // create the buffer and reads it. + byte[] buffer = new byte[avail]; + stream.read(buffer); + + // and return. + return buffer; + } + } catch (MalformedURLException e) { + // we'll just return null. + } catch (IOException e) { + // we'll just return null;. + } + + return null; + } + + /** + * Displays an error dialog box. This dialog box is ran asynchronously in the ui thread, + * therefore this method can be called from any thread. + * @param title The title of the dialog box + * @param message The error message + */ + public final static void displayError(final String title, final String message) { + // get the current Display + final Display display = getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, title, message); + } + }); + } + + /** + * Displays a warning dialog box. This dialog box is ran asynchronously in the ui thread, + * therefore this method can be called from any thread. + * @param title The title of the dialog box + * @param message The warning message + */ + public final static void displayWarning(final String title, final String message) { + // get the current Display + final Display display = getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openWarning(shell, title, message); + } + }); + } + + /** + * Display a yes/no question dialog box. This dialog is opened synchronously in the ui thread, + * therefore this message can be called from any thread. + * @param title The title of the dialog box + * @param message The error message + * @return true if OK was clicked. + */ + public final static boolean displayPrompt(final String title, final String message) { + // get the current Display and Shell + final Display display = getDisplay(); + + // we need to ask the user what he wants to do. + final boolean[] result = new boolean[1]; + display.syncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + result[0] = MessageDialog.openQuestion(shell, title, message); + } + }); + return result[0]; + } + + /** + * Logs a message to the default Eclipse log. + * + * @param severity The severity code. Valid values are: {@link IStatus#OK}, + * {@link IStatus#ERROR}, {@link IStatus#INFO}, {@link IStatus#WARNING} or + * {@link IStatus#CANCEL}. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static void log(int severity, String format, Object ... args) { + String message = String.format(format, args); + Status status = new Status(severity, PLUGIN_ID, message); + getDefault().getLog().log(status); + } + + /** + * Logs an exception to the default Eclipse log. + * <p/> + * The status severity is always set to ERROR. + * + * @param exception the exception to log. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static void log(Throwable exception, String format, Object ... args) { + String message = String.format(format, args); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception); + getDefault().getLog().log(status); + } + + /** + * This is a mix between log(Throwable) and printErrorToConsole. + * <p/> + * This logs the exception with an ERROR severity and the given printf-like format message. + * The same message is then printed on the Android error console with the associated tag. + * + * @param exception the exception to log. + * @param format The format string, like for {@link String#format(String, Object...)}. + * @param args The arguments for the format string, like for + * {@link String#format(String, Object...)}. + */ + public static synchronized void logAndPrintError(Throwable exception, String tag, + String format, Object ... args) { + if (sPlugin != null) { + String message = String.format(format, args); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception); + getDefault().getLog().log(status); + StreamHelper.printToStream(sPlugin.mAndroidConsoleErrorStream, tag, message); + showAndroidConsole(); + } + } + + /** + * Prints one or more error message to the android console. + * @param tag A tag to be associated with the message. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static synchronized void printErrorToConsole(String tag, Object... objects) { + if (sPlugin != null) { + StreamHelper.printToStream(sPlugin.mAndroidConsoleErrorStream, tag, objects); + + showAndroidConsole(); + } + } + + /** + * Prints one or more error message to the android console. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static void printErrorToConsole(Object... objects) { + printErrorToConsole((String)null, objects); + } + + /** + * Prints one or more error message to the android console. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static void printErrorToConsole(IProject project, Object... objects) { + String tag = project != null ? project.getName() : null; + printErrorToConsole(tag, objects); + } + + /** + * Prints one or more build messages to the android console, filtered by Build output verbosity. + * @param level Verbosity level of the message. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + * @see AdtConstants#BUILD_ALWAYS + * @see AdtConstants#BUILD_NORMAL + * @see AdtConstants#BUILD_VERBOSE + */ + public static synchronized void printBuildToConsole(int level, IProject project, + Object... objects) { + if (sPlugin != null) { + if (level <= sPlugin.mBuildVerbosity) { + String tag = project != null ? project.getName() : null; + StreamHelper.printToStream(sPlugin.mAndroidConsoleStream, tag, objects); + } + } + } + + /** + * Prints one or more message to the android console. + * @param tag The tag to be associated with the message. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static synchronized void printToConsole(String tag, Object... objects) { + if (sPlugin != null) { + StreamHelper.printToStream(sPlugin.mAndroidConsoleStream, tag, objects); + } + } + + /** + * Prints one or more message to the android console. + * @param project The project to which the message is associated. Can be null. + * @param objects the objects to print through their <code>toString</code> method. + */ + public static void printToConsole(IProject project, Object... objects) { + String tag = project != null ? project.getName() : null; + printToConsole(tag, objects); + } + + /** Force the display of the android console */ + public static void showAndroidConsole() { + // first make sure the console is in the workbench + EclipseUiHelper.showView(IConsoleConstants.ID_CONSOLE_VIEW, true); + + // now make sure it's not docked. + ConsolePlugin.getDefault().getConsoleManager().showConsoleView( + AdtPlugin.getDefault().getAndroidConsole()); + } + + /** + * Returns an standard PrintStream object for a specific project.<br> + * This PrintStream will add a date/project at the beginning of every + * <code>println()</code> output. + * + * @param project The project object + * @param prefix The prefix to be added to the message. Can be null. + * @return a new PrintStream + */ + public static synchronized PrintStream getOutPrintStream(IProject project, String prefix) { + if (sPlugin != null) { + return new AndroidPrintStream(project, prefix, sPlugin.mAndroidConsoleStream); + } + + return null; + } + + /** + * Returns an error PrintStream object for a specific project.<br> + * This PrintStream will add a date/project at the beginning of every + * <code>println()</code> output. + * + * @param project The project object + * @param prefix The prefix to be added to the message. Can be null. + * @return a new PrintStream + */ + public static synchronized PrintStream getErrPrintStream(IProject project, String prefix) { + if (sPlugin != null) { + return new AndroidPrintStream(project, prefix, sPlugin.mAndroidConsoleErrorStream); + } + + return null; + } + + /** + * Returns whether the Sdk has been loaded. + */ + public final LoadStatus getSdkLoadStatus() { + synchronized (getSdkLockObject()) { + return mSdkIsLoaded; + } + } + + /** + * Returns the lock object for SDK loading. If you wish to do things while the SDK is loading, + * you must synchronize on this object. + */ + public final Object getSdkLockObject() { + return mPostLoadProjectsToResolve; + } + + /** + * Sets the given {@link IJavaProject} to have its target resolved again once the SDK finishes + * to load. + */ + public final void setProjectToResolve(IJavaProject javaProject) { + synchronized (getSdkLockObject()) { + mPostLoadProjectsToResolve.add(javaProject); + } + } + + /** + * Sets the given {@link IJavaProject} to have its target checked for consistency + * once the SDK finishes to load. This is used if the target is resolved using cached + * information while the SDK is loading. + */ + public final void setProjectToCheck(IJavaProject javaProject) { + // only lock on + synchronized (getSdkLockObject()) { + mPostLoadProjectsToCheck.add(javaProject); + } + } + + /** + * Checks the location of the SDK is valid and if it is, grab the SDK API version + * from the SDK. + * @return false if the location is not correct. + */ + private boolean checkSdkLocationAndId() { + if (mOsSdkLocation == null || mOsSdkLocation.length() == 0) { + displayError(Messages.Dialog_Title_SDK_Location, Messages.SDK_Not_Setup); + return false; + } + + return checkSdkLocationAndId(mOsSdkLocation, new CheckSdkErrorHandler() { + @Override + public boolean handleError(String message) { + AdtPlugin.displayError(Messages.Dialog_Title_SDK_Location, + String.format(Messages.Error_Check_Prefs, message)); + return false; + } + + @Override + public boolean handleWarning(String message) { + AdtPlugin.displayWarning(Messages.Dialog_Title_SDK_Location, message); + return true; + } + }); + } + + /** + * Internal helper to perform the actual sdk location and id check. + * + * @param osSdkLocation The sdk directory, an OS path. + * @param errorHandler An checkSdkErrorHandler that can display a warning or an error. + * @return False if there was an error or the result from the errorHandler invocation. + */ + public boolean checkSdkLocationAndId(String osSdkLocation, CheckSdkErrorHandler errorHandler) { + if (osSdkLocation.endsWith(File.separator) == false) { + osSdkLocation = osSdkLocation + File.separator; + } + + File osSdkFolder = new File(osSdkLocation); + if (osSdkFolder.isDirectory() == false) { + return errorHandler.handleError( + String.format(Messages.Could_Not_Find_Folder, osSdkLocation)); + } + + String osTools = osSdkLocation + SdkConstants.OS_SDK_TOOLS_FOLDER; + File toolsFolder = new File(osTools); + if (toolsFolder.isDirectory() == false) { + return errorHandler.handleError( + String.format(Messages.Could_Not_Find_Folder_In_SDK, + SdkConstants.FD_TOOLS, osSdkLocation)); + } + + // check the path to various tools we use + String[] filesToCheck = new String[] { + osSdkLocation + getOsRelativeAdb(), + osSdkLocation + getOsRelativeEmulator() + }; + for (String file : filesToCheck) { + if (checkFile(file) == false) { + return errorHandler.handleError(String.format(Messages.Could_Not_Find, file)); + } + } + + // check the SDK build id/version and the plugin version. + return VersionCheck.checkVersion(osSdkLocation, errorHandler); + } + + /** + * Checks if a path reference a valid existing file. + * @param osPath the os path to check. + * @return true if the file exists and is, in fact, a file. + */ + private boolean checkFile(String osPath) { + File file = new File(osPath); + if (file.isFile() == false) { + return false; + } + + return true; + } + + /** + * Creates a job than can ping the usage server. + */ + private Job createPingUsageServerJob() { + // In order to not block the plugin loading, so we spawn another thread. + Job job = new Job("Android SDK Ping") { // Job name, visible in progress view + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + + // get the version of the plugin + String versionString = (String) getBundle().getHeaders().get( + Constants.BUNDLE_VERSION); + Version version = new Version(versionString); + + SdkStatsHelper.pingUsageServer("adt", version); //$NON-NLS-1$ + + return Status.OK_STATUS; + } catch (Throwable t) { + log(t, "pingUsageServer failed"); //$NON-NLS-1$ + return new Status(IStatus.ERROR, PLUGIN_ID, + "pingUsageServer failed", t); + } + } + }; + return job; + } + + /** + * Parses the SDK resources. + */ + private void parseSdkContent() { + // Perform the update in a thread (here an Eclipse runtime job) + // since this should never block the caller (especially the start method) + Job job = new Job(Messages.AdtPlugin_Android_SDK_Content_Loader) { + @SuppressWarnings("unchecked") + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + SubMonitor progress = SubMonitor.convert(monitor, + "Initialize SDK Manager", 100); + + Sdk sdk = Sdk.loadSdk(mOsSdkLocation); + + if (sdk != null) { + + progress.setTaskName(Messages.AdtPlugin_Parsing_Resources); + + int n = sdk.getTargets().length; + if (n > 0) { + int w = 60 / n; + for (IAndroidTarget target : sdk.getTargets()) { + SubMonitor p2 = progress.newChild(w); + IStatus status = new AndroidTargetParser(target).run(p2); + if (status.getCode() != IStatus.OK) { + synchronized (getSdkLockObject()) { + mSdkIsLoaded = LoadStatus.FAILED; + mPostLoadProjectsToResolve.clear(); + } + return status; + } + } + } + + synchronized (getSdkLockObject()) { + mSdkIsLoaded = LoadStatus.LOADED; + + progress.setTaskName("Check Projects"); + + // check the projects that need checking. + // The method modifies the list (it removes the project that + // do not need to be resolved again). + AndroidClasspathContainerInitializer.checkProjectsCache( + mPostLoadProjectsToCheck); + + mPostLoadProjectsToResolve.addAll(mPostLoadProjectsToCheck); + + // update the project that needs recompiling. + if (mPostLoadProjectsToResolve.size() > 0) { + IJavaProject[] array = mPostLoadProjectsToResolve.toArray( + new IJavaProject[mPostLoadProjectsToResolve.size()]); + AndroidClasspathContainerInitializer.updateProjects(array); + mPostLoadProjectsToResolve.clear(); + } + + progress.worked(10); + } + } + + // Notify resource changed listeners + progress.setTaskName("Refresh UI"); + progress.setWorkRemaining(mTargetChangeListeners.size()); + + // Clone the list before iterating, to avoid Concurrent Modification + // exceptions + final List<ITargetChangeListener> listeners = + (List<ITargetChangeListener>)mTargetChangeListeners.clone(); + final SubMonitor progress2 = progress; + AdtPlugin.getDisplay().syncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onTargetsLoaded(); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } finally { + progress2.worked(1); + } + } + } + }); + } finally { + if (monitor != null) { + monitor.done(); + } + } + + return Status.OK_STATUS; + } + }; + job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs + job.schedule(); + } + + /** Returns the global android console */ + public MessageConsole getAndroidConsole() { + return mAndroidConsole; + } + + // ----- Methods for Editors ------- + + public void startEditors() { + sAndroidLogoDesc = imageDescriptorFromPlugin(AdtPlugin.PLUGIN_ID, + "/icons/android.png"); //$NON-NLS-1$ + sAndroidLogo = sAndroidLogoDesc.createImage(); + + // get the stream to write in the android console. + MessageConsole androidConsole = AdtPlugin.getDefault().getAndroidConsole(); + mAndroidConsoleStream = androidConsole.newMessageStream(); + + mAndroidConsoleErrorStream = androidConsole.newMessageStream(); + mRed = new Color(getDisplay(), 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non ui thread, and beccause + // changing the console properties update the ui, we need to make this change + // in the ui thread. + getDisplay().asyncExec(new Runnable() { + public void run() { + mAndroidConsoleErrorStream.setColor(mRed); + } + }); + + // Add a resource listener to handle compiled resources. + IWorkspace ws = ResourcesPlugin.getWorkspace(); + mResourceMonitor = ResourceMonitor.startMonitoring(ws); + + if (mResourceMonitor != null) { + try { + setupDefaultEditor(mResourceMonitor); + ResourceManager.setup(mResourceMonitor); + } catch (Throwable t) { + log(t, "ResourceManager.setup failed"); //$NON-NLS-1$ + } + } + } + + /** + * The <code>AbstractUIPlugin</code> implementation of this <code>Plugin</code> + * method saves this plug-in's preference and dialog stores and shuts down + * its image registry (if they are in use). Subclasses may extend this + * method, but must send super <b>last</b>. A try-finally statement should + * be used where necessary to ensure that <code>super.shutdown()</code> is + * always done. + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + public void stopEditors() { + sAndroidLogo.dispose(); + + IconFactory.getInstance().Dispose(); + + // Remove the resource listener that handles compiled resources. + IWorkspace ws = ResourcesPlugin.getWorkspace(); + ResourceMonitor.stopMonitoring(ws); + + mRed.dispose(); + } + + /** + * Returns an Image for the small Android logo. + * + * Callers should not dispose it. + */ + public static Image getAndroidLogo() { + return sAndroidLogo; + } + + /** + * Returns an {@link ImageDescriptor} for the small Android logo. + * + * Callers should not dispose it. + */ + public static ImageDescriptor getAndroidLogoDesc() { + return sAndroidLogoDesc; + } + + /** + * Returns the ResourceMonitor object. + */ + public ResourceMonitor getResourceMonitor() { + return mResourceMonitor; + } + + /** + * Sets up the editor to register default editors for resource files when needed. + * + * This is called by the {@link AdtPlugin} during initialization. + * + * @param monitor The main Resource Monitor object. + */ + public void setupDefaultEditor(ResourceMonitor monitor) { + monitor.addFileListener(new IFileListener() { + + private static final String UNKNOWN_EDITOR = "unknown-editor"; //$NON-NLS-1$ + + /* (non-Javadoc) + * Sent when a file changed. + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * + * @see IFileListener#fileChanged + */ + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { + if (AndroidConstants.EXT_XML.equals(file.getFileExtension())) { + // The resources files must have a file path similar to + // project/res/.../*.xml + // There is no support for sub folders, so the segment count must be 4 + if (file.getFullPath().segmentCount() == 4) { + // check if we are inside the res folder. + String segment = file.getFullPath().segment(1); + if (segment.equalsIgnoreCase(SdkConstants.FD_RESOURCES)) { + // we are inside a res/ folder, get the actual ResourceFolder + ProjectResources resources = ResourceManager.getInstance(). + getProjectResources(file.getProject()); + + // This happens when importing old Android projects in Eclipse + // that lack the container (probably because resources fail to build + // properly.) + if (resources == null) { + log(IStatus.INFO, + "getProjectResources failed for path %1$s in project %2$s", //$NON-NLS-1$ + file.getFullPath().toOSString(), + file.getProject().getName()); + return; + } + + ResourceFolder resFolder = resources.getResourceFolder( + (IFolder)file.getParent()); + + if (resFolder != null) { + if (kind == IResourceDelta.ADDED) { + resourceAdded(file, resFolder.getType()); + } else if (kind == IResourceDelta.CHANGED) { + resourceChanged(file, resFolder.getType()); + } + } else { + // if the res folder is null, this means the name is invalid, + // in this case we remove whatever android editors that was set + // as the default editor. + IEditorDescriptor desc = IDE.getDefaultEditor(file); + String editorId = desc.getId(); + if (editorId.startsWith(AndroidConstants.EDITORS_NAMESPACE)) { + // reset the default editor. + IDE.setDefaultEditor(file, null); + } + } + } + } + } + } + + private void resourceAdded(IFile file, ResourceFolderType type) { + // set the default editor based on the type. + if (type == ResourceFolderType.LAYOUT) { + IDE.setDefaultEditor(file, LayoutEditor.ID); + } else if (type == ResourceFolderType.DRAWABLE + || type == ResourceFolderType.VALUES) { + IDE.setDefaultEditor(file, ResourcesEditor.ID); + } else if (type == ResourceFolderType.MENU) { + IDE.setDefaultEditor(file, MenuEditor.ID); + } else if (type == ResourceFolderType.XML) { + if (XmlEditor.canHandleFile(file)) { + IDE.setDefaultEditor(file, XmlEditor.ID); + } else { + // set a property to determine later if the XML can be handled + QualifiedName qname = new QualifiedName( + AdtPlugin.PLUGIN_ID, + UNKNOWN_EDITOR); + try { + file.setPersistentProperty(qname, "1"); + } catch (CoreException e) { + // pass + } + } + } + } + + private void resourceChanged(IFile file, ResourceFolderType type) { + if (type == ResourceFolderType.XML) { + IEditorDescriptor ed = IDE.getDefaultEditor(file); + if (ed == null || ed.getId() != XmlEditor.ID) { + QualifiedName qname = new QualifiedName( + AdtPlugin.PLUGIN_ID, + UNKNOWN_EDITOR); + String prop = null; + try { + prop = file.getPersistentProperty(qname); + } catch (CoreException e) { + // pass + } + if (prop != null && XmlEditor.canHandleFile(file)) { + try { + // remove the property & set editor + file.setPersistentProperty(qname, null); + IWorkbenchPage page = PlatformUI.getWorkbench(). + getActiveWorkbenchWindow().getActivePage(); + + IEditorPart oldEditor = page.findEditor(new FileEditorInput(file)); + if (oldEditor != null && + AdtPlugin.displayPrompt("Android XML Editor", + String.format("The file you just saved as been recognized as a file that could be better handled using the Android XML Editor. Do you want to edit '%1$s' using the Android XML editor instead?", + file.getFullPath()))) { + IDE.setDefaultEditor(file, XmlEditor.ID); + IEditorPart newEditor = page.openEditor( + new FileEditorInput(file), + XmlEditor.ID, + true, /* activate */ + IWorkbenchPage.MATCH_NONE); + + if (newEditor != null) { + page.closeEditor(oldEditor, true /* save */); + } + } + } catch (CoreException e) { + // setPersistentProperty or page.openEditor may have failed + } + } + } + } + } + + }, IResourceDelta.ADDED | IResourceDelta.CHANGED); + } + + /** + * Adds a new {@link ITargetChangeListener} to be notified when a new SDK is loaded, or when + * a project has its target changed. + */ + public void addTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.add(listener); + } + + /** + * Removes an existing {@link ITargetChangeListener}. + * @see #addTargetListener(ITargetChangeListener) + */ + public void removeTargetListener(ITargetChangeListener listener) { + mTargetChangeListeners.remove(listener); + } + + /** + * Updates all the {@link ITargetChangeListener} that a target has changed for a given project. + * <p/>Only editors related to that project should reload. + */ + @SuppressWarnings("unchecked") + public void updateTargetListener(final IProject project) { + final List<ITargetChangeListener> listeners = + (List<ITargetChangeListener>)mTargetChangeListeners.clone(); + + AdtPlugin.getDisplay().asyncExec(new Runnable() { + public void run() { + for (ITargetChangeListener listener : listeners) { + try { + listener.onProjectTargetChange(project); + } catch (Exception e) { + AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$ + } + } + } + }); + } + + public static synchronized OutputStream getErrorStream() { + return sPlugin.mAndroidConsoleErrorStream; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java new file mode 100644 index 0000000..a638810 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java @@ -0,0 +1,48 @@ + +package com.android.ide.eclipse.adt; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.messages"; //$NON-NLS-1$ + + public static String AdtPlugin_Android_SDK_Content_Loader; + + public static String AdtPlugin_Android_SDK_Resource_Parser; + + public static String AdtPlugin_Failed_To_Parse_s; + + public static String AdtPlugin_Failed_To_Start_s; + + public static String AdtPlugin_Parsing_Resources; + + public static String Could_Not_Find; + + public static String Could_Not_Find_Folder; + + public static String Could_Not_Find_Folder_In_SDK; + + public static String Dialog_Title_SDK_Location; + + public static String Error_Check_Prefs; + + public static String SDK_Not_Setup; + + public static String VersionCheck_Plugin_Too_Old; + + public static String VersionCheck_Plugin_Version_Failed; + + public static String VersionCheck_SDK_Build_Too_Low; + + public static String VersionCheck_SDK_Milestone_Too_Low; + + public static String VersionCheck_Unable_To_Parse_Version_s; + + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java new file mode 100644 index 0000000..6d85af3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt; + +import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler; +import com.android.sdklib.SdkConstants; + +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class handling the version check for the plugin vs. the SDK.<br> + * The plugin must be able to support all version of the SDK. + * + * <p/>An SDK can require a new version of the plugin. + * <p/>The SDK contains a file with the minimum version for the plugin. This file is inside the + * <code>tools/lib</code> directory, and is called <code>plugin.prop</code>.<br> + * Inside that text file, there is a line in the format "plugin.version=#.#.#". This is checked + * against the current plugin version.<br> + * + */ +final class VersionCheck { + /** + * Pattern to get the minimum plugin version supported by the SDK. This is read from + * the file <code>$SDK/tools/lib/plugin.prop</code>. + */ + private final static Pattern sPluginVersionPattern = Pattern.compile( + "^plugin.version=(\\d+)\\.(\\d+)\\.(\\d+).*$"); //$NON-NLS-1$ + + /** + * Checks the plugin and the SDK have compatible versions. + * @param osSdkPath The path to the SDK + * @return true if compatible. + */ + public static boolean checkVersion(String osSdkPath, CheckSdkErrorHandler errorHandler) { + AdtPlugin plugin = AdtPlugin.getDefault(); + String osLibs = osSdkPath + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + + // get the plugin property file, and grab the minimum plugin version required + // to work with the sdk + int minMajorVersion = -1; + int minMinorVersion = -1; + int minMicroVersion = -1; + try { + FileReader reader = new FileReader(osLibs + SdkConstants.FN_PLUGIN_PROP); + BufferedReader bReader = new BufferedReader(reader); + String line; + while ((line = bReader.readLine()) != null) { + Matcher m = sPluginVersionPattern.matcher(line); + if (m.matches()) { + minMajorVersion = Integer.parseInt(m.group(1)); + minMinorVersion = Integer.parseInt(m.group(2)); + minMicroVersion = Integer.parseInt(m.group(3)); + break; + } + } + } catch (FileNotFoundException e) { + // the build id will be null, and this is handled by the builders. + } catch (IOException e) { + // the build id will be null, and this is handled by the builders. + } + + // Failed to get the min plugin version number? + if (minMajorVersion == -1 || minMinorVersion == -1 || minMicroVersion ==-1) { + return errorHandler.handleWarning(Messages.VersionCheck_Plugin_Version_Failed); + } + + // test the plugin number + String versionString = (String) plugin.getBundle().getHeaders().get( + Constants.BUNDLE_VERSION); + Version version = new Version(versionString); + + boolean valid = true; + if (version.getMajor() < minMajorVersion) { + valid = false; + } else if (version.getMajor() == minMajorVersion) { + if (version.getMinor() < minMinorVersion) { + valid = false; + } else if (version.getMinor() == minMinorVersion) { + if (version.getMicro() < minMicroVersion) { + valid = false; + } + } + } + + if (valid == false) { + return errorHandler.handleWarning( + String.format(Messages.VersionCheck_Plugin_Too_Old, + minMajorVersion, minMinorVersion, minMicroVersion, versionString)); + } + + return true; // no error! + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java new file mode 100644 index 0000000..e71ae47 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java @@ -0,0 +1,1154 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.JavaResourceFilter; +import com.android.jarutils.SignedJarBuilder; +import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; +import com.android.jarutils.DebugKeyProvider.KeytoolException; +import com.android.jarutils.SignedJarBuilder.IZipEntryFilter; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.preference.IPreferenceStore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +public class ApkBuilder extends BaseBuilder { + + public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$ + + private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$ + private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$ + private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$ + + private static final String DX_PREFIX = "Dx"; //$NON-NLS-1$ + + /** + * Dex conversion flag. This is set to true if one of the changed/added/removed + * file is a .class file. Upon visiting all the delta resource, if this + * flag is true, then we know we'll have to make the "classes.dex" file. + */ + private boolean mConvertToDex = false; + + /** + * Package resources flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resource, if + * this flag is true, then we know we'll have to repackage the resources. + */ + private boolean mPackageResources = false; + + /** + * Final package build flag. + */ + private boolean mBuildFinalPackage = false; + + private PrintStream mOutStream = null; + private PrintStream mErrStream = null; + + /** + * Basic Resource Delta Visitor class to check if a referenced project had a change in its + * compiled java files. + */ + private static class ReferencedProjectDeltaVisitor implements IResourceDeltaVisitor { + + private boolean mConvertToDex = false; + private boolean mMakeFinalPackage; + + private IPath mOutputFolder; + private ArrayList<IPath> mSourceFolders; + + private ReferencedProjectDeltaVisitor(IJavaProject javaProject) { + try { + mOutputFolder = javaProject.getOutputLocation(); + mSourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); + } catch (JavaModelException e) { + } finally { + } + } + + /** + * {@inheritDoc} + * @throws CoreException + */ + public boolean visit(IResourceDelta delta) throws CoreException { + // no need to keep looking if we already know we need to convert + // to dex and make the final package. + if (mConvertToDex && mMakeFinalPackage) { + return false; + } + + // get the resource and the path segments. + IResource resource = delta.getResource(); + IPath resourceFullPath = resource.getFullPath(); + + if (mOutputFolder.isPrefixOf(resourceFullPath)) { + int type = resource.getType(); + if (type == IResource.FILE) { + String ext = resource.getFileExtension(); + if (AndroidConstants.EXT_CLASS.equals(ext)) { + mConvertToDex = true; + } + } + return true; + } else { + for (IPath sourceFullPath : mSourceFolders) { + if (sourceFullPath.isPrefixOf(resourceFullPath)) { + int type = resource.getType(); + if (type == IResource.FILE) { + // check if the file is a valid file that would be + // included during the final packaging. + if (checkFileForPackaging((IFile)resource)) { + mMakeFinalPackage = true; + } + + return false; + } else if (type == IResource.FOLDER) { + // if this is a folder, we check if this is a valid folder as well. + // If this is a folder that needs to be ignored, we must return false, + // so that we ignore its content. + return checkFolderForPackaging((IFolder)resource); + } + } + } + } + + return true; + } + + /** + * Returns if one of the .class file was modified. + */ + boolean needDexConvertion() { + return mConvertToDex; + } + + boolean needMakeFinalPackage() { + return mMakeFinalPackage; + } + } + + /** + * {@link IZipEntryFilter} to filter out everything that is not a standard java resources. + * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when + * we only want the java resources from external jars. + */ + private final IZipEntryFilter mJavaResourcesFilter = new JavaResourceFilter(); + + public ApkBuilder() { + super(); + } + + // build() returns a list of project from which this project depends for future compilation. + @SuppressWarnings("unchecked") //$NON-NLS-1$ + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) + throws CoreException { + // get a project object + IProject project = getProject(); + + // Top level check to make sure the build can move forward. + abortOnBadSetup(project); + + // get the list of referenced projects. + IProject[] referencedProjects = ProjectHelper.getReferencedProjects(project); + IJavaProject[] referencedJavaProjects = getJavaProjects(referencedProjects); + + // get the output folder, this method returns the path with a trailing + // separator + IJavaProject javaProject = JavaCore.create(project); + IFolder outputFolder = BaseProjectHelper.getOutputFolder(project); + + // now we need to get the classpath list + ArrayList<IPath> sourceList = BaseProjectHelper.getSourceClasspaths(javaProject); + + // First thing we do is go through the resource delta to not + // lose it if we have to abort the build for any reason. + ApkDeltaVisitor dv = null; + if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Start_Full_Apk_Build); + + mPackageResources = true; + mConvertToDex = true; + mBuildFinalPackage = true; + } else { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Start_Inc_Apk_Build); + + // go through the resources and see if something changed. + IResourceDelta delta = getDelta(project); + if (delta == null) { + mPackageResources = true; + mConvertToDex = true; + mBuildFinalPackage = true; + } else { + dv = new ApkDeltaVisitor(this, sourceList, outputFolder); + delta.accept(dv); + + // save the state + mPackageResources |= dv.getPackageResources(); + mConvertToDex |= dv.getConvertToDex(); + mBuildFinalPackage |= dv.getMakeFinalPackage(); + } + + // also go through the delta for all the referenced projects, until we are forced to + // compile anyway + for (int i = 0 ; i < referencedJavaProjects.length && + (mBuildFinalPackage == false || mConvertToDex == false); i++) { + IJavaProject referencedJavaProject = referencedJavaProjects[i]; + delta = getDelta(referencedJavaProject.getProject()); + if (delta != null) { + ReferencedProjectDeltaVisitor refProjectDv = new ReferencedProjectDeltaVisitor( + referencedJavaProject); + delta.accept(refProjectDv); + + // save the state + mConvertToDex |= refProjectDv.needDexConvertion(); + mBuildFinalPackage |= refProjectDv.needMakeFinalPackage(); + } + } + } + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex); + saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + if (dv != null && dv.mXmlError) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Xml_Error); + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway + return referencedProjects; + } + + if (outputFolder == null) { + // mark project and exit + markProject(AdtConstants.MARKER_ADT, Messages.Failed_To_Get_Output, + IMarker.SEVERITY_ERROR); + return referencedProjects; + } + + // first thing we do is check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder.length() == 0) { + // this has already been checked in the precompiler. Therefore, + // while we do have to cancel the build, we don't have to return + // any error or throw anything. + return referencedProjects; + } + + // get the extra configs for the project. + // The map contains (name, filter) where 'name' is a name to be used in the apk filename, + // and filter is the resource filter to be used in the aapt -c parameters to restrict + // which resource configurations to package in the apk. + Map<String, String> configs = Sdk.getCurrent().getProjectApkConfigs(project); + + // do some extra check, in case the output files are not present. This + // will force to recreate them. + IResource tmp = null; + + if (mPackageResources == false) { + // check the full resource package + tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_); + if (tmp == null || tmp.exists() == false) { + mPackageResources = true; + mBuildFinalPackage = true; + } else { + // if the full package is present, we check the filtered resource packages as well + if (configs != null) { + Set<Entry<String, String>> entrySet = configs.entrySet(); + + for (Entry<String, String> entry : entrySet) { + String filename = String.format(AndroidConstants.FN_RESOURCES_S_AP_, + entry.getKey()); + + tmp = outputFolder.findMember(filename); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, filename); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + mPackageResources = true; + mBuildFinalPackage = true; + break; + } + } + } + } + } + + // check classes.dex is present. If not we force to recreate it. + if (mConvertToDex == false) { + tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX); + if (tmp == null || tmp.exists() == false) { + mConvertToDex = true; + mBuildFinalPackage = true; + } + } + + // also check the final file(s)! + String finalPackageName = getFileName(project, null /*config*/); + if (mBuildFinalPackage == false) { + tmp = outputFolder.findMember(finalPackageName); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + mBuildFinalPackage = true; + } else if (configs != null) { + // if the full apk is present, we check the filtered apk as well + Set<Entry<String, String>> entrySet = configs.entrySet(); + + for (Entry<String, String> entry : entrySet) { + String filename = getFileName(project, entry.getKey()); + + tmp = outputFolder.findMember(filename); + if (tmp == null || (tmp instanceof IFile && + tmp.exists() == false)) { + String msg = String.format(Messages.s_Missing_Repackaging, filename); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + mBuildFinalPackage = true; + break; + } + } + } + } + + // at this point we know if we need to recreate the temporary apk + // or the dex file, but we don't know if we simply need to recreate them + // because they are missing + + // refresh the output directory first + IContainer ic = outputFolder.getParent(); + if (ic != null) { + ic.refreshLocal(IResource.DEPTH_ONE, monitor); + } + + // we need to test all three, as we may need to make the final package + // but not the intermediary ones. + if (mPackageResources || mConvertToDex || mBuildFinalPackage) { + IPath binLocation = outputFolder.getLocation(); + if (binLocation == null) { + markProject(AdtConstants.MARKER_ADT, Messages.Output_Missing, + IMarker.SEVERITY_ERROR); + return referencedProjects; + } + String osBinPath = binLocation.toOSString(); + + // Remove the old .apk. + // This make sure that if the apk is corrupted, then dx (which would attempt + // to open it), will not fail. + String osFinalPackagePath = osBinPath + File.separator + finalPackageName; + File finalPackage = new File(osFinalPackagePath); + + // if delete failed, this is not really a problem, as the final package generation + // handle already present .apk, and if that one failed as well, the user will be + // notified. + finalPackage.delete(); + + if (configs != null) { + Set<Entry<String, String>> entrySet = configs.entrySet(); + for (Entry<String, String> entry : entrySet) { + String packageFilepath = osBinPath + File.separator + + getFileName(project, entry.getKey()); + + finalPackage = new File(packageFilepath); + finalPackage.delete(); + } + } + + // first we check if we need to package the resources. + if (mPackageResources) { + // remove some aapt_package only markers. + removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE); + + // need to figure out some path before we can execute aapt; + + // resource to the AndroidManifest.xml file + IResource manifestResource = project .findMember( + AndroidConstants.WS_SEP + AndroidConstants.FN_ANDROID_MANIFEST); + + if (manifestResource == null + || manifestResource.exists() == false) { + // mark project and exit + String msg = String.format(Messages.s_File_Missing, + AndroidConstants.FN_ANDROID_MANIFEST); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return referencedProjects; + } + + // get the resource folder + IFolder resFolder = project.getFolder( + AndroidConstants.WS_RESOURCES); + + // and the assets folder + IFolder assetsFolder = project.getFolder( + AndroidConstants.WS_ASSETS); + + // we need to make sure this one exists. + if (assetsFolder.exists() == false) { + assetsFolder = null; + } + + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifestResource.getLocation(); + + if (resLocation != null && manifestLocation != null) { + String osResPath = resLocation.toOSString(); + String osManifestPath = manifestLocation.toOSString(); + + String osAssetsPath = null; + if (assetsFolder != null) { + osAssetsPath = assetsFolder.getLocation().toOSString(); + } + + // build the default resource package + if (executeAapt(project, osManifestPath, osResPath, + osAssetsPath, osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_AP_, null /*configFilter*/) == false) { + // aapt failed. Whatever files that needed to be marked + // have already been marked. We just return. + return referencedProjects; + } + + // now do the same thing for all the configured resource packages. + if (configs != null) { + Set<Entry<String, String>> entrySet = configs.entrySet(); + for (Entry<String, String> entry : entrySet) { + String outPathFormat = osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_S_AP_; + String outPath = String.format(outPathFormat, entry.getKey()); + if (executeAapt(project, osManifestPath, osResPath, + osAssetsPath, outPath, entry.getValue()) == false) { + // aapt failed. Whatever files that needed to be marked + // have already been marked. We just return. + return referencedProjects; + } + } + } + + // build has been done. reset the state of the builder + mPackageResources = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources); + } + } + + // then we check if we need to package the .class into classes.dex + if (mConvertToDex) { + if (executeDx(javaProject, osBinPath, osBinPath + File.separator + + AndroidConstants.FN_CLASSES_DEX, referencedJavaProjects) == false) { + // dx failed, we return + return referencedProjects; + } + + // build has been done. reset the state of the builder + mConvertToDex = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex); + } + + // now we need to make the final package from the intermediary apk + // and classes.dex. + // This is the default package with all the resources. + + String classesDexPath = osBinPath + File.separator + AndroidConstants.FN_CLASSES_DEX; + if (finalPackage(osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_, + classesDexPath,osFinalPackagePath, javaProject, + referencedJavaProjects) == false) { + return referencedProjects; + } + + // now do the same thing for all the configured resource packages. + if (configs != null) { + String resPathFormat = osBinPath + File.separator + + AndroidConstants.FN_RESOURCES_S_AP_; + + Set<Entry<String, String>> entrySet = configs.entrySet(); + for (Entry<String, String> entry : entrySet) { + // make the filename for the resource package. + String resPath = String.format(resPathFormat, entry.getKey()); + + // make the filename for the apk to generate + String apkOsFilePath = osBinPath + File.separator + + getFileName(project, entry.getKey()); + if (finalPackage(resPath, classesDexPath, apkOsFilePath, javaProject, + referencedJavaProjects) == false) { + return referencedProjects; + } + } + } + + // we are done. + + // get the resource to bin + outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor); + + // build has been done. reset the state of the builder + mBuildFinalPackage = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage); + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + "Build Success!"); + } + return referencedProjects; + } + + + @Override + protected void startupOnInitialize() { + super.startupOnInitialize(); + + // load the build status. We pass true as the default value to + // force a recompile in case the property was not found + mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , true); + mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true); + mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true); + } + + /** + * Executes aapt. If any error happen, files or the project will be marked. + * @param project The Project + * @param osManifestPath The path to the manifest file + * @param osResPath The path to the res folder + * @param osAssetsPath The path to the assets folder. This can be null. + * @param osOutFilePath The path to the temporary resource file to create. + * @param configFilter The configuration filter for the resources to include + * (used with -c option, for example "port,en,fr" to include portrait, English and French + * resources.) + * @return true if success, false otherwise. + */ + private boolean executeAapt(IProject project, String osManifestPath, + String osResPath, String osAssetsPath, String osOutFilePath, String configFilter) { + IAndroidTarget target = Sdk.getCurrent().getTarget(project); + + // Create the command line. + ArrayList<String> commandArray = new ArrayList<String>(); + commandArray.add(target.getPath(IAndroidTarget.AAPT)); + commandArray.add("package"); //$NON-NLS-1$ + commandArray.add("-f");//$NON-NLS-1$ + if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { + commandArray.add("-v"); //$NON-NLS-1$ + } + if (configFilter != null) { + commandArray.add("-c"); //$NON-NLS-1$ + commandArray.add(configFilter); + } + commandArray.add("-M"); //$NON-NLS-1$ + commandArray.add(osManifestPath); + commandArray.add("-S"); //$NON-NLS-1$ + commandArray.add(osResPath); + if (osAssetsPath != null) { + commandArray.add("-A"); //$NON-NLS-1$ + commandArray.add(osAssetsPath); + } + commandArray.add("-I"); //$NON-NLS-1$ + commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + commandArray.add("-F"); //$NON-NLS-1$ + commandArray.add(osOutFilePath); + + String command[] = commandArray.toArray( + new String[commandArray.size()]); + + if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : command) { + sb.append(c); + sb.append(' '); + } + AdtPlugin.printToConsole(project, sb.toString()); + } + + // launch + int execError = 1; + try { + // launch the command line process + Process process = Runtime.getRuntime().exec(command); + + // list to store each line of stderr + ArrayList<String> results = new ArrayList<String>(); + + // get the output and return code from the process + execError = grabProcessOutput(process, results); + + // attempt to parse the error output + boolean parsingError = parseAaptOutput(results, project); + + // if we couldn't parse the output we display it in the console. + if (parsingError) { + if (execError != 0) { + AdtPlugin.printErrorToConsole(project, results.toArray()); + } else { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_ALWAYS, project, + results.toArray()); + } + } + + // We need to abort if the exec failed. + if (execError != 0) { + // if the exec failed, and we couldn't parse the error output (and therefore + // not all files that should have been marked, were marked), we put a generic + // marker on the project and abort. + if (parsingError) { + markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors, + IMarker.SEVERITY_ERROR); + } + + // abort if exec failed. + return false; + } + } catch (IOException e1) { + String msg = String.format(Messages.AAPT_Exec_Error, command[0]); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (InterruptedException e) { + String msg = String.format(Messages.AAPT_Exec_Error, command[0]); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } + + return true; + } + + /** + * Execute the Dx tool for dalvik code conversion. + * @param javaProject The java project + * @param osBinPath the path to the output folder of the project + * @param osOutFilePath the path of the dex file to create. + * @param referencedJavaProjects the list of referenced projects for this project. + * + * @throws CoreException + */ + private boolean executeDx(IJavaProject javaProject, String osBinPath, String osOutFilePath, + IJavaProject[] referencedJavaProjects) throws CoreException { + IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + if (targetData == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); + } + + // get the dex wrapper + DexWrapper wrapper = targetData.getDexWrapper(); + + if (wrapper == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + Messages.ApkBuilder_UnableBuild_Dex_Not_loaded)); + } + + // make sure dx use the proper output streams. + // first make sure we actually have the streams available. + if (mOutStream == null) { + IProject project = getProject(); + mOutStream = AdtPlugin.getOutPrintStream(project, DX_PREFIX); + mErrStream = AdtPlugin.getErrPrintStream(project, DX_PREFIX); + } + + try { + // get the list of libraries to include with the source code + String[] libraries = getExternalJars(); + + // get the list of referenced projects output to add + String[] projectOutputs = getProjectOutputs(referencedJavaProjects); + + String[] fileNames = new String[1 + projectOutputs.length + libraries.length]; + + // first this project output + fileNames[0] = osBinPath; + + // then other project output + System.arraycopy(projectOutputs, 0, fileNames, 1, projectOutputs.length); + + // then external jars. + System.arraycopy(libraries, 0, fileNames, 1 + projectOutputs.length, libraries.length); + + int res = wrapper.run(osOutFilePath, fileNames, + AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE, + mOutStream, mErrStream); + + if (res != 0) { + // output error message and marker the project. + String message = String.format(Messages.Dalvik_Error_d, + res); + AdtPlugin.printErrorToConsole(getProject(), message); + markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR); + return false; + } + } catch (Throwable ex) { + String message = ex.getMessage(); + if (message == null) { + message = ex.getClass().getCanonicalName(); + } + message = String.format(Messages.Dalvik_Error_s, message); + AdtPlugin.printErrorToConsole(getProject(), message); + markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR); + if ((ex instanceof NoClassDefFoundError) + || (ex instanceof NoSuchMethodError)) { + AdtPlugin.printErrorToConsole(getProject(), Messages.Incompatible_VM_Warning, + Messages.Requires_1_5_Error); + } + return false; + } + + return true; + } + + /** + * Makes the final package. Package the dex files, the temporary resource file into the final + * package file. + * @param intermediateApk The path to the temporary resource file. + * @param dex The path to the dex file. + * @param output The path to the final package file to create. + * @param javaProject + * @param referencedJavaProjects + * @return true if success, false otherwise. + */ + private boolean finalPackage(String intermediateApk, String dex, String output, + final IJavaProject javaProject, IJavaProject[] referencedJavaProjects) { + FileOutputStream fos = null; + try { + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String osKeyPath = store.getString(AdtPlugin.PREFS_CUSTOM_DEBUG_KEYSTORE); + if (osKeyPath == null || new File(osKeyPath).exists() == false) { + osKeyPath = DebugKeyProvider.getDefaultKeyStoreOsPath(); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + Messages.ApkBuilder_Using_Default_Key); + } else { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Using_s_To_Sign, osKeyPath)); + } + + // TODO: get the store type from somewhere else. + DebugKeyProvider provider = new DebugKeyProvider(osKeyPath, null /* storeType */, + new IKeyGenOutput() { + public void err(String message) { + AdtPlugin.printErrorToConsole(javaProject.getProject(), + Messages.ApkBuilder_Signing_Key_Creation_s + message); + } + + public void out(String message) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, + javaProject.getProject(), + Messages.ApkBuilder_Signing_Key_Creation_s + message); + } + }); + PrivateKey key = provider.getDebugKey(); + X509Certificate certificate = (X509Certificate)provider.getCertificate(); + + if (key == null) { + String msg = String.format(Messages.Final_Archive_Error_s, + Messages.ApkBuilder_Unable_To_Gey_Key); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } + + // compare the certificate expiration date + if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { + // TODO, regenerate a new one. + String msg = String.format(Messages.Final_Archive_Error_s, + String.format(Messages.ApkBuilder_Certificate_Expired_on_s, + DateFormat.getInstance().format(certificate.getNotAfter()))); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } + + // create the jar builder. + fos = new FileOutputStream(output); + SignedJarBuilder builder = new SignedJarBuilder(fos, key, certificate); + + // add the intermediate file containing the compiled resources. + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s, intermediateApk)); + FileInputStream fis = new FileInputStream(intermediateApk); + try { + builder.writeZip(fis, null /* filter */); + } finally { + fis.close(); + } + + // Now we add the new file to the zip archive for the classes.dex file. + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s, AndroidConstants.FN_CLASSES_DEX)); + File entryFile = new File(dex); + builder.writeFile(entryFile, AndroidConstants.FN_CLASSES_DEX); + + // Now we write the standard resources from the project and the referenced projects. + writeStandardResources(builder, javaProject, referencedJavaProjects); + + // Now we write the standard resources from the external libraries + for (String libraryOsPath : getExternalJars()) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s, libraryOsPath)); + try { + fis = new FileInputStream(libraryOsPath); + builder.writeZip(fis, mJavaResourcesFilter); + } finally { + fis.close(); + } + } + + // now write the native libraries. + // First look if the lib folder is there. + IResource libFolder = javaProject.getProject().findMember(SdkConstants.FD_NATIVE_LIBS); + if (libFolder != null && libFolder.exists() && + libFolder.getType() == IResource.FOLDER) { + // look inside and put .so in lib/* by keeping the relative folder path. + writeNativeLibraries(libFolder.getFullPath().segmentCount(), builder, libFolder); + } + + // close the jar file and write the manifest and sign it. + builder.close(); + } catch (GeneralSecurityException e1) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage()); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (IOException e1) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage()); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (KeytoolException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // output more info in the console + AdtPlugin.printErrorToConsole(javaProject.getProject(), + msg, + String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()), + Messages.ApkBuilder_Update_or_Execute_manually_s, + e.getCommandLine()); + } catch (AndroidLocationException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // and also output it in the console + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + } catch (CoreException e) { + // mark project and return + String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage()); + AdtPlugin.printErrorToConsole(javaProject.getProject(), msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + // pass. + } + } + } + + return true; + } + + /** + * Writes native libraries into a {@link SignedJarBuilder}. + * <p/>This recursively go through folder and writes .so files. + * The path in the archive is based on the root folder containing the libraries in the project. + * Its segment count is passed to the method to compute the resources path relative to the root + * folder. + * Native libraries in the archive must be in a "lib" folder. Everything in the project native + * lib folder directly goes in this "lib" folder in the archive. + * + * + * @param rootSegmentCount The number of segment of the path of the folder containing the + * libraries. This is used to compute the path in the archive. + * @param jarBuilder the {@link SignedJarBuilder} used to create the archive. + * @param resource the IResource to write. + * @throws CoreException + * @throws IOException + */ + private void writeNativeLibraries(int rootSegmentCount, SignedJarBuilder jarBuilder, + IResource resource) throws CoreException, IOException { + if (resource.getType() == IResource.FILE) { + IPath path = resource.getFullPath(); + + // check the extension. + if (path.getFileExtension().equalsIgnoreCase(AndroidConstants.EXT_NATIVE_LIB)) { + // remove the first segment to build the path inside the archive. + path = path.removeFirstSegments(rootSegmentCount); + + // add it to the archive. + IPath apkPath = new Path(SdkConstants.FD_APK_NATIVE_LIBS); + apkPath = apkPath.append(path); + + // writes the file in the apk. + jarBuilder.writeFile(resource.getLocation().toFile(), apkPath.toString()); + } + } else if (resource.getType() == IResource.FOLDER) { + IResource[] members = ((IFolder)resource).members(); + for (IResource member : members) { + writeNativeLibraries(rootSegmentCount, jarBuilder, member); + } + } + } + + /** + * Writes the standard resources of a project and its referenced projects + * into a {@link SignedJarBuilder}. + * Standard resources are non java/aidl files placed in the java package folders. + * @param jarBuilder the {@link SignedJarBuilder}. + * @param javaProject the javaProject object. + * @param referencedJavaProjects the java projects that this project references. + * @throws IOException + */ + private void writeStandardResources(SignedJarBuilder jarBuilder, IJavaProject javaProject, + IJavaProject[] referencedJavaProjects) throws IOException { + IWorkspace ws = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot wsRoot = ws.getRoot(); + + // create a list of path already put into the archive, in order to detect conflict + ArrayList<String> list = new ArrayList<String>(); + + writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list); + + for (IJavaProject referencedJavaProject : referencedJavaProjects) { + writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list); + } + } + + /** + * Writes the standard resources of a {@link IJavaProject} into a {@link SignedJarBuilder}. + * Standard resources are non java/aidl files placed in the java package folders. + * @param jarBuilder the {@link SignedJarBuilder}. + * @param javaProject the javaProject object. + * @param wsRoot the {@link IWorkspaceRoot}. + * @param list a list of files already added to the archive, to detect conflicts. + * @throws IOException + */ + private void writeStandardProjectResources(SignedJarBuilder jarBuilder, + IJavaProject javaProject, IWorkspaceRoot wsRoot, ArrayList<String> list) + throws IOException { + // get the source pathes + ArrayList<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); + + // loop on them and then recursively go through the content looking for matching files. + for (IPath sourcePath : sourceFolders) { + IResource sourceResource = wsRoot.findMember(sourcePath); + if (sourceResource != null && sourceResource.getType() == IResource.FOLDER) { + writeStandardSourceFolderResources(jarBuilder, sourcePath, (IFolder)sourceResource, + list); + } + } + } + + /** + * Recursively writes the standard resources of a source folder into a {@link SignedJarBuilder}. + * Standard resources are non java/aidl files placed in the java package folders. + * @param jarBuilder the {@link SignedJarBuilder}. + * @param sourceFolder the {@link IPath} of the source folder. + * @param currentFolder The current folder we're recursively processing. + * @param list a list of files already added to the archive, to detect conflicts. + * @throws IOException + */ + private void writeStandardSourceFolderResources(SignedJarBuilder jarBuilder, IPath sourceFolder, + IFolder currentFolder, ArrayList<String> list) throws IOException { + try { + IResource[] members = currentFolder.members(); + + for (IResource member : members) { + int type = member.getType(); + if (type == IResource.FILE && member.exists()) { + if (checkFileForPackaging((IFile)member)) { + // this files must be added to the archive. + IPath fullPath = member.getFullPath(); + + // We need to create its path inside the archive. + // This path is relative to the source folder. + IPath relativePath = fullPath.removeFirstSegments( + sourceFolder.segmentCount()); + String zipPath = relativePath.toString(); + + // lets check it's not already in the list of path added to the archive + if (list.indexOf(zipPath) != -1) { + AdtPlugin.printErrorToConsole(getProject(), + String.format( + Messages.ApkBuilder_s_Conflict_with_file_s, + fullPath, zipPath)); + } else { + // get the File object + File entryFile = member.getLocation().toFile(); + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + String.format(Messages.ApkBuilder_Packaging_s_into_s, fullPath, zipPath)); + + // write it in the zip archive + jarBuilder.writeFile(entryFile, zipPath); + + // and add it to the list of entries + list.add(zipPath); + } + } + } else if (type == IResource.FOLDER) { + if (checkFolderForPackaging((IFolder)member)) { + writeStandardSourceFolderResources(jarBuilder, sourceFolder, + (IFolder)member, list); + } + } + } + } catch (CoreException e) { + // if we can't get the members of the folder, we just don't do anything. + } + } + + /** + * Returns the list of the output folders for the specified {@link IJavaProject} objects. + * @param referencedJavaProjects the java projects. + * @return an array, always. Can be empty. + * @throws CoreException + */ + private String[] getProjectOutputs(IJavaProject[] referencedJavaProjects) throws CoreException { + ArrayList<String> list = new ArrayList<String>(); + + IWorkspace ws = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot wsRoot = ws.getRoot(); + + for (IJavaProject javaProject : referencedJavaProjects) { + // get the output folder + IPath path = null; + try { + path = javaProject.getOutputLocation(); + } catch (JavaModelException e) { + continue; + } + + IResource outputResource = wsRoot.findMember(path); + if (outputResource != null && outputResource.getType() == IResource.FOLDER) { + String outputOsPath = outputResource.getLocation().toOSString(); + + list.add(outputOsPath); + } + } + + return list.toArray(new String[list.size()]); + } + + /** + * Returns an array of {@link IJavaProject} matching the provided {@link IProject} objects. + * @param projects the IProject objects. + * @return an array, always. Can be empty. + * @throws CoreException + */ + private IJavaProject[] getJavaProjects(IProject[] projects) throws CoreException { + ArrayList<IJavaProject> list = new ArrayList<IJavaProject>(); + + for (IProject p : projects) { + if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { + + list.add(JavaCore.create(p)); + } + } + + return list.toArray(new IJavaProject[list.size()]); + } + + /** + * Returns the apk filename for the given project + * @param project The project. + * @param config An optional config name. Can be null. + */ + private static String getFileName(IProject project, String config) { + if (config != null) { + return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$ + } + + return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + } + + /** + * Checks a {@link IFile} to make sure it should be packaged as standard resources. + * @param file the IFile representing the file. + * @return true if the file should be packaged as standard java resources. + */ + static boolean checkFileForPackaging(IFile file) { + String name = file.getName(); + + String ext = file.getFileExtension(); + return JavaResourceFilter.checkFileForPackaging(name, ext); + } + + /** + * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as + * standard Java resource. + * @param folder the {@link IFolder} to check. + */ + static boolean checkFolderForPackaging(IFolder folder) { + String name = folder.getName(); + return JavaResourceFilter.checkFolderForPackaging(name); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java new file mode 100644 index 0000000..5d6793a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.build.BaseBuilder.BaseDeltaVisitor; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; + +import java.util.ArrayList; + +/** + * Delta resource visitor looking for changes that will trigger a new packaging of an Android + * application. + * <p/> + * This looks for the following changes: + * <ul> + * <li>Any change to the AndroidManifest.xml file</li> + * <li>Any change inside the assets/ folder</li> + * <li>Any file change inside the res/ folder</li> + * <li>Any .class file change inside the output folder</li> + * <li>Any change to the classes.dex inside the output folder</li> + * <li>Any change to the packaged resources file inside the output folder</li> + * <li>Any change to a non java/aidl file inside the source folders</li> + * <li>Any change to .so file inside the lib (native library) folder</li> + * </ul> + */ +public class ApkDeltaVisitor extends BaseDeltaVisitor + implements IResourceDeltaVisitor { + + /** + * compile flag. This is set to true if one of the changed/added/removed + * file is a .class file. Upon visiting all the delta resources, if this + * flag is true, then we know we'll have to make the "classes.dex" file. + */ + private boolean mConvertToDex = false; + + /** + * compile flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resources, if + * this flag is true, then we know we'll have to make the intermediate + * apk file. + */ + private boolean mPackageResources = false; + + /** + * Final package flag. This is set to true if one of the changed/added/removed + * file is a non java file (or aidl) in the resource folder. Upon visiting all the + * delta resources, if this flag is true, then we know we'll have to make the final + * package. + */ + private boolean mMakeFinalPackage = false; + + /** List of source folders. */ + private ArrayList<IPath> mSourceFolders; + + private IPath mOutputPath; + + private IPath mAssetPath; + + private IPath mResPath; + + private IPath mLibFolder; + + /** + * Builds the object with a specified output folder. + * @param builder the xml builder using this object to visit the + * resource delta. + * @param sourceFolders the list of source folders for the project, relative to the workspace. + * @param outputfolder the output folder of the project. + */ + public ApkDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders, + IFolder outputfolder) { + super(builder); + mSourceFolders = sourceFolders; + + if (outputfolder != null) { + mOutputPath = outputfolder.getFullPath(); + } + + IResource assetFolder = builder.getProject().findMember(SdkConstants.FD_ASSETS); + if (assetFolder != null) { + mAssetPath = assetFolder.getFullPath(); + } + + IResource resFolder = builder.getProject().findMember(SdkConstants.FD_RESOURCES); + if (resFolder != null) { + mResPath = resFolder.getFullPath(); + } + + IResource libFolder = builder.getProject().findMember(SdkConstants.FD_NATIVE_LIBS); + if (libFolder != null) { + mLibFolder = libFolder.getFullPath(); + } + } + + public boolean getConvertToDex() { + return mConvertToDex; + } + + public boolean getPackageResources() { + return mPackageResources; + } + + public boolean getMakeFinalPackage() { + return mMakeFinalPackage; + } + + /** + * {@inheritDoc} + * @throws CoreException + * + * @see org.eclipse.core.resources.IResourceDeltaVisitor + * #visit(org.eclipse.core.resources.IResourceDelta) + */ + public boolean visit(IResourceDelta delta) throws CoreException { + // if all flags are true, we can stop going through the resource delta. + if (mConvertToDex && mPackageResources && mMakeFinalPackage) { + return false; + } + + // we are only going to look for changes in res/, src/ and in + // AndroidManifest.xml since the delta visitor goes through the main + // folder before its childre we can check when the path segment + // count is 2 (format will be /$Project/folder) and make sure we are + // processing res/, src/ or AndroidManifest.xml + IResource resource = delta.getResource(); + IPath path = resource.getFullPath(); + String[] pathSegments = path.segments(); + int type = resource.getType(); + + // since the delta visitor also visits the root we return true if + // segments.length = 1 + if (pathSegments.length == 1) { + return true; + } + + // check the manifest. + if (pathSegments.length == 2 && + AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(pathSegments[1])) { + // if the manifest changed we have to repackage the + // resources. + mPackageResources = true; + mMakeFinalPackage = true; + + // we don't want to go to the children, not like they are + // any for this resource anyway. + return false; + } + + // check the other folders. + if (mOutputPath != null && mOutputPath.isPrefixOf(path)) { + // a resource changed inside the output folder. + if (type == IResource.FILE) { + // just check this is a .class file. Any modification will + // trigger a change in the classes.dex file + String ext = resource.getFileExtension(); + if (AndroidConstants.EXT_CLASS.equalsIgnoreCase(ext)) { + mConvertToDex = true; + mMakeFinalPackage = true; + + // no need to check the children, as we are in a package + // and there can only be subpackage children containing + // only .class files + return false; + } + + // check for a few files directly in the output folder and force + // rebuild if they have been deleted. + if (delta.getKind() == IResourceDelta.REMOVED) { + IPath parentPath = path.removeLastSegments(1); + if (mOutputPath.equals(parentPath)) { + String resourceName = resource.getName(); + // check if classes.dex was removed + if (resourceName.equalsIgnoreCase(AndroidConstants.FN_CLASSES_DEX)) { + mConvertToDex = true; + mMakeFinalPackage = true; + } else if (resourceName.equalsIgnoreCase( + AndroidConstants.FN_RESOURCES_AP_) || + AndroidConstants.PATTERN_RESOURCES_S_AP_.matcher( + resourceName).matches()) { + // or if the default resources.ap_ or a configured version + // (resources-###.ap_) was removed. + mPackageResources = true; + mMakeFinalPackage = true; + } + } + } + } + + // if this is a folder, we only go visit it if we don't already know + // that we need to convert to dex already. + return mConvertToDex == false; + } else if (mResPath != null && mResPath.isPrefixOf(path)) { + // in the res folder we are looking for any file modification + // (we don't care about folder being added/removed, only content + // is important) + if (type == IResource.FILE) { + mPackageResources = true; + mMakeFinalPackage = true; + return false; + } + + // for folders, return true only if we don't already know we have to + // package the resources. + return mPackageResources == false; + } else if (mAssetPath != null && mAssetPath.isPrefixOf(path)) { + // this is the assets folder that was modified. + // we don't care what content was changed. All we care + // about is that something changed inside. No need to visit + // the children even. + mPackageResources = true; + mMakeFinalPackage = true; + return false; + } else if (mLibFolder != null && mLibFolder.isPrefixOf(path)) { + // inside the native library folder. Test if the changed resource is a .so file. + if (type == IResource.FILE && + path.getFileExtension().equalsIgnoreCase(AndroidConstants.EXT_NATIVE_LIB)) { + mMakeFinalPackage = true; + return false; // return false for file. + } + + // for folders, return true only if we don't already know we have to make the + // final package. + return mMakeFinalPackage == false; + } else { + // we are in a folder that is neither the resource folders, nor the output. + // check against all the source folders, unless we already know we need to do + // the final package. + // This could be a source folder or a folder leading to a source folder. + // However we only check this if we don't already know that we need to build the + // package anyway + if (mMakeFinalPackage == false) { + for (IPath sourcePath : mSourceFolders) { + if (sourcePath.isPrefixOf(path)) { + // In the source folders, we are looking for any kind of + // modification related to file that are not java files. + // Also excluded are aidl files, and package.html files + if (type == IResource.FOLDER) { + // always visit the subfolders, unless the folder is not to be included + return ApkBuilder.checkFolderForPackaging((IFolder)resource); + } else if (type == IResource.FILE) { + if (ApkBuilder.checkFileForPackaging((IFile)resource)) { + mMakeFinalPackage = true; + } + + return false; + } + + } + } + } + } + + // if the folder is not inside one of the folders we are interested in (res, assets, output, + // source folders), it could be a folder leading to them, so we return true. + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java new file mode 100644 index 0000000..e2e9728 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java @@ -0,0 +1,929 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.common.project.XmlErrorHandler; +import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.xml.sax.SAXException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Base builder for XML files. This class allows for basic XML parsing with + * error checking and marking the files for errors/warnings. + */ +abstract class BaseBuilder extends IncrementalProjectBuilder { + + // TODO: rename the pattern to something that makes sense + javadoc comments. + + /** + * Single line aapt warning for skipping files.<br> + * " (skipping hidden file '<file path>'" + */ + private final static Pattern sPattern0Line1 = Pattern.compile( + "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$ + + /** + * First line of dual line aapt error.<br> + * "ERROR at line <line>: <error>"<br> + * " (Occurred while parsing <path>)" + */ + private final static Pattern sPattern1Line1 = Pattern.compile( + "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.<br> + * "ERROR at line <line>: <error>"<br> + * " (Occurred while parsing <path>)"<br> + * @see #sPattern1Line1 + */ + private final static Pattern sPattern1Line2 = Pattern.compile( + "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$ + /** + * First line of dual line aapt error.<br> + * "ERROR: <error>"<br> + * "Defined at file <path> line <line>" + */ + private final static Pattern sPattern2Line1 = Pattern.compile( + "^ERROR:\\s+(.+)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.<br> + * "ERROR: <error>"<br> + * "Defined at file <path> line <line>"<br> + * @see #sPattern2Line1 + */ + private final static Pattern sPattern2Line2 = Pattern.compile( + "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$ + /** + * Single line aapt error<br> + * "<path> line <line>: <error>" + */ + private final static Pattern sPattern3Line1 = Pattern.compile( + "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$ + /** + * First line of dual line aapt error.<br> + * "ERROR parsing XML file <path>"<br> + * "<error> at line <line>" + */ + private final static Pattern sPattern4Line1 = Pattern.compile( + "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$ + /** + * Second line of dual line aapt error.<br> + * "ERROR parsing XML file <path>"<br> + * "<error> at line <line>"<br> + * @see #sPattern4Line1 + */ + private final static Pattern sPattern4Line2 = Pattern.compile( + "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$ + + /** + * Single line aapt warning<br> + * "<path>:<line>: <error>" + */ + private final static Pattern sPattern5Line1 = Pattern.compile( + "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$ + + /** + * Single line aapt error<br> + * "<path>:<line>: <error>" + */ + private final static Pattern sPattern6Line1 = Pattern.compile( + "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$ + + /** + * 4 line aapt error<br> + * "ERROR: 9-path image <path> malformed"<br> + * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br> + * 'ERROR: failure processing <path>) + */ + private final static Pattern sPattern7Line1 = Pattern.compile( + "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$ + + private final static Pattern sPattern8Line1 = Pattern.compile( + "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$ + + /** + * 2 line aapt error<br> + * "ERROR: Invalid configuration: foo"<br> + * " ^^^"<br> + * There's no need to parse the 2nd line. + */ + private final static Pattern sPattern9Line1 = Pattern.compile( + "^Invalid configuration: (.+)$"); //$NON-NLS-1$ + + /** SAX Parser factory. */ + private SAXParserFactory mParserFactory; + + /** + * Base Resource Delta Visitor to handle XML error + */ + protected static class BaseDeltaVisitor implements XmlErrorListener { + + /** The Xml builder used to validate XML correctness. */ + protected BaseBuilder mBuilder; + + /** + * XML error flag. if true, we keep parsing the ResourceDelta but the + * compilation will not happen (we're putting markers) + */ + public boolean mXmlError = false; + + public BaseDeltaVisitor(BaseBuilder builder) { + mBuilder = builder; + } + + /** + * Finds a matching Source folder for the current path. This checkds if the current path + * leads to, or is a source folder. + * @param sourceFolders The list of source folders + * @param pathSegments The segments of the current path + * @return The segments of the source folder, or null if no match was found + */ + protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders, + String[] pathSegments) { + + for (IPath p : sourceFolders) { + // check if we are inside one of those source class path + + // get the segments + String[] srcSegments = p.segments(); + + // compare segments. We want the path of the resource + // we're visiting to be + boolean valid = true; + int segmentCount = pathSegments.length; + + for (int i = 0 ; i < segmentCount; i++) { + String s1 = pathSegments[i]; + String s2 = srcSegments[i]; + + if (s1.equalsIgnoreCase(s2) == false) { + valid = false; + break; + } + } + + if (valid) { + // this folder, or one of this children is a source + // folder! + // we return its segments + return srcSegments; + } + } + + return null; + } + + /** + * Sent when an XML error is detected. + * @see XmlErrorListener + */ + public void errorFound() { + mXmlError = true; + } + } + + public BaseBuilder() { + super(); + mParserFactory = SAXParserFactory.newInstance(); + + // FIXME when the compiled XML support for namespace is in, set this to true. + mParserFactory.setNamespaceAware(false); + } + + /** + * Checks an Xml file for validity. Errors/warnings will be marked on the + * file + * @param resource the resource to check + * @param visitor a valid resource delta visitor + */ + protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) { + + // first make sure this is an xml file + if (resource instanceof IFile) { + IFile file = (IFile)resource; + + // remove previous markers + removeMarkersFromFile(file, AndroidConstants.MARKER_XML); + + // create the error handler + XmlErrorHandler reporter = new XmlErrorHandler(file, visitor); + try { + // parse + getParser().parse(file.getContents(), reporter); + } catch (Exception e1) { + } + } + } + + /** + * Returns the SAXParserFactory, instantiating it first if it's not already + * created. + * @return the SAXParserFactory object + * @throws ParserConfigurationException + * @throws SAXException + */ + protected final SAXParser getParser() throws ParserConfigurationException, + SAXException { + return mParserFactory.newSAXParser(); + } + + /** + * Adds a marker to the current project. + * + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param severity the severity of the marker. + */ + protected final void markProject(String markerId, String message, int severity) { + BaseProjectHelper.addMarker(getProject(), markerId, message, severity); + } + + + /** + * Removes markers from a file. + * @param file The file from which to delete the markers. + * @param markerId The id of the markers to remove. If null, all marker of + * type <code>IMarker.PROBLEM</code> will be removed. + */ + protected final void removeMarkersFromFile(IFile file, String markerId) { + try { + if (file.exists()) { + file.deleteMarkers(markerId, true, IResource.DEPTH_ZERO); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, file.toString()); + AdtPlugin.printErrorToConsole(getProject(), msg); + } + } + + /** + * Removes markers from a container and its children. + * @param folder The container from which to delete the markers. + * @param markerId The id of the markers to remove. If null, all marker of + * type <code>IMarker.PROBLEM</code> will be removed. + */ + protected final void removeMarkersFromContainer(IContainer folder, String markerId) { + try { + if (folder.exists()) { + folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString()); + AdtPlugin.printErrorToConsole(getProject(), msg); + } + } + + /** + * Removes markers from a project and its children. + * @param project The project from which to delete the markers + * @param markerId The id of the markers to remove. If null, all marker of + * type <code>IMarker.PROBLEM</code> will be removed. + */ + protected final static void removeMarkersFromProject(IProject project, + String markerId) { + try { + if (project.exists()) { + project.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + String msg = String.format(Messages.Marker_Delete_Error, markerId, project.getName()); + AdtPlugin.printErrorToConsole(project, msg); + } + } + + /** + * Get the stderr output of a process and return when the process is done. + * @param process The process to get the ouput from + * @param results The array to store the stderr output + * @return the process return code. + * @throws InterruptedException + */ + protected final int grabProcessOutput(final Process process, + final ArrayList<String> results) + throws InterruptedException { + // Due to the limited buffer size on windows for the standard io (stderr, stdout), we + // *need* to read both stdout and stderr all the time. If we don't and a process output + // a large amount, this could deadlock the process. + + // read the lines as they come. if null is returned, it's + // because the process finished + new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + results.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }.start(); + + new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + IProject project = getProject(); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, + project, line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + + }.start(); + + // get the return code from the process + return process.waitFor(); + } + + /** + * Parse the output of aapt and mark the incorrect file with error markers + * + * @param results the output of aapt + * @param project the project containing the file to mark + * @return true if the parsing failed, false if success. + */ + protected final boolean parseAaptOutput(ArrayList<String> results, + IProject project) { + // nothing to parse? just return false; + if (results.size() == 0) { + return false; + } + + // get the root of the project so that we can make IFile from full + // file path + String osRoot = project.getLocation().toOSString(); + + Matcher m; + + for (int i = 0; i < results.size(); i++) { + String p = results.get(i); + + m = sPattern0Line1.matcher(p); + if (m.matches()) { + // we ignore those (as this is an ignore message from aapt) + continue; + } + + m = sPattern1Line1.matcher(p); + if (m.matches()) { + String lineStr = m.group(1); + String msg = m.group(2); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern1Line2); + if (m == null) { + return true; + } + + String location = m.group(1); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + continue; + } + + // this needs to be tested before Pattern2 since they both start with 'ERROR:' + m = sPattern7Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String msg = p; // default msg is the line in case we don't find anything else + + if (++i < results.size()) { + msg = results.get(i).trim(); + if (++i < results.size()) { + msg = msg + " - " + results.get(i).trim(); //$NON-NLS-1$ + + // skip the next line + i++; + } + } + + // display the error + if (checkAndMark(location, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern2Line1.matcher(p); + if (m.matches()) { + // get the msg + String msg = m.group(1); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern2Line2); + if (m == null) { + return true; + } + + String location = m.group(1); + String lineStr = m.group(2); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + continue; + } + + m = sPattern3Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern4Line1.matcher(p); + if (m.matches()) { + // get the filename. + String location = m.group(1); + + // get the matcher for the next line. + m = getNextLineMatcher(results, ++i, sPattern4Line2); + if (m == null) { + return true; + } + + String msg = m.group(1); + String lineStr = m.group(2); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern5Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern6Line1.matcher(p); + if (m.matches()) { + String location = m.group(1); + String lineStr = m.group(2); + String msg = m.group(3); + + // check the values and attempt to mark the file. + if (checkAndMark(location, lineStr, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern8Line1.matcher(p); + if (m.matches()) { + String location = m.group(2); + String msg = m.group(1); + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sPattern9Line1.matcher(p); + if (m.matches()) { + String badConfig = m.group(1); + String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig); + + // skip the next line + i++; + + // check the values and attempt to mark the file. + if (checkAndMark(null /*location*/, null, msg, osRoot, project, + AndroidConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + // invalid line format, flag as error, and bail + return true; + } + + return false; + } + + + + /** + * Saves a String property into the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + protected boolean saveProjectStringProperty(String propertyName, String value) { + IProject project = getProject(); + return ProjectHelper.saveStringProperty(project, propertyName, value); + } + + + /** + * Loads a String property from the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return the property value or null if it was not found. + */ + protected String loadProjectStringProperty(String propertyName) { + IProject project = getProject(); + return ProjectHelper.loadStringProperty(project, propertyName); + } + + /** + * Saves a property into the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + protected boolean saveProjectBooleanProperty(String propertyName, boolean value) { + IProject project = getProject(); + return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value)); + } + + /** + * Loads a boolean property from the persistent storage of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param defaultValue The default value to return if the property was not found. + * @return the property value or the default value if the property was not found. + */ + protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) { + IProject project = getProject(); + return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue); + } + + /** + * Saves the path of a resource into the persistent storate of the project. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param resource the resource which path is saved. + * @return true if the save succeeded + */ + protected boolean saveProjectResourceProperty(String propertyName, IResource resource) { + return ProjectHelper.saveResourceProperty(getProject(), propertyName, resource); + } + + /** + * Loads the path of a resource from the persistent storage of the project, and returns the + * corresponding IResource object. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return The corresponding IResource object (or children interface) or null + */ + protected IResource loadProjectResourceProperty(String propertyName) { + IProject project = getProject(); + return ProjectHelper.loadResourceProperty(project, propertyName); + } + + /** + * Check if the parameters gotten from the error output are valid, and mark + * the file with an AAPT marker. + * @param location the full OS path of the error file. If null, the project is marked + * @param lineStr + * @param message + * @param root The root directory of the project, in OS specific format. + * @param project + * @param markerId The marker id to put. + * @param severity The severity of the marker to put (IMarker.SEVERITY_*) + * @return true if the parameters were valid and the file was marked successfully. + * + * @see IMarker + */ + private final boolean checkAndMark(String location, String lineStr, + String message, String root, IProject project, String markerId, int severity) { + // check this is in fact a file + if (location != null) { + File f = new File(location); + if (f.exists() == false) { + return false; + } + } + + // get the line number + int line = -1; // default value for error with no line. + + if (lineStr != null) { + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return false; + } + } + + // add the marker + IResource f2 = project; + if (location != null) { + f2 = getResourceFromFullPath(location, root, project); + if (f2 == null) { + return false; + } + } + + // check if there's a similar marker already, since aapt is launched twice + boolean markerAlreadyExists = false; + try { + IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO); + + for (IMarker marker : markers) { + int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (tmpLine != line) { + break; + } + + int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); + if (tmpSeverity != severity) { + break; + } + + String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); + if (tmpMsg == null || tmpMsg.equals(message) == false) { + break; + } + + // if we're here, all the marker attributes are equals, we found it + // and exit + markerAlreadyExists = true; + break; + } + + } catch (CoreException e) { + // if we couldn't get the markers, then we just mark the file again + // (since markerAlreadyExists is initialized to false, we do nothing) + } + + if (markerAlreadyExists == false) { + if (line != -1) { + BaseProjectHelper.addMarker(f2, markerId, message, line, + severity); + } else { + BaseProjectHelper.addMarker(f2, markerId, message, severity); + } + } + + return true; + } + + /** + * Returns a matching matcher for the next line + * @param lines The array of lines + * @param nextIndex The index of the next line + * @param pattern The pattern to match + * @return null if error or no match, the matcher otherwise. + */ + private final Matcher getNextLineMatcher(ArrayList<String> lines, + int nextIndex, Pattern pattern) { + // unless we can't, because we reached the last line + if (nextIndex == lines.size()) { + // we expected a 2nd line, so we flag as error + // and we bail + return null; + } + + Matcher m = pattern.matcher(lines.get(nextIndex)); + if (m.matches()) { + return m; + } + + return null; + } + + private IResource getResourceFromFullPath(String filename, String root, + IProject project) { + if (filename.startsWith(root)) { + String file = filename.substring(root.length()); + + // get the resource + IResource r = project.findMember(file); + + // if the resource is valid, we add the marker + if (r.exists()) { + return r; + } + } + + return null; + } + + /** + * Returns an array of external jar files used by the project. + * @return an array of OS-specific absolute file paths + */ + protected final String[] getExternalJars() { + // get the current project + IProject project = getProject(); + + // get a java project from it + IJavaProject javaProject = JavaCore.create(project); + + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + + ArrayList<String> oslibraryList = new ArrayList<String>(); + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY || + e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + // if this is a classpath variable reference, we resolve it. + if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + e = JavaCore.getResolvedClasspathEntry(e); + } + + // get the IPath + IPath path = e.getPath(); + + // check the name ends with .jar + if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + boolean local = false; + IResource resource = wsRoot.findMember(path); + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + local = true; + oslibraryList.add(resource.getLocation().toOSString()); + } + + if (local == false) { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.exists()) { + oslibraryList.add(osFullPath); + } else { + String message = String.format( Messages.Couldnt_Locate_s_Error, + path); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, + project, message); + + // Also put a warning marker on the project + markProject(AdtConstants.MARKER_ADT, message, + IMarker.SEVERITY_WARNING); + } + } + } + } + } + } + + return oslibraryList.toArray(new String[oslibraryList.size()]); + } + + /** + * Aborts the build if the SDK/project setups are broken. This does not + * display any errors. + * + * @param project The {@link IJavaProject} being compiled. + * @throws CoreException + */ + protected final void abortOnBadSetup(IProject project) throws CoreException { + // check if we have finished loading the SDK. + if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADED) { + // we exit silently + stopBuild("SDK is not loaded yet"); + } + + // check the compiler compliance level. + if (ProjectHelper.checkCompilerCompliance(project) != + ProjectHelper.COMPILER_COMPLIANCE_OK) { + // we exit silently + stopBuild(Messages.Compiler_Compliance_Error); + } + + // Check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder == null || osSdkFolder.length() == 0) { + stopBuild(Messages.No_SDK_Setup_Error); + } + + IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + if (projectTarget == null) { + // no target. error has been output by the container initializer: + // exit silently. + stopBuild("Project has no target"); + } + } + + /** + * Throws an exception to cancel the build. + * + * @param error the error message + * @param args the printf-style arguments to the error message. + * @throws CoreException + */ + protected final void stopBuild(String error, Object... args) throws CoreException { + throw new CoreException(new Status(IStatus.CANCEL, AdtPlugin.PLUGIN_ID, + String.format(error, args))); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java new file mode 100644 index 0000000..65ad4f5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Wrapper to access dex.jar through reflection. + * <p/>Since there is no proper api to call the method in the dex library, this wrapper is going + * to access it through reflection. + */ +public final class DexWrapper { + + private final static String DEX_MAIN = "com.android.dx.command.dexer.Main"; //$NON-NLS-1$ + private final static String DEX_CONSOLE = "com.android.dx.command.DxConsole"; //$NON-NLS-1$ + private final static String DEX_ARGS = "com.android.dx.command.dexer.Main$Arguments"; //$NON-NLS-1$ + + private final static String MAIN_RUN = "run"; //$NON-NLS-1$ + + private Method mRunMethod; + + private Constructor<?> mArgConstructor; + private Field mArgOutName; + private Field mArgVerbose; + private Field mArgJarOutput; + private Field mArgFileNames; + + private Field mConsoleOut; + private Field mConsoleErr; + + /** + * Loads the dex library from a file path. + * + * The loaded library can be used via + * {@link DexWrapper#run(String, String[], boolean, PrintStream, PrintStream)}. + * + * @param osFilepath the location of the dex.jar file. + * @return an IStatus indicating the result of the load. + */ + public synchronized IStatus loadDex(String osFilepath) { + try { + File f = new File(osFilepath); + if (f.isFile() == false) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format( + Messages.DexWrapper_s_does_not_exists, osFilepath)); + } + URL url = f.toURL(); + + URLClassLoader loader = new URLClassLoader(new URL[] { url }, + DexWrapper.class.getClassLoader()); + + // get the classes. + Class<?> mainClass = loader.loadClass(DEX_MAIN); + Class<?> consoleClass = loader.loadClass(DEX_CONSOLE); + Class<?> argClass = loader.loadClass(DEX_ARGS); + + try { + // now get the fields/methods we need + mRunMethod = mainClass.getMethod(MAIN_RUN, argClass); + + mArgConstructor = argClass.getConstructor(); + mArgOutName = argClass.getField("outName"); //$NON-NLS-1$ + mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$ + mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$ + mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$ + + mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$ + mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$ + + } catch (SecurityException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_API, e); + } catch (NoSuchMethodException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Method, e); + } catch (NoSuchFieldException e) { + return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Field, e); + } + + return Status.OK_STATUS; + } catch (MalformedURLException e) { + // really this should not happen. + return createErrorStatus( + String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); + } catch (ClassNotFoundException e) { + return createErrorStatus( + String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e); + } + } + + /** + * Runs the dex command. + * @param osOutFilePath the OS path to the outputfile (classes.dex + * @param osFilenames list of input source files (.class and .jar files) + * @param verbose verbose mode. + * @param outStream the stdout console + * @param errStream the stderr console + * @return the integer return code of com.android.dx.command.dexer.Main.run() + * @throws CoreException + */ + public synchronized int run(String osOutFilePath, String[] osFilenames, + boolean verbose, PrintStream outStream, PrintStream errStream) throws CoreException { + + try { + // set the stream + mConsoleErr.set(null /* obj: static field */, errStream); + mConsoleOut.set(null /* obj: static field */, outStream); + + // create the Arguments object. + Object args = mArgConstructor.newInstance(); + mArgOutName.set(args, osOutFilePath); + mArgFileNames.set(args, osFilenames); + mArgJarOutput.set(args, false); + mArgVerbose.set(args, verbose); + + // call the run method + Object res = mRunMethod.invoke(null /* obj: static method */, args); + + if (res instanceof Integer) { + return ((Integer)res).intValue(); + } + + return -1; + } catch (IllegalAccessException e) { + throw new CoreException(createErrorStatus( + String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, e.getMessage()), e)); + } catch (InstantiationException e) { + throw new CoreException(createErrorStatus( + String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, e.getMessage()), e)); + } catch (InvocationTargetException e) { + throw new CoreException(createErrorStatus( + String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, e.getMessage()), e)); + } + } + + private static IStatus createErrorStatus(String message, Exception e) { + AdtPlugin.log(e, message); + AdtPlugin.printErrorToConsole(Messages.DexWrapper_Dex_Loader, message); + + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, e); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java new file mode 100644 index 0000000..0100049 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java @@ -0,0 +1,137 @@ + +package com.android.ide.eclipse.adt.build; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.build.build_messages"; //$NON-NLS-1$ + + public static String AAPT_Error; + + public static String AAPT_Exec_Error; + + public static String Added_s_s_Needs_Updating; + + public static String AIDL_Exec_Error; + + public static String AIDL_Java_Conflict; + + public static String ApkBuilder_Certificate_Expired_on_s; + + public static String ApkBuilder_JAVA_HOME_is_s; + + public static String ApkBuilder_Packaging_s; + + public static String ApkBuilder_Packaging_s_into_s; + + public static String ApkBuilder_s_Conflict_with_file_s; + + public static String ApkBuilder_Signing_Key_Creation_s; + + public static String ApkBuilder_Unable_To_Gey_Key; + + public static String ApkBuilder_UnableBuild_Dex_Not_loaded; + + public static String ApkBuilder_Update_or_Execute_manually_s; + + public static String ApkBuilder_Using_Default_Key; + + public static String ApkBuilder_Using_s_To_Sign; + + public static String Checking_Package_Change; + + public static String Compiler_Compliance_Error; + + public static String Couldnt_Locate_s_Error; + + public static String Dalvik_Error_d; + + public static String Dalvik_Error_s; + + public static String Delete_Obsolete_Error; + + public static String DexWrapper_Dex_Loader; + + public static String DexWrapper_Failed_to_load_s; + + public static String DexWrapper_s_does_not_exists; + + public static String DexWrapper_SecuryEx_Unable_To_Find_API; + + public static String DexWrapper_SecuryEx_Unable_To_Find_Field; + + public static String DexWrapper_SecuryEx_Unable_To_Find_Method; + + public static String DexWrapper_Unable_To_Execute_Dex_s; + + public static String DX_Jar_Error; + + public static String Failed_To_Get_Output; + + public static String Final_Archive_Error_s; + + public static String Incompatible_VM_Warning; + + public static String Marker_Delete_Error; + + public static String No_SDK_Setup_Error; + + public static String Nothing_To_Compile; + + public static String Output_Missing; + + public static String Package_s_Doesnt_Exist_Error; + + public static String Preparing_Generated_Files; + + public static String Project_Has_Errors; + + public static String Refreshing_Res; + + public static String Removing_Generated_Classes; + + public static String Requires_1_5_Error; + + public static String Requires_Class_Compatibility_5; + + public static String Requires_Compiler_Compliance_5; + + public static String Requires_Source_Compatibility_5; + + public static String s_Contains_Xml_Error; + + public static String s_Doesnt_Declare_Package_Error; + + public static String s_File_Missing; + + public static String s_Missing_Repackaging; + + public static String s_Modified_Manually_Recreating_s; + + public static String s_Modified_Recreating_s; + + public static String s_Removed_Recreating_s; + + public static String s_Removed_s_Needs_Updating; + + public static String Start_Full_Apk_Build; + + public static String Start_Full_Pre_Compiler; + + public static String Start_Inc_Apk_Build; + + public static String Start_Inc_Pre_Compiler; + + public static String Unparsed_AAPT_Errors; + + public static String Unparsed_AIDL_Errors; + + public static String Xml_Error; + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java new file mode 100644 index 0000000..a0e446c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java @@ -0,0 +1,1150 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.FixLaunchConfig; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.common.project.XmlErrorHandler.BasicXmlErrorListener; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourceAttributes; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Pre Java Compiler. + * This incremental builder performs 2 tasks: + * <ul> + * <li>compiles the resources located in the res/ folder, along with the + * AndroidManifest.xml file into the R.java class.</li> + * <li>compiles any .aidl files into a corresponding java file.</li> + * </ul> + * + */ +public class PreCompilerBuilder extends BaseBuilder { + + public static final String ID = "com.android.ide.eclipse.adt.PreCompilerBuilder"; //$NON-NLS-1$ + + private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$ + + private static final String PROPERTY_SOURCE_FOLDER = + "manifestPackageSourceFolder"; //$NON-NLS-1$ + + private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$ + + static final String PROPERTY_ANDROID_GENERATED = "androidGenerated"; //$NON-NLS-1$ + static final String PROPERTY_ANDROID_CONFLICT = "androidConflict"; //$NON-NLS-1$ + + /** + * Single line aidl error<br> + * "<path>:<line>: <error>" + */ + private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):\\s(.+)$"); //$NON-NLS-1$ + + /** + * Compile flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resources, if + * this flag is true, then we know we'll have to compile the resources + * into R.java + */ + private boolean mCompileResources = false; + + /** List of .aidl files found that are modified or new. */ + private final ArrayList<IFile> mAidlToCompile = new ArrayList<IFile>(); + + /** List of .aidl files that have been removed. */ + private final ArrayList<IFile> mAidlToRemove = new ArrayList<IFile>(); + + /** cache of the java package defined in the manifest */ + private String mManifestPackage; + + /** Source folder containing the java package defined in the manifest. */ + private IFolder mManifestPackageSourceFolder; + + /** + * Progress monitor waiting the end of the process to set a persistent value + * in a file. This is typically used in conjunction with <code>IResource.refresh()</code>, + * since this call is asysnchronous, and we need to wait for it to finish for the file + * to be known by eclipse, before we can call <code>resource.setPersistentProperty</code> on + * a new file. + */ + private static class RefreshProgressMonitor implements IProgressMonitor { + private boolean mCancelled = false; + private IFile mNewFile; + private IFile mSource; + private boolean mDoneExecuted = false; + public RefreshProgressMonitor(IFile newFile, IFile source) { + mNewFile = newFile; + mSource = source; + } + + public void beginTask(String name, int totalWork) { + } + + public void done() { + if (mDoneExecuted == false) { + mDoneExecuted = true; + if (mNewFile.exists()) { + ProjectHelper.saveResourceProperty(mNewFile, PROPERTY_ANDROID_GENERATED, + mSource); + try { + mNewFile.setDerived(true); + } catch (CoreException e) { + // This really shouldn't happen since we check that the resource exist. + // Worst case scenario, the resource isn't marked as derived. + } + } + } + } + + public void internalWorked(double work) { + } + + public boolean isCanceled() { + return mCancelled; + } + + public void setCanceled(boolean value) { + mCancelled = value; + } + + public void setTaskName(String name) { + } + + public void subTask(String name) { + } + + public void worked(int work) { + } + } + + /** + * Progress Monitor setting up to two files as derived once their parent is refreshed. + * This is used as ProgressMonitor to refresh the R.java/Manifest.java parent (to display + * the newly created files in the package explorer). + */ + private static class DerivedProgressMonitor implements IProgressMonitor { + private boolean mCancelled = false; + private IFile mFile1; + private IFile mFile2; + private boolean mDoneExecuted = false; + public DerivedProgressMonitor(IFile file1, IFile file2) { + mFile1 = file1; + mFile2 = file2; + } + + public void beginTask(String name, int totalWork) { + } + + public void done() { + if (mDoneExecuted == false) { + if (mFile1 != null && mFile1.exists()) { + mDoneExecuted = true; + try { + mFile1.setDerived(true); + } catch (CoreException e) { + // This really shouldn't happen since we check that the resource edit. + // Worst case scenario, the resource isn't marked as derived. + } + } + if (mFile2 != null && mFile2.exists()) { + try { + mFile2.setDerived(true); + } catch (CoreException e) { + // This really shouldn't happen since we check that the resource edit. + // Worst case scenario, the resource isn't marked as derived. + } + } + } + } + + public void internalWorked(double work) { + } + + public boolean isCanceled() { + return mCancelled; + } + + public void setCanceled(boolean value) { + mCancelled = value; + } + + public void setTaskName(String name) { + } + + public void subTask(String name) { + } + + public void worked(int work) { + } + } + + public PreCompilerBuilder() { + super(); + } + + // build() returns a list of project from which this project depends for future compilation. + @SuppressWarnings("unchecked") //$NON-NLS-1$ + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) + throws CoreException { + // First thing we do is go through the resource delta to not + // lose it if we have to abort the build for any reason. + + // get the project objects + IProject project = getProject(); + + // Top level check to make sure the build can move forward. + abortOnBadSetup(project); + + IJavaProject javaProject = JavaCore.create(project); + IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + + // now we need to get the classpath list + ArrayList<IPath> sourceList = BaseProjectHelper.getSourceClasspaths(javaProject); + + PreCompilerDeltaVisitor dv = null; + String javaPackage = null; + + if (kind == FULL_BUILD) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Start_Full_Pre_Compiler); + mCompileResources = true; + buildAidlCompilationList(project, sourceList); + } else { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Start_Inc_Pre_Compiler); + + // Go through the resources and see if something changed. + // Even if the mCompileResources flag is true from a previously aborted + // build, we need to go through the Resource delta to get a possible + // list of aidl files to compile/remove. + IResourceDelta delta = getDelta(project); + if (delta == null) { + mCompileResources = true; + buildAidlCompilationList(project, sourceList); + } else { + dv = new PreCompilerDeltaVisitor(this, sourceList); + delta.accept(dv); + + // record the state + mCompileResources |= dv.getCompileResources(); + + // handle aidl modification + if (dv.getFullAidlRecompilation()) { + buildAidlCompilationList(project, sourceList); + } else { + mergeAidlFileModifications(dv.getAidlToCompile(), + dv.getAidlToRemove()); + } + + // get the java package from the visitor + javaPackage = dv.getManifestPackage(); + } + } + + // store the build status in the persistent storage + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mCompileResources); + // TODO also needs to store the list of aidl to compile/remove + + // if there was some XML errors, we just return w/o doing + // anything since we've put some markers in the files anyway. + if (dv != null && dv.mXmlError) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Xml_Error); + + // This interrupts the build. The next builders will not run. + stopBuild(Messages.Xml_Error); + } + + + // get the manifest file + IFile manifest = AndroidManifestHelper.getManifest(project); + + if (manifest == null) { + String msg = String.format(Messages.s_File_Missing, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // lets check the XML of the manifest first, if that hasn't been done by the + // resource delta visitor yet. + if (dv == null || dv.getCheckedManifestXml() == false) { + BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); + AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(manifest, + errorListener); + + if (errorListener.mHasXmlError == true) { + // there was an error in the manifest, its file has been marked, + // by the XmlErrorHandler. + // We return; + String msg = String.format(Messages.s_Contains_Xml_Error, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // get the java package from the parser + javaPackage = parser.getPackage(); + } + + if (javaPackage == null || javaPackage.length() == 0) { + // looks like the AndroidManifest file isn't valid. + String msg = String.format(Messages.s_Doesnt_Declare_Package_Error, + AndroidConstants.FN_ANDROID_MANIFEST); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + msg); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // at this point we have the java package. We need to make sure it's not a different package + // than the previous one that were built. + if (javaPackage.equals(mManifestPackage) == false) { + // The manifest package has changed, the user may want to update + // the launch configuration + if (mManifestPackage != null) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Checking_Package_Change); + + FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, javaPackage); + flc.start(); + } + + // now we delete the generated classes from their previous location + deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, + mManifestPackageSourceFolder, mManifestPackage); + deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, + mManifestPackageSourceFolder, mManifestPackage); + + // record the new manifest package, and save it. + mManifestPackage = javaPackage; + saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage); + } + + if (mCompileResources) { + // we need to figure out where to store the R class. + // get the parent folder for R.java and update mManifestPackageSourceFolder + IFolder packageFolder = getManifestPackageFolder(project, sourceList); + + // at this point, either we have found the package or not. + // if we haven't well it's time to tell the user and abort + if (mManifestPackageSourceFolder == null) { + // mark the manifest file + String message = String.format(Messages.Package_s_Doesnt_Exist_Error, + mManifestPackage); + BaseProjectHelper.addMarker(manifest, AndroidConstants.MARKER_AAPT_COMPILE, message, + IMarker.SEVERITY_ERROR); + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, message); + + // abort + // This interrupts the build. The next builders will not run. + stopBuild(message); + } + + + // found the folder in which to write the stuff + + // get the resource folder + IFolder resFolder = project.getFolder(AndroidConstants.WS_RESOURCES); + + // get the file system path + IPath outputLocation = mManifestPackageSourceFolder.getLocation(); + IPath resLocation = resFolder.getLocation(); + IPath manifestLocation = manifest.getLocation(); + + // those locations have to exist for us to do something! + if (outputLocation != null && resLocation != null + && manifestLocation != null) { + String osOutputPath = outputLocation.toOSString(); + String osResPath = resLocation.toOSString(); + String osManifestPath = manifestLocation.toOSString(); + + // remove the aapt markers + removeMarkersFromFile(manifest, AndroidConstants.MARKER_AAPT_COMPILE); + removeMarkersFromContainer(resFolder, AndroidConstants.MARKER_AAPT_COMPILE); + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Preparing_Generated_Files); + + // since the R.java file may be already existing in read-only + // mode we need to make it readable so that aapt can overwrite + // it + IFile rJavaFile = packageFolder.getFile(AndroidConstants.FN_RESOURCE_CLASS); + prepareFileForExternalModification(rJavaFile); + + // do the same for the Manifest.java class + IFile manifestJavaFile = packageFolder.getFile(AndroidConstants.FN_MANIFEST_CLASS); + prepareFileForExternalModification(manifestJavaFile); + + // we actually need to delete the manifest.java as it may become empty and in this + // case aapt doesn't generate an empty one, but instead doesn't touch it. + manifestJavaFile.delete(true, null); + + // launch aapt: create the command line + ArrayList<String> array = new ArrayList<String>(); + array.add(projectTarget.getPath(IAndroidTarget.AAPT)); + array.add("package"); //$NON-NLS-1$ + array.add("-m"); //$NON-NLS-1$ + if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { + array.add("-v"); //$NON-NLS-1$ + } + array.add("-J"); //$NON-NLS-1$ + array.add(osOutputPath); + array.add("-M"); //$NON-NLS-1$ + array.add(osManifestPath); + array.add("-S"); //$NON-NLS-1$ + array.add(osResPath); + array.add("-I"); //$NON-NLS-1$ + array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR)); + + if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) { + StringBuilder sb = new StringBuilder(); + for (String c : array) { + sb.append(c); + sb.append(' '); + } + String cmd_line = sb.toString(); + AdtPlugin.printToConsole(project, cmd_line); + } + + // launch + int execError = 1; + try { + // launch the command line process + Process process = Runtime.getRuntime().exec( + array.toArray(new String[array.size()])); + + // list to store each line of stderr + ArrayList<String> results = new ArrayList<String>(); + + // get the output and return code from the process + execError = grabProcessOutput(process, results); + + // attempt to parse the error output + boolean parsingError = parseAaptOutput(results, project); + + // if we couldn't parse the output we display it in the console. + if (parsingError) { + if (execError != 0) { + AdtPlugin.printErrorToConsole(project, results.toArray()); + } else { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_NORMAL, + project, results.toArray()); + } + } + + if (execError != 0) { + // if the exec failed, and we couldn't parse the error output (and therefore + // not all files that should have been marked, were marked), we put a + // generic marker on the project and abort. + if (parsingError) { + markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors, + IMarker.SEVERITY_ERROR); + } + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.AAPT_Error); + + // abort if exec failed. + // This interrupts the build. The next builders will not run. + stopBuild(Messages.AAPT_Error); + } + } catch (IOException e1) { + // something happen while executing the process, + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } catch (InterruptedException e) { + // we got interrupted waiting for the process to end... + // mark the project and exit + String msg = String.format(Messages.AAPT_Exec_Error, array.get(0)); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(msg); + } + + // if the return code was OK, we refresh the folder that + // contains R.java to force a java recompile. + if (execError == 0) { + // now set the R.java/Manifest.java file as read only. + finishJavaFilesAfterExternalModification(rJavaFile, manifestJavaFile); + + // build has been done. reset the state of the builder + mCompileResources = false; + + // and store it + saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mCompileResources); + } + } + } else { + // nothing to do + } + + // now handle the aidl stuff. + // look for a preprocessed aidl file + IResource projectAidl = project.findMember("project.aidl"); //$NON-NLS-1$ + String folderAidlPath = null; + if (projectAidl != null && projectAidl.exists()) { + folderAidlPath = projectAidl.getLocation().toOSString(); + } + boolean aidlStatus = handleAidl(projectTarget, sourceList, folderAidlPath, monitor); + + if (aidlStatus == false && mCompileResources == false) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Nothing_To_Compile); + } + + return null; + } + + @Override + protected void clean(IProgressMonitor monitor) throws CoreException { + super.clean(monitor); + + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(), + Messages.Removing_Generated_Classes); + + // check if we have the R.java info already. + if (mManifestPackageSourceFolder != null && mManifestPackage != null) { + deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS, + mManifestPackageSourceFolder, mManifestPackage); + deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS, + mManifestPackageSourceFolder, mManifestPackage); + } + + // FIXME: delete all java generated from aidl. + } + + @Override + protected void startupOnInitialize() { + super.startupOnInitialize(); + + // load the previous IFolder and java package. + mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE); + IResource resource = loadProjectResourceProperty(PROPERTY_SOURCE_FOLDER); + if (resource instanceof IFolder) { + mManifestPackageSourceFolder = (IFolder)resource; + } + + // Load the current compile flag. We ask for true if not found to force a + // recompile. + mCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true); + } + + /** + * Delete the a generated java class associated with the specified java package. + * @param filename Name of the generated file to remove. + * @param sourceFolder The source Folder containing the old java package. + * @param javaPackage the old java package + */ + private void deleteObsoleteGeneratedClass(String filename, IFolder sourceFolder, + String javaPackage) { + if (sourceFolder == null || javaPackage == null) { + return; + } + + // convert the java package into path + String[] segments = javaPackage.split(AndroidConstants.RE_DOT); + + StringBuilder path = new StringBuilder(); + for (String s : segments) { + path.append(AndroidConstants.WS_SEP_CHAR); + path.append(s); + } + + // appends the name of the generated file + path.append(AndroidConstants.WS_SEP_CHAR); + path.append(filename); + + Path iPath = new Path(path.toString()); + + // Find a matching resource object. + IResource javaFile = sourceFolder.findMember(iPath); + if (javaFile != null && javaFile.exists() && javaFile.getType() == IResource.FILE) { + try { + // remove the read-only tag + prepareFileForExternalModification((IFile)javaFile); + + // delete + javaFile.delete(true, null); + + // refresh parent + javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, new NullProgressMonitor()); + + } catch (CoreException e) { + // failed to delete it, the user will have to delete it manually. + String message = String.format(Messages.Delete_Obsolete_Error, path); + IProject project = getProject(); + AdtPlugin.printErrorToConsole(project, message); + AdtPlugin.printErrorToConsole(project, e.getMessage()); + } + } + } + + /** + * Looks for the folder containing the package defined in the manifest. It looks in the + * list of source folders for the one containing folders matching the package defined in the + * manifest (from the field <code>mManifestPackage</code>). It returns the final folder, which + * will contain the R class, and update the field <code>mManifestPackageSourceFolder</code> + * to be the source folder containing the full package. + * @param project The project. + * @param sourceList The list of source folders for the project. + * @return the package that will contain the R class or null if the folder was not found. + * @throws CoreException + */ + private IFolder getManifestPackageFolder(IProject project, ArrayList<IPath> sourceList) + throws CoreException { + // split the package in segments + String[] packageSegments = mManifestPackage.split(AndroidConstants.RE_DOT); + + // we look for 2 folders. + // 1. The source folder that contains the full java package. + // we will store the folder in the field mJavaSourceFolder, for reuse during + IFolder manifestPackageSourceFolder = null; + // subsequent builds. This is the folder we will give to aapt. + // 2. The folder actually containing the R.java files. We need this one to do a refresh + IFolder packageFolder = null; + + for (IPath iPath : sourceList) { + int packageSegmentIndex = 0; + + // the path is relative to the workspace. We ignore the first segment, + // when getting the resource from the IProject object. + IResource classpathEntry = project.getFolder(iPath.removeFirstSegments(1)); + + if (classpathEntry instanceof IFolder) { + IFolder classpathFolder = (IFolder)classpathEntry; + IFolder folder = classpathFolder; + + boolean failed = false; + while (failed == false + && packageSegmentIndex < packageSegments.length) { + + // loop on that folder content looking for folders + // that match the package + // defined in AndroidManifest.xml + + // get the folder content + IResource[] content = folder.members(); + + // this is the segment we look for + String segment = packageSegments[packageSegmentIndex]; + + // did we find it at this level + boolean found = false; + + for (IResource r : content) { + // look for the java package segment + if (r instanceof IFolder) { + if (r.getName().equals(segment)) { + // we need to skip to the next one + folder = (IFolder)r; + packageSegmentIndex++; + found = true; + break; + } + } + } + + // if we didn't find it at this level we just fail. + if (found == false) { + failed = true; + } + } + + // if we didn't fail then we found it. no point in + // looping through the rest + // or the classpathEntry + if (failed == false) { + // save the target folder reference + manifestPackageSourceFolder = classpathFolder; + packageFolder = folder; + break; + } + } + } + + // save the location of the folder into the persistent storage + if (manifestPackageSourceFolder != mManifestPackageSourceFolder) { + mManifestPackageSourceFolder = manifestPackageSourceFolder; + saveProjectResourceProperty(PROPERTY_SOURCE_FOLDER, mManifestPackageSourceFolder); + } + return packageFolder; + } + + /** + * Compiles aidl files into java. This will also removes old java files + * created from aidl files that are now gone. + * @param projectTarget Target of the project + * @param sourceFolders the list of source folders, relative to the workspace. + * @param folderAidlPath + * @param monitor the projess monitor + * @returns true if it did something + * @throws CoreException + */ + private boolean handleAidl(IAndroidTarget projectTarget, ArrayList<IPath> sourceFolders, + String folderAidlPath, IProgressMonitor monitor) throws CoreException { + if (mAidlToCompile.size() == 0 && mAidlToRemove.size() == 0) { + return false; + } + + + // create the command line + String[] command = new String[4 + sourceFolders.size() + (folderAidlPath != null ? 1 : 0)]; + int index = 0; + int aidlIndex; + command[index++] = projectTarget.getPath(IAndroidTarget.AIDL); + command[aidlIndex = index++] = "-p"; //$NON-NLS-1$ + if (folderAidlPath != null) { + command[index++] = "-p" + folderAidlPath; //$NON-NLS-1$ + } + + // since the path are relative to the workspace and not the project itself, we need + // the workspace root. + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + for (IPath p : sourceFolders) { + IFolder f = wsRoot.getFolder(p); + command[index++] = "-I" + f.getLocation().toOSString(); //$NON-NLS-1$ + } + + // list of files that have failed compilation. + ArrayList<IFile> stillNeedCompilation = new ArrayList<IFile>(); + + // if an aidl file is being removed before we managed to compile it, it'll be in + // both list. We *need* to remove it from the compile list or it'll never go away. + for (IFile aidlFile : mAidlToRemove) { + int pos = mAidlToCompile.indexOf(aidlFile); + if (pos != -1) { + mAidlToCompile.remove(pos); + } + } + + // loop until we've compile them all + for (IFile aidlFile : mAidlToCompile) { + // Remove the AIDL error markers from the aidl file + removeMarkersFromFile(aidlFile, AndroidConstants.MARKER_AIDL); + + // get the path + IPath iPath = aidlFile.getLocation(); + String osPath = iPath.toOSString(); + + // get the parent container + IContainer parentContainer = aidlFile.getParent(); + + // replace the extension in both the full path and the + // last segment + String osJavaPath = osPath.replaceAll(AndroidConstants.RE_AIDL_EXT, + AndroidConstants.DOT_JAVA); + String javaName = aidlFile.getName().replaceAll(AndroidConstants.RE_AIDL_EXT, + AndroidConstants.DOT_JAVA); + + // check if we can compile it, or if there is a conflict with a java file + boolean conflict = ProjectHelper.loadBooleanProperty(aidlFile, + PROPERTY_ANDROID_CONFLICT, false); + if (conflict) { + String msg = String.format(Messages.AIDL_Java_Conflict, javaName, + aidlFile.getName()); + + // put a marker + BaseProjectHelper.addMarker(aidlFile, AndroidConstants.MARKER_AIDL, msg, + IMarker.SEVERITY_ERROR); + + // output an error + AdtPlugin.printErrorToConsole(getProject(), msg); + + stillNeedCompilation.add(aidlFile); + + // move on to next file + continue; + } + + // get the resource for the java file. + Path javaIPath = new Path(javaName); + IFile javaFile = parentContainer.getFile(javaIPath); + + // if the file was read-only, this will make it readable. + prepareFileForExternalModification(javaFile); + + // finish to set the command line. + command[aidlIndex] = "-p" + Sdk.getCurrent().getTarget(aidlFile.getProject()).getPath( + IAndroidTarget.ANDROID_AIDL); //$NON-NLS-1$ + command[index] = osPath; + command[index + 1] = osJavaPath; + + // launch the process + if (execAidl(command, aidlFile) == false) { + // aidl failed. File should be marked. We add the file to the list + // of file that will need compilation again. + stillNeedCompilation.add(aidlFile); + + // and we move on to the next one. + continue; + } else { + // since the exec worked, we refresh the parent, and set the + // file as read only. + finishFileAfterExternalModification(javaFile, aidlFile); + } + } + + // change the list to only contains the file that have failed compilation + mAidlToCompile.clear(); + mAidlToCompile.addAll(stillNeedCompilation); + + // Remove the java files created from aidl files that have been removed. + for (IFile aidlFile : mAidlToRemove) { + // make the java filename + String javaName = aidlFile.getName().replaceAll( + AndroidConstants.RE_AIDL_EXT, + AndroidConstants.DOT_JAVA); + + // get the parent container + IContainer ic = aidlFile.getParent(); + + // and get the IFile corresponding to the java file. + IFile javaFile = ic.getFile(new Path(javaName)); + if (javaFile != null && javaFile.exists() ) { + // check if this java file has a persistent data marking it as generated by + // the builder. + // While we put the aidl path as a resource, internally it's all string anyway. + // We use loadStringProperty, because loadResourceProperty tries to match + // the string value (a path in this case) with an existing resource, but + // the aidl file was deleted, so it would return null, even though the property + // existed. + String aidlPath = ProjectHelper.loadStringProperty(javaFile, + PROPERTY_ANDROID_GENERATED); + + if (aidlPath != null) { + // This confirms the java file was generated by the builder, + // we can delete the aidlFile. + javaFile.delete(true, null); + + // Refresh parent. + ic.refreshLocal(IResource.DEPTH_ONE, monitor); + } + } + } + mAidlToRemove.clear(); + + return true; + } + + /** + * Execute the aidl command line, parse the output, and mark the aidl file + * with any reported errors. + * @param command the String array containing the command line to execute. + * @param file The IFile object representing the aidl file being + * compiled. + * @return false if the exec failed, and build needs to be aborted. + */ + private boolean execAidl(String[] command, IFile file) { + // do the exec + try { + Process p = Runtime.getRuntime().exec(command); + + // list to store each line of stderr + ArrayList<String> results = new ArrayList<String>(); + + // get the output and return code from the process + int result = grabProcessOutput(p, results); + + // attempt to parse the error output + boolean error = parseAidlOutput(results, file); + + // If the process failed and we couldn't parse the output + // we pring a message, mark the project and exit + if (result != 0 && error == true) { + // display the message in the console. + AdtPlugin.printErrorToConsole(getProject(), results.toArray()); + + // mark the project and exit + markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AIDL_Errors, + IMarker.SEVERITY_ERROR); + return false; + } + } catch (IOException e) { + // mark the project and exit + String msg = String.format(Messages.AIDL_Exec_Error, command[0]); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } catch (InterruptedException e) { + // mark the project and exit + String msg = String.format(Messages.AIDL_Exec_Error, command[0]); + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); + return false; + } + + return true; + } + + /** + * Goes through the build paths and fills the list of aidl files to compile + * ({@link #mAidlToCompile}). + * @param project The project. + * @param buildPaths The list of build paths. + */ + private void buildAidlCompilationList(IProject project, + ArrayList<IPath> buildPaths) { + for (IPath p : buildPaths) { + // Because the path contains the name of the project as well, we + // need to remove it, to access the final folder. + String[] segments = p.segments(); + IContainer folder = project; + for (int i = 1; i < segments.length; i++) { + IResource r = folder.findMember(segments[i]); + if (r != null && r.exists() && + r.getType() == IResource.FOLDER) { + folder = (IContainer)r; + } else { + // hmm looks like the build path is corrupted/wrong. + // reset and break + folder = project; + break; + } + } + + // did we ge a folder? + if (folder != project) { + // then we scan! + scanContainerForAidl(folder); + } + } + } + + /** + * Scans a container and fills the list of aidl files to compile. + * @param container The container to scan. + */ + private void scanContainerForAidl(IContainer container) { + try { + IResource[] members = container.members(); + for (IResource r : members) { + // get the type of the resource + switch (r.getType()) { + case IResource.FILE: + // if this a file, check that the file actually exist + // and that it's an aidl file + if (r.exists() && + AndroidConstants.EXT_AIDL.equalsIgnoreCase(r.getFileExtension())) { + mAidlToCompile.add((IFile)r); + } + break; + case IResource.FOLDER: + // recursively go through children + scanContainerForAidl((IFolder)r); + break; + default: + // this would mean it's a project or the workspace root + // which is unlikely to happen. we do nothing + break; + } + } + } catch (CoreException e) { + // Couldn't get the members list for some reason. Just return. + } + } + + + /** + * Parse the output of aidl and mark the file with any errors. + * @param lines The output to parse. + * @param file The file to mark with error. + * @return true if the parsing failed, false if success. + */ + private boolean parseAidlOutput(ArrayList<String> lines, IFile file) { + // nothing to parse? just return false; + if (lines.size() == 0) { + return false; + } + + Matcher m; + + for (int i = 0; i < lines.size(); i++) { + String p = lines.get(i); + + m = sAidlPattern1.matcher(p); + if (m.matches()) { + // we can ignore group 1 which is the location since we already + // have a IFile object representing the aidl file. + String lineStr = m.group(2); + String msg = m.group(3); + + // get the line number + int line = 0; + try { + line = Integer.parseInt(lineStr); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid + // file number. Parsing failed and we return true + return true; + } + + // mark the file + BaseProjectHelper.addMarker(file, AndroidConstants.MARKER_AIDL, msg, line, + IMarker.SEVERITY_ERROR); + + // success, go to the next line + continue; + } + + // invalid line format, flag as error, and bail + return true; + } + + return false; + } + + /** + * Merge the current list of aidl file to compile/remove with the new one. + * @param toCompile List of file to compile + * @param toRemove List of file to remove + */ + private void mergeAidlFileModifications(ArrayList<IFile> toCompile, + ArrayList<IFile> toRemove) { + + // loop through the new toRemove list, and add it to the old one, + // plus remove any file that was still to compile and that are now + // removed + for (IFile r : toRemove) { + if (mAidlToRemove.indexOf(r) == -1) { + mAidlToRemove.add(r); + } + + int index = mAidlToCompile.indexOf(r); + if (index != -1) { + mAidlToCompile.remove(index); + } + } + + // now loop through the new files to compile and add it to the list. + // Also look for them in the remove list, this would mean that they + // were removed, then added back, and we shouldn't remove them, just + // recompile them. + for (IFile r : toCompile) { + if (mAidlToCompile.indexOf(r) == -1) { + mAidlToCompile.add(r); + } + + int index = mAidlToRemove.indexOf(r); + if (index != -1) { + mAidlToRemove.remove(index); + } + } + } + + /** + * Prepare an already existing file for modification. File generated from + * command line processed are marked as read-only. This method prepares + * them (mark them as read-write) before the command line process is + * started. A check is made to be sure the file exists. + * @param file The IResource object for the file to prepare. + * @throws CoreException + */ + private void prepareFileForExternalModification(IFile file) + throws CoreException { + // file may not exist yet, so we check that. + if (file != null && file.exists()) { + // get the attributes. + ResourceAttributes ra = file.getResourceAttributes(); + if (ra != null) { + // change the attributes + ra.setReadOnly(false); + + // set the new attributes in the file. + file.setResourceAttributes(ra); + } + } + } + + /** + * Finish a file created/modified by an outside command line process. + * The file is marked as modified by Android, and the parent folder is refreshed, so that, + * in case the file didn't exist beforehand, the file appears in the package explorer. + * @param rFile The R file to "finish". + * @param manifestFile The manifest file to "finish". + * @throws CoreException + */ + private void finishJavaFilesAfterExternalModification(IFile rFile, IFile manifestFile) + throws CoreException { + IContainer parent = rFile.getParent(); + + IProgressMonitor monitor = new DerivedProgressMonitor(rFile, manifestFile); + + // refresh the parent node in the package explorer. Once this is done the custom progress + // monitor will mark them as derived. + parent.refreshLocal(IResource.DEPTH_ONE, monitor); + } + + /** + * Finish a file created/modified by an outside command line process. + * The file is marked as modified by Android, and the parent folder is refreshed, so that, + * in case the file didn't exist beforehand, the file appears in the package explorer. + * @param file The file to "finish". + * @param aidlFile The AIDL file to "finish". + * @throws CoreException + */ + private void finishFileAfterExternalModification(IFile file, IFile aidlFile) + throws CoreException { + IContainer parent = file.getParent(); + + // we need to add a link to the aidl file. + // We need to wait for the refresh of the parent to be done, so we'll do + // it in the monitor. This will also set the file as derived. + IProgressMonitor monitor = new RefreshProgressMonitor(file, aidlFile); + + // refresh the parent node in the package explorer. + parent.refreshLocal(IResource.DEPTH_ONE, monitor); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java new file mode 100644 index 0000000..f4778d7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.build.BaseBuilder.BaseDeltaVisitor; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; + +import java.util.ArrayList; + +/** + * Resource Delta visitor for the pre-compiler. + */ +class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements + IResourceDeltaVisitor { + + // Result fields. + /** + * Compile flag. This is set to true if one of the changed/added/removed + * file is a resource file. Upon visiting all the delta resources, if + * this flag is true, then we know we'll have to compile the resources + * into R.java + */ + private boolean mCompileResources = false; + + /** List of .aidl files found that are modified or new. */ + private final ArrayList<IFile> mAidlToCompile = new ArrayList<IFile>(); + + /** List of .aidl files that have been removed. */ + private final ArrayList<IFile> mAidlToRemove = new ArrayList<IFile>(); + + /** Aidl forced recompilation flag. This is set to true if project.aidl is modified. */ + private boolean mFullAidlCompilation = false; + + /** Manifest check/parsing flag. */ + private boolean mCheckedManifestXml = false; + + /** Application Pacakge, gathered from the parsing of the manifest */ + private String mJavaPackage = null; + + // Internal usage fields. + /** + * In Resource folder flag. This allows us to know if we're in the + * resource folder. + */ + private boolean mInRes = false; + + /** + * In Source folder flag. This allows us to know if we're in a source + * folder. + */ + private boolean mInSrc = false; + + /** List of source folders. */ + private ArrayList<IPath> mSourceFolders; + + + public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders) { + super(builder); + mSourceFolders = sourceFolders; + } + + public boolean getCompileResources() { + return mCompileResources; + } + + public ArrayList<IFile> getAidlToCompile() { + return mAidlToCompile; + } + + public ArrayList<IFile> getAidlToRemove() { + return mAidlToRemove; + } + + public boolean getFullAidlRecompilation() { + return mFullAidlCompilation; + } + + /** + * Returns whether the manifest file was parsed/checked for error during the resource delta + * visiting. + */ + public boolean getCheckedManifestXml() { + return mCheckedManifestXml; + } + + /** + * Returns the manifest package if the manifest was checked/parsed. + * <p/> + * This can return null in two cases: + * <ul> + * <li>The manifest was not part of the resource change delta, and the manifest was + * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> + * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), + * but the package declaration is missing</li> + * </ul> + * @return the manifest package or null. + */ + public String getManifestPackage() { + return mJavaPackage; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.core.resources.IResourceDeltaVisitor + * #visit(org.eclipse.core.resources.IResourceDelta) + */ + public boolean visit(IResourceDelta delta) throws CoreException { + // we are only going to look for changes in res/, source folders and in + // AndroidManifest.xml since the delta visitor goes through the main + // folder before its children we can check when the path segment + // count is 2 (format will be /$Project/folder) and make sure we are + // processing res/, source folders or AndroidManifest.xml + + IResource resource = delta.getResource(); + IPath path = resource.getFullPath(); + String[] segments = path.segments(); + + // since the delta visitor also visits the root we return true if + // segments.length = 1 + if (segments.length == 1) { + return true; + } else if (segments.length == 2) { + // if we are at an item directly under the root directory, + // then we are not yet in a source or resource folder + mInRes = mInSrc = false; + + if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) { + // this is the resource folder that was modified. we want to + // see its content. + + // since we're going to visit its children next, we set the + // flag + mInRes = true; + mInSrc = false; + return true; + } else if (AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(segments[1])) { + // any change in the manifest could trigger a new R.java + // class, so we don't need to check the delta kind + if (delta.getKind() != IResourceDelta.REMOVED) { + // parse the manifest for errors + AndroidManifestParser parser = BaseProjectHelper.parseManifestForError( + (IFile)resource, this); + + if (parser != null) { + mJavaPackage = parser.getPackage(); + } + + mCheckedManifestXml = true; + } + mCompileResources = true; + + // we don't want to go to the children, not like they are + // any for this resource anyway. + return false; + } else if (AndroidConstants.FN_PROJECT_AIDL.equalsIgnoreCase(segments[1])) { + // need to force recompilation of all the aidl files + mFullAidlCompilation = true; + } + } + + // at this point we can either be in the source folder or in the + // resource folder or in a different folder that contains a source + // folder. + // This is due to not all source folder being src/. Some could be + // something/somethingelse/src/ + + // so first we test if we already know we are in a source or + // resource folder. + + if (mInSrc) { + // if we are in the res folder, we are looking for the following changes: + // - added/removed/modified aidl files. + // - missing R.java file + + // if the resource is a folder, we just go straight to the children + if (resource.getType() == IResource.FOLDER) { + return true; + } + + if (resource.getType() != IResource.FILE) { + return false; + } + IFile file = (IFile)resource; + + // get the modification kind + int kind = delta.getKind(); + + if (kind == IResourceDelta.ADDED) { + // we only care about added files (inside the source folders), if they + // are aidl files. + + // get the extension of the resource + String ext = resource.getFileExtension(); + + if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { + // look for an already existing matching java file + String javaName = resource.getName().replaceAll( + AndroidConstants.RE_AIDL_EXT, + AndroidConstants.DOT_JAVA); + + // get the parent container + IContainer ic = resource.getParent(); + + IFile javaFile = ic.getFile(new Path(javaName)); + if (javaFile != null && javaFile.exists()) { + // check if that file was generated by the plugin. Normally those files are + // deleted automatically, but it's better to check. + String aidlPath = ProjectHelper.loadStringProperty(javaFile, + PreCompilerBuilder.PROPERTY_ANDROID_GENERATED); + if (aidlPath == null) { + // mark the aidl file that it cannot be compile just yet + ProjectHelper.saveBooleanProperty(file, + PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, true); + } + + // we add it anyway so that we can try to compile it at every compilation + // until the conflict is fixed. + mAidlToCompile.add(file); + + } else { + // the java file doesn't exist, we can safely add the file to the list + // of files to compile. + mAidlToCompile.add(file); + } + } + + return false; + } + + // get the filename + String fileName = segments[segments.length - 1]; + + boolean outputMessage = false; + + // Special case of R.java/Manifest.java. + // FIXME: This does not check the package. Any modification of R.java/Manifest.java in another project will trigger a new recompilation of the resources. + if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) || + AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) { + // if it was removed, there's a possibility that it was removed due to a + // package change, or an aidl that was removed, but the only thing + // that will happen is that we'll have an extra build. Not much of a problem. + mCompileResources = true; + + // we want a warning + outputMessage = true; + } else { + + // get the extension of the resource + String ext = resource.getFileExtension(); + + if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { + if (kind == IResourceDelta.REMOVED) { + mAidlToRemove.add(file); + } else { + mAidlToCompile.add(file); + } + } else { + if (kind == IResourceDelta.REMOVED) { + // the file has been removed. we need to check it's a java file and that + // there's a matching aidl file. We can't check its persistent storage + // anymore. + if (AndroidConstants.EXT_JAVA.equalsIgnoreCase(ext)) { + String aidlFile = resource.getName().replaceAll( + AndroidConstants.RE_JAVA_EXT, + AndroidConstants.DOT_AIDL); + + // get the parent container + IContainer ic = resource.getParent(); + + IFile f = ic.getFile(new Path(aidlFile)); + if (f != null && f.exists() ) { + // make sure that the aidl file is not in conflict anymore, in + // case the java file was not generated by us. + if (ProjectHelper.loadBooleanProperty(f, + PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, false)) { + ProjectHelper.saveBooleanProperty(f, + PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, false); + } else { + outputMessage = true; + } + mAidlToCompile.add(f); + } + } + } else { + // check if it's an android generated java file. + IResource aidlSource = ProjectHelper.loadResourceProperty( + file, PreCompilerBuilder.PROPERTY_ANDROID_GENERATED); + + if (aidlSource != null && aidlSource.exists() && + aidlSource.getType() == IResource.FILE) { + // it looks like this was a java file created from an aidl file. + // we need to add the aidl file to the list of aidl file to compile + mAidlToCompile.add((IFile)aidlSource); + outputMessage = true; + } + } + } + } + + if (outputMessage) { + if (kind == IResourceDelta.REMOVED) { + // We pring an error just so that it's red, but it's just a warning really. + String msg = String.format(Messages.s_Removed_Recreating_s, fileName); + AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + } else if (kind == IResourceDelta.CHANGED) { + // the file was modified manually! we can't allow it. + String msg = String.format(Messages.s_Modified_Manually_Recreating_s, fileName); + AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); + } + } + + // no children. + return false; + } else if (mInRes) { + // if we are in the res folder, we are looking for the following + // changes: + // - added/removed/modified xml files. + // - added/removed files of any other type + + // if the resource is a folder, we just go straight to the + // children + if (resource.getType() == IResource.FOLDER) { + return true; + } + + // get the extension of the resource + String ext = resource.getFileExtension(); + int kind = delta.getKind(); + + String p = resource.getProjectRelativePath().toString(); + String message = null; + switch (kind) { + case IResourceDelta.CHANGED: + // display verbose message + message = String.format(Messages.s_Modified_Recreating_s, p, + AndroidConstants.FN_RESOURCE_CLASS); + break; + case IResourceDelta.ADDED: + // display verbose message + message = String.format(Messages.Added_s_s_Needs_Updating, p, + AndroidConstants.FN_RESOURCE_CLASS); + break; + case IResourceDelta.REMOVED: + // display verbose message + message = String.format(Messages.s_Removed_s_Needs_Updating, p, + AndroidConstants.FN_RESOURCE_CLASS); + break; + } + if (message != null) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, + mBuilder.getProject(), message); + } + + if (AndroidConstants.EXT_XML.equalsIgnoreCase(ext)) { + if (kind != IResourceDelta.REMOVED) { + // check xml Validity + mBuilder.checkXML(resource, this); + } + + // if we are going through this resource, it was modified + // somehow. + // we don't care if it was an added/removed/changed event + mCompileResources = true; + return false; + } else { + // this is a non xml resource. + if (kind == IResourceDelta.ADDED + || kind == IResourceDelta.REMOVED) { + mCompileResources = true; + return false; + } + } + } else if (resource instanceof IFolder) { + // in this case we may be inside a folder that contains a source + // folder. + String[] sourceFolderSegments = findMatchingSourceFolder(mSourceFolders, segments); + if (sourceFolderSegments != null) { + // we have a match! + mInRes = false; + + // Check if the current folder is actually a source folder + if (sourceFolderSegments.length == segments.length) { + mInSrc = true; + } + + // and return true to visit the content, no matter what + return true; + } + + // if we're here, we are visiting another folder + // like /$Project/bin/ for instance (we get notified for changes + // in .class!) + // This could also be another source folder and we have found + // R.java in a previous source folder + // We don't want to visit its children + return false; + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java new file mode 100644 index 0000000..19d7185 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; + +import java.util.Map; + +/** + * Resource manager builder whose only purpose is to refresh the resource folder + * so that the other builder use an up to date version. + */ +public class ResourceManagerBuilder extends BaseBuilder { + + public static final String ID = "com.android.ide.eclipse.adt.ResourceManagerBuilder"; //$NON-NLS-1$ + + public ResourceManagerBuilder() { + super(); + } + + // build() returns a list of project from which this project depends for future compilation. + @SuppressWarnings("unchecked") //$NON-NLS-1$ + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) + throws CoreException { + // Get the project. + IProject project = getProject(); + + // Clear the project of the generic markers + BaseBuilder.removeMarkersFromProject(project, AdtConstants.MARKER_ADT); + + // Check the compiler compliance level, displaying the error message + // since this is the first builder. + int res = ProjectHelper.checkCompilerCompliance(project); + String errorMessage = null; + switch (res) { + case ProjectHelper.COMPILER_COMPLIANCE_LEVEL: + errorMessage = Messages.Requires_Compiler_Compliance_5; + case ProjectHelper.COMPILER_COMPLIANCE_SOURCE: + errorMessage = Messages.Requires_Source_Compatibility_5; + case ProjectHelper.COMPILER_COMPLIANCE_CODEGEN_TARGET: + errorMessage = Messages.Requires_Class_Compatibility_5; + } + + if (errorMessage != null) { + BaseProjectHelper.addMarker(project, AdtConstants.MARKER_ADT, errorMessage, + IMarker.SEVERITY_ERROR); + AdtPlugin.printErrorToConsole(project, errorMessage); + + // interrupt the build. The next builders will not run. + stopBuild(errorMessage); + } + + // Check that the SDK directory has been setup. + String osSdkFolder = AdtPlugin.getOsSdkFolder(); + + if (osSdkFolder == null || osSdkFolder.length() == 0) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.No_SDK_Setup_Error); + markProject(AdtConstants.MARKER_ADT, Messages.No_SDK_Setup_Error, + IMarker.SEVERITY_ERROR); + + // This interrupts the build. The next builders will not run. + stopBuild(Messages.No_SDK_Setup_Error); + } + + // check if we have finished loading the SDK. + if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADED) { + // we exit silently + // This interrupts the build. The next builders will not run. + stopBuild("SDK is not loaded yet"); + } + + // check the project has a target + IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + if (projectTarget == null) { + // no target. marker has been set by the container initializer: exit silently. + // This interrupts the build. The next builders will not run. + stopBuild("Project has no target"); + } + + // Check the preference to be sure we are supposed to refresh + // the folders. + if (AdtPlugin.getAutoResRefresh()) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, + Messages.Refreshing_Res); + + // refresh the res folder. + IFolder resFolder = project.getFolder( + AndroidConstants.WS_RESOURCES); + resFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + + // Also refresh the assets folder to make sure the ApkBuilder + // will now it's changed and will force a new resource packaging. + IFolder assetsFolder = project.getFolder( + AndroidConstants.WS_ASSETS); + assetsFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties new file mode 100644 index 0000000..8ba43d4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties @@ -0,0 +1,61 @@ +Start_Full_Apk_Build=Starting full Package build. +Start_Full_Pre_Compiler=Starting full Pre Compiler. +Start_Inc_Apk_Build=Starting incremental Package build: Checking resource changes. +Start_Inc_Pre_Compiler=Starting incremental Pre Compiler: Checking resource changes. +Xml_Error=Error in an XML file: aborting build. +s_Missing_Repackaging=%1$s missing. Repackaging. +Project_Has_Errors=Project contains error(s). Package Builder aborted. +Failed_To_Get_Output=Failed to get project output folder\! +Output_Missing=Output folder missing\! Make sure your project is configured properly. +s_File_Missing=%1$s file missing\! +Unparsed_AAPT_Errors=Unparsed aapt error(s)\! Check the console for output. +Unparsed_AIDL_Errors=Unparsed aidl error\! Check the console for output. +AAPT_Exec_Error=Error executing aapt. Please check aapt is present at %1$s +Dalvik_Error_d=Conversion to Dalvik format failed with error %1$d +DX_Jar_Error=Dx.jar is not found inside the plugin. Reinstall ADT\! +Dalvik_Error_s=Conversion to Dalvik format failed: %1$s +Incompatible_VM_Warning=Note: You may be using an incompatible virtual machine or class library. +Requires_1_5_Error=This program requires JDK 1.5 compatibility. +Final_Archive_Error_s=Error generating final archive: %1$s +Marker_Delete_Error=Failed to delete marker '%1$s' for %2$s +Couldnt_Locate_s_Error=Could not locate '%1$s'. This will not be added to the package. +Compiler_Compliance_Error=Compiler compliance level not compatible: Build aborted. +No_SDK_Setup_Error=SDK directory has not been setup. Please go to the Android preferences and enter the location of the SDK. +s_Contains_Xml_Error=%1$s contains XML error: Build aborted. +s_Doesnt_Declare_Package_Error=%1$s does not declare a Java package: Build aborted. +Checking_Package_Change=Checking Java package value did not change... +Package_s_Doesnt_Exist_Error=Package '%1$s' does not exist\! +Preparing_Generated_Files=Preparing generated java files for update/creation. +AAPT_Error='aapt' error. Pre Compiler Build aborted. +Nothing_To_Compile=Nothing to pre compile\! +Removing_Generated_Classes=Removing generated java classes. +Delete_Obsolete_Error=Failed to delete obsolete %1$s, please delete it manually +DexWrapper_Dex_Loader=Dex Loader +AIDL_Java_Conflict=%1$s is in the way of %2$s, remove it or rename of one the files. +AIDL_Exec_Error=Error executing aidl. Please check aidl is present at %1$s +s_Removed_Recreating_s=%1$s was removed\! Recreating %1$s\! +s_Modified_Manually_Recreating_s=%1$s was modified manually\! Reverting to generated version\! +s_Modified_Recreating_s='%1$s' was modified, %2$s needs to be updated. +Added_s_s_Needs_Updating=New resource file: '%1$s', %2$s needs to be updated. +s_Removed_s_Needs_Updating='%1$s' was removed, %2$s needs to be updated. +Requires_Compiler_Compliance_5=Android requires compiler compliance level 5.0. Please fix project properties. +Requires_Source_Compatibility_5=Android requires source compatibility set to 5.0. Please fix project properties. +Requires_Class_Compatibility_5=Android requires .class compatibility set to 5.0. Please fix project properties. +Refreshing_Res=Refreshing resource folders. +DexWrapper_s_does_not_exists=%1$s does not exist or is not a file +DexWrapper_Failed_to_load_s=Failed to load %1$s +DexWrapper_Unable_To_Execute_Dex_s=Unable to execute dex: %1$s +DexWrapper_SecuryEx_Unable_To_Find_API=SecurityException: Unable to find API for dex.jar +DexWrapper_SecuryEx_Unable_To_Find_Method=SecurityException: Unable to find method for dex.jar +DexWrapper_SecuryEx_Unable_To_Find_Field=SecurityException: Unable to find field for dex.jar +ApkBuilder_UnableBuild_Dex_Not_loaded=Unable to build: the file dex.jar was not loaded from the SDK folder\! +ApkBuilder_Using_Default_Key=Using default debug key to sign package +ApkBuilder_Using_s_To_Sign=Using '%1$s' to sign package +ApkBuilder_Signing_Key_Creation_s=Signing Key Creation: +ApkBuilder_Unable_To_Gey_Key=Unable to get debug signature key +ApkBuilder_Certificate_Expired_on_s=Debug certificate expired on %1$s\! +ApkBuilder_Packaging_s=Packaging %1$s +ApkBuilder_JAVA_HOME_is_s=The Java VM Home used is: %1$s +ApkBuilder_Update_or_Execute_manually_s=Update it if necessary, or manually execute the following command: +ApkBuilder_s_Conflict_with_file_s=%1$s conflicts with another file already put at %2$s +ApkBuilder_Packaging_s_into_s=Packaging %1$s into %2$s diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java new file mode 100644 index 0000000..3d60401 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.launching; + +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.Launch; +import org.eclipse.debug.core.model.ISourceLocator; + +/** + * Custom implementation of Launch to allow access to the LaunchManager + * + */ +class AndroidLaunch extends Launch { + + /** + * Basic constructor does nothing special + * @param launchConfiguration + * @param mode + * @param locator + */ + public AndroidLaunch(ILaunchConfiguration launchConfiguration, String mode, + ISourceLocator locator) { + super(launchConfiguration, mode, locator); + } + + /** Stops the launch, and removes it from the launch manager */ + public void stopLaunch() { + ILaunchManager mgr = getLaunchManager(); + + if (canTerminate()) { + try { + terminate(); + } catch (DebugException e) { + // well looks like we couldn't stop it. nothing else to be + // done really + } + } + // remove the launch + mgr.removeLaunch(this); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java new file mode 100644 index 0000000..ac003df --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java @@ -0,0 +1,1838 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.launching; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.SyncService.SyncResult; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.debug.launching.DeviceChooserDialog.DeviceChooserResponse; +import com.android.ide.eclipse.adt.debug.ui.EmulatorConfigTab; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.AvdManager.AvdInfo; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jdt.launching.IVMConnector; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Controls the launch of Android application either on a device or on the + * emulator. If an emulator is already running, this class will attempt to reuse + * it. + */ +public final class AndroidLaunchController implements IDebugBridgeChangeListener, + IDeviceChangeListener, IClientChangeListener { + + private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$ + private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$ + private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$ + private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$ + private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$ + + private static final int MAX_ATTEMPT_COUNT = 5; + + private final static Pattern sAmErrorType = Pattern.compile("Error type (\\d+)"); //$NON-NLS-1$ + + /** + * A delayed launch waiting for a device to be present or ready before the + * application is launched. + */ + static final class DelayedLaunchInfo { + /** The device on which to launch the app */ + Device mDevice = null; + + /** The eclipse project */ + IProject mProject; + + /** Package name */ + String mPackageName; + + /** fully qualified name of the activity */ + String mActivity; + + /** IFile to the package (.apk) file */ + IFile mPackageFile; + + /** Debuggable attribute of the manifest file. */ + Boolean mDebuggable = null; + + /** Required ApiVersionNumber by the app. 0 means no requirements */ + int mRequiredApiVersionNumber = 0; + + InstallRetryMode mRetryMode = InstallRetryMode.NEVER; + + /** + * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT}, + * {@link LaunchConfigDelegate#ACTION_ACTIVITY}, + * {@link LaunchConfigDelegate#ACTION_DO_NOTHING} + */ + int mLaunchAction; + + /** the launch object */ + AndroidLaunch mLaunch; + + /** the monitor object */ + IProgressMonitor mMonitor; + + /** debug mode flag */ + boolean mDebugMode; + + int mAttemptCount = 0; + + boolean mCancelled = false; + + /** Basic constructor with activity and package info. */ + private DelayedLaunchInfo(IProject project, String packageName, String activity, + IFile pack, Boolean debuggable, int requiredApiVersionNumber, int launchAction, + AndroidLaunch launch, IProgressMonitor monitor) { + mProject = project; + mPackageName = packageName; + mActivity = activity; + mPackageFile = pack; + mLaunchAction = launchAction; + mLaunch = launch; + mMonitor = monitor; + mDebuggable = debuggable; + mRequiredApiVersionNumber = requiredApiVersionNumber; + } + } + + /** + * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection + * to running application. The integer is the port on which to connect. + * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> + */ + private final static HashMap<ILaunchConfiguration, Integer> sRunningAppMap = + new HashMap<ILaunchConfiguration, Integer>(); + + private final static Object sListLock = sRunningAppMap; + + /** + * List of {@link DelayedLaunchInfo} waiting for an emulator to connect. + * <p>Once an emulator has connected, {@link DelayedLaunchInfo#mDevice} is set and the + * DelayedLaunchInfo object is moved to {@link AndroidLaunchController#mWaitingForReadyEmulatorList}. + * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> + */ + private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches = + new ArrayList<DelayedLaunchInfo>(); + + /** + * List of application waiting to be launched on a device/emulator.<br> + * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> + * */ + private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList = + new ArrayList<DelayedLaunchInfo>(); + + /** + * Application waiting to show up as waiting for debugger. + * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> + */ + private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications = + new ArrayList<DelayedLaunchInfo>(); + + /** + * List of clients that have appeared as waiting for debugger before their name was available. + * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> + */ + private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>(); + + /** static instance for singleton */ + private static AndroidLaunchController sThis = new AndroidLaunchController(); + + enum InstallRetryMode { + NEVER, ALWAYS, PROMPT; + } + + /** + * Launch configuration data. This stores the result of querying the + * {@link ILaunchConfiguration} so that it's only done once. + */ + static final class AndroidLaunchConfiguration { + + /** + * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT}, + * {@link LaunchConfigDelegate#ACTION_ACTIVITY}, + * {@link LaunchConfigDelegate#ACTION_DO_NOTHING} + */ + public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION; + + public static final boolean AUTO_TARGET_MODE = true; + + /** + * Target selection mode. + * <ul> + * <li><code>true</code>: automatic mode, see {@link #AUTO_TARGET_MODE}</li> + * <li><code>false</code>: manual mode</li> + * </ul> + */ + public boolean mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE; + + /** + * Indicates whether the emulator should be called with -wipe-data + */ + public boolean mWipeData = LaunchConfigDelegate.DEFAULT_WIPE_DATA; + + /** + * Indicates whether the emulator should be called with -no-boot-anim + */ + public boolean mNoBootAnim = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM; + + /** + * AVD Name. + */ + public String mAvdName = null; + + public String mNetworkSpeed = EmulatorConfigTab.getSpeed( + LaunchConfigDelegate.DEFAULT_SPEED); + public String mNetworkDelay = EmulatorConfigTab.getDelay( + LaunchConfigDelegate.DEFAULT_DELAY); + + /** + * Optional custom command line parameter to launch the emulator + */ + public String mEmulatorCommandLine; + + /** + * Initialized the structure from an ILaunchConfiguration object. + * @param config + */ + public void set(ILaunchConfiguration config) { + try { + mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, + mLaunchAction); + } catch (CoreException e1) { + // nothing to be done here, we'll use the default value + } + + try { + mTargetMode = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, + mTargetMode); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + + try { + mAvdName = config.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, mAvdName); + } catch (CoreException e) { + } + + int index = LaunchConfigDelegate.DEFAULT_SPEED; + try { + index = config.getAttribute(LaunchConfigDelegate.ATTR_SPEED, index); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + mNetworkSpeed = EmulatorConfigTab.getSpeed(index); + + index = LaunchConfigDelegate.DEFAULT_DELAY; + try { + index = config.getAttribute(LaunchConfigDelegate.ATTR_DELAY, index); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + mNetworkDelay = EmulatorConfigTab.getDelay(index); + + try { + mEmulatorCommandLine = config.getAttribute( + LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$ + } catch (CoreException e) { + // lets not do anything here, we'll use the default value + } + + try { + mWipeData = config.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, mWipeData); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + + try { + mNoBootAnim = config.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, + mNoBootAnim); + } catch (CoreException e) { + // nothing to be done here, we'll use the default value + } + } + } + + /** + * Output receiver for am process (activity Manager); + */ + private final class AMReceiver extends MultiLineReceiver { + private DelayedLaunchInfo mLaunchInfo; + private Device mDevice; + + /** + * Basic constructor. + * @param launchInfo The launch info associated with the am process. + * @param device The device on which the launch is done. + */ + public AMReceiver(DelayedLaunchInfo launchInfo, Device device) { + mLaunchInfo = launchInfo; + mDevice = device; + } + + @Override + public void processNewLines(String[] lines) { + // first we check if one starts with error + ArrayList<String> array = new ArrayList<String>(); + boolean error = false; + boolean warning = false; + for (String s : lines) { + // ignore empty lines. + if (s.length() == 0) { + continue; + } + + // check for errors that output an error type, if the attempt count is still + // valid. If not the whole text will be output in the console + if (mLaunchInfo.mAttemptCount < MAX_ATTEMPT_COUNT && + mLaunchInfo.mCancelled == false) { + Matcher m = sAmErrorType.matcher(s); + if (m.matches()) { + // get the error type + int type = Integer.parseInt(m.group(1)); + + final int waitTime = 3; + String msg; + + switch (type) { + case 1: + /* Intended fall through */ + case 2: + msg = String.format( + "Device not ready. Waiting %1$d seconds before next attempt.", + waitTime); + break; + case 3: + msg = String.format( + "New package not yet registered with the system. Waiting %1$d seconds before next attempt.", + waitTime); + break; + default: + msg = String.format( + "Device not ready (%2$d). Waiting %1$d seconds before next attempt.", + waitTime, type); + break; + + } + + AdtPlugin.printToConsole(mLaunchInfo.mProject, msg); + + // launch another thread, that waits a bit and attempts another launch + new Thread("Delayed Launch attempt") { + @Override + public void run() { + try { + sleep(waitTime * 1000); + } catch (InterruptedException e) { + } + + launchApp(mLaunchInfo, mDevice); + } + }.start(); + + // no need to parse the rest + return; + } + } + + // check for error if needed + if (error == false && s.startsWith("Error:")) { //$NON-NLS-1$ + error = true; + } + if (warning == false && s.startsWith("Warning:")) { //$NON-NLS-1$ + warning = true; + } + + // add the line to the list + array.add("ActivityManager: " + s); //$NON-NLS-1$ + } + + // then we display them in the console + if (warning || error) { + AdtPlugin.printErrorToConsole(mLaunchInfo.mProject, array.toArray()); + } else { + AdtPlugin.printToConsole(mLaunchInfo.mProject, array.toArray()); + } + + // if error then we cancel the launch, and remove the delayed info + if (error) { + mLaunchInfo.mLaunch.stopLaunch(); + synchronized (sListLock) { + mWaitingForReadyEmulatorList.remove(mLaunchInfo); + } + } + } + + public boolean isCancelled() { + return false; + } + } + + /** + * Output receiver for "pm install package.apk" command line. + */ + private final static class InstallReceiver extends MultiLineReceiver { + + private final static String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ + private final static Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ + + private String mSuccess = null; + + public InstallReceiver() { + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.length() > 0) { + if (line.startsWith(SUCCESS_OUTPUT)) { + mSuccess = null; + } else { + Matcher m = FAILURE_PATTERN.matcher(line); + if (m.matches()) { + mSuccess = m.group(1); + } + } + } + } + } + + public boolean isCancelled() { + return false; + } + + public String getSuccess() { + return mSuccess; + } + } + + + /** private constructor to enforce singleton */ + private AndroidLaunchController() { + AndroidDebugBridge.addDebugBridgeChangeListener(this); + AndroidDebugBridge.addDeviceChangeListener(this); + AndroidDebugBridge.addClientChangeListener(this); + } + + /** + * Returns the singleton reference. + */ + public static AndroidLaunchController getInstance() { + return sThis; + } + + + /** + * Launches a remote java debugging session on an already running application + * @param project The project of the application to debug. + * @param debugPort The port to connect the debugger to. + */ + public static void debugRunningApp(IProject project, int debugPort) { + // get an existing or new launch configuration + ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project); + + if (config != null) { + setPortLaunchConfigAssociation(config, debugPort); + + // and launch + DebugUITools.launch(config, ILaunchManager.DEBUG_MODE); + } + } + + /** + * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}. + * @param project the project + * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was + * an error when creating a new one. + */ + public static ILaunchConfiguration getLaunchConfig(IProject project) { + // get the launch manager + ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); + + // now get the config type for our particular android type. + ILaunchConfigurationType configType = manager.getLaunchConfigurationType( + LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); + + String name = project.getName(); + + // search for an existing launch configuration + ILaunchConfiguration config = findConfig(manager, configType, name); + + // test if we found one or not + if (config == null) { + // Didn't find a matching config, so we make one. + // It'll be made in the "working copy" object first. + ILaunchConfigurationWorkingCopy wc = null; + + try { + // make the working copy object + wc = configType.newInstance(null, + manager.generateUniqueLaunchConfigurationNameFrom(name)); + + // set the project name + wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name); + + // set the launch mode to default. + wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, + LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION); + + // set default target mode + wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, + LaunchConfigDelegate.DEFAULT_TARGET_MODE); + + // default AVD: None + wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String)null); + + // set the default network speed + wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED, + LaunchConfigDelegate.DEFAULT_SPEED); + + // and delay + wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY, + LaunchConfigDelegate.DEFAULT_DELAY); + + // default wipe data mode + wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, + LaunchConfigDelegate.DEFAULT_WIPE_DATA); + + // default disable boot animation option + wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, + LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM); + + // set default emulator options + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String emuOptions = store.getString(AdtPlugin.PREFS_EMU_OPTIONS); + wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions); + + // map the config and the project + wc.setMappedResources(getResourcesToMap(project)); + + // save the working copy to get the launch config object which we return. + return wc.doSave(); + + } catch (CoreException e) { + String msg = String.format( + "Failed to create a Launch config for project '%1$s': %2$s", + project.getName(), e.getMessage()); + AdtPlugin.printErrorToConsole(project, msg); + + // no launch! + return null; + } + } + + return config; + } + + /** + * Returns the list of resources to map to a Launch Configuration. + * @param project the project associated to the launch configuration. + */ + public static IResource[] getResourcesToMap(IProject project) { + ArrayList<IResource> array = new ArrayList<IResource>(2); + array.add(project); + + AndroidManifestHelper helper = new AndroidManifestHelper(project); + IFile manifest = helper.getManifestIFile(); + if (manifest != null) { + array.add(manifest); + } + + return array.toArray(new IResource[array.size()]); + } + + /** + * Launches an android app on the device or emulator + * + * @param project The project we're launching + * @param mode the mode in which to launch, one of the mode constants + * defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or + * <code>DEBUG_MODE</code>. + * @param apk the resource to the apk to launch. + * @param debuggable the debuggable value of the app, or null if not set. + * @param requiredApiVersionNumber the api version required by the app, or -1 if none. + * @param activity the class to provide to am to launch + * @param config the launch configuration + * @param launch the launch object + */ + public void launch(final IProject project, String mode, IFile apk, + String packageName, Boolean debuggable, int requiredApiVersionNumber, String activity, + final AndroidLaunchConfiguration config, final AndroidLaunch launch, + IProgressMonitor monitor) { + + String message; + if (config.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { + message = String.format("Only Syncing Application Package"); + } else { + message = String.format("Launching: %1$s", activity); + } + AdtPlugin.printToConsole(project, message); + + // create the launch info + final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName, + activity, apk, debuggable, requiredApiVersionNumber, config.mLaunchAction, + launch, monitor); + + // set the debug mode + launchInfo.mDebugMode = mode.equals(ILaunchManager.DEBUG_MODE); + + // get the SDK + Sdk currentSdk = Sdk.getCurrent(); + AvdManager avdManager = currentSdk.getAvdManager(); + + // get the project target + final IAndroidTarget projectTarget = currentSdk.getTarget(project); + + // FIXME: check errors on missing sdk, AVD manager, or project target. + + // device chooser response object. + final DeviceChooserResponse response = new DeviceChooserResponse(); + + /* + * Launch logic: + * - Manually Mode + * Always display a UI that lets a user see the current running emulators/devices. + * The UI must show which devices are compatibles, and allow launching new emulators + * with compatible (and not yet running) AVD. + * - Automatic Way + * * Preferred AVD set. + * If Preferred AVD is not running: launch it. + * Launch the application on the preferred AVD. + * * No preferred AVD. + * Count the number of compatible emulators/devices. + * If != 1, display a UI similar to manual mode. + * If == 1, launch the application on this AVD/device. + */ + + if (config.mTargetMode == AndroidLaunchConfiguration.AUTO_TARGET_MODE) { + // if we are in automatic target mode, we need to find the current devices + Device[] devices = AndroidDebugBridge.getBridge().getDevices(); + + // first check if we have a preferred AVD name, and if it actually exists, and is valid + // (ie able to run the project). + // We need to check this in case the AVD was recreated with a different target that is + // not compatible. + AvdInfo preferredAvd = null; + if (config.mAvdName != null) { + preferredAvd = avdManager.getAvd(config.mAvdName); + if (projectTarget.isCompatibleBaseFor(preferredAvd.getTarget()) == false) { + preferredAvd = null; + + AdtPlugin.printErrorToConsole(project, String.format( + "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...", + config.mAvdName, projectTarget.getName())); + } + } + + if (preferredAvd != null) { + // look for a matching device + for (Device d : devices) { + String deviceAvd = d.getAvdName(); + if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) { + response.setDeviceToUse(d); + + AdtPlugin.printToConsole(project, String.format( + "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'", + config.mAvdName, d)); + + continueLaunch(response, project, launch, launchInfo, config); + return; + } + } + + // at this point we have a valid preferred AVD that is not running. + // We need to start it. + response.setAvdToLaunch(preferredAvd); + + AdtPlugin.printToConsole(project, String.format( + "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.", + config.mAvdName)); + + continueLaunch(response, project, launch, launchInfo, config); + return; + } + + // no (valid) preferred AVD? look for one. + HashMap<Device, AvdInfo> compatibleRunningAvds = new HashMap<Device, AvdInfo>(); + boolean hasDevice = false; // if there's 1+ device running, we may force manual mode, + // as we cannot always detect proper compatibility with + // devices. This is the case if the project target is not + // a standard platform + for (Device d : devices) { + String deviceAvd = d.getAvdName(); + if (deviceAvd != null) { // physical devices return null. + AvdInfo info = avdManager.getAvd(deviceAvd); + if (info != null && projectTarget.isCompatibleBaseFor(info.getTarget())) { + compatibleRunningAvds.put(d, info); + } + } else { + if (projectTarget.isPlatform()) { // means this can run on any device as long + // as api level is high enough + String apiString = d.getProperty(SdkManager.PROP_VERSION_SDK); + try { + int apiNumber = Integer.parseInt(apiString); + if (apiNumber >= projectTarget.getApiVersionNumber()) { + // device is compatible with project + compatibleRunningAvds.put(d, null); + continue; + } + } catch (NumberFormatException e) { + // do nothing, we'll consider it a non compatible device below. + } + } + hasDevice = true; + } + } + + // depending on the number of devices, we'll simulate an automatic choice + // from the device chooser or simply show up the device chooser. + if (hasDevice == false && compatibleRunningAvds.size() == 0) { + // if zero emulators/devices, we launch an emulator. + // We need to figure out which AVD first. + + // we are going to take the closest AVD. ie a compatible AVD that has the API level + // closest to the project target. + AvdInfo[] avds = avdManager.getAvds(); + AvdInfo defaultAvd = null; + for (AvdInfo avd : avds) { + if (projectTarget.isCompatibleBaseFor(avd.getTarget())) { + if (defaultAvd == null || + avd.getTarget().getApiVersionNumber() < + defaultAvd.getTarget().getApiVersionNumber()) { + defaultAvd = avd; + } + } + } + + if (defaultAvd != null) { + response.setAvdToLaunch(defaultAvd); + + AdtPlugin.printToConsole(project, String.format( + "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'", + defaultAvd.getName())); + + continueLaunch(response, project, launch, launchInfo, config); + return; + } else { + // FIXME: ask the user if he wants to create a AVD. + // we found no compatible AVD. + AdtPlugin.printErrorToConsole(project, String.format( + "Failed to find a AVD compatible with target '%1$s'. Launch aborted.", + projectTarget.getName())); + launch.stopLaunch(); + return; + } + } else if (hasDevice == false && compatibleRunningAvds.size() == 1) { + Entry<Device, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next(); + response.setDeviceToUse(e.getKey()); + + // get the AvdInfo, if null, the device is a physical device. + AvdInfo avdInfo = e.getValue(); + if (avdInfo != null) { + message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'", + response.getDeviceToUse(), e.getValue().getName()); + } else { + message = String.format("Automatic Target Mode: using device '%1$s'", + response.getDeviceToUse()); + } + AdtPlugin.printToConsole(project, message); + + continueLaunch(response, project, launch, launchInfo, config); + return; + } + + // if more than one device, we'll bring up the DeviceChooser dialog below. + if (compatibleRunningAvds.size() >= 2) { + message = "Automatic Target Mode: Several compatible targets. Please select a target device."; + } else if (hasDevice) { + message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device."; + } + + AdtPlugin.printToConsole(project, message); + } + + // bring up the device chooser. + AdtPlugin.getDisplay().asyncExec(new Runnable() { + public void run() { + try { + // open the chooser dialog. It'll fill 'response' with the device to use + // or the AVD to launch. + DeviceChooserDialog dialog = new DeviceChooserDialog( + AdtPlugin.getDisplay().getActiveShell(), + response, launchInfo.mPackageName, projectTarget); + if (dialog.open() == Dialog.OK) { + AndroidLaunchController.this.continueLaunch(response, project, launch, + launchInfo, config); + } else { + AdtPlugin.printErrorToConsole(project, "Launch canceled!"); + launch.stopLaunch(); + return; + } + } catch (Exception e) { + // there seems to be some case where the shell will be null. (might be + // an OS X bug). Because of this the creation of the dialog will throw + // and IllegalArg exception interrupting the launch with no user feedback. + // So we trap all the exception and display something. + String msg = e.getMessage(); + if (msg == null) { + msg = e.getClass().getCanonicalName(); + } + AdtPlugin.printErrorToConsole(project, + String.format("Error during launch: %s", msg)); + launch.stopLaunch(); + } + } + }); + } + + /** + * Continues the launch based on the DeviceChooser response. + * @param response the device chooser response + * @param project The project being launched + * @param launch The eclipse launch info + * @param launchInfo The {@link DelayedLaunchInfo} + * @param config The config needed to start a new emulator. + */ + void continueLaunch(final DeviceChooserResponse response, final IProject project, + final AndroidLaunch launch, final DelayedLaunchInfo launchInfo, + final AndroidLaunchConfiguration config) { + + // Since this is called from the UI thread we spawn a new thread + // to finish the launch. + new Thread() { + @Override + public void run() { + if (response.getAvdToLaunch() != null) { + // there was no selected device, we start a new emulator. + synchronized (sListLock) { + AvdInfo info = response.getAvdToLaunch(); + mWaitingForEmulatorLaunches.add(launchInfo); + AdtPlugin.printToConsole(project, String.format( + "Launching a new emulator with Virtual Device '%1$s'", + info.getName())); + boolean status = launchEmulator(config, info); + + if (status == false) { + // launching the emulator failed! + AdtPlugin.displayError("Emulator Launch", + "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing."); + + // stop the launch and return + mWaitingForEmulatorLaunches.remove(launchInfo); + AdtPlugin.printErrorToConsole(project, "Launch canceled!"); + launch.stopLaunch(); + return; + } + + return; + } + } else if (response.getDeviceToUse() != null) { + launchInfo.mDevice = response.getDeviceToUse(); + simpleLaunch(launchInfo, launchInfo.mDevice); + } + } + }.start(); + } + + /** + * Queries for a debugger port for a specific {@link ILaunchConfiguration}. + * <p/> + * If the configuration and a debugger port where added through + * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method + * will return the debugger port, and remove the configuration from the list. + * @param launchConfig the {@link ILaunchConfiguration} + * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the + * configuration was not setup. + */ + static int getPortForConfig(ILaunchConfiguration launchConfig) { + synchronized (sListLock) { + Integer port = sRunningAppMap.get(launchConfig); + if (port != null) { + sRunningAppMap.remove(launchConfig); + return port; + } + } + + return LaunchConfigDelegate.INVALID_DEBUG_PORT; + } + + /** + * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of + * launch config to connect directly to a running app instead of doing full launch (sync, + * launch, and connect to). + * @param launchConfig the {@link ILaunchConfiguration} object. + * @param port The debugger port to connect to. + */ + private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig, + int port) { + synchronized (sListLock) { + sRunningAppMap.put(launchConfig, port); + } + } + + /** + * Checks the build information, and returns whether the launch should continue. + * <p/>The value tested are: + * <ul> + * <li>Minimum API version requested by the application. If the target device does not match, + * the launch is canceled.</li> + * <li>Debuggable attribute of the application and whether or not the device requires it. If + * the device requires it and it is not set in the manifest, the launch will be forced to + * "release" mode instead of "debug"</li> + * <ul> + */ + private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, Device device) { + if (device != null) { + // check the app required API level versus the target device API level + + String deviceApiVersionName = device.getProperty(Device.PROP_BUILD_VERSION); + String value = device.getProperty(Device.PROP_BUILD_VERSION_NUMBER); + int deviceApiVersionNumber = 0; + try { + deviceApiVersionNumber = Integer.parseInt(value); + } catch (NumberFormatException e) { + // pass, we'll keep the deviceVersionNumber value at 0. + } + + if (launchInfo.mRequiredApiVersionNumber == 0) { + // warn the API level requirement is not set. + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "WARNING: Application does not specify an API level requirement!"); + + // and display the target device API level (if known) + if (deviceApiVersionName == null || deviceApiVersionNumber == 0) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "WARNING: Unknown device API version!"); + } else { + AdtPlugin.printErrorToConsole(launchInfo.mProject, String.format( + "Device API version is %1$d (Android %2$s)", deviceApiVersionNumber, + deviceApiVersionName)); + } + } else { // app requires a specific API level + if (deviceApiVersionName == null || deviceApiVersionNumber == 0) { + AdtPlugin.printToConsole(launchInfo.mProject, + "WARNING: Unknown device API version!"); + } else if (deviceApiVersionNumber < launchInfo.mRequiredApiVersionNumber) { + String msg = String.format( + "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).", + launchInfo.mRequiredApiVersionNumber, deviceApiVersionNumber, + deviceApiVersionName); + AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + + // abort the launch + return false; + } + } + + // now checks that the device/app can be debugged (if needed) + if (device.isEmulator() == false && launchInfo.mDebugMode) { + String debuggableDevice = device.getProperty(Device.PROP_DEBUGGABLE); + if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$ + // the device is "secure" and requires apps to declare themselves as debuggable! + if (launchInfo.mDebuggable == null) { + String message1 = String.format( + "Device '%1$s' requires that applications explicitely declare themselves as debuggable in their manifest.", + device.getSerialNumber()); + String message2 = String.format("Application '%1$s' does not have the attribute 'debuggable' set to TRUE in its manifest and cannot be debugged.", + launchInfo.mPackageName); + AdtPlugin.printErrorToConsole(launchInfo.mProject, message1, message2); + + // because am -D does not check for ro.debuggable and the + // 'debuggable' attribute, it is important we do not use the -D option + // in this case or the app will wait for a debugger forever and never + // really launch. + launchInfo.mDebugMode = false; + } else if (launchInfo.mDebuggable == Boolean.FALSE) { + String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.", + launchInfo.mPackageName); + AdtPlugin.printErrorToConsole(launchInfo.mProject, message); + + // because am -D does not check for ro.debuggable and the + // 'debuggable' attribute, it is important we do not use the -D option + // in this case or the app will wait for a debugger forever and never + // really launch. + launchInfo.mDebugMode = false; + } + } + } + } + + return true; + } + + /** + * Do a simple launch on the specified device, attempting to sync the new + * package, and then launching the application. Failed sync/launch will + * stop the current AndroidLaunch and return false; + * @param launchInfo + * @param device + * @return true if succeed + */ + private boolean simpleLaunch(DelayedLaunchInfo launchInfo, Device device) { + // API level check + if (checkBuildInfo(launchInfo, device) == false) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, "Launch canceled!"); + launchInfo.mLaunch.stopLaunch(); + return false; + } + + // sync the app + if (syncApp(launchInfo, device) == false) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, "Launch canceled!"); + launchInfo.mLaunch.stopLaunch(); + return false; + } + + // launch the app + launchApp(launchInfo, device); + + return true; + } + + + /** + * Syncs the application on the device/emulator. + * + * @param launchInfo The Launch information object. + * @param device the device on which to sync the application + * @return true if the install succeeded. + */ + private boolean syncApp(DelayedLaunchInfo launchInfo, Device device) { + SyncService sync = device.getSyncService(); + if (sync != null) { + IPath path = launchInfo.mPackageFile.getLocation(); + String message = String.format("Uploading %1$s onto device '%2$s'", + path.lastSegment(), device.getSerialNumber()); + AdtPlugin.printToConsole(launchInfo.mProject, message); + + String osLocalPath = path.toOSString(); + String apkName = launchInfo.mPackageFile.getName(); + String remotePath = "/data/local/tmp/" + apkName; //$NON-NLS-1$ + + SyncResult result = sync.pushFile(osLocalPath, remotePath, + SyncService.getNullProgressMonitor()); + + if (result.getCode() != SyncService.RESULT_OK) { + String msg = String.format("Failed to upload %1$s on '%2$s': %3$s", + apkName, device.getSerialNumber(), result.getMessage()); + AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + return false; + } + + // Now that the package is uploaded, we can install it properly. + // This will check that there isn't another apk declaring the same package, or + // that another install used a different key. + boolean installResult = installPackage(launchInfo, remotePath, device); + + // now we delete the app we sync'ed + try { + device.executeShellCommand("rm " + remotePath, new MultiLineReceiver() { //$NON-NLS-1$ + @Override + public void processNewLines(String[] lines) { + // pass + } + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, String.format( + "Failed to delete temporary package: %1$s", e.getMessage())); + return false; + } + + return installResult; + } + + String msg = String.format( + "Failed to upload %1$s on device '%2$s': Unable to open sync connection!", + launchInfo.mPackageFile.getName(), device.getSerialNumber()); + AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + + return false; + } + + /** + * Installs the application package that was pushed to a temporary location on the device. + * @param launchInfo The launch information + * @param remotePath The remote path of the package. + * @param device The device on which the launch is done. + */ + private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath, + final Device device) { + + String message = String.format("Installing %1$s...", launchInfo.mPackageFile.getName()); + AdtPlugin.printToConsole(launchInfo.mProject, message); + + try { + String result = doInstall(launchInfo, remotePath, device, false /* reinstall */); + + /* For now we force to retry the install (after uninstalling) because there's no + * other way around it: adb install does not want to update a package w/o uninstalling + * the old one first! + */ + return checkInstallResult(result, device, launchInfo, remotePath, + InstallRetryMode.ALWAYS); + } catch (IOException e) { + // do nothing, we'll return false + } + + return false; + } + + /** + * Checks the result of an installation, and takes optional actions based on it. + * @param result the result string from the installation + * @param device the device on which the installation occured. + * @param launchInfo the {@link DelayedLaunchInfo} + * @param remotePath the temporary path of the package on the device + * @param retryMode indicates what to do in case, a package already exists. + * @return <code>true<code> if success, <code>false</code> otherwise. + * @throws IOException + */ + private boolean checkInstallResult(String result, Device device, DelayedLaunchInfo launchInfo, + String remotePath, InstallRetryMode retryMode) throws IOException { + if (result == null) { + AdtPlugin.printToConsole(launchInfo.mProject, "Success!"); + return true; + } else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$ + if (retryMode == InstallRetryMode.PROMPT) { + boolean prompt = AdtPlugin.displayPrompt("Application Install", + "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?"); + if (prompt) { + retryMode = InstallRetryMode.ALWAYS; + } else { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Installation error! The package already exists."); + return false; + } + } + + if (retryMode == InstallRetryMode.ALWAYS) { + /* + * TODO: create a UI that gives the dev the choice to: + * - clean uninstall on launch + * - full uninstall if application exists. + * - soft uninstall if application exists (keeps the app data around). + * - always ask (choice of soft-reinstall, full reinstall) + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Application already exists, uninstalling..."); + String res = doUninstall(device, launchInfo); + if (res == null) { + AdtPlugin.printToConsole(launchInfo.mProject, "Success!"); + } else { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format("Failed to uninstall: %1$s", res)); + return false; + } + */ + + AdtPlugin.printToConsole(launchInfo.mProject, + "Application already exists. Attempting to re-install instead..."); + String res = doInstall(launchInfo, remotePath, device, true /* reinstall */); + return checkInstallResult(res, device, launchInfo, remotePath, + InstallRetryMode.NEVER); + } + + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Installation error! The package already exists."); + } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$ + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Installation failed due to invalid APK file!", + "Please check logcat output for more details."); + } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$ + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Installation failed due to invalid URI!", + "Please check logcat output for more details."); + } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$ + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format("Installation failed: Could not copy %1$s to its final location!", + launchInfo.mPackageFile.getName()), + "Please check logcat output for more details."); + } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Re-installation failed due to different application signatures.", + "You must perform a full uninstall of the application. WARNING: This will remove the application data!", + String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.mPackageName)); + } else { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format("Installation error: %1$s", result), + "Please check logcat output for more details."); + } + + return false; + } + + /** + * Performs the uninstallation of an application. + * @param device the device on which to install the application. + * @param launchInfo the {@link DelayedLaunchInfo}. + * @return a {@link String} with an error code, or <code>null</code> if success. + * @throws IOException + */ + @SuppressWarnings("unused") + private String doUninstall(Device device, DelayedLaunchInfo launchInfo) throws IOException { + InstallReceiver receiver = new InstallReceiver(); + try { + device.executeShellCommand("pm uninstall " + launchInfo.mPackageName, //$NON-NLS-1$ + receiver); + } catch (IOException e) { + String msg = String.format( + "Failed to uninstall %1$s: %2$s", launchInfo.mPackageName, e.getMessage()); + AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + throw e; + } + + return receiver.getSuccess(); + } + + /** + * Performs the installation of an application whose package has been uploaded on the device. + * <p/>Before doing it, if the application is already running on the device, it is killed. + * @param launchInfo the {@link DelayedLaunchInfo}. + * @param remotePath the path of the application package in the device tmp folder. + * @param device the device on which to install the application. + * @param reinstall + * @return a {@link String} with an error code, or <code>null</code> if success. + * @throws IOException + */ + private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath, + final Device device, boolean reinstall) throws IOException { + // kill running application + Client application = device.getClient(launchInfo.mPackageName); + if (application != null) { + application.kill(); + } + + InstallReceiver receiver = new InstallReceiver(); + try { + String cmd = String.format( + reinstall ? "pm install -r \"%1$s\"" : "pm install \"%1$s\"", //$NON-NLS-1$ //$NON-NLS-2$ + remotePath); //$NON-NLS-1$ //$NON-NLS-2$ + device.executeShellCommand(cmd, receiver); + } catch (IOException e) { + String msg = String.format( + "Failed to install %1$s on device '%2$s': %3$s", + launchInfo.mPackageFile.getName(), device.getSerialNumber(), e.getMessage()); + AdtPlugin.printErrorToConsole(launchInfo.mProject, msg); + throw e; + } + + return receiver.getSuccess(); + } + + /** + * launches an application on a device or emulator + * + * @param info the {@link DelayedLaunchInfo} that indicates the activity to launch + * @param device the device or emulator to launch the application on + */ + private void launchApp(final DelayedLaunchInfo info, Device device) { + // if we're not supposed to do anything, just stop the Launch item and return; + if (info.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { + String msg = String.format("%1$s installed on device", + info.mPackageFile.getFullPath().toOSString()); + AdtPlugin.printToConsole(info.mProject, msg, "Done!"); + info.mLaunch.stopLaunch(); + return; + } + try { + String msg = String.format("Starting activity %1$s on device ", info.mActivity, + info.mDevice); + AdtPlugin.printToConsole(info.mProject, msg); + + // In debug mode, we need to add the info to the list of application monitoring + // client changes. + if (info.mDebugMode) { + synchronized (sListLock) { + if (mWaitingForDebuggerApplications.contains(info) == false) { + mWaitingForDebuggerApplications.add(info); + } + } + } + + // increment launch attempt count, to handle retries and timeouts + info.mAttemptCount++; + + // now we actually launch the app. + device.executeShellCommand("am start" //$NON-NLS-1$ + + (info.mDebugMode ? " -D" //$NON-NLS-1$ + : "") //$NON-NLS-1$ + + " -n " //$NON-NLS-1$ + + info.mPackageName + "/" //$NON-NLS-1$ + + info.mActivity.replaceAll("\\$", "\\\\\\$"), //$NON-NLS-1$ //$NON-NLS-2$ + new AMReceiver(info, device)); + + // if the app is not a debug app, we need to do some clean up, as + // the process is done! + if (info.mDebugMode == false) { + // stop the launch object, since there's no debug, and it can't + // provide any control over the app + info.mLaunch.stopLaunch(); + } + } catch (IOException e) { + // something went wrong trying to launch the app. + // lets stop the Launch + AdtPlugin.printErrorToConsole(info.mProject, + String.format("Launch error: %s", e.getMessage())); + info.mLaunch.stopLaunch(); + + // and remove it from the list of app waiting for debuggers + synchronized (sListLock) { + mWaitingForDebuggerApplications.remove(info); + } + } + } + + private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) { + + // split the custom command line in segments + ArrayList<String> customArgs = new ArrayList<String>(); + boolean has_wipe_data = false; + if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) { + String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$ + + // we need to remove the empty strings + for (String s : segments) { + if (s.length() > 0) { + customArgs.add(s); + if (!has_wipe_data && s.equals(FLAG_WIPE_DATA)) { + has_wipe_data = true; + } + } + } + } + + boolean needs_wipe_data = config.mWipeData && !has_wipe_data; + if (needs_wipe_data) { + if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) { + needs_wipe_data = false; + } + } + + // build the command line based on the available parameters. + ArrayList<String> list = new ArrayList<String>(); + + list.add(AdtPlugin.getOsAbsoluteEmulator()); + list.add(FLAG_AVD); + list.add(avdToLaunch.getName()); + + if (config.mNetworkSpeed != null) { + list.add(FLAG_NETSPEED); + list.add(config.mNetworkSpeed); + } + + if (config.mNetworkDelay != null) { + list.add(FLAG_NETDELAY); + list.add(config.mNetworkDelay); + } + + if (needs_wipe_data) { + list.add(FLAG_WIPE_DATA); + } + + if (config.mNoBootAnim) { + list.add(FLAG_NO_BOOT_ANIM); + } + + list.addAll(customArgs); + + // convert the list into an array for the call to exec. + String[] command = list.toArray(new String[list.size()]); + + // launch the emulator + try { + Process process = Runtime.getRuntime().exec(command); + grabEmulatorOutput(process); + } catch (IOException e) { + return false; + } + + return true; + } + + /** + * Looks for and returns an existing {@link ILaunchConfiguration} object for a + * specified project. + * @param manager The {@link ILaunchManager}. + * @param type The {@link ILaunchConfigurationType}. + * @param projectName The name of the project + * @return an existing <code>ILaunchConfiguration</code> object matching the project, or + * <code>null</code>. + */ + private static ILaunchConfiguration findConfig(ILaunchManager manager, + ILaunchConfigurationType type, String projectName) { + try { + ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type); + + for (ILaunchConfiguration config : configs) { + if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, + "").equals(projectName)) { //$NON-NLS-1$ + return config; + } + } + } catch (CoreException e) { + MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(), + "Launch Error", e.getStatus().getMessage()); + } + + // didn't find anything that matches. Return null + return null; + + } + + + /** + * Connects a remote debugger on the specified port. + * @param debugPort The port to connect the debugger to + * @param launch The associated AndroidLaunch object. + * @param monitor A Progress monitor + * @return false if cancelled by the monitor + * @throws CoreException + */ + public static boolean connectRemoteDebugger(int debugPort, + AndroidLaunch launch, IProgressMonitor monitor) + throws CoreException { + // get some default parameters. + int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT); + + HashMap<String, String> newMap = new HashMap<String, String>(); + + newMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$ + + newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$ + + newMap.put("timeout", Integer.toString(connectTimeout)); + + // get the default VM connector + IVMConnector connector = JavaRuntime.getDefaultVMConnector(); + + // connect to remote VM + connector.connect(newMap, monitor, launch); + + // check for cancellation + if (monitor.isCanceled()) { + IDebugTarget[] debugTargets = launch.getDebugTargets(); + for (IDebugTarget target : debugTargets) { + if (target.canDisconnect()) { + target.disconnect(); + } + } + return false; + } + + return true; + } + + /** + * Launch a new thread that connects a remote debugger on the specified port. + * @param debugPort The port to connect the debugger to + * @param androidLaunch The associated AndroidLaunch object. + * @param monitor A Progress monitor + * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor) + */ + public static void launchRemoteDebugger( final int debugPort, final AndroidLaunch androidLaunch, + final IProgressMonitor monitor) { + new Thread("Debugger connection") { //$NON-NLS-1$ + @Override + public void run() { + try { + connectRemoteDebugger(debugPort, androidLaunch, monitor); + } catch (CoreException e) { + androidLaunch.stopLaunch(); + } + monitor.done(); + } + }.start(); + } + + /** + * Sent when a new {@link AndroidDebugBridge} is started. + * <p/> + * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + * + * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge) + */ + public void bridgeChanged(AndroidDebugBridge bridge) { + // The adb server has changed. We cancel any pending launches. + String message1 = "adb server change: cancelling '%1$s' launch!"; + String message2 = "adb server change: cancelling sync!"; + synchronized (sListLock) { + for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) { + if (launchInfo.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, message2); + } else { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format(message1, launchInfo.mActivity)); + } + launchInfo.mLaunch.stopLaunch(); + } + for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) { + if (launchInfo.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, message2); + } else { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format(message1, launchInfo.mActivity)); + } + launchInfo.mLaunch.stopLaunch(); + } + + mWaitingForReadyEmulatorList.clear(); + mWaitingForDebuggerApplications.clear(); + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(Device) + */ + public void deviceConnected(Device device) { + synchronized (sListLock) { + // look if there's an app waiting for a device + if (mWaitingForEmulatorLaunches.size() > 0) { + // get/remove first launch item from the list + // FIXME: what if we have multiple launches waiting? + DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0); + mWaitingForEmulatorLaunches.remove(0); + + // give the launch item its device for later use. + launchInfo.mDevice = device; + + // and move it to the other list + mWaitingForReadyEmulatorList.add(launchInfo); + + // and tell the user about it + AdtPlugin.printToConsole(launchInfo.mProject, + String.format("New emulator found: %1$s", device.getSerialNumber())); + AdtPlugin.printToConsole(launchInfo.mProject, + String.format("Waiting for HOME ('%1$s') to be launched...", + AdtPlugin.getDefault().getPreferenceStore().getString( + AdtPlugin.PREFS_HOME_PACKAGE))); + } + } + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(Device) + */ + @SuppressWarnings("unchecked") + public void deviceDisconnected(Device device) { + // any pending launch on this device must be canceled. + String message = "%1$s disconnected! Cancelling '%2$s' launch!"; + synchronized (sListLock) { + ArrayList<DelayedLaunchInfo> copyList = + (ArrayList<DelayedLaunchInfo>)mWaitingForReadyEmulatorList.clone(); + for (DelayedLaunchInfo launchInfo : copyList) { + if (launchInfo.mDevice == device) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format(message, device.getSerialNumber(), launchInfo.mActivity)); + launchInfo.mLaunch.stopLaunch(); + mWaitingForReadyEmulatorList.remove(launchInfo); + } + } + copyList = (ArrayList<DelayedLaunchInfo>)mWaitingForDebuggerApplications.clone(); + for (DelayedLaunchInfo launchInfo : copyList) { + if (launchInfo.mDevice == device) { + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format(message, device.getSerialNumber(), launchInfo.mActivity)); + launchInfo.mLaunch.stopLaunch(); + mWaitingForDebuggerApplications.remove(launchInfo); + } + } + } + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(Device, int) + */ + public void deviceChanged(Device device, int changeMask) { + // We could check if any starting device we care about is now ready, but we can wait for + // its home app to show up, so... + } + + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + public void clientChanged(final Client client, int changeMask) { + boolean connectDebugger = false; + if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) { + String applicationName = client.getClientData().getClientDescription(); + if (applicationName != null) { + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String home = store.getString(AdtPlugin.PREFS_HOME_PACKAGE); + + if (home.equals(applicationName)) { + + // looks like home is up, get its device + Device device = client.getDevice(); + + // look for application waiting for home + synchronized (sListLock) { + for (int i = 0 ; i < mWaitingForReadyEmulatorList.size() ;) { + DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i); + if (launchInfo.mDevice == device) { + // it's match, remove from the list + mWaitingForReadyEmulatorList.remove(i); + + // We couldn't check earlier the API level of the device + // (it's asynchronous when the device boot, and usually + // deviceConnected is called before it's queried for its build info) + // so we check now + if (checkBuildInfo(launchInfo, device) == false) { + // device is not the proper API! + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Launch canceled!"); + launchInfo.mLaunch.stopLaunch(); + return; + } + + AdtPlugin.printToConsole(launchInfo.mProject, + String.format("HOME is up on device '%1$s'", + device.getSerialNumber())); + + // attempt to sync the new package onto the device. + if (syncApp(launchInfo, device)) { + // application package is sync'ed, lets attempt to launch it. + launchApp(launchInfo, device); + } else { + // failure! Cancel and return + AdtPlugin.printErrorToConsole(launchInfo.mProject, + "Launch canceled!"); + launchInfo.mLaunch.stopLaunch(); + } + + break; + } else { + i++; + } + } + } + } + + // check if it's already waiting for a debugger, and if so we connect to it. + if (client.getClientData().getDebuggerConnectionStatus() == ClientData.DEBUGGER_WAITING) { + // search for this client in the list; + synchronized (sListLock) { + int index = mUnknownClientsWaitingForDebugger.indexOf(client); + if (index != -1) { + connectDebugger = true; + mUnknownClientsWaitingForDebugger.remove(client); + } + } + } + } + } + + // if it's not home, it could be an app that is now in debugger mode that we're waiting for + // lets check it + + if ((changeMask & Client.CHANGE_DEBUGGER_INTEREST) == Client.CHANGE_DEBUGGER_INTEREST) { + ClientData clientData = client.getClientData(); + String applicationName = client.getClientData().getClientDescription(); + if (clientData.getDebuggerConnectionStatus() == ClientData.DEBUGGER_WAITING) { + // Get the application name, and make sure its valid. + if (applicationName == null) { + // looks like we don't have the client yet, so we keep it around for when its + // name becomes available. + synchronized (sListLock) { + mUnknownClientsWaitingForDebugger.add(client); + } + return; + } else { + connectDebugger = true; + } + } + } + + if (connectDebugger) { + Log.d("adt", "Debugging " + client); + // now check it against the apps waiting for a debugger + String applicationName = client.getClientData().getClientDescription(); + Log.d("adt", "App Name: " + applicationName); + synchronized (sListLock) { + for (int i = 0 ; i < mWaitingForDebuggerApplications.size() ;) { + final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i); + if (client.getDevice() == launchInfo.mDevice && + applicationName.equals(launchInfo.mPackageName)) { + // this is a match. We remove the launch info from the list + mWaitingForDebuggerApplications.remove(i); + + // and connect the debugger. + String msg = String.format( + "Attempting to connect debugger to '%1$s' on port %2$d", + launchInfo.mPackageName, client.getDebuggerListenPort()); + AdtPlugin.printToConsole(launchInfo.mProject, msg); + + new Thread("Debugger Connection") { //$NON-NLS-1$ + @Override + public void run() { + try { + if (connectRemoteDebugger( + client.getDebuggerListenPort(), + launchInfo.mLaunch, launchInfo.mMonitor) == false) { + return; + } + } catch (CoreException e) { + // well something went wrong. + AdtPlugin.printErrorToConsole(launchInfo.mProject, + String.format("Launch error: %s", e.getMessage())); + // stop the launch + launchInfo.mLaunch.stopLaunch(); + } + + launchInfo.mMonitor.done(); + } + }.start(); + + // we're done processing this client. + return; + + } else { + i++; + } + } + } + + // if we get here, we haven't found an app that we were launching, so we look + // for opened android projects that contains the app asking for a debugger. + // If we find one, we automatically connect to it. + IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); + + if (project != null) { + debugRunningApp(project, client.getDebuggerListenPort()); + } + } + } + + /** + * Get the stderr/stdout outputs of a process and return when the process is done. + * Both <b>must</b> be read or the process will block on windows. + * @param process The process to get the ouput from + */ + private void grabEmulatorOutput(final Process process) { + // read the lines as they come. if null is returned, it's + // because the process finished + new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + AdtPlugin.printErrorToConsole("Emulator", line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }.start(); + + new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + AdtPlugin.printToConsole("Emulator", line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }.start(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java new file mode 100644 index 0000000..a260350 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java @@ -0,0 +1,705 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.launching; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Device.DeviceState; +import com.android.ddmuilib.IImageLoader; +import com.android.ddmuilib.ImageHelper; +import com.android.ddmuilib.TableHelper; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.AvdManager.AvdInfo; +import com.android.sdkuilib.AvdSelector; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; + +import java.util.ArrayList; + +public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { + + private final static int ICON_WIDTH = 16; + + private final static String PREFS_COL_SERIAL = "deviceChooser.serial"; //$NON-NLS-1$ + private final static String PREFS_COL_STATE = "deviceChooser.state"; //$NON-NLS-1$ + private final static String PREFS_COL_AVD = "deviceChooser.avd"; //$NON-NLS-1$ + private final static String PREFS_COL_TARGET = "deviceChooser.target"; //$NON-NLS-1$ + private final static String PREFS_COL_DEBUG = "deviceChooser.debug"; //$NON-NLS-1$ + + private Table mDeviceTable; + private TableViewer mViewer; + private AvdSelector mPreferredAvdSelector; + + private Image mDeviceImage; + private Image mEmulatorImage; + private Image mMatchImage; + private Image mNoMatchImage; + private Image mWarningImage; + + private final DeviceChooserResponse mResponse; + private final String mPackageName; + private final IAndroidTarget mProjectTarget; + private final Sdk mSdk; + + private final AvdInfo[] mFullAvdList; + + private Button mDeviceRadioButton; + + private boolean mDisableAvdSelectionChange = false; + + /** + * Basic Content Provider for a table full of {@link Device} objects. The input is + * a {@link AndroidDebugBridge}. + */ + private class ContentProvider implements IStructuredContentProvider { + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return ((AndroidDebugBridge)inputElement).getDevices(); + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + + /** + * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}. + * It provides labels and images for {@link Device} objects. + */ + private class LabelProvider implements ITableLabelProvider { + + public Image getColumnImage(Object element, int columnIndex) { + if (element instanceof Device) { + Device device = (Device)element; + switch (columnIndex) { + case 0: + return device.isEmulator() ? mEmulatorImage : mDeviceImage; + + case 2: + // check for compatibility. + if (device.isEmulator() == false) { // physical device + // get the api level of the device + try { + String apiValue = device.getProperty( + IDevice.PROP_BUILD_VERSION_NUMBER); + if (apiValue != null) { + int api = Integer.parseInt(apiValue); + if (api >= mProjectTarget.getApiVersionNumber()) { + // if the project is compiling against an add-on, the optional + // API may be missing from the device. + return mProjectTarget.isPlatform() ? + mMatchImage : mWarningImage; + } else { + return mNoMatchImage; + } + } else { + return mWarningImage; + } + } catch (NumberFormatException e) { + // lets consider the device non compatible + return mNoMatchImage; + } + } else { + // get the AvdInfo + AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName()); + if (info == null) { + return mWarningImage; + } + return mProjectTarget.isCompatibleBaseFor(info.getTarget()) ? + mMatchImage : mNoMatchImage; + } + } + } + + return null; + } + + public String getColumnText(Object element, int columnIndex) { + if (element instanceof Device) { + Device device = (Device)element; + switch (columnIndex) { + case 0: + return device.getSerialNumber(); + case 1: + if (device.isEmulator()) { + return device.getAvdName(); + } else { + return "N/A"; // devices don't have AVD names. + } + case 2: + if (device.isEmulator()) { + AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName()); + if (info == null) { + return "?"; + } + return info.getTarget().getFullName(); + } else { + String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); + if (deviceBuild == null) { + return "unknown"; + } + return deviceBuild; + } + case 3: + String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return "Yes"; + } else { + return ""; + } + case 4: + return getStateString(device); + } + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + public static class DeviceChooserResponse { + private AvdInfo mAvdToLaunch; + private Device mDeviceToUse; + + public void setDeviceToUse(Device d) { + mDeviceToUse = d; + mAvdToLaunch = null; + } + + public void setAvdToLaunch(AvdInfo avd) { + mAvdToLaunch = avd; + mDeviceToUse = null; + } + + public Device getDeviceToUse() { + return mDeviceToUse; + } + + public AvdInfo getAvdToLaunch() { + return mAvdToLaunch; + } + } + + public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName, + IAndroidTarget projectTarget) { + super(parent); + mResponse = response; + mPackageName = packageName; + mProjectTarget = projectTarget; + mSdk = Sdk.getCurrent(); + + // get the full list of Android Virtual Devices + AvdManager avdManager = mSdk.getAvdManager(); + if (avdManager != null) { + mFullAvdList = avdManager.getAvds(); + } else { + mFullAvdList = null; + } + + loadImages(); + } + + private void cleanup() { + // done listening. + AndroidDebugBridge.removeDeviceChangeListener(this); + + mEmulatorImage.dispose(); + mDeviceImage.dispose(); + mMatchImage.dispose(); + mNoMatchImage.dispose(); + mWarningImage.dispose(); + } + + @Override + protected void okPressed() { + cleanup(); + super.okPressed(); + } + + @Override + protected void cancelPressed() { + cleanup(); + super.cancelPressed(); + } + + @Override + protected Control createContents(Composite parent) { + Control content = super.createContents(parent); + + // this must be called after createContents() has happened so that the + // ok button has been created (it's created after the call to createDialogArea) + updateDefaultSelection(); + + return content; + } + + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, true)); + + mDeviceRadioButton = new Button(top, SWT.RADIO); + mDeviceRadioButton.setText("Choose a running Android device"); + mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + boolean deviceMode = mDeviceRadioButton.getSelection(); + + mDeviceTable.setEnabled(deviceMode); + mPreferredAvdSelector.setEnabled(!deviceMode); + + if (deviceMode) { + handleDeviceSelection(); + } else { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getFirstSelected()); + } + + enableOkButton(); + } + }); + mDeviceRadioButton.setSelection(true); + + + // offset the selector from the radio button + Composite offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION); + GridData gd; + mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.heightHint = 100; + + mDeviceTable.setHeaderVisible(true); + mDeviceTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDeviceTable, "Serial Number", + SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ + PREFS_COL_SERIAL, store); + + TableHelper.createTableColumn(mDeviceTable, "AVD Name", + SWT.LEFT, "engineering", //$NON-NLS-1$ + PREFS_COL_AVD, store); + + TableHelper.createTableColumn(mDeviceTable, "Target", + SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ + PREFS_COL_TARGET, store); + + TableHelper.createTableColumn(mDeviceTable, "Debug", + SWT.LEFT, "Debug", //$NON-NLS-1$ + PREFS_COL_DEBUG, store); + + TableHelper.createTableColumn(mDeviceTable, "State", + SWT.LEFT, "bootloader", //$NON-NLS-1$ + PREFS_COL_STATE, store); + + // create the viewer for it + mViewer = new TableViewer(mDeviceTable); + mViewer.setContentProvider(new ContentProvider()); + mViewer.setLabelProvider(new LabelProvider()); + mViewer.setInput(AndroidDebugBridge.getBridge()); + mViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof Device) { + mResponse.setDeviceToUse((Device)object); + } + } + } + }); + + Button radio2 = new Button(top, SWT.RADIO); + radio2.setText("Launch a new Android Virtual Device"); + + // offset the selector from the radio button + offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mPreferredAvdSelector = new AvdSelector(offsetComp, getNonRunningAvds(), mProjectTarget, + false /*allowMultipleSelection*/); + mPreferredAvdSelector.setTableHeightHint(100); + mPreferredAvdSelector.setEnabled(false); + mDeviceTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + handleDeviceSelection(); + } + }); + + mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mDisableAvdSelectionChange == false) { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getFirstSelected()); + enableOkButton(); + } + } + }); + + AndroidDebugBridge.addDeviceChangeListener(this); + + return top; + } + + private void loadImages() { + IImageLoader ddmsLoader = DdmsPlugin.getImageLoader(); + Display display = DdmsPlugin.getDisplay(); + IImageLoader adtLoader = AdtPlugin.getImageLoader(); + + if (mDeviceImage == null) { + mDeviceImage = ImageHelper.loadImage(ddmsLoader, display, + "device.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + if (mEmulatorImage == null) { + mEmulatorImage = ImageHelper.loadImage(ddmsLoader, display, + "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_BLUE)); + } + + if (mMatchImage == null) { + mMatchImage = ImageHelper.loadImage(adtLoader, display, + "match.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_GREEN)); + } + + if (mNoMatchImage == null) { + mNoMatchImage = ImageHelper.loadImage(adtLoader, display, + "error.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + + if (mWarningImage == null) { + mWarningImage = ImageHelper.loadImage(adtLoader, display, + "warning.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } + + } + + /** + * Returns a display string representing the state of the device. + * @param d the device + */ + private static String getStateString(Device d) { + DeviceState deviceState = d.getState(); + if (deviceState == DeviceState.ONLINE) { + return "Online"; + } else if (deviceState == DeviceState.OFFLINE) { + return "Offline"; + } else if (deviceState == DeviceState.BOOTLOADER) { + return "Bootloader"; + } + + return "??"; + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(Device) + */ + public void deviceConnected(Device device) { + final DeviceChooserDialog dialog = this; + exec(new Runnable() { + public void run() { + if (mDeviceTable.isDisposed() == false) { + // refresh all + mViewer.refresh(); + + // update the selection + updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD.) + refillAvdList(); + } else { + // table is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(dialog); + } + + } + }); + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(Device) + */ + public void deviceDisconnected(Device device) { + deviceConnected(device); + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(Device, int) + */ + public void deviceChanged(final Device device, int changeMask) { + if ((changeMask & (Device.CHANGE_STATE | Device.CHANGE_BUILD_INFO)) != 0) { + final DeviceChooserDialog dialog = this; + exec(new Runnable() { + public void run() { + if (mDeviceTable.isDisposed() == false) { + // refresh the device + mViewer.refresh(device); + + // update the defaultSelection. + updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD). This is done on deviceChanged because the avd name + // of a (emulator) device may be updated as the emulator boots. + refillAvdList(); + + // if the changed device is the current selection, + // we update the OK button based on its state. + if (device == mResponse.getDeviceToUse()) { + enableOkButton(); + } + + } else { + // table is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(dialog); + } + } + }); + } + } + + /** + * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). + */ + private boolean isDeviceMode() { + return mDeviceRadioButton.getSelection(); + } + + /** + * Enables or disables the OK button of the dialog based on various selections in the dialog. + */ + private void enableOkButton() { + Button okButton = getButton(IDialogConstants.OK_ID); + + if (isDeviceMode()) { + okButton.setEnabled(mResponse.getDeviceToUse() != null && + mResponse.getDeviceToUse().isOnline()); + } else { + okButton.setEnabled(mResponse.getAvdToLaunch() != null); + } + } + + /** + * Executes the {@link Runnable} in the UI thread. + * @param runnable the runnable to execute. + */ + private void exec(Runnable runnable) { + try { + Display display = mDeviceTable.getDisplay(); + display.asyncExec(runnable); + } catch (SWTException e) { + // tree is disposed, we need to do something. lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(this); + } + } + + private void handleDeviceSelection() { + int count = mDeviceTable.getSelectionCount(); + if (count != 1) { + handleSelection(null); + } else { + int index = mDeviceTable.getSelectionIndex(); + Object data = mViewer.getElementAt(index); + if (data instanceof Device) { + handleSelection((Device)data); + } else { + handleSelection(null); + } + } + } + + private void handleSelection(Device device) { + mResponse.setDeviceToUse(device); + enableOkButton(); + } + + /** + * Look for a default device to select. This is done by looking for the running + * clients on each device and finding one similar to the one being launched. + * <p/> + * This is done every time the device list changed unless there is a already selection. + */ + private void updateDefaultSelection() { + if (mDeviceTable.getSelectionCount() == 0) { + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + + Device[] devices = bridge.getDevices(); + + for (Device device : devices) { + Client[] clients = device.getClients(); + + for (Client client : clients) { + + if (mPackageName.equals(client.getClientData().getClientDescription())) { + // found a match! Select it. + mViewer.setSelection(new StructuredSelection(device)); + handleSelection(device); + + // and we're done. + return; + } + } + } + } + + handleDeviceSelection(); + } + + /** + * Returns the list of {@link AvdInfo} that are not already running in an emulator. + */ + private AvdInfo[] getNonRunningAvds() { + ArrayList<AvdInfo> list = new ArrayList<AvdInfo>(); + + Device[] devices = AndroidDebugBridge.getBridge().getDevices(); + + // loop through all the Avd and put the one that are not running in the list. + avdLoop: for (AvdInfo info : mFullAvdList) { + for (Device d : devices) { + if (info.getName().equals(d.getAvdName())) { + continue avdLoop; + } + } + list.add(info); + } + + return list.toArray(new AvdInfo[list.size()]); + } + + /** + * Refills the AVD list keeping the current selection. + */ + private void refillAvdList() { + AvdInfo[] array = getNonRunningAvds(); + + // save the current selection + AvdInfo selected = mPreferredAvdSelector.getFirstSelected(); + + // disable selection change. + mDisableAvdSelectionChange = true; + + // set the new list in the selector + mPreferredAvdSelector.setAvds(array, mProjectTarget); + + // attempt to reselect the proper avd if needed + if (selected != null) { + if (mPreferredAvdSelector.setSelection(selected) == false) { + // looks like the selection is lost. this can happen if an emulator + // running the AVD that was selected was launched from outside of Eclipse). + mResponse.setAvdToLaunch(null); + enableOkButton(); + } + } + + // enable the selection change + mDisableAvdSelectionChange = false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java new file mode 100644 index 0000000..bbd320b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.launching; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController.AndroidLaunchConfiguration; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.model.LaunchConfigurationDelegate; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; + +/** + * Implementation of an eclipse LauncConfigurationDelegate to launch android + * application in debug. + */ +public class LaunchConfigDelegate extends LaunchConfigurationDelegate { + final static int INVALID_DEBUG_PORT = -1; + + public final static String ANDROID_LAUNCH_TYPE_ID = + "com.android.ide.eclipse.adt.debug.LaunchConfigType"; //$NON-NLS-1$ + + /** Target mode parameters: true is automatic, false is manual */ + public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$ + public static final boolean DEFAULT_TARGET_MODE = true; //automatic mode + + /** + * Launch action: + * <ul> + * <li>0: launch default activity</li> + * <li>1: launch specified activity. See {@link #ATTR_ACTIVITY}</li> + * <li>2: Do Nothing</li> + * </ul> + */ + public final static String ATTR_LAUNCH_ACTION = AdtPlugin.PLUGIN_ID + ".action"; //$NON-NLS-1$ + + /** Default launch action. This launches the activity that is setup to be found in the HOME + * screen. + */ + public final static int ACTION_DEFAULT = 0; + /** Launch action starting a specific activity. */ + public final static int ACTION_ACTIVITY = 1; + /** Launch action that does nothing. */ + public final static int ACTION_DO_NOTHING = 2; + /** Default launch action value. */ + public final static int DEFAULT_LAUNCH_ACTION = ACTION_DEFAULT; + + /** + * Activity to be launched if {@link #ATTR_LAUNCH_ACTION} is 1 + */ + public static final String ATTR_ACTIVITY = AdtPlugin.PLUGIN_ID + ".activity"; //$NON-NLS-1$ + + public static final String ATTR_AVD_NAME = AdtPlugin.PLUGIN_ID + ".avd"; //$NON-NLS-1$ + + public static final String ATTR_SPEED = AdtPlugin.PLUGIN_ID + ".speed"; //$NON-NLS-1$ + + /** + * Index of the default network speed setting for the emulator.<br> + * Get the emulator option with <code>EmulatorConfigTab.getSpeed(index)</code> + */ + public static final int DEFAULT_SPEED = 0; + + public static final String ATTR_DELAY = AdtPlugin.PLUGIN_ID + ".delay"; //$NON-NLS-1$ + + /** + * Index of the default network latency setting for the emulator.<br> + * Get the emulator option with <code>EmulatorConfigTab.getDelay(index)</code> + */ + public static final int DEFAULT_DELAY = 0; + + public static final String ATTR_COMMANDLINE = AdtPlugin.PLUGIN_ID + ".commandline"; //$NON-NLS-1$ + + public static final String ATTR_WIPE_DATA = AdtPlugin.PLUGIN_ID + ".wipedata"; //$NON-NLS-1$ + public static final boolean DEFAULT_WIPE_DATA = false; + + public static final String ATTR_NO_BOOT_ANIM = AdtPlugin.PLUGIN_ID + ".nobootanim"; //$NON-NLS-1$ + public static final boolean DEFAULT_NO_BOOT_ANIM = false; + + public static final String ATTR_DEBUG_PORT = + AdtPlugin.PLUGIN_ID + ".debugPort"; //$NON-NLS-1$ + + public void launch(ILaunchConfiguration configuration, String mode, + ILaunch launch, IProgressMonitor monitor) throws CoreException { + // We need to check if it's a standard launch or if it's a launch + // to debug an application already running. + int debugPort = AndroidLaunchController.getPortForConfig(configuration); + + // get the project + IProject project = getProject(configuration); + + // first we make sure the launch is of the proper type + AndroidLaunch androidLaunch = null; + if (launch instanceof AndroidLaunch) { + androidLaunch = (AndroidLaunch)launch; + } else { + // wrong type, not sure how we got there, but we don't do + // anything else + AdtPlugin.printErrorToConsole(project, "Wrong Launch Type!"); + return; + } + + // if we have a valid debug port, this means we're debugging an app + // that's already launched. + if (debugPort != INVALID_DEBUG_PORT) { + AndroidLaunchController.launchRemoteDebugger(debugPort, androidLaunch, monitor); + return; + } + + if (project == null) { + AdtPlugin.printErrorToConsole("Couldn't get project object!"); + androidLaunch.stopLaunch(); + return; + } + + // check if the project has errors, and abort in this case. + if (ProjectHelper.hasError(project, true)) { + AdtPlugin.displayError("Android Launch", + "Your project contains error(s), please fix them before running your application."); + return; + } + + AdtPlugin.printToConsole(project, "------------------------------"); //$NON-NLS-1$ + AdtPlugin.printToConsole(project, "Android Launch!"); + + // check if the project is using the proper sdk. + // if that throws an exception, we simply let it propage to the caller. + if (checkAndroidProject(project) == false) { + AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!"); + androidLaunch.stopLaunch(); + return; + } + + // Check adb status and abort if needed. + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge == null || bridge.isConnected() == false) { + try { + int connections = -1; + int restarts = -1; + if (bridge != null) { + connections = bridge.getConnectionAttemptCount(); + restarts = bridge.getRestartAttemptCount(); + } + + // if we get -1, the device monitor is not even setup (anymore?). + // We need to ask the user to restart eclipse. + // This shouldn't happen, but it's better to let the user know in case it does. + if (connections == -1 || restarts == -1) { + AdtPlugin.printErrorToConsole(project, + "The connection to adb is down, and a severe error has occured.", + "You must restart adb and Eclipse.", + String.format( + "Please ensure that adb is correctly located at '%1$s' and can be executed.", + AdtPlugin.getOsAbsoluteAdb())); + return; + } + + if (restarts == 0) { + AdtPlugin.printErrorToConsole(project, + "Connection with adb was interrupted.", + String.format("%1$s attempts have been made to reconnect.", connections), + "You may want to manually restart adb from the Devices view."); + } else { + AdtPlugin.printErrorToConsole(project, + "Connection with adb was interrupted, and attempts to reconnect have failed.", + String.format("%1$s attempts have been made to restart adb.", restarts), + "You may want to manually restart adb from the Devices view."); + + } + return; + } finally { + androidLaunch.stopLaunch(); + } + } + + // since adb is working, we let the user know + // TODO have a verbose mode for launch with more info (or some of the less useful info we now have). + AdtPlugin.printToConsole(project, "adb is running normally."); + + // make a config class + AndroidLaunchConfiguration config = new AndroidLaunchConfiguration(); + + // fill it with the config coming from the ILaunchConfiguration object + config.set(configuration); + + // get the launch controller singleton + AndroidLaunchController controller = AndroidLaunchController.getInstance(); + + // get the application package + IFile applicationPackage = getApplicationPackage(project); + if (applicationPackage == null) { + androidLaunch.stopLaunch(); + return; + } + + // we need some information from the manifest + AndroidManifestParser manifestParser = AndroidManifestParser.parse( + BaseProjectHelper.getJavaProject(project), null /* errorListener */, + true /* gatherData */, false /* markErrors */); + + if (manifestParser == null) { + AdtPlugin.printErrorToConsole(project, "Failed to parse AndroidManifest: aborting!"); + androidLaunch.stopLaunch(); + return; + } + + String activityName = null; + + if (config.mLaunchAction == ACTION_ACTIVITY) { + // Get the activity name defined in the config + activityName = getActivityName(configuration); + + // Get the full activity list and make sure the one we got matches. + String[] activities = manifestParser.getActivities(); + + // first we check that there are, in fact, activities. + if (activities.length == 0) { + // if the activities list is null, then the manifest is empty + // and we can't launch the app. We'll revert to a sync-only launch + AdtPlugin.printErrorToConsole(project, + "The Manifest defines no activity!", + "The launch will only sync the application package on the device!"); + config.mLaunchAction = ACTION_DO_NOTHING; + } else if (activityName == null) { + // if the activity we got is null, we look for the default one. + AdtPlugin.printErrorToConsole(project, + "No activity specified! Getting the launcher activity."); + activityName = manifestParser.getLauncherActivity(); + + // if there's no default activity. We revert to a sync-only launch. + if (activityName == null) { + revertToNoActionLaunch(project, config); + } + } else { + + // check the one we got from the config matches any from the list + boolean match = false; + for (String a : activities) { + if (a != null && a.equals(activityName)) { + match = true; + break; + } + } + + // if we didn't find a match, we revert to the default activity if any. + if (match == false) { + AdtPlugin.printErrorToConsole(project, + "The specified activity does not exist! Getting the launcher activity."); + activityName = manifestParser.getLauncherActivity(); + + // if there's no default activity. We revert to a sync-only launch. + if (activityName == null) { + revertToNoActionLaunch(project, config); + } + } + } + } else if (config.mLaunchAction == ACTION_DEFAULT) { + activityName = manifestParser.getLauncherActivity(); + + // if there's no default activity. We revert to a sync-only launch. + if (activityName == null) { + revertToNoActionLaunch(project, config); + } + } + + // everything seems fine, we ask the launch controller to handle + // the rest + controller.launch(project, mode, applicationPackage, manifestParser.getPackage(), + manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(), + activityName, config, androidLaunch, monitor); + } + + @Override + public boolean buildForLaunch(ILaunchConfiguration configuration, + String mode, IProgressMonitor monitor) throws CoreException { + + // need to check we have everything + IProject project = getProject(configuration); + + if (project != null) { + // force an incremental build to be sure the resources will + // be updated if they were not saved before the launch was launched. + return true; + } + + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + 1 /* code, unused */, "Can't find the project!", null /* exception */)); + } + + /** + * {@inheritDoc} + * @throws CoreException + */ + @Override + public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) + throws CoreException { + return new AndroidLaunch(configuration, mode, null); + } + + /** + * Returns the IProject object matching the name found in the configuration + * object under the name + * <code>IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME</code> + * @param configuration + * @return The IProject object or null + */ + private IProject getProject(ILaunchConfiguration configuration){ + // get the project name from the config + String projectName; + try { + projectName = configuration.getAttribute( + IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, ""); + } catch (CoreException e) { + return null; + } + + // get the current workspace + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + + // and return the project with the name from the config + return workspace.getRoot().getProject(projectName); + } + + /** + * Checks the project is an android project. + * @param project The project to check + * @return true if the project is an android SDK. + * @throws CoreException + */ + private boolean checkAndroidProject(IProject project) throws CoreException { + // check if the project is a java and an android project. + if (project.hasNature(JavaCore.NATURE_ID) == false) { + String msg = String.format("%1$s is not a Java project!", project.getName()); + AdtPlugin.displayError("Android Launch", msg); + return false; + } + + if (project.hasNature(AndroidConstants.NATURE) == false) { + String msg = String.format("%1$s is not an Android project!", project.getName()); + AdtPlugin.displayError("Android Launch", msg); + return false; + } + + return true; + } + + + /** + * Returns the android package file as an IFile object for the specified + * project. + * @param project The project + * @return The android package as an IFile object or null if not found. + */ + private IFile getApplicationPackage(IProject project) { + // get the output folder + IFolder outputLocation = BaseProjectHelper.getOutputFolder(project); + + if (outputLocation == null) { + AdtPlugin.printErrorToConsole(project, + "Failed to get the output location of the project. Check build path properties" + ); + return null; + } + + + // get the package path + String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + IResource r = outputLocation.findMember(packageName); + + // check the package is present + if (r instanceof IFile && r.exists()) { + return (IFile)r; + } + + String msg = String.format("Could not find %1$s!", packageName); + AdtPlugin.printErrorToConsole(project, msg); + + return null; + } + + /** + * Returns the name of the activity. + */ + private String getActivityName(ILaunchConfiguration configuration) { + String empty = ""; + String activityName; + try { + activityName = configuration.getAttribute(ATTR_ACTIVITY, empty); + } catch (CoreException e) { + return null; + } + + return (activityName != empty) ? activityName : null; + } + + private final void revertToNoActionLaunch(IProject project, AndroidLaunchConfiguration config) { + AdtPlugin.printErrorToConsole(project, + "No Launcher activity found!", + "The launch will only sync the application package on the device!"); + config.mLaunchAction = ACTION_DO_NOTHING; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java new file mode 100644 index 0000000..92677f1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.launching; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.ILaunchShortcut; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IEditorPart; + +/** + * Launch shortcut to launch debug/run configuration directly. + */ +public class LaunchShortcut implements ILaunchShortcut { + + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchShortcut#launch( + * org.eclipse.jface.viewers.ISelection, java.lang.String) + */ + public void launch(ISelection selection, String mode) { + if (selection instanceof IStructuredSelection) { + + // get the object and the project from it + IStructuredSelection structSelect = (IStructuredSelection)selection; + Object o = structSelect.getFirstElement(); + + // get the first (and normally only) element + if (o instanceof IAdaptable) { + IResource r = (IResource)((IAdaptable)o).getAdapter(IResource.class); + + // get the project from the resource + if (r != null) { + IProject project = r.getProject(); + + if (project != null) { + // and launch + launch(project, mode); + } + } + } + } + } + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchShortcut#launch( + * org.eclipse.ui.IEditorPart, java.lang.String) + */ + public void launch(IEditorPart editor, String mode) { + // since we force the shortcut to only work on selection in the + // package explorer, this will never be called. + } + + + /** + * Launch a config for the specified project. + * @param project The project to launch + * @param mode The launch mode ("debug", "run" or "profile") + */ + private void launch(IProject project, String mode) { + // get an existing or new launch configuration + ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project); + + if (config != null) { + // and launch! + DebugUITools.launch(config, mode); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java new file mode 100644 index 0000000..d919c1f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.ui; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.avd.AvdManager.AvdInfo; +import com.android.sdkuilib.AvdSelector; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.ui.AbstractLaunchConfigurationTab; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** + * Launch configuration tab to control the parameters of the Emulator + */ +public class EmulatorConfigTab extends AbstractLaunchConfigurationTab { + + private final static String[][] NETWORK_SPEEDS = new String[][] { + { "Full", "full" }, //$NON-NLS-2$ + { "GSM", "gsm" }, //$NON-NLS-2$ + { "HSCSD", "hscsd" }, //$NON-NLS-2$ + { "GPRS", "gprs" }, //$NON-NLS-2$ + { "EDGE", "edge" }, //$NON-NLS-2$ + { "UMTS", "umts" }, //$NON-NLS-2$ + { "HSPDA", "hsdpa" }, //$NON-NLS-2$ + }; + + private final static String[][] NETWORK_LATENCIES = new String[][] { + { "None", "none" }, //$NON-NLS-2$ + { "GPRS", "gprs" }, //$NON-NLS-2$ + { "EDGE", "edge" }, //$NON-NLS-2$ + { "UMTS", "umts" }, //$NON-NLS-2$ + }; + + private Button mAutoTargetButton; + private Button mManualTargetButton; + + private AvdSelector mPreferredAvdSelector; + + private Combo mSpeedCombo; + + private Combo mDelayCombo; + + private Group mEmulatorOptionsGroup; + + private Text mEmulatorCLOptions; + + private Button mWipeDataButton; + + private Button mNoBootAnimButton; + + private Label mPreferredAvdLabel; + + /** + * Returns the emulator ready speed option value. + * @param value The index of the combo selection. + */ + public static String getSpeed(int value) { + try { + return NETWORK_SPEEDS[value][1]; + } catch (ArrayIndexOutOfBoundsException e) { + return NETWORK_SPEEDS[LaunchConfigDelegate.DEFAULT_SPEED][1]; + } + } + + /** + * Returns the emulator ready network latency value. + * @param value The index of the combo selection. + */ + public static String getDelay(int value) { + try { + return NETWORK_LATENCIES[value][1]; + } catch (ArrayIndexOutOfBoundsException e) { + return NETWORK_LATENCIES[LaunchConfigDelegate.DEFAULT_DELAY][1]; + } + } + + /** + * + */ + public EmulatorConfigTab() { + } + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite) + */ + public void createControl(Composite parent) { + Font font = parent.getFont(); + + Composite topComp = new Composite(parent, SWT.NONE); + setControl(topComp); + GridLayout topLayout = new GridLayout(); + topLayout.numColumns = 1; + topLayout.verticalSpacing = 0; + topComp.setLayout(topLayout); + topComp.setFont(font); + + GridData gd; + GridLayout layout; + + // radio button for the target mode + Group targetModeGroup = new Group(topComp, SWT.NONE); + targetModeGroup.setText("Device Target Selection Mode"); + gd = new GridData(GridData.FILL_HORIZONTAL); + targetModeGroup.setLayoutData(gd); + layout = new GridLayout(); + layout.numColumns = 1; + targetModeGroup.setLayout(layout); + targetModeGroup.setFont(font); + + mManualTargetButton = new Button(targetModeGroup, SWT.RADIO); + mManualTargetButton.setText("Manual"); + // Since there are only 2 radio buttons, we can put a listener on only one (they + // are both called on select and unselect event. + + // add the radio button + mAutoTargetButton = new Button(targetModeGroup, SWT.RADIO); + mAutoTargetButton.setText("Automatic"); + mAutoTargetButton.setSelection(true); + mAutoTargetButton.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + updateLaunchConfigurationDialog(); + + boolean auto = mAutoTargetButton.getSelection(); + mPreferredAvdSelector.setEnabled(auto); + mPreferredAvdLabel.setEnabled(auto); + } + }); + + Composite offsetComp = new Composite(targetModeGroup, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mPreferredAvdLabel = new Label(offsetComp, SWT.NONE); + mPreferredAvdLabel.setText("Select a preferred Android Virtual Device:"); + AvdInfo[] avds = new AvdInfo[0]; + mPreferredAvdSelector = new AvdSelector(offsetComp, avds, + false /*allowMultipleSelection*/); + mPreferredAvdSelector.setTableHeightHint(100); + mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateLaunchConfigurationDialog(); + } + }); + + // emulator size + mEmulatorOptionsGroup = new Group(topComp, SWT.NONE); + mEmulatorOptionsGroup.setText("Emulator launch parameters:"); + mEmulatorOptionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(); + layout.numColumns = 2; + mEmulatorOptionsGroup.setLayout(layout); + mEmulatorOptionsGroup.setFont(font); + + // network options + new Label(mEmulatorOptionsGroup, SWT.NONE).setText("Network Speed:"); + + mSpeedCombo = new Combo(mEmulatorOptionsGroup, SWT.READ_ONLY); + for (String[] speed : NETWORK_SPEEDS) { + mSpeedCombo.add(speed[0]); + } + mSpeedCombo.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + updateLaunchConfigurationDialog(); + } + }); + mSpeedCombo.pack(); + + new Label(mEmulatorOptionsGroup, SWT.NONE).setText("Network Latency:"); + + mDelayCombo = new Combo(mEmulatorOptionsGroup, SWT.READ_ONLY); + + for (String[] delay : NETWORK_LATENCIES) { + mDelayCombo.add(delay[0]); + } + mDelayCombo.addSelectionListener(new SelectionAdapter() { + // called when selection changes + @Override + public void widgetSelected(SelectionEvent e) { + updateLaunchConfigurationDialog(); + } + }); + mDelayCombo.pack(); + + // wipe data option + mWipeDataButton = new Button(mEmulatorOptionsGroup, SWT.CHECK); + mWipeDataButton.setText("Wipe User Data"); + mWipeDataButton.setToolTipText("Check this if you want to wipe your user data each time you start the emulator. You will be prompted for confirmation when the emulator starts."); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + mWipeDataButton.setLayoutData(gd); + mWipeDataButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateLaunchConfigurationDialog(); + } + }); + + // no boot anim option + mNoBootAnimButton = new Button(mEmulatorOptionsGroup, SWT.CHECK); + mNoBootAnimButton.setText("Disable Boot Animation"); + mNoBootAnimButton.setToolTipText("Check this if you want to disable the boot animation. This can help the emulator start faster on slow machines."); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + mNoBootAnimButton.setLayoutData(gd); + mNoBootAnimButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateLaunchConfigurationDialog(); + } + }); + + // custom command line option for emulator + Label l = new Label(mEmulatorOptionsGroup, SWT.NONE); + l.setText("Additional Emulator Command Line Options"); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + l.setLayoutData(gd); + + mEmulatorCLOptions = new Text(mEmulatorOptionsGroup, SWT.BORDER); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + mEmulatorCLOptions.setLayoutData(gd); + mEmulatorCLOptions.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + updateLaunchConfigurationDialog(); + } + }); + } + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName() + */ + public String getName() { + return "Target"; + } + + @Override + public Image getImage() { + return DdmsPlugin.getImageLoader().loadImage("emulator.png", null); //$NON-NLS-1$ + } + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration) + */ + public void initializeFrom(ILaunchConfiguration configuration) { + AvdManager avdManager = Sdk.getCurrent().getAvdManager(); + + boolean value = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic + try { + value = configuration.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, value); + } catch (CoreException e) { + // let's not do anything here, we'll use the default value + } + mAutoTargetButton.setSelection(value); + mManualTargetButton.setSelection(!value); + + // look for the project name to get its target. + String stringValue = ""; + try { + stringValue = configuration.getAttribute( + IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, stringValue); + } catch (CoreException ce) { + // let's not do anything here, we'll use the default value + } + + IProject project = null; + + // get the list of existing Android projects from the workspace. + IJavaProject[] projects = BaseProjectHelper.getAndroidProjects(); + if (projects != null) { + // look for the project whose name we read from the configuration. + for (IJavaProject p : projects) { + if (p.getElementName().equals(stringValue)) { + project = p.getProject(); + break; + } + } + } + + // update the AVD list + AvdInfo[] avds = null; + if (avdManager != null) { + avds = avdManager.getAvds(); + } + + IAndroidTarget projectTarget = null; + if (project != null) { + projectTarget = Sdk.getCurrent().getTarget(project); + } else { + avds = null; // no project? we don't want to display any "compatible" AVDs. + } + + mPreferredAvdSelector.setAvds(avds, projectTarget); + + stringValue = ""; + try { + stringValue = configuration.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, + stringValue); + } catch (CoreException e) { + // let's not do anything here, we'll use the default value + } + + if (stringValue != null && stringValue.length() > 0 && avdManager != null) { + AvdInfo targetAvd = avdManager.getAvd(stringValue); + mPreferredAvdSelector.setSelection(targetAvd); + } else { + mPreferredAvdSelector.setSelection(null); + } + + value = LaunchConfigDelegate.DEFAULT_WIPE_DATA; + try { + value = configuration.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, value); + } catch (CoreException e) { + // let's not do anything here, we'll use the default value + } + mWipeDataButton.setSelection(value); + + value = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM; + try { + value = configuration.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, value); + } catch (CoreException e) { + // let's not do anything here, we'll use the default value + } + mNoBootAnimButton.setSelection(value); + + int index = -1; + + index = LaunchConfigDelegate.DEFAULT_SPEED; + try { + index = configuration.getAttribute(LaunchConfigDelegate.ATTR_SPEED, + index); + } catch (CoreException e) { + // let's not do anything here, we'll use the default value + } + if (index == -1) { + mSpeedCombo.clearSelection(); + } else { + mSpeedCombo.select(index); + } + + index = LaunchConfigDelegate.DEFAULT_DELAY; + try { + index = configuration.getAttribute(LaunchConfigDelegate.ATTR_DELAY, + index); + } catch (CoreException e) { + // let's not do anything here, we'll put a proper value in + // performApply anyway + } + if (index == -1) { + mDelayCombo.clearSelection(); + } else { + mDelayCombo.select(index); + } + + String commandLine = null; + try { + commandLine = configuration.getAttribute( + LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$ + } catch (CoreException e) { + // let's not do anything here, we'll use the default value + } + if (commandLine != null) { + mEmulatorCLOptions.setText(commandLine); + } + } + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy) + */ + public void performApply(ILaunchConfigurationWorkingCopy configuration) { + configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, + mAutoTargetButton.getSelection()); + AvdInfo avd = mPreferredAvdSelector.getFirstSelected(); + if (avd != null) { + configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, avd.getName()); + } else { + configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String)null); + } + configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED, + mSpeedCombo.getSelectionIndex()); + configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY, + mDelayCombo.getSelectionIndex()); + configuration.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, + mEmulatorCLOptions.getText()); + configuration.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, + mWipeDataButton.getSelection()); + configuration.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, + mNoBootAnimButton.getSelection()); + } + + /* (non-Javadoc) + * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy) + */ + public void setDefaults(ILaunchConfigurationWorkingCopy configuration) { + configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, + LaunchConfigDelegate.DEFAULT_TARGET_MODE); + configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED, + LaunchConfigDelegate.DEFAULT_SPEED); + configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY, + LaunchConfigDelegate.DEFAULT_DELAY); + configuration.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, + LaunchConfigDelegate.DEFAULT_WIPE_DATA); + configuration.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, + LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM); + + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String emuOptions = store.getString(AdtPlugin.PREFS_EMU_OPTIONS); + configuration.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java new file mode 100644 index 0000000..c0dbd54 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.ui; + +import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup; +import org.eclipse.debug.ui.CommonTab; +import org.eclipse.debug.ui.ILaunchConfigurationDialog; +import org.eclipse.debug.ui.ILaunchConfigurationTab; + +/** + * Tab group object for Android Launch Config type. + */ +public class LaunchConfigTabGroup extends AbstractLaunchConfigurationTabGroup { + + public LaunchConfigTabGroup() { + } + + public void createTabs(ILaunchConfigurationDialog dialog, String mode) { + ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] { + new MainLaunchConfigTab(), + new EmulatorConfigTab(), + new CommonTab() + }; + setTabs(tabs); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java new file mode 100644 index 0000000..6a40ed0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.debug.ui; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController; +import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.common.project.ProjectChooserHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.ui.AbstractLaunchConfigurationTab; +import org.eclipse.debug.ui.ILaunchConfigurationTab; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Text; + +/** + * Class for the main launch configuration tab. + */ +public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab { + + protected static final String EMPTY_STRING = ""; //$NON-NLS-1$ + + protected Text mProjText; + private Button mProjButton; + + private Combo mActivityCombo; + private String[] mActivities; + + private WidgetListener mListener = new WidgetListener(); + + private Button mDefaultActionButton; + private Button mActivityActionButton; + private Button mDoNothingActionButton; + private int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION; + + private ProjectChooserHelper mProjectChooserHelper; + + /** + * A listener which handles widget change events for the controls in this + * tab. + */ + private class WidgetListener implements ModifyListener, SelectionListener { + + public void modifyText(ModifyEvent e) { + IProject project = checkParameters(); + loadActivities(project); + setDirty(true); + } + + public void widgetDefaultSelected(SelectionEvent e) {/* do nothing */ + } + + public void widgetSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source == mProjButton) { + handleProjectButtonSelected(); + } else { + checkParameters(); + } + } + } + + public MainLaunchConfigTab() { + } + + public void createControl(Composite parent) { + mProjectChooserHelper = new ProjectChooserHelper(parent.getShell()); + + Font font = parent.getFont(); + Composite comp = new Composite(parent, SWT.NONE); + setControl(comp); + GridLayout topLayout = new GridLayout(); + topLayout.verticalSpacing = 0; + comp.setLayout(topLayout); + comp.setFont(font); + createProjectEditor(comp); + createVerticalSpacer(comp, 1); + + // create the combo for the activity chooser + Group group = new Group(comp, SWT.NONE); + group.setText("Launch Action:"); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + group.setLayoutData(gd); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + group.setFont(font); + + mDefaultActionButton = new Button(group, SWT.RADIO); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + mDefaultActionButton.setLayoutData(gd); + mDefaultActionButton.setText("Launch Default Activity"); + mDefaultActionButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // event are received for both selection and deselection, so we only process + // the selection event to avoid doing it twice. + if (mDefaultActionButton.getSelection() == true) { + mLaunchAction = LaunchConfigDelegate.ACTION_DEFAULT; + mActivityCombo.setEnabled(false); + checkParameters(); + } + } + }); + + mActivityActionButton = new Button(group, SWT.RADIO); + mActivityActionButton.setText("Launch:"); + mActivityActionButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // event are received for both selection and deselection, so we only process + // the selection event to avoid doing it twice. + if (mActivityActionButton.getSelection() == true) { + mLaunchAction = LaunchConfigDelegate.ACTION_ACTIVITY; + mActivityCombo.setEnabled(true); + checkParameters(); + } + } + }); + + mActivityCombo = new Combo(group, SWT.DROP_DOWN | SWT.READ_ONLY); + gd = new GridData(GridData.FILL_HORIZONTAL); + mActivityCombo.setLayoutData(gd); + mActivityCombo.clearSelection(); + mActivityCombo.setEnabled(false); + mActivityCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + checkParameters(); + } + }); + + mDoNothingActionButton = new Button(group, SWT.RADIO); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + mDoNothingActionButton.setLayoutData(gd); + mDoNothingActionButton.setText("Do Nothing"); + mDoNothingActionButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // event are received for both selection and deselection, so we only process + // the selection event to avoid doing it twice. + if (mDoNothingActionButton.getSelection() == true) { + mLaunchAction = LaunchConfigDelegate.ACTION_DO_NOTHING; + mActivityCombo.setEnabled(false); + checkParameters(); + } + } + }); + + } + + public String getName() { + return "Android"; + } + + @Override + public Image getImage() { + return AdtPlugin.getImageLoader().loadImage("mainLaunchTab.png", null); + } + + + public void performApply(ILaunchConfigurationWorkingCopy configuration) { + configuration.setAttribute( + IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, mProjText.getText()); + configuration.setAttribute( + IJavaLaunchConfigurationConstants.ATTR_ALLOW_TERMINATE, true); + + // add the launch mode + configuration.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, mLaunchAction); + + // add the activity + int selection = mActivityCombo.getSelectionIndex(); + if (mActivities != null && selection >=0 && selection < mActivities.length) { + configuration.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, mActivities[selection]); + } + + // link the project and the launch config. + mapResources(configuration); + } + + public void setDefaults(ILaunchConfigurationWorkingCopy configuration) { + configuration.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, + LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION); + } + + /** + * Creates the widgets for specifying a main type. + * + * @param parent the parent composite + */ + protected void createProjectEditor(Composite parent) { + Font font = parent.getFont(); + Group group = new Group(parent, SWT.NONE); + group.setText("Project:"); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + group.setLayoutData(gd); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + group.setFont(font); + mProjText = new Text(group, SWT.SINGLE | SWT.BORDER); + gd = new GridData(GridData.FILL_HORIZONTAL); + mProjText.setLayoutData(gd); + mProjText.setFont(font); + mProjText.addModifyListener(mListener); + mProjButton = createPushButton(group, "Browse...", null); + mProjButton.addSelectionListener(mListener); + } + + /** + * returns the default listener from this class. For all subclasses this + * listener will only provide the functi Jaonality of updating the current + * tab + * + * @return a widget listener + */ + protected WidgetListener getDefaultListener() { + return mListener; + } + + /** + * Return the {@link IJavaProject} corresponding to the project name in the project + * name text field, or null if the text does not match a project name. + * @param javaModel the Java Model object corresponding for the current workspace root. + * @return a IJavaProject object or null. + */ + protected IJavaProject getJavaProject(IJavaModel javaModel) { + String projectName = mProjText.getText().trim(); + if (projectName.length() < 1) { + return null; + } + return javaModel.getJavaProject(projectName); + } + + /** + * Show a dialog that lets the user select a project. This in turn provides + * context for the main type, allowing the user to key a main type name, or + * constraining the search for main types to the specified project. + */ + protected void handleProjectButtonSelected() { + IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject( + mProjText.getText().trim()); + if (javaProject == null) { + return; + }// end if + String projectName = javaProject.getElementName(); + mProjText.setText(projectName); + + // get the list of activities and fill the combo + IProject project = javaProject.getProject(); + loadActivities(project); + }// end handle selected + + /** + * Initializes this tab's controls with values from the given + * launch configuration. This method is called when + * a configuration is selected to view or edit, after this + * tab's control has been created. + * + * @param config launch configuration + * + * @see ILaunchConfigurationTab + */ + public void initializeFrom(ILaunchConfiguration config) { + String projectName = EMPTY_STRING; + try { + projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, + EMPTY_STRING); + }// end try + catch (CoreException ce) { + } + mProjText.setText(projectName); + + // get the list of projects + IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null); + + if (projects != null) { + // look for the currently selected project + IProject proj = null; + for (IJavaProject p : projects) { + if (p.getElementName().equals(projectName)) { + proj = p.getProject(); + break; + } + } + + loadActivities(proj); + } + + // load the launch action. + mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION; + try { + mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, + mLaunchAction); + } catch (CoreException e) { + // nothing to be done really. launchAction will keep its default value. + } + + mDefaultActionButton.setSelection(mLaunchAction == LaunchConfigDelegate.ACTION_DEFAULT); + mActivityActionButton.setSelection(mLaunchAction == LaunchConfigDelegate.ACTION_ACTIVITY); + mDoNothingActionButton.setSelection( + mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING); + + // now look for the activity and load it if present, otherwise, revert + // to the current one. + String activityName = EMPTY_STRING; + try { + activityName = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, EMPTY_STRING); + }// end try + catch (CoreException ce) { + // nothing to be done really. activityName will stay empty + } + + if (mLaunchAction != LaunchConfigDelegate.ACTION_ACTIVITY) { + mActivityCombo.setEnabled(false); + mActivityCombo.clearSelection(); + } else { + mActivityCombo.setEnabled(true); + if (activityName == null || activityName.equals(EMPTY_STRING)) { + mActivityCombo.clearSelection(); + } else if (mActivities != null && mActivities.length > 0) { + // look for the name of the activity in the combo. + boolean found = false; + for (int i = 0 ; i < mActivities.length ; i++) { + if (activityName.equals(mActivities[i])) { + found = true; + mActivityCombo.select(i); + break; + } + } + + // if we haven't found a matching activity we clear the combo selection + if (found == false) { + mActivityCombo.clearSelection(); + } + } + } + } + + /** + * Associates the launch config and the project. This allows Eclipse to delete the launch + * config when the project is deleted. + * + * @param config the launch config working copy. + */ + protected void mapResources(ILaunchConfigurationWorkingCopy config) { + // get the java model + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + + // get the IJavaProject described by the text field. + IJavaProject javaProject = getJavaProject(javaModel); + IResource[] resources = null; + if (javaProject != null) { + resources = AndroidLaunchController.getResourcesToMap(javaProject.getProject()); + } + config.setMappedResources(resources); + } + + /** + * Loads the ui with the activities of the specified project, and stores the + * activities in <code>mActivities</code>. + * <p/> + * First activity is selected by default if present. + * + * @param project The project to load the activities from. + */ + private void loadActivities(IProject project) { + if (project != null) { + try { + // parse the manifest for the list of activities. + AndroidManifestParser manifestParser = AndroidManifestParser.parse( + BaseProjectHelper.getJavaProject(project), null /* errorListener */, + true /* gatherData */, false /* markErrors */); + if (manifestParser != null) { + mActivities = manifestParser.getActivities(); + + mActivityCombo.removeAll(); + + if (mActivities.length > 0) { + if (mLaunchAction == LaunchConfigDelegate.ACTION_ACTIVITY) { + mActivityCombo.setEnabled(true); + } + for (String s : mActivities) { + mActivityCombo.add(s); + } + } else { + mActivityCombo.setEnabled(false); + } + + // the selection will be set when we update the ui from the current + // config object. + mActivityCombo.clearSelection(); + + return; + } + + } catch (CoreException e) { + // The AndroidManifest parsing failed. The builders must have reported the errors + // already so there's nothing to do. + } + } + + // if we reach this point, either project is null, or we got an exception during + // the parsing. In either case, we empty the activity list. + mActivityCombo.removeAll(); + mActivities = null; + } + + /** + * Checks the parameters for correctness, and update the error message and buttons. + * @return the current IProject of this launch config. + */ + private IProject checkParameters() { + try { + //test the project name first! + String text = mProjText.getText(); + if (text.length() == 0) { + setErrorMessage("Project Name is required!"); + } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) { + setErrorMessage("Project name contains unsupported characters!"); + } else { + IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null); + IProject found = null; + for (IJavaProject javaProject : projects) { + if (javaProject.getProject().getName().equals(text)) { + found = javaProject.getProject(); + break; + } + + } + + if (found != null) { + setErrorMessage(null); + } else { + setErrorMessage(String.format("There is no android project named '%1$s'", + text)); + } + + return found; + } + } finally { + updateLaunchConfigurationDialog(); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties new file mode 100644 index 0000000..dfb0eb2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties @@ -0,0 +1,16 @@ +Dialog_Title_SDK_Location=Android SDK Location +SDK_Not_Setup=The location of the Android SDK has not been setup. Please go to Preferences > Android and set it up +Error_Check_Prefs=%1$s\nPlease check the Android plugin's preferences. +Could_Not_Find_Folder=Could not find SDK folder '%1$s'. +Could_Not_Find_Folder_In_SDK=Could not find folder '%1$s' inside SDK '%2$s'. +Could_Not_Find=Could not find %1$s\! +VersionCheck_SDK_Milestone_Too_Low=This version of ADT requires a SDK in version M%1$d or above.\n\nCurrent version is %2$s.\n\nPlease update your SDK to the latest version. +VersionCheck_SDK_Build_Too_Low=This version of ADT requires a SDK in version %1$d (M%2$d) or above.\n\nCurrent version is %3$s.\n\nPlease update your SDK to the latest version. +VersionCheck_Plugin_Version_Failed=Failed to get the required ADT version number from the SDK.\n\nThe Android Developer Toolkit may not work properly. +VersionCheck_Unable_To_Parse_Version_s=Unable to parse sdk build version: %1$s +VersionCheck_Plugin_Too_Old=This Android SDK requires Android Developer Toolkit version %1$d.%2$d.%3$d or above.\n\nCurrent version is %4$s.\n\nPlease update ADT to the latest version. +AdtPlugin_Failed_To_Start_s=Failed to start %1$s +AdtPlugin_Android_SDK_Content_Loader=Android SDK Content Loader +AdtPlugin_Parsing_Resources=Parsing Resources +AdtPlugin_Android_SDK_Resource_Parser=Android SDK Resource Parser +AdtPlugin_Failed_To_Parse_s=Failed to parse: diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java new file mode 100644 index 0000000..458f78e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.preferences; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; +import com.android.sdklib.IAndroidTarget; +import com.android.sdkuilib.SdkTargetSelector; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.preference.DirectoryFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import java.io.File; + +/** + * This class represents a preference page that is contributed to the + * Preferences dialog. By subclassing <samp>FieldEditorPreferencePage</samp>, + * we can use the field support built into JFace that allows us to create a page + * that is small and knows how to save, restore and apply itself. + * <p> + * This page is used to modify preferences only. They are stored in the + * preference store that belongs to the main plug-in class. That way, + * preferences can be accessed directly via the preference store. + */ + +public class AndroidPreferencePage extends FieldEditorPreferencePage implements + IWorkbenchPreferencePage { + + public AndroidPreferencePage() { + super(GRID); + setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore()); + setDescription(Messages.AndroidPreferencePage_Title); + } + + /** + * Creates the field editors. Field editors are abstractions of the common + * GUI blocks needed to manipulate various types of preferences. Each field + * editor knows how to save and restore itself. + */ + @Override + public void createFieldEditors() { + + addField(new SdkDirectoryFieldEditor(AdtPlugin.PREFS_SDK_DIR, + Messages.AndroidPreferencePage_SDK_Location_, getFieldEditorParent())); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.IWorkbenchPreferencePage#init(org.eclipse.ui.IWorkbench) + */ + public void init(IWorkbench workbench) { + } + + /** + * Custom version of DirectoryFieldEditor which validates that the directory really + * contains an SDK. + * + * There's a known issue here, which is really a rare edge-case: if the pref dialog is open + * which a given sdk directory and the *content* of the directory changes such that the sdk + * state changed (i.e. from valid to invalid or vice versa), the pref panel will display or + * hide the error as appropriate but the pref panel will fail to validate the apply/ok buttons + * appropriately. The easy workaround is to cancel the pref panel and enter it again. + */ + private static class SdkDirectoryFieldEditor extends DirectoryFieldEditor { + + private SdkTargetSelector mTargetSelector; + private TargetChangedListener mTargetChangeListener; + + public SdkDirectoryFieldEditor(String name, String labelText, Composite parent) { + super(name, labelText, parent); + setEmptyStringAllowed(false); + } + + /** + * Method declared on StringFieldEditor and overridden in DirectoryFieldEditor. + * Checks whether the text input field contains a valid directory. + * + * @return True if the apply/ok button should be enabled in the pref panel + */ + @Override + protected boolean doCheckState() { + String fileName = getTextControl().getText(); + fileName = fileName.trim(); + + if (fileName.indexOf(',') >= 0 || fileName.indexOf(';') >= 0) { + setErrorMessage(Messages.AndroidPreferencePage_ERROR_Reserved_Char); + return false; // Apply/OK must be disabled + } + + File file = new File(fileName); + if (!file.isDirectory()) { + setErrorMessage(JFaceResources.getString( + "DirectoryFieldEditor.errorMessage")); //$NON-NLS-1$ + return false; + } + + boolean ok = AdtPlugin.getDefault().checkSdkLocationAndId(fileName, + new AdtPlugin.CheckSdkErrorHandler() { + @Override + public boolean handleError(String message) { + setErrorMessage(message.replaceAll("\n", " ")); //$NON-NLS-1$ //$NON-NLS-2$ + return false; // Apply/OK must be disabled + } + + @Override + public boolean handleWarning(String message) { + showMessage(message.replaceAll("\n", " ")); //$NON-NLS-1$ //$NON-NLS-2$ + return true; // Apply/OK must be enabled + } + }); + if (ok) clearMessage(); + return ok; + } + + @Override + public Text getTextControl(Composite parent) { + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + return super.getTextControl(parent); + } + + /* (non-Javadoc) + * Method declared on StringFieldEditor (and FieldEditor). + */ + @Override + protected void doFillIntoGrid(Composite parent, int numColumns) { + super.doFillIntoGrid(parent, numColumns); + + GridData gd; + Label l = new Label(parent, SWT.NONE); + l.setText("Note: The list of SDK Targets below is only reloaded once you hit 'Apply' or 'OK'."); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = numColumns; + l.setLayoutData(gd); + + try { + // We may not have an sdk if the sdk path pref is empty or not valid. + Sdk sdk = Sdk.getCurrent(); + IAndroidTarget[] targets = sdk != null ? sdk.getTargets() : null; + + mTargetSelector = new SdkTargetSelector(parent, + targets, + false, /*allowSelection*/ + false /*multipleSelection*/); + gd = (GridData) mTargetSelector.getLayoutData(); + gd.horizontalSpan = numColumns; + + if (mTargetChangeListener == null) { + mTargetChangeListener = new TargetChangedListener(); + AdtPlugin.getDefault().addTargetListener(mTargetChangeListener); + } + } catch (Exception e) { + // We need to catch *any* exception that arises here, otherwise it disables + // the whole pref panel. We can live without the Sdk target selector but + // not being able to actually set an sdk path. + AdtPlugin.log(e, "SdkTargetSelector failed"); + } + } + + @Override + public void dispose() { + super.dispose(); + if (mTargetChangeListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetChangeListener); + mTargetChangeListener = null; + } + } + + private class TargetChangedListener implements ITargetChangeListener { + public void onProjectTargetChange(IProject changedProject) { + // do nothing. + } + + public void onTargetsLoaded() { + if (mTargetSelector != null) { + // We may not have an sdk if the sdk path pref is empty or not valid. + Sdk sdk = Sdk.getCurrent(); + IAndroidTarget[] targets = sdk != null ? sdk.getTargets() : null; + + mTargetSelector.setTargets(targets); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java new file mode 100644 index 0000000..e64c2f4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.preferences; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.DebugKeyProvider.KeytoolException; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.FileFieldEditor; +import org.eclipse.jface.preference.RadioGroupFieldEditor; +import org.eclipse.jface.preference.StringFieldEditor; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Date; + +/** + * Preference page for build options. + * + */ +public class BuildPreferencePage extends FieldEditorPreferencePage implements + IWorkbenchPreferencePage { + + final static String BUILD_STR_SILENT = "silent"; //$NON-NLS-1$ + final static String BUILD_STR_NORMAL = "normal"; //$NON-NLS-1$ + final static String BUILD_STR_VERBOSE = "verbose"; //$NON-NLS-1$ + + public BuildPreferencePage() { + super(GRID); + setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore()); + setDescription(Messages.BuildPreferencePage_Title); + } + + public static int getBuildLevel(String buildPrefValue) { + if (BUILD_STR_SILENT.equals(buildPrefValue)) { + return AdtConstants.BUILD_ALWAYS; + } else if (BUILD_STR_VERBOSE.equals(buildPrefValue)) { + return AdtConstants.BUILD_VERBOSE; + } + + return AdtConstants.BUILD_NORMAL; + } + + @Override + protected void createFieldEditors() { + addField(new BooleanFieldEditor(AdtPlugin.PREFS_RES_AUTO_REFRESH, + Messages.BuildPreferencePage_Auto_Refresh_Resources_on_Build, + getFieldEditorParent())); + + RadioGroupFieldEditor rgfe = new RadioGroupFieldEditor( + AdtPlugin.PREFS_BUILD_VERBOSITY, + Messages.BuildPreferencePage_Build_Output, 1, new String[][] { + { Messages.BuildPreferencePage_Silent, BUILD_STR_SILENT }, + { Messages.BuildPreferencePage_Normal, BUILD_STR_NORMAL }, + { Messages.BuildPreferencePage_Verbose, BUILD_STR_VERBOSE } + }, + getFieldEditorParent(), true); + addField(rgfe); + + addField(new ReadOnlyFieldEditor(AdtPlugin.PREFS_DEFAULT_DEBUG_KEYSTORE, + Messages.BuildPreferencePage_Default_KeyStore, getFieldEditorParent())); + + addField(new KeystoreFieldEditor(AdtPlugin.PREFS_CUSTOM_DEBUG_KEYSTORE, + Messages.BuildPreferencePage_Custom_Keystore, getFieldEditorParent())); + + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.IWorkbenchPreferencePage#init(org.eclipse.ui.IWorkbench) + */ + public void init(IWorkbench workbench) { + } + + /** + * A read-only string field editor. + */ + private static class ReadOnlyFieldEditor extends StringFieldEditor { + + public ReadOnlyFieldEditor(String name, String labelText, Composite parent) { + super(name, labelText, parent); + } + + @Override + protected void createControl(Composite parent) { + super.createControl(parent); + + Text control = getTextControl(); + control.setEditable(false); + } + } + + /** + * Custom {@link FileFieldEditor} that checks that the keystore is valid. + */ + private static class KeystoreFieldEditor extends FileFieldEditor { + public KeystoreFieldEditor(String name, String label, Composite parent) { + super(name, label, parent); + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + } + + @Override + protected boolean checkState() { + String fileName = getTextControl().getText(); + fileName = fileName.trim(); + + // empty values are considered ok. + if (fileName.length() > 0) { + File file = new File(fileName); + if (file.isFile()) { + // attempt to load the debug key. + try { + DebugKeyProvider provider = new DebugKeyProvider(fileName, + null /* storeType */, null /* key gen output */); + PrivateKey key = provider.getDebugKey(); + X509Certificate certificate = (X509Certificate)provider.getCertificate(); + + if (key == null || certificate == null) { + showErrorMessage("Unable to find debug key in keystore!"); + return false; + } + + Date today = new Date(); + if (certificate.getNotAfter().compareTo(today) < 0) { + showErrorMessage("Certificate is expired!"); + return false; + } + + if (certificate.getNotBefore().compareTo(today) > 0) { + showErrorMessage("Certificate validity is in the future!"); + return false; + } + + // we're good! + clearErrorMessage(); + return true; + } catch (GeneralSecurityException e) { + handleException(e); + return false; + } catch (IOException e) { + handleException(e); + return false; + } catch (KeytoolException e) { + handleException(e); + return false; + } catch (AndroidLocationException e) { + handleException(e); + return false; + } + + + } else { + // file does not exist. + showErrorMessage("Not a valid keystore path."); + return false; // Apply/OK must be disabled + } + } + + clearErrorMessage(); + return true; + } + + @Override + public Text getTextControl(Composite parent) { + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + return super.getTextControl(parent); + } + + /** + * Set the error message from a {@link Throwable}. If the exception has no message, try + * to get the message from the cause. + * @param t the Throwable. + */ + private void handleException(Throwable t) { + String msg = t.getMessage(); + if (msg == null) { + Throwable cause = t.getCause(); + if (cause != null) { + handleException(cause); + } else { + setErrorMessage("Uknown error when getting the debug key!"); + } + + return; + } + + // valid text, display it. + showErrorMessage(msg); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java new file mode 100644 index 0000000..8fd72c1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.preferences; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.StringFieldEditor; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +/** + * Settings page for launch related preferences. + */ +public class LaunchPreferencePage extends FieldEditorPreferencePage implements + IWorkbenchPreferencePage { + + public LaunchPreferencePage() { + super(GRID); + setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore()); + setDescription(Messages.LaunchPreferencePage_Title); + } + + @Override + protected void createFieldEditors() { + addField(new StringFieldEditor(AdtPlugin.PREFS_EMU_OPTIONS, + Messages.LaunchPreferencePage_Default_Emu_Options, getFieldEditorParent())); + + addField(new StringFieldEditor(AdtPlugin.PREFS_HOME_PACKAGE, + Messages.LaunchPreferencePage_Default_HOME_Package, getFieldEditorParent())); + } + + public void init(IWorkbench workbench) { + // pass + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java new file mode 100644 index 0000000..e0197b9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java @@ -0,0 +1,43 @@ + +package com.android.ide.eclipse.adt.preferences; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.preferences.messages"; //$NON-NLS-1$ + + public static String AndroidPreferencePage_ERROR_Reserved_Char; + + public static String AndroidPreferencePage_SDK_Location_; + + public static String AndroidPreferencePage_Title; + + public static String BuildPreferencePage_Auto_Refresh_Resources_on_Build; + + public static String BuildPreferencePage_Build_Output; + + public static String BuildPreferencePage_Custom_Keystore; + + public static String BuildPreferencePage_Default_KeyStore; + + public static String BuildPreferencePage_Normal; + + public static String BuildPreferencePage_Silent; + + public static String BuildPreferencePage_Title; + + public static String BuildPreferencePage_Verbose; + + public static String LaunchPreferencePage_Default_Emu_Options; + + public static String LaunchPreferencePage_Default_HOME_Package; + + public static String LaunchPreferencePage_Title; + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java new file mode 100644 index 0000000..2b1fb66 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.preferences; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.DebugKeyProvider.KeytoolException; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.jface.preference.IPreferenceStore; + +/** + * Class used to initialize default preference values. + */ +public class PreferenceInitializer extends AbstractPreferenceInitializer { + + /* + * (non-Javadoc) + * + * @see org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer + * #initializeDefaultPreferences() + */ + @Override + public void initializeDefaultPreferences() { + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + + store.setDefault(AdtPlugin.PREFS_RES_AUTO_REFRESH, true); + + store.setDefault(AdtPlugin.PREFS_BUILD_VERBOSITY, BuildPreferencePage.BUILD_STR_NORMAL); + + store.setDefault(AdtPlugin.PREFS_HOME_PACKAGE, "android.process.acore"); //$NON-NLS-1$ + + try { + store.setDefault(AdtPlugin.PREFS_DEFAULT_DEBUG_KEYSTORE, + DebugKeyProvider.getDefaultKeyStoreOsPath()); + } catch (KeytoolException e) { + AdtPlugin.log(e, "Get default debug keystore path failed"); //$NON-NLS-1$ + } catch (AndroidLocationException e) { + AdtPlugin.log(e, "Get default debug keystore path failed"); //$NON-NLS-1$ + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties new file mode 100644 index 0000000..865ac19 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties @@ -0,0 +1,14 @@ +BuildPreferencePage_Title=Build Settings: +BuildPreferencePage_Auto_Refresh_Resources_on_Build=Automatically refresh Resources and Assets folder on build +BuildPreferencePage_Build_Output=Build output +BuildPreferencePage_Silent=Silent +BuildPreferencePage_Normal=Normal +BuildPreferencePage_Verbose=Verbose +BuildPreferencePage_Default_KeyStore=Default debug keystore: +BuildPreferencePage_Custom_Keystore=Custom debug keystore: +LaunchPreferencePage_Title=Launch Settings: +LaunchPreferencePage_Default_Emu_Options=Default emulator options: +LaunchPreferencePage_Default_HOME_Package=Default HOME package: +AndroidPreferencePage_Title=Android Preferences +AndroidPreferencePage_SDK_Location_=SDK Location: +AndroidPreferencePage_ERROR_Reserved_Char=Reserved characters ',' and ';' cannot be used in the SDK Location. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java new file mode 100644 index 0000000..9bcadaf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.build.ApkBuilder; +import com.android.ide.eclipse.adt.build.PreCompilerBuilder; +import com.android.ide.eclipse.adt.build.ResourceManagerBuilder; +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IProjectNature; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.JavaCore; + +/** + * Project nature for the Android Projects. + */ +public class AndroidNature implements IProjectNature { + + /** the project this nature object is associated with */ + private IProject mProject; + + /** + * Configures this nature for its project. This is called by the workspace + * when natures are added to the project using + * <code>IProject.setDescription</code> and should not be called directly + * by clients. The nature extension id is added to the list of natures + * before this method is called, and need not be added here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * <code>IProject.setDescription</code>, but the nature will remain in + * the project description. + * + * The Android nature adds the pre-builder and the APK builder if necessary. + * + * @see org.eclipse.core.resources.IProjectNature#configure() + * @throws CoreException if configuration fails. + */ + public void configure() throws CoreException { + configureResourceManagerBuilder(mProject); + configurePreBuilder(mProject); + configureApkBuilder(mProject); + } + + /** + * De-configures this nature for its project. This is called by the + * workspace when natures are removed from the project using + * <code>IProject.setDescription</code> and should not be called directly + * by clients. The nature extension id is removed from the list of natures + * before this method is called, and need not be removed here. + * + * Exceptions thrown by this method will be propagated back to the caller of + * <code>IProject.setDescription</code>, but the nature will still be + * removed from the project description. + * + * The Android nature removes the custom pre builder and APK builder. + * + * @see org.eclipse.core.resources.IProjectNature#deconfigure() + * @throws CoreException if configuration fails. + */ + public void deconfigure() throws CoreException { + // remove the android builders + removeBuilder(mProject, ResourceManagerBuilder.ID); + removeBuilder(mProject, PreCompilerBuilder.ID); + removeBuilder(mProject, ApkBuilder.ID); + } + + /** + * Returns the project to which this project nature applies. + * + * @return the project handle + * @see org.eclipse.core.resources.IProjectNature#getProject() + */ + public IProject getProject() { + return mProject; + } + + /** + * Sets the project to which this nature applies. Used when instantiating + * this project nature runtime. This is called by + * <code>IProject.create()</code> or + * <code>IProject.setDescription()</code> and should not be called + * directly by clients. + * + * @param project the project to which this nature applies + * @see org.eclipse.core.resources.IProjectNature#setProject(org.eclipse.core.resources.IProject) + */ + public void setProject(IProject project) { + mProject = project; + } + + /** + * Adds the Android Nature and the Java Nature to the project if it doesn't + * already have them. + * + * @param project An existing or new project to update + * @param monitor An optional progress monitor. Can be null. + * @throws CoreException if fails to change the nature. + */ + public static synchronized void setupProjectNatures(IProject project, + IProgressMonitor monitor) throws CoreException { + if (project == null || !project.isOpen()) return; + if (monitor == null) monitor = new NullProgressMonitor(); + + // Add the natures. We need to add the Java nature first, so it adds its builder to the + // project first. This way, when the android nature is added, we can control where to put + // the android builders in relation to the java builder. + // Adding the java nature after the android one, would place the java builder before the + // android builders. + addNatureToProjectDescription(project, JavaCore.NATURE_ID, monitor); + addNatureToProjectDescription(project, AndroidConstants.NATURE, monitor); + } + + /** + * Add the specified nature to the specified project. The nature is only + * added if not already present. + * <p/> + * Android Natures are always inserted at the beginning of the list of natures in order to + * have the jdt views/dialogs display the proper icon. + * + * @param project The project to modify. + * @param natureId The Id of the nature to add. + * @param monitor An existing progress monitor. + * @throws CoreException if fails to change the nature. + */ + private static void addNatureToProjectDescription(IProject project, + String natureId, IProgressMonitor monitor) throws CoreException { + if (!project.hasNature(natureId)) { + + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + String[] newNatures = new String[natures.length + 1]; + + // Android natures always come first. + if (natureId.equals(AndroidConstants.NATURE)) { + System.arraycopy(natures, 0, newNatures, 1, natures.length); + newNatures[0] = natureId; + } else { + System.arraycopy(natures, 0, newNatures, 0, natures.length); + newNatures[natures.length] = natureId; + } + + description.setNatureIds(newNatures); + project.setDescription(description, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the ResourceManagerBuilder, if its not already there. It'll insert + * itself as the first builder. + * @throws CoreException + * + */ + public static void configureResourceManagerBuilder(IProject project) + throws CoreException { + // get the builder list + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + // look for the builder in case it's already there. + for (int i = 0; i < commands.length; ++i) { + if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + // it's not there, lets add it at the beginning of the builders + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 1, commands.length); + ICommand command = desc.newCommand(); + command.setBuilderName(ResourceManagerBuilder.ID); + newCommands[0] = command; + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + /** + * Adds the PreCompilerBuilder if its not already there. It'll check for + * presence of the ResourceManager and insert itself right after. + * @param project + * @throws CoreException + */ + public static void configurePreBuilder(IProject project) + throws CoreException { + // get the builder list + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + // look for the builder in case it's already there. + for (int i = 0; i < commands.length; ++i) { + if (PreCompilerBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + // we need to add it after the resource manager builder. + // Let's look for it + int index = -1; + for (int i = 0; i < commands.length; ++i) { + if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) { + index = i; + break; + } + } + + // we're inserting after + index++; + + // do the insertion + + // copy the builders before. + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 0, index); + + // insert the new builder + ICommand command = desc.newCommand(); + command.setBuilderName(PreCompilerBuilder.ID); + newCommands[index] = command; + + // copy the builder after + System.arraycopy(commands, index, newCommands, index + 1, commands.length-index); + + // set the new builders in the project + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + public static void configureApkBuilder(IProject project) + throws CoreException { + // Add the .apk builder at the end if it's not already there + IProjectDescription desc = project.getDescription(); + ICommand[] commands = desc.getBuildSpec(); + + for (int i = 0; i < commands.length; ++i) { + if (ApkBuilder.ID.equals(commands[i].getBuilderName())) { + return; + } + } + + ICommand[] newCommands = new ICommand[commands.length + 1]; + System.arraycopy(commands, 0, newCommands, 0, commands.length); + ICommand command = desc.newCommand(); + command.setBuilderName(ApkBuilder.ID); + newCommands[commands.length] = command; + desc.setBuildSpec(newCommands); + project.setDescription(desc, null); + } + + /** + * Removes a builder from the project. + * @param project The project to remove the builder from. + * @param id The String ID of the builder to remove. + * @return true if the builder was found and removed. + * @throws CoreException + */ + public static boolean removeBuilder(IProject project, String id) throws CoreException { + IProjectDescription description = project.getDescription(); + ICommand[] commands = description.getBuildSpec(); + for (int i = 0; i < commands.length; ++i) { + if (id.equals(commands[i].getBuilderName())) { + ICommand[] newCommands = new ICommand[commands.length - 1]; + System.arraycopy(commands, 0, newCommands, 0, i); + System.arraycopy(commands, i + 1, newCommands, i, commands.length - i - 1); + description.setBuildSpec(newCommands); + project.setDescription(description, null); + return true; + } + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java new file mode 100644 index 0000000..ffb2535 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +import java.util.Iterator; + +/** + * Converts a project created with the activity creator into an + * Android project. + */ +public class ConvertToAndroidAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /* + * (non-Javadoc) + * + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + // pass + } + + /* + * (non-Javadoc) + * + * @see IActionDelegate#run(IAction) + */ + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + for (Iterator<?> it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject)element; + } else if (element instanceof IAdaptable) { + project = (IProject)((IAdaptable)element).getAdapter(IProject.class); + } + if (project != null) { + convertProject(project); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see IActionDelegate#selectionChanged(IAction, ISelection) + */ + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } + + /** + * Toggles sample nature on a project + * + * @param project to have sample nature added or removed + */ + private void convertProject(final IProject project) { + new Job("Convert Project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask(String.format( + "Convert %1$s to Android", project.getName()), 5); + } + + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // check if the project already has the android nature. + for (int i = 0; i < natures.length; ++i) { + if (AndroidConstants.NATURE.equals(natures[i])) { + // we shouldn't be here as the visibility of the item + // is dependent on the project. + return new Status(Status.WARNING, AdtPlugin.PLUGIN_ID, + "Project is already an Android project"); + } + } + + if (monitor != null) { + monitor.worked(1); + } + + String[] newNatures = new String[natures.length + 1]; + System.arraycopy(natures, 0, newNatures, 1, natures.length); + newNatures[0] = AndroidConstants.NATURE; + + // set the new nature list in the project + description.setNatureIds(newNatures); + project.setDescription(description, null); + if (monitor != null) { + monitor.worked(1); + } + + // Fix the classpath entries. + // get a java project + IJavaProject javaProject = JavaCore.create(project); + ProjectHelper.fixProjectClasspathEntries(javaProject); + if (monitor != null) { + monitor.worked(1); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }.schedule(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java new file mode 100644 index 0000000..a1b3c38 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Action going through all the source of a project and creating a pre-processed aidl file + * with all the custom parcelable classes. + */ +public class CreateAidlImportAction implements IObjectActionDelegate { + + private ISelection mSelection; + + public CreateAidlImportAction() { + // pass + } + + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + // pass + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction) + */ + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + for (Iterator<?> it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject)element; + } else if (element instanceof IAdaptable) { + project = (IProject)((IAdaptable)element).getAdapter(IProject.class); + } + if (project != null) { + final IProject fproject = project; + new Job("Aidl preprocess") { + @Override + protected IStatus run(IProgressMonitor monitor) { + return createImportFile(fproject, monitor); + } + }.schedule(); + } + } + } + } + + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } + + private IStatus createImportFile(IProject project, IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask(String.format( + "Creating aid preprocess file for %1$s", project.getName()), 1); + } + + ArrayList<String> parcelables = new ArrayList<String>(); + + IJavaProject javaProject = JavaCore.create(project); + + IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); + + for (IPackageFragmentRoot root : roots) { + if (root.isArchive() == false && root.isExternal() == false) { + parsePackageFragmentRoot(root, parcelables, monitor); + } + } + + // create the file with the parcelables + if (parcelables.size() > 0) { + IPath path = project.getLocation(); + path = path.append(AndroidConstants.FN_PROJECT_AIDL); + + File f = new File(path.toOSString()); + if (f.exists() == false) { + if (f.createNewFile() == false) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Failed to create /project.aidl"); + } + } + + FileWriter fw = new FileWriter(f); + + fw.write("// This file is auto-generated by the\n"); + fw.write("// 'Create Aidl preprocess file for Parcelable classes'\n"); + fw.write("// action. Do not modify!\n\n"); + + for (String parcelable : parcelables) { + fw.write("parcelable "); //$NON-NLS-1$ + fw.write(parcelable); + fw.append(";\n"); //$NON-NLS-1$ + } + + fw.close(); + + // need to refresh the level just below the project to make sure it's being picked + // up by eclipse. + project.refreshLocal(IResource.DEPTH_ONE, monitor); + } + + if (monitor != null) { + monitor.worked(1); + monitor.done(); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (IOException e) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Failed to create /project.aidl", e); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + + private void parsePackageFragmentRoot(IPackageFragmentRoot root, + ArrayList<String> parcelables, IProgressMonitor monitor) throws JavaModelException { + + IJavaElement[] elements = root.getChildren(); + + for (IJavaElement element : elements) { + if (element instanceof IPackageFragment) { + ICompilationUnit[] compilationUnits = + ((IPackageFragment)element).getCompilationUnits(); + + for (ICompilationUnit unit : compilationUnits) { + IType[] types = unit.getTypes(); + + for (IType type : types) { + parseType(type, parcelables, monitor); + } + } + } + } + } + + private void parseType(IType type, ArrayList<String> parcelables, IProgressMonitor monitor) + throws JavaModelException { + // first look in this type if it somehow extends parcelable. + ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(monitor); + + IType[] superInterfaces = typeHierarchy.getAllSuperInterfaces(type); + for (IType superInterface : superInterfaces) { + if (AndroidConstants.CLASS_PARCELABLE.equals(superInterface.getFullyQualifiedName())) { + parcelables.add(type.getFullyQualifiedName()); + } + } + + // then look in inner types. + IType[] innerTypes = type.getTypes(); + + for (IType innerType : innerTypes) { + parseType(innerType, parcelables, monitor); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java new file mode 100644 index 0000000..3d7e929 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.common.project.ExportHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; + +public class ExportAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection)mSelection; + // get the unique selected item. + if (selection.size() == 1) { + Object element = selection.getFirstElement(); + + // get the project object from it. + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element).getAdapter(IProject.class); + } + + // and finally do the action + if (project != null) { + ExportHelper.exportProject(project); + } + } + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java new file mode 100644 index 0000000..9ade490 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.project.export.ExportWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPart; + +public class ExportWizardAction implements IObjectActionDelegate { + + private ISelection mSelection; + private IWorkbench mWorkbench; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench(); + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection)mSelection; + + // call the export wizard on the current selection. + ExportWizard wizard = new ExportWizard(); + wizard.init(mWorkbench, selection); + WizardDialog dialog = new WizardDialog(mWorkbench.getDisplay().getActiveShell(), + wizard); + dialog.open(); + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java new file mode 100644 index 0000000..b8a0b0c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; + +import java.util.ArrayList; + +/** + * Class to fix the launch configuration of a project if the java package + * defined in the manifest has been changed.<br> + * This fix can be done synchronously, or asynchronously.<br> + * <code>start()</code> will start a thread that will do the fix.<br> + * <code>run()</code> will do the fix in the current thread.<br><br> + * By default, the fix first display a dialog to the user asking if he/she wants to + * do the fix. This can be overriden by calling <code>setDisplayPrompt(false)</code>. + * + */ +public class FixLaunchConfig extends Thread { + + private IProject mProject; + private String mOldPackage; + private String mNewPackage; + + private boolean mDisplayPrompt = true; + + public FixLaunchConfig(IProject project, String oldPackage, String newPackage) { + super(); + + mProject = project; + mOldPackage = oldPackage; + mNewPackage = newPackage; + } + + /** + * Set the display prompt. If true run()/start() first ask the user if he/she wants + * to fix the Launch Config + * @param displayPrompt + */ + public void setDisplayPrompt(boolean displayPrompt) { + mDisplayPrompt = displayPrompt; + } + + /** + * Fix the Launch configurations. + */ + @Override + public void run() { + + if (mDisplayPrompt) { + // ask the user if he really wants to fix the launch config + boolean res = AdtPlugin.displayPrompt( + "Launch Configuration Update", + "The package definition in the manifest changed.\nDo you want to update your Launch Configuration(s)?"); + + if (res == false) { + return; + } + } + + // get the list of config for the project + String projectName = mProject.getName(); + ILaunchConfiguration[] configs = findConfigs(mProject.getName()); + + // loop through all the config and update the package + for (ILaunchConfiguration config : configs) { + try { + // get the working copy so that we can make changes. + ILaunchConfigurationWorkingCopy copy = config.getWorkingCopy(); + + // get the attributes for the activity + String activity = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, + ""); //$NON-NLS-1$ + + // manifests can define activities that are not in the defined package, + // so we need to make sure the activity is inside the old package. + if (activity.startsWith(mOldPackage)) { + // create the new activity + activity = mNewPackage + activity.substring(mOldPackage.length()); + + // put it in the copy + copy.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, activity); + + // save the config + copy.doSave(); + } + } catch (CoreException e) { + // couldn't get the working copy. we output the error in the console + String msg = String.format("Failed to modify %1$s: %2$s", projectName, + e.getMessage()); + AdtPlugin.printErrorToConsole(mProject, msg); + } + } + + } + + /** + * Looks for and returns all existing Launch Configuration object for a + * specified project. + * @param projectName The name of the project + * @return all the ILaunchConfiguration object. If none are present, an empty array is + * returned. + */ + private static ILaunchConfiguration[] findConfigs(String projectName) { + // get the launch manager + ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); + + // now get the config type for our particular android type. + ILaunchConfigurationType configType = manager. + getLaunchConfigurationType(LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); + + // create a temp list to hold all the valid configs + ArrayList<ILaunchConfiguration> list = new ArrayList<ILaunchConfiguration>(); + + try { + ILaunchConfiguration[] configs = manager.getLaunchConfigurations(configType); + + for (ILaunchConfiguration config : configs) { + if (config.getAttribute( + IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, + "").equals(projectName)) { //$NON-NLS-1$ + list.add(config); + } + } + } catch (CoreException e) { + } + + return list.toArray(new ILaunchConfiguration[list.size()]); + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java new file mode 100644 index 0000000..3043818 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; + +import java.util.Iterator; + +/** + * Action to fix the project properties: + * <ul> + * <li>Make sure the framework archive is present with the link to the java + * doc</li> + * </ul> + */ +public class FixProjectAction implements IObjectActionDelegate { + + private ISelection mSelection; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + + for (Iterator<?> it = ((IStructuredSelection) mSelection).iterator(); + it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element) + .getAdapter(IProject.class); + } + if (project != null) { + fixProject(project); + } + } + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } + + private void fixProject(final IProject project) { + new Job("Fix Project Properties") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (monitor != null) { + monitor.beginTask("Fix Project Properties", 6); + } + + ProjectHelper.fixProject(project); + if (monitor != null) { + monitor.worked(1); + } + + // fix the nature order to have the proper project icon + ProjectHelper.fixProjectNatureOrder(project); + if (monitor != null) { + monitor.worked(1); + } + + // now we fix the builders + AndroidNature.configureResourceManagerBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + AndroidNature.configurePreBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + AndroidNature.configureApkBuilder(project); + if (monitor != null) { + monitor.worked(1); + } + + return Status.OK_STATUS; + } catch (JavaModelException e) { + return e.getJavaModelStatus(); + } catch (CoreException e) { + return e.getStatus(); + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + }.schedule(); + } + + /** + * @see IWorkbenchWindowActionDelegate#init + */ + public void init(IWorkbenchWindow window) { + // pass + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java new file mode 100644 index 0000000..7fc3318 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IDecoration; +import org.eclipse.jface.viewers.ILabelDecorator; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ILightweightLabelDecorator; + +/** + * A {@link ILabelDecorator} associated with an org.eclipse.ui.decorators extension. + * This is used to add android icons in some special folders in the package explorer. + */ +public class FolderDecorator implements ILightweightLabelDecorator { + + private ImageDescriptor mDescriptor; + + public FolderDecorator() { + mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png"); + } + + public void decorate(Object element, IDecoration decoration) { + if (element instanceof IFolder) { + IFolder folder = (IFolder)element; + + // get the project and make sure this is an android project + IProject project = folder.getProject(); + + try { + if (project.hasNature(AndroidConstants.NATURE)) { + // check the folder is directly under the project. + if (folder.getParent().getType() == IResource.PROJECT) { + String name = folder.getName(); + if (name.equals(SdkConstants.FD_ASSETS)) { + decorate(decoration, " [Android assets]"); + decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT); + } else if (name.equals(SdkConstants.FD_RESOURCES)) { + decorate(decoration, " [Android resources]"); + decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT); + } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) { + decorate(decoration, " [Native Libraries]"); + } + } + } + } catch (CoreException e) { + // log the error + AdtPlugin.log(e, "Unable to get nature of project '%s'.", project.getName()); + } + } + } + + public void decorate(IDecoration decoration, String suffix) { + decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT); + + // this is broken as it changes the color of the whole text, not only of the decoration. + // TODO: figure out how to change the color of the decoration only. +// decoration.addSuffix(suffix); +// ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme(); +// ColorRegistry registry = theme.getColorRegistry(); +// decoration.setForegroundColor(registry.get("org.eclipse.jdt.ui.ColoredLabels.decorations")); + + } + + public boolean isLabelProperty(Object element, String property) { + // at this time return false. + return false; + } + + public void addListener(ILabelProviderListener listener) { + // No state change will affect the rendering. + } + + + + public void removeListener(ILabelProviderListener listener) { + // No state change will affect the rendering. + } + + public void dispose() { + // nothind to dispose + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java new file mode 100644 index 0000000..c117b4e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.editors.wizards.NewXmlFileWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPart; + +public class NewXmlFileWizardAction implements IObjectActionDelegate { + + private ISelection mSelection; + private IWorkbench mWorkbench; + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench(); + } + + public void run(IAction action) { + if (mSelection instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection)mSelection; + + // call the new xml file wizard on the current selection. + NewXmlFileWizard wizard = new NewXmlFileWizard(); + wizard.init(mWorkbench, selection); + WizardDialog dialog = new WizardDialog(mWorkbench.getDisplay().getActiveShell(), + wizard); + dialog.open(); + } + } + + public void selectionChanged(IAction action, ISelection selection) { + this.mSelection = selection; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java new file mode 100644 index 0000000..cbeddd7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.AndroidManifestParser; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.launching.JavaRuntime; + +import java.util.ArrayList; + +/** + * Utility class to manipulate Project parameters/properties. + */ +public final class ProjectHelper { + public final static int COMPILER_COMPLIANCE_OK = 0; + public final static int COMPILER_COMPLIANCE_LEVEL = 1; + public final static int COMPILER_COMPLIANCE_SOURCE = 2; + public final static int COMPILER_COMPLIANCE_CODEGEN_TARGET = 3; + + /** + * Adds the corresponding source folder to the class path entries. + * + * @param entries The class path entries to read. A copy will be returned. + * @param new_entry The parent source folder to remove. + * @return A new class path entries array. + */ + public static IClasspathEntry[] addEntryToClasspath( + IClasspathEntry[] entries, IClasspathEntry new_entry) { + int n = entries.length; + IClasspathEntry[] newEntries = new IClasspathEntry[n + 1]; + System.arraycopy(entries, 0, newEntries, 0, n); + newEntries[n] = new_entry; + return newEntries; + } + + /** + * Remove a classpath entry from the array. + * @param entries The class path entries to read. A copy will be returned + * @param index The index to remove. + * @return A new class path entries array. + */ + public static IClasspathEntry[] removeEntryFromClasspath( + IClasspathEntry[] entries, int index) { + int n = entries.length; + IClasspathEntry[] newEntries = new IClasspathEntry[n-1]; + + // copy the entries before index + System.arraycopy(entries, 0, newEntries, 0, index); + + // copy the entries after index + System.arraycopy(entries, index + 1, newEntries, index, + entries.length - index - 1); + + return newEntries; + } + + /** + * Converts a OS specific path into a path valid for the java doc location + * attributes of a project. + * @param javaDocOSLocation The OS specific path. + * @return a valid path for the java doc location. + */ + public static String getJavaDocPath(String javaDocOSLocation) { + // first thing we do is convert the \ into / + String javaDoc = javaDocOSLocation.replaceAll("\\\\", //$NON-NLS-1$ + AndroidConstants.WS_SEP); + + // then we add file: at the beginning for unix path, and file:/ for non + // unix path + if (javaDoc.startsWith(AndroidConstants.WS_SEP)) { + return "file:" + javaDoc; //$NON-NLS-1$ + } + + return "file:/" + javaDoc; //$NON-NLS-1$ + } + + /** + * Look for a specific classpath entry by full path and return its index. + * @param entries The entry array to search in. + * @param entryPath The OS specific path of the entry. + * @param entryKind The kind of the entry. Accepted values are 0 + * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, + * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, + * and IClasspathEntry.CPE_CONTAINER + * @return the index of the found classpath entry or -1. + */ + public static int findClasspathEntryByPath(IClasspathEntry[] entries, + String entryPath, int entryKind) { + for (int i = 0 ; i < entries.length ; i++) { + IClasspathEntry entry = entries[i]; + + int kind = entry.getEntryKind(); + + if (kind == entryKind || entryKind == 0) { + // get the path + IPath path = entry.getPath(); + + String osPathString = path.toOSString(); + if (osPathString.equals(entryPath)) { + return i; + } + } + } + + // not found, return bad index. + return -1; + } + + /** + * Look for a specific classpath entry for file name only and return its + * index. + * @param entries The entry array to search in. + * @param entryName The filename of the entry. + * @param entryKind The kind of the entry. Accepted values are 0 + * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT, + * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE, + * and IClasspathEntry.CPE_CONTAINER + * @param startIndex Index where to start the search + * @return the index of the found classpath entry or -1. + */ + public static int findClasspathEntryByName(IClasspathEntry[] entries, + String entryName, int entryKind, int startIndex) { + if (startIndex < 0) { + startIndex = 0; + } + for (int i = startIndex ; i < entries.length ; i++) { + IClasspathEntry entry = entries[i]; + + int kind = entry.getEntryKind(); + + if (kind == entryKind || entryKind == 0) { + // get the path + IPath path = entry.getPath(); + String name = path.segment(path.segmentCount()-1); + + if (name.equals(entryName)) { + return i; + } + } + } + + // not found, return bad index. + return -1; + } + + /** + * Fix the project. This checks the SDK location. + * @param project The project to fix. + * @throws JavaModelException + */ + public static void fixProject(IProject project) throws JavaModelException { + if (AdtPlugin.getOsSdkFolder().length() == 0) { + AdtPlugin.printToConsole(project, "Unknown SDK Location, project not fixed."); + return; + } + + // get a java project + IJavaProject javaProject = JavaCore.create(project); + fixProjectClasspathEntries(javaProject); + } + + /** + * Fix the project classpath entries. The method ensures that: + * <ul> + * <li>The project does not reference any old android.zip/android.jar archive.</li> + * <li>The project does not use its output folder as a sourc folder.</li> + * <li>The project does not reference a desktop JRE</li> + * <li>The project references the AndroidClasspathContainer. + * </ul> + * @param javaProject The project to fix. + * @throws JavaModelException + */ + public static void fixProjectClasspathEntries(IJavaProject javaProject) + throws JavaModelException { + + // get the project classpath + IClasspathEntry[] entries = javaProject.getRawClasspath(); + IClasspathEntry[] oldEntries = entries; + + // check if the JRE is set as library + int jreIndex = ProjectHelper.findClasspathEntryByPath(entries, JavaRuntime.JRE_CONTAINER, + IClasspathEntry.CPE_CONTAINER); + if (jreIndex != -1) { + // the project has a JRE included, we remove it + entries = ProjectHelper.removeEntryFromClasspath(entries, jreIndex); + } + + // get the output folder + IPath outputFolder = javaProject.getOutputLocation(); + + boolean foundContainer = false; + + for (int i = 0 ; i < entries.length ;) { + // get the entry and kind + IClasspathEntry entry = entries[i]; + int kind = entry.getEntryKind(); + + if (kind == IClasspathEntry.CPE_SOURCE) { + IPath path = entry.getPath(); + + if (path.equals(outputFolder)) { + entries = ProjectHelper.removeEntryFromClasspath(entries, i); + + // continue, to skip the i++; + continue; + } + } else if (kind == IClasspathEntry.CPE_CONTAINER) { + if (AndroidClasspathContainerInitializer.checkPath(entry.getPath())) { + foundContainer = true; + } + } + + i++; + } + + // if the framework container is not there, we add it + if (foundContainer == false) { + // add the android container to the array + entries = ProjectHelper.addEntryToClasspath(entries, + AndroidClasspathContainerInitializer.getContainerEntry()); + } + + // set the new list of entries to the project + if (entries != oldEntries) { + javaProject.setRawClasspath(entries, new NullProgressMonitor()); + } + + // If needed, check and fix compiler compliance and source compatibility + ProjectHelper.checkAndFixCompilerCompliance(javaProject); + } + /** + * Checks the project compiler compliance level is supported. + * @param javaProject The project to check + * @return <ul> + * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li> + * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li> + * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li> + * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li> + * </ul> + */ + public static final int checkCompilerCompliance(IJavaProject javaProject) { + // get the project compliance level option + String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true); + + // check it against a list of valid compliance level strings. + if (checkCompliance(compliance) == false) { + // if we didn't find the proper compliance level, we return an error + return COMPILER_COMPLIANCE_LEVEL; + } + + // otherwise we check source compatibility + String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true); + + // check it against a list of valid compliance level strings. + if (checkCompliance(source) == false) { + // if we didn't find the proper compliance level, we return an error + return COMPILER_COMPLIANCE_SOURCE; + } + + // otherwise check codegen level + String codeGen = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true); + + // check it against a list of valid compliance level strings. + if (checkCompliance(codeGen) == false) { + // if we didn't find the proper compliance level, we return an error + return COMPILER_COMPLIANCE_CODEGEN_TARGET; + } + + return COMPILER_COMPLIANCE_OK; + } + + /** + * Checks the project compiler compliance level is supported. + * @param project The project to check + * @return <ul> + * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li> + * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li> + * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li> + * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li> + * </ul> + */ + public static final int checkCompilerCompliance(IProject project) { + // get the java project from the IProject resource object + IJavaProject javaProject = JavaCore.create(project); + + // check and return the result. + return checkCompilerCompliance(javaProject); + } + + + /** + * Checks, and fixes if needed, the compiler compliance level, and the source compatibility + * level + * @param project The project to check and fix. + */ + public static final void checkAndFixCompilerCompliance(IProject project) { + // get the java project from the IProject resource object + IJavaProject javaProject = JavaCore.create(project); + + // Now we check the compiler compliance level and make sure it is valid + checkAndFixCompilerCompliance(javaProject); + } + + /** + * Checks, and fixes if needed, the compiler compliance level, and the source compatibility + * level + * @param javaProject The Java project to check and fix. + */ + public static final void checkAndFixCompilerCompliance(IJavaProject javaProject) { + if (checkCompilerCompliance(javaProject) != COMPILER_COMPLIANCE_OK) { + // setup the preferred compiler compliance level. + javaProject.setOption(JavaCore.COMPILER_COMPLIANCE, + AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOption(JavaCore.COMPILER_SOURCE, + AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); + javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, + AndroidConstants.COMPILER_COMPLIANCE_PREFERRED); + + // clean the project to make sure we recompile + try { + javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD, + new NullProgressMonitor()); + } catch (CoreException e) { + AdtPlugin.printErrorToConsole(javaProject.getProject(), + "Project compiler settings changed. Clean your project."); + } + } + } + + /** + * Returns a {@link IProject} by its running application name, as it returned by the AVD. + * <p/> + * <var>applicationName</var> will in most case be the package declared in the manifest, but + * can, in some cases, be a custom process name declared in the manifest, in the + * <code>application</code>, <code>activity</code>, <code>receiver</code>, or + * <code>service</code> nodes. + * @param applicationName The application name. + * @return a project or <code>null</code> if no matching project were found. + */ + public static IProject findAndroidProjectByAppName(String applicationName) { + // Get the list of project for the current workspace + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProject[] projects = workspace.getRoot().getProjects(); + + // look for a project that matches the packageName of the app + // we're trying to debug + for (IProject p : projects) { + if (p.isOpen()) { + try { + if (p.hasNature(AndroidConstants.NATURE) == false) { + // ignore non android projects + continue; + } + } catch (CoreException e) { + // failed to get the nature? skip project. + continue; + } + + AndroidManifestHelper androidManifest = new AndroidManifestHelper(p); + + // check that there is indeed a manifest file. + if (androidManifest.getManifestIFile() == null) { + // no file? skip this project. + continue; + } + + AndroidManifestParser parser = null; + try { + parser = AndroidManifestParser.parseForData( + androidManifest.getManifestIFile()); + } catch (CoreException e) { + // skip this project. + continue; + } + + String manifestPackage = parser.getPackage(); + + if (manifestPackage != null && manifestPackage.equals(applicationName)) { + // this is the project we were looking for! + return p; + } else { + // if the package and application name don't match, + // we look for other possible process names declared in the manifest. + String[] processes = parser.getProcesses(); + for (String process : processes) { + if (process.equals(applicationName)) { + return p; + } + } + } + } + } + + return null; + + } + + public static void fixProjectNatureOrder(IProject project) throws CoreException { + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // if the android nature is not the first one, we reorder them + if (AndroidConstants.NATURE.equals(natures[0]) == false) { + // look for the index + for (int i = 0 ; i < natures.length ; i++) { + if (AndroidConstants.NATURE.equals(natures[i])) { + // if we try to just reorder the array in one pass, this doesn't do + // anything. I guess JDT check that we are actually adding/removing nature. + // So, first we'll remove the android nature, and then add it back. + + // remove the android nature + removeNature(project, AndroidConstants.NATURE); + + // now add it back at the first index. + description = project.getDescription(); + natures = description.getNatureIds(); + + String[] newNatures = new String[natures.length + 1]; + + // first one is android + newNatures[0] = AndroidConstants.NATURE; + + // next the rest that was before the android nature + System.arraycopy(natures, 0, newNatures, 1, natures.length); + + // set the new natures + description.setNatureIds(newNatures); + project.setDescription(description, null); + + // and stop + break; + } + } + } + } + + + /** + * Removes a specific nature from a project. + * @param project The project to remove the nature from. + * @param nature The nature id to remove. + * @throws CoreException + */ + public static void removeNature(IProject project, String nature) throws CoreException { + IProjectDescription description = project.getDescription(); + String[] natures = description.getNatureIds(); + + // check if the project already has the android nature. + for (int i = 0; i < natures.length; ++i) { + if (nature.equals(natures[i])) { + String[] newNatures = new String[natures.length - 1]; + if (i > 0) { + System.arraycopy(natures, 0, newNatures, 0, i); + } + System.arraycopy(natures, i + 1, newNatures, i, natures.length - i - 1); + description.setNatureIds(newNatures); + project.setDescription(description, null); + + return; + } + } + + } + + /** + * Returns if the project has error level markers. + * @param includeReferencedProjects flag to also test the referenced projects. + * @throws CoreException + */ + public static boolean hasError(IProject project, boolean includeReferencedProjects) + throws CoreException { + IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); + if (markers != null && markers.length > 0) { + // the project has marker(s). even though they are "problem" we + // don't know their severity. so we loop on them and figure if they + // are warnings or errors + for (IMarker m : markers) { + int s = m.getAttribute(IMarker.SEVERITY, -1); + if (s == IMarker.SEVERITY_ERROR) { + return true; + } + } + } + + // test the referenced projects if needed. + if (includeReferencedProjects) { + IProject[] projects = getReferencedProjects(project); + + for (IProject p : projects) { + if (hasError(p, false)) { + return true; + } + } + } + + return false; + } + + /** + * Saves a String property into the persistent storage of a resource. + * @param resource The resource into which the string value is saved. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + public static boolean saveStringProperty(IResource resource, String propertyName, + String value) { + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); + + try { + resource.setPersistentProperty(qname, value); + } catch (CoreException e) { + return false; + } + + return true; + } + + /** + * Loads a String property from the persistent storage of a resource. + * @param resource The resource from which the string value is loaded. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return the property value or null if it was not found. + */ + public static String loadStringProperty(IResource resource, String propertyName) { + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName); + + try { + String value = resource.getPersistentProperty(qname); + return value; + } catch (CoreException e) { + return null; + } + } + + /** + * Saves a property into the persistent storage of a resource. + * @param resource The resource into which the boolean value is saved. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value the value to save + * @return true if the save succeeded. + */ + public static boolean saveBooleanProperty(IResource resource, String propertyName, + boolean value) { + return saveStringProperty(resource, propertyName, Boolean.toString(value)); + } + + /** + * Loads a boolean property from the persistent storage of the project. + * @param resource The resource from which the boolean value is loaded. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param defaultValue The default value to return if the property was not found. + * @return the property value or the default value if the property was not found. + */ + public static boolean loadBooleanProperty(IResource resource, String propertyName, + boolean defaultValue) { + String value = loadStringProperty(resource, propertyName); + if (value != null) { + return Boolean.parseBoolean(value); + } + + return defaultValue; + } + + /** + * Saves the path of a resource into the persistent storate of the project. + * @param resource The resource into which the resource path is saved. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @param value The resource to save. It's its path that is actually stored. If null, an + * empty string is stored. + * @return true if the save succeeded + */ + public static boolean saveResourceProperty(IResource resource, String propertyName, + IResource value) { + if (value != null) { + IPath iPath = value.getProjectRelativePath(); + return saveStringProperty(resource, propertyName, iPath.toString()); + } + + return saveStringProperty(resource, propertyName, ""); //$NON-NLS-1$ + } + + /** + * Loads the path of a resource from the persistent storage of the project, and returns the + * corresponding IResource object, if it exists in the same project as <code>resource</code>. + * @param resource The resource from which the resource path is loaded. + * @param propertyName the name of the property. The id of the plugin is added to this string. + * @return The corresponding IResource object (or children interface) or null + */ + public static IResource loadResourceProperty(IResource resource, String propertyName) { + String value = loadStringProperty(resource, propertyName); + + if (value != null && value.length() > 0) { + return resource.getProject().findMember(value); + } + + return null; + } + + /** + * Returns the list of referenced project that are opened and Java projects. + * @param project + * @return list of opened referenced java project. + * @throws CoreException + */ + public static IProject[] getReferencedProjects(IProject project) throws CoreException { + IProject[] projects = project.getReferencedProjects(); + + ArrayList<IProject> list = new ArrayList<IProject>(); + + for (IProject p : projects) { + if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) { + list.add(p); + } + } + + return list.toArray(new IProject[list.size()]); + } + + + /** + * Checks a Java project compiler level option against a list of supported versions. + * @param optionValue the Compiler level option. + * @return true if the option value is supproted. + */ + private static boolean checkCompliance(String optionValue) { + for (String s : AndroidConstants.COMPILER_COMPLIANCE) { + if (s != null && s.equals(optionValue)) { + return true; + } + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java new file mode 100644 index 0000000..399eac9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.jarutils.KeystoreHelper; +import com.android.jarutils.SignedJarBuilder; +import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; +import com.android.jarutils.DebugKeyProvider.KeytoolException; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IExportWizard; +import org.eclipse.ui.IWorkbench; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Export wizard to export an apk signed with a release key/certificate. + */ +public final class ExportWizard extends Wizard implements IExportWizard { + + private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$ + + private static final String PAGE_PROJECT_CHECK = "Page_ProjectCheck"; //$NON-NLS-1$ + private static final String PAGE_KEYSTORE_SELECTION = "Page_KeystoreSelection"; //$NON-NLS-1$ + private static final String PAGE_KEY_CREATION = "Page_KeyCreation"; //$NON-NLS-1$ + private static final String PAGE_KEY_SELECTION = "Page_KeySelection"; //$NON-NLS-1$ + private static final String PAGE_KEY_CHECK = "Page_KeyCheck"; //$NON-NLS-1$ + + static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$ + static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$ + static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$ + + /** + * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback. + */ + static abstract class ExportWizardPage extends WizardPage { + + /** bit mask constant for project data change event */ + protected static final int DATA_PROJECT = 0x001; + /** bit mask constant for keystore data change event */ + protected static final int DATA_KEYSTORE = 0x002; + /** bit mask constant for key data change event */ + protected static final int DATA_KEY = 0x004; + + protected static final VerifyListener sPasswordVerifier = new VerifyListener() { + public void verifyText(VerifyEvent e) { + // verify the characters are valid for password. + int len = e.text.length(); + + // first limit to 127 characters max + if (len + ((Text)e.getSource()).getText().length() > 127) { + e.doit = false; + return; + } + + // now only take non control characters + for (int i = 0 ; i < len ; i++) { + if (e.text.charAt(i) < 32) { + e.doit = false; + return; + } + } + } + }; + + /** + * Bit mask indicating what changed while the page was hidden. + * @see #DATA_PROJECT + * @see #DATA_KEYSTORE + * @see #DATA_KEY + */ + protected int mProjectDataChanged = 0; + + ExportWizardPage(String name) { + super(name); + } + + abstract void onShow(); + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + onShow(); + mProjectDataChanged = 0; + } + } + + final void projectDataChanged(int changeMask) { + mProjectDataChanged |= changeMask; + } + + /** + * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a + * {@link Throwable} object. + */ + protected final void onException(Throwable t) { + String message = getExceptionMessage(t); + + setErrorMessage(message); + setPageComplete(false); + } + } + + private ExportWizardPage mPages[] = new ExportWizardPage[5]; + + private IProject mProject; + + private String mKeystore; + private String mKeystorePassword; + private boolean mKeystoreCreationMode; + + private String mKeyAlias; + private String mKeyPassword; + private int mValidity; + private String mDName; + + private PrivateKey mPrivateKey; + private X509Certificate mCertificate; + + private String mDestinationPath; + private String mApkFilePath; + private String mApkFileName; + + private ExportWizardPage mKeystoreSelectionPage; + private ExportWizardPage mKeyCreationPage; + private ExportWizardPage mKeySelectionPage; + private ExportWizardPage mKeyCheckPage; + + private boolean mKeyCreationMode; + + private List<String> mExistingAliases; + + public ExportWizard() { + setHelpAvailable(false); // TODO have help + setWindowTitle("Export Android Application"); + setImageDescriptor(); + } + + @Override + public void addPages() { + addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK)); + addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this, + PAGE_KEYSTORE_SELECTION)); + addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION)); + addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION)); + addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK)); + } + + @Override + public boolean performFinish() { + // first we make sure export is fine if the destination file already exists + File f = new File(mDestinationPath); + if (f.isFile()) { + if (AdtPlugin.displayPrompt("Export Wizard", + "File already exists. Do you want to overwrite it?") == false) { + return false; + } + } + + // save the properties + ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore); + ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias); + ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, mDestinationPath); + + try { + if (mKeystoreCreationMode || mKeyCreationMode) { + final ArrayList<String> output = new ArrayList<String>(); + if (KeystoreHelper.createNewStore( + mKeystore, + null /*storeType*/, + mKeystorePassword, + mKeyAlias, + mKeyPassword, + mDName, + mValidity, + new IKeyGenOutput() { + public void err(String message) { + output.add(message); + } + public void out(String message) { + output.add(message); + } + }) == false) { + // keystore creation error! + displayError(output.toArray(new String[output.size()])); + return false; + } + + // keystore is created, now load the private key and certificate. + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(mKeystore); + keyStore.load(fis, mKeystorePassword.toCharArray()); + fis.close(); + PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( + mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray())); + + if (entry != null) { + mPrivateKey = entry.getPrivateKey(); + mCertificate = (X509Certificate)entry.getCertificate(); + } else { + // this really shouldn't happen since we now let the user choose the key + // from a list read from the store. + displayError("Could not find key"); + return false; + } + } + + // check the private key/certificate again since it may have been created just above. + if (mPrivateKey != null && mCertificate != null) { + FileOutputStream fos = new FileOutputStream(mDestinationPath); + SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate); + + // get the input file. + FileInputStream fis = new FileInputStream(mApkFilePath); + try { + builder.writeZip(fis, null /* filter */); + } finally { + fis.close(); + } + + builder.close(); + fos.close(); + + return true; + } + } catch (FileNotFoundException e) { + displayError(e); + } catch (NoSuchAlgorithmException e) { + displayError(e); + } catch (IOException e) { + displayError(e); + } catch (GeneralSecurityException e) { + displayError(e); + } catch (KeytoolException e) { + displayError(e); + } + + return false; + } + + @Override + public boolean canFinish() { + // check if we have the apk to resign, the destination location, and either + // a private key/certificate or the creation mode. In creation mode, unless + // all the key/keystore info is valid, the user cannot reach the last page, so there's + // no need to check them again here. + return mApkFilePath != null && + ((mPrivateKey != null && mCertificate != null) + || mKeystoreCreationMode || mKeyCreationMode) && + mDestinationPath != null; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection) + */ + public void init(IWorkbench workbench, IStructuredSelection selection) { + // get the project from the selection + Object selected = selection.getFirstElement(); + + if (selected instanceof IProject) { + mProject = (IProject)selected; + } else if (selected instanceof IAdaptable) { + IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class); + if (r != null) { + mProject = r.getProject(); + } + } + } + + ExportWizardPage getKeystoreSelectionPage() { + return mKeystoreSelectionPage; + } + + ExportWizardPage getKeyCreationPage() { + return mKeyCreationPage; + } + + ExportWizardPage getKeySelectionPage() { + return mKeySelectionPage; + } + + ExportWizardPage getKeyCheckPage() { + return mKeyCheckPage; + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } + + IProject getProject() { + return mProject; + } + + void setProject(IProject project, String apkFilePath, String filename) { + mProject = project; + mApkFilePath = apkFilePath; + mApkFileName = filename; + + updatePageOnChange(ExportWizardPage.DATA_PROJECT); + } + + String getApkFilename() { + return mApkFileName; + } + + void setKeystore(String path) { + mKeystore = path; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); + } + + String getKeystore() { + return mKeystore; + } + + void setKeystoreCreationMode(boolean createStore) { + mKeystoreCreationMode = createStore; + updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); + } + + boolean getKeystoreCreationMode() { + return mKeystoreCreationMode; + } + + + void setKeystorePassword(String password) { + mKeystorePassword = password; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); + } + + String getKeystorePassword() { + return mKeystorePassword; + } + + void setKeyCreationMode(boolean createKey) { + mKeyCreationMode = createKey; + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + boolean getKeyCreationMode() { + return mKeyCreationMode; + } + + void setExistingAliases(List<String> aliases) { + mExistingAliases = aliases; + } + + List<String> getExistingAliases() { + return mExistingAliases; + } + + void setKeyAlias(String name) { + mKeyAlias = name; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + String getKeyAlias() { + return mKeyAlias; + } + + void setKeyPassword(String password) { + mKeyPassword = password; + mPrivateKey = null; + mCertificate = null; + + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + String getKeyPassword() { + return mKeyPassword; + } + + void setValidity(int validity) { + mValidity = validity; + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + int getValidity() { + return mValidity; + } + + void setDName(String dName) { + mDName = dName; + updatePageOnChange(ExportWizardPage.DATA_KEY); + } + + String getDName() { + return mDName; + } + + void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + void setDestination(String path) { + mDestinationPath = path; + } + + void updatePageOnChange(int changeMask) { + for (ExportWizardPage page : mPages) { + page.projectDataChanged(changeMask); + } + } + + private void displayError(String... messages) { + String message = null; + if (messages.length == 1) { + message = messages[0]; + } else { + StringBuilder sb = new StringBuilder(messages[0]); + for (int i = 1; i < messages.length; i++) { + sb.append('\n'); + sb.append(messages[i]); + } + + message = sb.toString(); + } + + AdtPlugin.displayError("Export Wizard", message); + } + + private void displayError(Exception e) { + String message = getExceptionMessage(e); + displayError(message); + + AdtPlugin.log(e, "Export Wizard Error"); + } + + /** + * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns + * <code>null</code>, the method is called again on the cause of the Throwable object. + * <p/>If no Throwable in the chain has a valid message, the canonical name of the first + * exception is returned. + */ + private static String getExceptionMessage(Throwable t) { + String message = t.getMessage(); + if (message == null) { + Throwable cause = t.getCause(); + if (cause != null) { + return getExceptionMessage(cause); + } + + // no more cause and still no message. display the first exception. + return t.getClass().getCanonicalName(); + } + + return message; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java new file mode 100644 index 0000000..c64bf10 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.export; + +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.widgets.FormText; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; + +/** + * Final page of the wizard that checks the key and ask for the ouput location. + */ +final class KeyCheckPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private PrivateKey mPrivateKey; + private X509Certificate mCertificate; + private Text mDestination; + private boolean mFatalSigningError; + private FormText mDetailText; + + protected KeyCheckPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Destination and key/certificate checks"); + setDescription(""); // TODO + } + + public void createControl(Composite parent) { + setErrorMessage(null); + setMessage(null); + + // build the ui. + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(3, false); + gl.verticalSpacing *= 3; + composite.setLayout(gl); + + GridData gd; + + new Label(composite, SWT.NONE).setText("Destination APK file:"); + mDestination = new Text(composite, SWT.BORDER); + mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mDestination.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onDestinationChange(); + } + }); + final Button browseButton = new Button(composite, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE); + + fileDialog.setText("Destination file name"); + fileDialog.setFileName(mWizard.getApkFilename()); + + String saveLocation = fileDialog.open(); + if (saveLocation != null) { + mDestination.setText(saveLocation); + } + } + }); + + mDetailText = new FormText(composite, SWT.NONE); + mDetailText.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.horizontalSpan = 3; + + setControl(composite); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & DATA_PROJECT) != 0) { + // reset the destination from the content of the project + IProject project = mWizard.getProject(); + + String destination = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_DESTINATION); + if (destination != null) { + mDestination.setText(destination); + } + } + + // if anything change we basically reload the data. + if (mProjectDataChanged != 0) { + mFatalSigningError = false; + + // reset the wizard with no key/cert to make it not finishable, unless a valid + // key/cert is found. + mWizard.setSigningInfo(null, null); + + if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) { + int validity = mWizard.getValidity(); + StringBuilder sb = new StringBuilder( + String.format("<form><p>Certificate expires in %d years.</p>", + validity)); + + if (validity < 25) { + sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"); + sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>"); + sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, "); + sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>"); + sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>"); + } + + sb.append("</form>"); + mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */); + } else { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(mWizard.getKeystore()); + keyStore.load(fis, mWizard.getKeystorePassword().toCharArray()); + fis.close(); + PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( + mWizard.getKeyAlias(), + new KeyStore.PasswordProtection( + mWizard.getKeyPassword().toCharArray())); + + if (entry != null) { + mPrivateKey = entry.getPrivateKey(); + mCertificate = (X509Certificate)entry.getCertificate(); + } else { + setErrorMessage("Unable to find key."); + + setPageComplete(false); + } + } catch (FileNotFoundException e) { + // this was checked at the first previous step and will not happen here, unless + // the file was removed during the export wizard execution. + onException(e); + } catch (KeyStoreException e) { + onException(e); + } catch (NoSuchAlgorithmException e) { + onException(e); + } catch (UnrecoverableEntryException e) { + onException(e); + } catch (CertificateException e) { + onException(e); + } catch (IOException e) { + onException(e); + } + + if (mPrivateKey != null && mCertificate != null) { + Calendar expirationCalendar = Calendar.getInstance(); + expirationCalendar.setTime(mCertificate.getNotAfter()); + Calendar today = Calendar.getInstance(); + + if (expirationCalendar.before(today)) { + mDetailText.setText(String.format( + "<form><p>Certificate expired on %s</p></form>", + mCertificate.getNotAfter().toString()), + true /* parseTags */, true /* expandURLs */); + + // fatal error = nothing can make the page complete. + mFatalSigningError = true; + + setErrorMessage("Certificate is expired."); + setPageComplete(false); + } else { + // valid, key/cert: put it in the wizard so that it can be finished + mWizard.setSigningInfo(mPrivateKey, mCertificate); + + StringBuilder sb = new StringBuilder(String.format( + "<form><p>Certificate expires on %s.</p>", + mCertificate.getNotAfter().toString())); + + int expirationYear = expirationCalendar.get(Calendar.YEAR); + int thisYear = today.get(Calendar.YEAR); + + if (thisYear + 25 < expirationYear) { + // do nothing + } else { + if (expirationYear == thisYear) { + sb.append("<p>The certificate expires this year.</p>"); + } else { + int count = expirationYear-thisYear; + sb.append(String.format( + "<p>The Certificate expires in %1$s %2$s.</p>", + count, count == 1 ? "year" : "years")); + } + + sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"); + sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>"); + sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, "); + sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>"); + sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>"); + } + + sb.append("</form>"); + + mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */); + } + mDetailText.getParent().layout(); + } else { + // fatal error = nothing can make the page complete. + mFatalSigningError = true; + } + } + } + + onDestinationChange(); + } + + private void onDestinationChange() { + if (mFatalSigningError == false) { + // reset messages for now. + setErrorMessage(null); + setMessage(null); + + String path = mDestination.getText().trim(); + + if (path.length() == 0) { + setErrorMessage("Enter destination for the APK file."); + mWizard.setDestination(null); // this is to reset canFinish in the wizard + setPageComplete(false); + return; + } + + File file = new File(path); + if (file.isDirectory()) { + setErrorMessage("Destination is a directory."); + mWizard.setDestination(null); // this is to reset canFinish in the wizard + setPageComplete(false); + return; + } + + File parentFile = file.getParentFile(); + if (parentFile == null || parentFile.isDirectory() == false) { + setErrorMessage("Not a valid directory."); + mWizard.setDestination(null); // this is to reset canFinish in the wizard + setPageComplete(false); + return; + } + + // no error, set the destination in the wizard. + mWizard.setDestination(path); + setPageComplete(true); + + // However, we should also test if the file already exists. + if (file.isFile()) { + setMessage("Destination file already exists.", WARNING); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java new file mode 100644 index 0000000..d7365f7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.export; + +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.List; + +/** + * Key creation page. + */ +final class KeyCreationPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private Text mAlias; + private Text mKeyPassword; + private Text mKeyPassword2; + private Text mCnField; + private boolean mDisableOnChange = false; + private Text mOuField; + private Text mOField; + private Text mLField; + private Text mStField; + private Text mCField; + private String mDName; + private int mValidity = 0; + private List<String> mExistingAliases; + + + protected KeyCreationPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Key Creation"); + setDescription(""); // TODO? + } + + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(2, false); + composite.setLayout(gl); + + GridData gd; + + new Label(composite, SWT.NONE).setText("Alias:"); + mAlias = new Text(composite, SWT.BORDER); + mAlias.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Password:"); + mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeyPassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeyPassword.addVerifyListener(sPasswordVerifier); + + new Label(composite, SWT.NONE).setText("Confirm:"); + mKeyPassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeyPassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeyPassword2.addVerifyListener(sPasswordVerifier); + + new Label(composite, SWT.NONE).setText("Validity (years):"); + final Text validityText = new Text(composite, SWT.BORDER); + validityText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + validityText.addVerifyListener(new VerifyListener() { + public void verifyText(VerifyEvent e) { + // check for digit only. + for (int i = 0 ; i < e.text.length(); i++) { + char letter = e.text.charAt(i); + if (letter < '0' || letter > '9') { + e.doit = false; + return; + } + } + } + }); + + new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData( + gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + + new Label(composite, SWT.NONE).setText("First and Last Name:"); + mCnField = new Text(composite, SWT.BORDER); + mCnField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Organizational Unit:"); + mOuField = new Text(composite, SWT.BORDER); + mOuField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Organization:"); + mOField = new Text(composite, SWT.BORDER); + mOField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("City or Locality:"); + mLField = new Text(composite, SWT.BORDER); + mLField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("State or Province:"); + mStField = new Text(composite, SWT.BORDER); + mStField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(composite, SWT.NONE).setText("Country Code (XX):"); + mCField = new Text(composite, SWT.BORDER); + mCField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + mAlias.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeyAlias(mAlias.getText().trim()); + onChange(); + } + }); + mKeyPassword.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeyPassword(mKeyPassword.getText()); + onChange(); + } + }); + mKeyPassword2.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onChange(); + } + }); + + validityText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + try { + mValidity = Integer.parseInt(validityText.getText()); + } catch (NumberFormatException e2) { + // this should only happen if the text field is empty due to the verifyListener. + mValidity = 0; + } + mWizard.setValidity(mValidity); + onChange(); + } + }); + + ModifyListener dNameListener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + onDNameChange(); + } + }; + + mCnField.addModifyListener(dNameListener); + mOuField.addModifyListener(dNameListener); + mOField.addModifyListener(dNameListener); + mLField.addModifyListener(dNameListener); + mStField.addModifyListener(dNameListener); + mCField.addModifyListener(dNameListener); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) { + // reset the keystore/alias from the content of the project + IProject project = mWizard.getProject(); + + // disable onChange for now. we'll call it once at the end. + mDisableOnChange = true; + + String alias = ProjectHelper.loadStringProperty(project, ExportWizard.PROPERTY_ALIAS); + if (alias != null) { + mAlias.setText(alias); + } + + // get the existing list of keys if applicable + if (mWizard.getKeyCreationMode()) { + mExistingAliases = mWizard.getExistingAliases(); + } else { + mExistingAliases = null; + } + + // reset the passwords + mKeyPassword.setText(""); //$NON-NLS-1$ + mKeyPassword2.setText(""); //$NON-NLS-1$ + + // enable onChange, and call it to display errors and enable/disable pageCompleted. + mDisableOnChange = false; + onChange(); + } + } + + @Override + public IWizardPage getPreviousPage() { + if (mWizard.getKeyCreationMode()) { // this means we create a key from an existing store + return mWizard.getKeySelectionPage(); + } + + return mWizard.getKeystoreSelectionPage(); + } + + @Override + public IWizardPage getNextPage() { + return mWizard.getKeyCheckPage(); + } + + /** + * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}. + */ + private void onChange() { + if (mDisableOnChange) { + return; + } + + setErrorMessage(null); + setMessage(null); + + if (mAlias.getText().trim().length() == 0) { + setErrorMessage("Enter key alias."); + setPageComplete(false); + return; + } else if (mExistingAliases != null) { + // we cannot use indexOf, because we need to do a case-insensitive check + String keyAlias = mAlias.getText().trim(); + for (String alias : mExistingAliases) { + if (alias.equalsIgnoreCase(keyAlias)) { + setErrorMessage("Key alias already exists in keystore."); + setPageComplete(false); + return; + } + } + } + + String value = mKeyPassword.getText(); + if (value.length() == 0) { + setErrorMessage("Enter key password."); + setPageComplete(false); + return; + } else if (value.length() < 6) { + setErrorMessage("Key password is too short - must be at least 6 characters."); + setPageComplete(false); + return; + } + + if (value.equals(mKeyPassword2.getText()) == false) { + setErrorMessage("Key passwords don't match."); + setPageComplete(false); + return; + } + + if (mValidity == 0) { + setErrorMessage("Key certificate validity is required."); + setPageComplete(false); + return; + } else if (mValidity < 25) { + setMessage("A 25 year certificate validity is recommended.", WARNING); + } else if (mValidity > 1000) { + setErrorMessage("Key certificate validity must be between 1 and 1000 years."); + setPageComplete(false); + return; + } + + if (mDName == null || mDName.length() == 0) { + setErrorMessage("At least one Certificate issuer field is required to be non-empty."); + setPageComplete(false); + return; + } + + setPageComplete(true); + } + + /** + * Handles changes in the DName fields. + */ + private void onDNameChange() { + StringBuilder sb = new StringBuilder(); + + buildDName("CN", mCnField, sb); + buildDName("OU", mOuField, sb); + buildDName("O", mOField, sb); + buildDName("L", mLField, sb); + buildDName("ST", mStField, sb); + buildDName("C", mCField, sb); + + mDName = sb.toString(); + mWizard.setDName(mDName); + + onChange(); + } + + /** + * Builds the distinguished name string with the provided {@link StringBuilder}. + * @param prefix the prefix of the entry. + * @param textField The {@link Text} field containing the entry value. + * @param sb the string builder containing the dname. + */ + private void buildDName(String prefix, Text textField, StringBuilder sb) { + if (textField != null) { + String value = textField.getText().trim(); + if (value.length() > 0) { + if (sb.length() > 0) { + sb.append(","); + } + + sb.append(prefix); + sb.append('='); + sb.append(value); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java new file mode 100644 index 0000000..2fcd757 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.export; + +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Enumeration; + +/** + * Key Selection Page. This is used when an existing keystore is used. + */ +final class KeySelectionPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private Label mKeyAliasesLabel; + private Combo mKeyAliases; + private Label mKeyPasswordLabel; + private Text mKeyPassword; + private boolean mDisableOnChange = false; + private Button mUseExistingKey; + private Button mCreateKey; + + protected KeySelectionPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Key alias selection"); + setDescription(""); // TODO + } + + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(3, false); + composite.setLayout(gl); + + GridData gd; + + mUseExistingKey = new Button(composite, SWT.RADIO); + mUseExistingKey.setText("Use existing key"); + mUseExistingKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + mUseExistingKey.setSelection(true); + + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = 0; + gd.widthHint = 50; + mKeyAliasesLabel = new Label(composite, SWT.NONE); + mKeyAliasesLabel.setText("Alias:"); + mKeyAliases = new Combo(composite, SWT.READ_ONLY); + mKeyAliases.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = 0; + gd.widthHint = 50; + mKeyPasswordLabel = new Label(composite, SWT.NONE); + mKeyPasswordLabel.setText("Password:"); + mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeyPassword.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mCreateKey = new Button(composite, SWT.RADIO); + mCreateKey.setText("Create new key"); + mCreateKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + mUseExistingKey.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mWizard.setKeyCreationMode(!mUseExistingKey.getSelection()); + enableWidgets(); + onChange(); + } + }); + + mKeyAliases.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mWizard.setKeyAlias(mKeyAliases.getItem(mKeyAliases.getSelectionIndex())); + onChange(); + } + }); + + mKeyPassword.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeyPassword(mKeyPassword.getText()); + onChange(); + } + }); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) { + // disable onChange for now. we'll call it once at the end. + mDisableOnChange = true; + + // reset the alias from the content of the project + try { + // reset to using a key + mWizard.setKeyCreationMode(false); + mUseExistingKey.setSelection(true); + mCreateKey.setSelection(false); + enableWidgets(); + + // remove the content of the alias combo always and first, in case the + // keystore password is wrong + mKeyAliases.removeAll(); + + // get the alias list (also used as a keystore password test) + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream fis = new FileInputStream(mWizard.getKeystore()); + keyStore.load(fis, mWizard.getKeystorePassword().toCharArray()); + fis.close(); + + Enumeration<String> aliases = keyStore.aliases(); + + // get the alias from the project previous export, and look for a match as + // we add the aliases to the combo. + IProject project = mWizard.getProject(); + + String keyAlias = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_ALIAS); + + ArrayList<String> aliasList = new ArrayList<String>(); + + int selection = -1; + int count = 0; + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + mKeyAliases.add(alias); + aliasList.add(alias); + if (selection == -1 && alias.equalsIgnoreCase(keyAlias)) { + selection = count; + } + count++; + } + + mWizard.setExistingAliases(aliasList); + + if (selection != -1) { + mKeyAliases.select(selection); + + // since a match was found and is selected, we need to give it to + // the wizard as well + mWizard.setKeyAlias(keyAlias); + } else { + mKeyAliases.clearSelection(); + } + + // reset the password + mKeyPassword.setText(""); //$NON-NLS-1$ + + // enable onChange, and call it to display errors and enable/disable pageCompleted. + mDisableOnChange = false; + onChange(); + } catch (KeyStoreException e) { + onException(e); + } catch (FileNotFoundException e) { + onException(e); + } catch (NoSuchAlgorithmException e) { + onException(e); + } catch (CertificateException e) { + onException(e); + } catch (IOException e) { + onException(e); + } finally { + // in case we exit with an exception, we need to reset this + mDisableOnChange = false; + } + } + } + + @Override + public IWizardPage getPreviousPage() { + return mWizard.getKeystoreSelectionPage(); + } + + @Override + public IWizardPage getNextPage() { + if (mWizard.getKeyCreationMode()) { + return mWizard.getKeyCreationPage(); + } + + return mWizard.getKeyCheckPage(); + } + + /** + * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}. + */ + private void onChange() { + if (mDisableOnChange) { + return; + } + + setErrorMessage(null); + setMessage(null); + + if (mWizard.getKeyCreationMode() == false) { + if (mKeyAliases.getSelectionIndex() == -1) { + setErrorMessage("Select a key alias."); + setPageComplete(false); + return; + } + + if (mKeyPassword.getText().trim().length() == 0) { + setErrorMessage("Enter key password."); + setPageComplete(false); + return; + } + } + + setPageComplete(true); + } + + private void enableWidgets() { + boolean useKey = !mWizard.getKeyCreationMode(); + mKeyAliasesLabel.setEnabled(useKey); + mKeyAliases.setEnabled(useKey); + mKeyPassword.setEnabled(useKey); + mKeyPasswordLabel.setEnabled(useKey); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java new file mode 100644 index 0000000..c5a4d47 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.export; + +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** + * Keystore selection page. This page allows to choose to create a new keystore or use an + * existing one. + */ +final class KeystoreSelectionPage extends ExportWizardPage { + + private final ExportWizard mWizard; + private Button mUseExistingKeystore; + private Button mCreateKeystore; + private Text mKeystore; + private Text mKeystorePassword; + private Label mConfirmLabel; + private Text mKeystorePassword2; + private boolean mDisableOnChange = false; + + protected KeystoreSelectionPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Keystore selection"); + setDescription(""); //TODO + } + + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout gl = new GridLayout(3, false); + composite.setLayout(gl); + + GridData gd; + + mUseExistingKeystore = new Button(composite, SWT.RADIO); + mUseExistingKeystore.setText("Use existing keystore"); + mUseExistingKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + mUseExistingKeystore.setSelection(true); + + mCreateKeystore = new Button(composite, SWT.RADIO); + mCreateKeystore.setText("Create new keystore"); + mCreateKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + + new Label(composite, SWT.NONE).setText("Location:"); + mKeystore = new Text(composite, SWT.BORDER); + mKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + final Button browseButton = new Button(composite, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + FileDialog fileDialog; + if (mUseExistingKeystore.getSelection()) { + fileDialog = new FileDialog(browseButton.getShell(),SWT.OPEN); + fileDialog.setText("Load Keystore"); + } else { + fileDialog = new FileDialog(browseButton.getShell(),SWT.SAVE); + fileDialog.setText("Select Keystore Name"); + } + + String fileName = fileDialog.open(); + if (fileName != null) { + mKeystore.setText(fileName); + } + } + }); + + new Label(composite, SWT.NONE).setText("Password:"); + mKeystorePassword = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeystorePassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeystorePassword.addVerifyListener(sPasswordVerifier); + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + + mConfirmLabel = new Label(composite, SWT.NONE); + mConfirmLabel.setText("Confirm:"); + mKeystorePassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD); + mKeystorePassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mKeystorePassword2.addVerifyListener(sPasswordVerifier); + new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + mKeystorePassword2.setEnabled(false); + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + mUseExistingKeystore.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + boolean createStore = !mUseExistingKeystore.getSelection(); + mKeystorePassword2.setEnabled(createStore); + mConfirmLabel.setEnabled(createStore); + mWizard.setKeystoreCreationMode(createStore); + onChange(); + } + }); + + mKeystore.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeystore(mKeystore.getText().trim()); + onChange(); + } + }); + + mKeystorePassword.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + mWizard.setKeystorePassword(mKeystorePassword.getText()); + onChange(); + } + }); + + mKeystorePassword2.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onChange(); + } + }); + } + + @Override + public IWizardPage getNextPage() { + if (mUseExistingKeystore.getSelection()) { + return mWizard.getKeySelectionPage(); + } + + return mWizard.getKeyCreationPage(); + } + + @Override + void onShow() { + // fill the texts with information loaded from the project. + if ((mProjectDataChanged & DATA_PROJECT) != 0) { + // reset the keystore/alias from the content of the project + IProject project = mWizard.getProject(); + + // disable onChange for now. we'll call it once at the end. + mDisableOnChange = true; + + String keystore = ProjectHelper.loadStringProperty(project, + ExportWizard.PROPERTY_KEYSTORE); + if (keystore != null) { + mKeystore.setText(keystore); + } + + // reset the passwords + mKeystorePassword.setText(""); //$NON-NLS-1$ + mKeystorePassword2.setText(""); //$NON-NLS-1$ + + // enable onChange, and call it to display errors and enable/disable pageCompleted. + mDisableOnChange = false; + onChange(); + } + } + + /** + * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}. + */ + private void onChange() { + if (mDisableOnChange) { + return; + } + + setErrorMessage(null); + setMessage(null); + + boolean createStore = !mUseExistingKeystore.getSelection(); + + // checks the keystore path is non null. + String keystore = mKeystore.getText().trim(); + if (keystore.length() == 0) { + setErrorMessage("Enter path to keystore."); + setPageComplete(false); + return; + } else { + File f = new File(keystore); + if (f.exists() == false) { + if (createStore == false) { + setErrorMessage("Keystore does not exist."); + setPageComplete(false); + return; + } + } else if (f.isDirectory()) { + setErrorMessage("Keystore path is a directory."); + setPageComplete(false); + return; + } else if (f.isFile()) { + if (createStore) { + setErrorMessage("File already exists."); + setPageComplete(false); + return; + } + } + } + + String value = mKeystorePassword.getText(); + if (value.length() == 0) { + setErrorMessage("Enter keystore password."); + setPageComplete(false); + return; + } else if (createStore && value.length() < 6) { + setErrorMessage("Keystore password is too short - must be at least 6 characters."); + setPageComplete(false); + return; + } + + if (createStore) { + if (mKeystorePassword2.getText().length() == 0) { + setErrorMessage("Confirm keystore password."); + setPageComplete(false); + return; + } + + if (mKeystorePassword.getText().equals(mKeystorePassword2.getText()) == false) { + setErrorMessage("Keystore passwords do not match."); + setPageComplete(false); + return; + } + } + + setPageComplete(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java new file mode 100644 index 0000000..e161e18 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.export; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestParser; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.common.project.ProjectChooserHelper; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** + * First Export Wizard Page. Display warning/errors. + */ +final class ProjectCheckPage extends ExportWizardPage { + private final static String IMG_ERROR = "error.png"; //$NON-NLS-1$ + private final static String IMG_WARNING = "warning.png"; //$NON-NLS-1$ + + private final ExportWizard mWizard; + private Display mDisplay; + private Image mError; + private Image mWarning; + private boolean mHasMessage = false; + private Composite mTopComposite; + private Composite mErrorComposite; + private Text mProjectText; + private ProjectChooserHelper mProjectChooserHelper; + private boolean mFirstOnShow = true; + + protected ProjectCheckPage(ExportWizard wizard, String pageName) { + super(pageName); + mWizard = wizard; + + setTitle("Project Checks"); + setDescription("Performs a set of checks to make sure the application can be exported."); + } + + public void createControl(Composite parent) { + mProjectChooserHelper = new ProjectChooserHelper(parent.getShell()); + mDisplay = parent.getDisplay(); + + GridLayout gl = null; + GridData gd = null; + + mTopComposite = new Composite(parent, SWT.NONE); + mTopComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mTopComposite.setLayout(new GridLayout(1, false)); + + // composite for the project selection. + Composite projectComposite = new Composite(mTopComposite, SWT.NONE); + projectComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + projectComposite.setLayout(gl = new GridLayout(3, false)); + gl.marginHeight = gl.marginWidth = 0; + + Label label = new Label(projectComposite, SWT.NONE); + label.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + label.setText("Select the project to export:"); + + new Label(projectComposite, SWT.NONE).setText("Project:"); + mProjectText = new Text(projectComposite, SWT.BORDER); + mProjectText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + mProjectText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + handleProjectNameChange(); + } + }); + + Button browseButton = new Button(projectComposite, SWT.PUSH); + browseButton.setText("Browse..."); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject( + mProjectText.getText().trim()); + + if (javaProject != null) { + IProject project = javaProject.getProject(); + + // set the new name in the text field. The modify listener will take + // care of updating the status and the ExportWizard object. + mProjectText.setText(project.getName()); + } + } + }); + + setControl(mTopComposite); + } + + @Override + void onShow() { + if (mFirstOnShow) { + // get the project and init the ui + IProject project = mWizard.getProject(); + if (project != null) { + mProjectText.setText(project.getName()); + } + + mFirstOnShow = false; + } + } + + private void buildErrorUi(IProject project) { + // Show description the first time + setErrorMessage(null); + setMessage(null); + setPageComplete(true); + mHasMessage = false; + + // composite parent for the warning/error + GridLayout gl = null; + mErrorComposite = new Composite(mTopComposite, SWT.NONE); + mErrorComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + gl.verticalSpacing *= 3; // more spacing than normal. + mErrorComposite.setLayout(gl); + + if (project == null) { + setErrorMessage("Select project to export."); + mHasMessage = true; + } else { + try { + if (project.hasNature(AndroidConstants.NATURE) == false) { + addError(mErrorComposite, "Project is not an Android project."); + } else { + // check for errors + if (ProjectHelper.hasError(project, true)) { + addError(mErrorComposite, "Project has compilation error(s)"); + } + + // check the project output + IFolder outputIFolder = BaseProjectHelper.getOutputFolder(project); + if (outputIFolder != null) { + String outputOsPath = outputIFolder.getLocation().toOSString(); + String apkFilePath = outputOsPath + File.separator + project.getName() + + AndroidConstants.DOT_ANDROID_PACKAGE; + + File f = new File(apkFilePath); + if (f.isFile() == false) { + addError(mErrorComposite, + String.format("%1$s/%2$s/%1$s%3$s does not exists!", + project.getName(), + outputIFolder.getName(), + AndroidConstants.DOT_ANDROID_PACKAGE)); + } + } else { + addError(mErrorComposite, + "Unable to get the output folder of the project!"); + } + + + // project is an android project, we check the debuggable attribute. + AndroidManifestParser manifestParser = AndroidManifestParser.parse( + BaseProjectHelper.getJavaProject(project), null /* errorListener */, + true /* gatherData */, false /* markErrors */); + + Boolean debuggable = manifestParser.getDebuggable(); + + if (debuggable != null && debuggable == Boolean.TRUE) { + addWarning(mErrorComposite, + "The manifest 'debuggable' attribute is set to true.\nYou should set it to false for applications that you release to the public."); + } + + // check for mapview stuff + } + } catch (CoreException e) { + // unable to access nature + addError(mErrorComposite, "Unable to get project nature"); + } + } + + if (mHasMessage == false) { + Label label = new Label(mErrorComposite, SWT.NONE); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + label.setLayoutData(gd); + label.setText("No errors found. Click Next."); + } + + mTopComposite.layout(); + } + + /** + * Adds an error label to a {@link Composite} object. + * @param parent the Composite parent. + * @param message the error message. + */ + private void addError(Composite parent, String message) { + if (mError == null) { + mError = AdtPlugin.getImageLoader().loadImage(IMG_ERROR, mDisplay); + } + + new Label(parent, SWT.NONE).setImage(mError); + Label label = new Label(parent, SWT.NONE); + label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + label.setText(message); + + setErrorMessage("Application cannot be exported due to the error(s) below."); + setPageComplete(false); + mHasMessage = true; + } + + /** + * Adds a warning label to a {@link Composite} object. + * @param parent the Composite parent. + * @param message the warning message. + */ + private void addWarning(Composite parent, String message) { + if (mWarning == null) { + mWarning = AdtPlugin.getImageLoader().loadImage(IMG_WARNING, mDisplay); + } + + new Label(parent, SWT.NONE).setImage(mWarning); + Label label = new Label(parent, SWT.NONE); + label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + label.setText(message); + + mHasMessage = true; + } + + /** + * Checks the parameters for correctness, and update the error message and buttons. + */ + private void handleProjectNameChange() { + setPageComplete(false); + + if (mErrorComposite != null) { + mErrorComposite.dispose(); + mErrorComposite = null; + } + + // update the wizard with the new project + mWizard.setProject(null, null, null); + + //test the project name first! + String text = mProjectText.getText().trim(); + if (text.length() == 0) { + setErrorMessage("Select project to export."); + } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) { + setErrorMessage("Project name contains unsupported characters!"); + } else { + IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null); + IProject found = null; + for (IJavaProject javaProject : projects) { + if (javaProject.getProject().getName().equals(text)) { + found = javaProject.getProject(); + break; + } + + } + + if (found != null) { + setErrorMessage(null); + + // update the wizard with the new project + setApkFilePathInWizard(found); + + // now rebuild the error ui. + buildErrorUi(found); + } else { + setErrorMessage(String.format("There is no android project named '%1$s'", + text)); + } + } + } + + private void setApkFilePathInWizard(IProject project) { + if (project != null) { + IFolder outputIFolder = BaseProjectHelper.getOutputFolder(project); + if (outputIFolder != null) { + String outputOsPath = outputIFolder.getLocation().toOSString(); + String apkFilePath = outputOsPath + File.separator + project.getName() + + AndroidConstants.DOT_ANDROID_PACKAGE; + + File f = new File(apkFilePath); + if (f.isFile()) { + mWizard.setProject(project, apkFilePath, f.getName()); + return; + } + } + } + + mWizard.setProject(null, null, null); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java new file mode 100644 index 0000000..c7cb427 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.internal; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; + +/** + * Classpath container for the Android projects. + */ +class AndroidClasspathContainer implements IClasspathContainer { + + private IClasspathEntry[] mClasspathEntry; + private IPath mContainerPath; + private String mName; + + /** + * Constructs the container with the {@link IClasspathEntry} representing the android + * framework jar file and the container id + * @param entries the entries representing the android framework and optional libraries. + * @param path the path containing the classpath container id. + * @param name the name of the container to display. + */ + AndroidClasspathContainer(IClasspathEntry[] entries, IPath path, String name) { + mClasspathEntry = entries; + mContainerPath = path; + mName = name; + } + + public IClasspathEntry[] getClasspathEntries() { + return mClasspathEntry; + } + + public String getDescription() { + return mName; + } + + public int getKind() { + return IClasspathContainer.K_DEFAULT_SYSTEM; + } + + public IPath getPath() { + return mContainerPath; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java new file mode 100644 index 0000000..d686830 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.internal; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.ClasspathContainerInitializer; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.regex.Pattern; + +/** + * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to + * {@link IProject}s. This removes the hard-coded path to the android.jar. + */ +public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer { + /** The container id for the android framework jar file */ + private final static String CONTAINER_ID = + "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ + + /** path separator to store multiple paths in a single property. This is guaranteed to not + * be in a path. + */ + private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ + + private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ + private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ + private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ + private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; + + private final static int PATH_ANDROID_JAR = 0; + private final static int PATH_ANDROID_SRC = 1; + private final static int PATH_ANDROID_DOCS = 2; + private final static int PATH_ANDROID_OPT_DOCS = 3; + + public AndroidClasspathContainerInitializer() { + // pass + } + + /** + * Binds a classpath container to a {@link IClasspathContainer} for a given project, + * or silently fails if unable to do so. + * @param containerPath the container path that is the container id. + * @param project the project to bind + */ + @Override + public void initialize(IPath containerPath, IJavaProject project) throws CoreException { + if (CONTAINER_ID.equals(containerPath.toString())) { + JavaCore.setClasspathContainer(new Path(CONTAINER_ID), + new IJavaProject[] { project }, + new IClasspathContainer[] { allocateAndroidContainer(project) }, + new NullProgressMonitor()); + } + } + + /** + * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER} + * linking to the Android Framework. + */ + public static IClasspathEntry getContainerEntry() { + return JavaCore.newContainerEntry(new Path(CONTAINER_ID)); + } + + /** + * Checks the {@link IPath} objects against the android framework container id and + * returns <code>true</code> if they are identical. + * @param path the <code>IPath</code> to check. + */ + public static boolean checkPath(IPath path) { + return CONTAINER_ID.equals(path.toString()); + } + + /** + * Updates the {@link IJavaProject} objects with new android framework container. This forces + * JDT to recompile them. + * @param androidProjects the projects to update. + * @return <code>true</code> if success, <code>false</code> otherwise. + */ + public static boolean updateProjects(IJavaProject[] androidProjects) { + try { + // Allocate a new AndroidClasspathContainer, and associate it to the android framework + // container id for each projects. + // By providing a new association between a container id and a IClasspathContainer, + // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with + // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of + // the projects. + int projectCount = androidProjects.length; + + IClasspathContainer[] containers = new IClasspathContainer[projectCount]; + for (int i = 0 ; i < projectCount; i++) { + containers[i] = allocateAndroidContainer(androidProjects[i]); + } + + // give each project their new container in one call. + JavaCore.setClasspathContainer( + new Path(CONTAINER_ID), + androidProjects, containers, new NullProgressMonitor()); + + return true; + } catch (JavaModelException e) { + return false; + } + } + + /** + * Allocates and returns an {@link AndroidClasspathContainer} object with the proper + * path to the framework jar file. + * @param javaProject The java project that will receive the container. + */ + private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { + final IProject iProject = javaProject.getProject(); + + String markerMessage = null; + boolean outputToConsole = true; + + try { + AdtPlugin plugin = AdtPlugin.getDefault(); + + // get the lock object for project manipulation during SDK load. + Object lock = plugin.getSdkLockObject(); + synchronized (lock) { + boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; + + // check if the project has a valid target. + IAndroidTarget target = null; + if (sdkIsLoaded) { + target = Sdk.getCurrent().getTarget(iProject); + } + + // if we are loaded and the target is non null, we create a valid ClassPathContainer + if (sdkIsLoaded && target != null) { + String targetName = target.getFullName(); + + return new AndroidClasspathContainer( + createClasspathEntries(iProject, target, targetName), + new Path(CONTAINER_ID), targetName); + } + + // In case of error, we'll try different thing to provide the best error message + // possible. + // Get the project's target's hash string (if it exists) + String hashString = Sdk.getProjectTargetHashString(iProject); + + if (hashString == null || hashString.length() == 0) { + // if there is no hash string we only show this if the SDK is loaded. + // For a project opened at start-up with no target, this would be displayed + // twice, once when the project is opened, and once after the SDK has + // finished loading. + // By testing the sdk is loaded, we only show this once in the console. + if (sdkIsLoaded) { + markerMessage = String.format( + "Project has no target set. Edit the project properties to set one."); + } + } else if (sdkIsLoaded) { + markerMessage = String.format( + "Unable to resolve target '%s'", hashString); + } else { + // this is the case where there is a hashString but the SDK is not yet + // loaded and therefore we can't get the target yet. + // We check if there is a cache of the needed information. + AndroidClasspathContainer container = getContainerFromCache(iProject); + + if (container == null) { + // either the cache was wrong (ie folder does not exists anymore), or + // there was no cache. In this case we need to make sure the project + // is resolved again after the SDK is loaded. + plugin.setProjectToResolve(javaProject); + + markerMessage = String.format( + "Unable to resolve target '%s' until the SDK is loaded.", + hashString); + + // let's not log this one to the console as it will happen at every boot, + // and it's expected. (we do keep the error marker though). + outputToConsole = false; + + } else { + // we created a container from the cache, so we register the project + // to be checked for cache validity once the SDK is loaded + plugin.setProjectToCheck(javaProject); + + // and return the container + return container; + } + + } + + // return a dummy container to replace the one we may have had before. + // It'll be replaced by the real when if/when the target is resolved if/when the + // SDK finishes loading. + return new IClasspathContainer() { + public IClasspathEntry[] getClasspathEntries() { + return new IClasspathEntry[0]; + } + + public String getDescription() { + return "Unable to get system library for the project"; + } + + public int getKind() { + return IClasspathContainer.K_DEFAULT_SYSTEM; + } + + public IPath getPath() { + return null; + } + }; + } + } finally { + if (markerMessage != null) { + // log the error and put the marker on the project if we can. + if (outputToConsole) { + AdtPlugin.printBuildToConsole(AdtConstants.BUILD_ALWAYS, iProject, + markerMessage); + } + + try { + BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, markerMessage, + -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); + } catch (CoreException e) { + // In some cases, the workspace may be locked for modification when we + // pass here. + // We schedule a new job to put the marker after. + final String fmessage = markerMessage; + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, + fmessage, -1, IMarker.SEVERITY_ERROR, + IMarker.PRIORITY_HIGH); + } catch (CoreException e2) { + return e2.getStatus(); + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } + } else { + // no error, remove potential MARKER_TARGETs. + try { + if (iProject.exists()) { + iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, + IResource.DEPTH_INFINITE); + } + } catch (CoreException ce) { + // In some cases, the workspace may be locked for modification when we pass + // here, so we schedule a new job to put the marker after. + Job markerJob = new Job("Android SDK: Resolving error markers") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, + IResource.DEPTH_INFINITE); + } catch (CoreException e2) { + return e2.getStatus(); + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } + } + } + } + + /** + * Creates and returns an array of {@link IClasspathEntry} objects for the android + * framework and optional libraries. + * <p/>This references the OS path to the android.jar and the + * java doc directory. This is dynamically created when a project is opened, + * and never saved in the project itself, so there's no risk of storing an + * obsolete path. + * The method also stores the paths used to create the entries in the project persistent + * properties. A new {@link AndroidClasspathContainer} can be created from the stored path + * using the {@link #getContainerFromCache(IProject)} method. + * @param project + * @param target The target that contains the libraries. + * @param targetName + */ + private static IClasspathEntry[] createClasspathEntries(IProject project, + IAndroidTarget target, String targetName) { + + // get the path from the target + String[] paths = getTargetPaths(target); + + // create the classpath entry from the paths + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); + + // paths now contains all the path required to recreate the IClasspathEntry with no + // target info. We encode them in a single string, with each path separated by + // OS path separator. + StringBuilder sb = new StringBuilder(CACHE_VERSION); + for (String p : paths) { + sb.append(PATH_SEPARATOR); + sb.append(p); + } + + // store this in a project persistent property + ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); + ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); + + return entries; + } + + /** + * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. + */ + private static AndroidClasspathContainer getContainerFromCache(IProject project) { + // get the cached info from the project persistent properties. + String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); + String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); + if (cache == null || targetNameCache == null) { + return null; + } + + // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. + if (cache.startsWith(CACHE_VERSION_SEP) == false) { + return null; + } + + cache = cache.substring(CACHE_VERSION_SEP.length()); + + // the cache contains multiple paths, separated by a character guaranteed to not be in + // the path (\u001C). + // The first 3 are for android.jar (jar, source, doc), the rest are for the optional + // libraries and should contain at least one doc and a jar (if there are any libraries). + // Therefore, the path count should be 3 or 5+ + String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (paths.length < 3 || paths.length == 4) { + return null; + } + + // now we check the paths actually exist. + // There's an exception: If the source folder for android.jar does not exist, this is + // not a problem, so we skip it. + // Also paths[PATH_ANDROID_DOCS] is a URI to the javadoc, so we test it a bit differently. + try { + if (new File(paths[PATH_ANDROID_JAR]).exists() == false || + new File(new URI(paths[PATH_ANDROID_DOCS])).exists() == false) { + return null; + } + } catch (URISyntaxException e) { + return null; + } finally { + + } + + for (int i = 3 ; i < paths.length; i++) { + String path = paths[i]; + if (path.length() > 0) { + File f = new File(path); + if (f.exists() == false) { + return null; + } + } + } + + IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); + + return new AndroidClasspathContainer(entries, + new Path(CONTAINER_ID), targetNameCache); + } + + /** + * Generates an array of {@link IClasspathEntry} from a set of paths. + * @see #getTargetPaths(IAndroidTarget) + */ + private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) { + ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>(); + + // First, we create the IClasspathEntry for the framework. + // now add the android framework to the class path. + // create the path object. + IPath android_lib = new Path(paths[PATH_ANDROID_JAR]); + IPath android_src = new Path(paths[PATH_ANDROID_SRC]); + + // create the java doc link. + IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + paths[PATH_ANDROID_DOCS]); + + // create the access rule to restrict access to classes in com.android.internal + IAccessRule accessRule = JavaCore.newAccessRule( + new Path("com/android/internal/**"), //$NON-NLS-1$ + IAccessRule.K_NON_ACCESSIBLE); + + IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib, + android_src, // source attachment path + null, // default source attachment root path. + new IAccessRule[] { accessRule }, + new IClasspathAttribute[] { cpAttribute }, + false // not exported. + ); + + list.add(frameworkClasspathEntry); + + // now deal with optional libraries + if (paths.length >= 5) { + String docPath = paths[PATH_ANDROID_OPT_DOCS]; + int i = 4; + while (i < paths.length) { + Path jarPath = new Path(paths[i++]); + + IClasspathAttribute[] attributes = null; + if (docPath.length() > 0) { + attributes = new IClasspathAttribute[] { + JavaCore.newClasspathAttribute( + IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, + docPath) + }; + } + + IClasspathEntry entry = JavaCore.newLibraryEntry( + jarPath, + null, // source attachment path + null, // default source attachment root path. + null, + attributes, + false // not exported. + ); + list.add(entry); + } + } + + return list.toArray(new IClasspathEntry[list.size()]); + } + + /** + * Checks the projects' caches. If the cache was valid, the project is removed from the list. + * @param projects the list of projects to check. + */ + public static void checkProjectsCache(ArrayList<IJavaProject> projects) { + int i = 0; + projectLoop: while (i < projects.size()) { + IJavaProject javaProject = projects.get(i); + IProject iProject = javaProject.getProject(); + + // get the target from the project and its paths + IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); + if (target == null) { + // this is really not supposed to happen. This would mean there are cached paths, + // but default.properties was deleted. Keep the project in the list to force + // a resolve which will display the error. + i++; + continue; + } + + String[] targetPaths = getTargetPaths(target); + + // now get the cached paths + String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); + if (cache == null) { + // this should not happen. We'll force resolve again anyway. + i++; + continue; + } + + String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); + if (cachedPaths.length < 3 || cachedPaths.length == 4) { + // paths length is wrong. simply resolve the project again + i++; + continue; + } + + // Now we compare the paths. The first 4 can be compared directly. + // because of case sensitiveness we need to use File objects + + if (targetPaths.length != cachedPaths.length) { + // different paths, force resolve again. + i++; + continue; + } + + // compare the main paths (android.jar, main sources, main javadoc) + if (new File(targetPaths[PATH_ANDROID_JAR]).equals( + new File(cachedPaths[PATH_ANDROID_JAR])) == false || + new File(targetPaths[PATH_ANDROID_SRC]).equals( + new File(cachedPaths[PATH_ANDROID_SRC])) == false || + new File(targetPaths[PATH_ANDROID_DOCS]).equals( + new File(cachedPaths[PATH_ANDROID_DOCS])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + if (cachedPaths.length > PATH_ANDROID_OPT_DOCS) { + // compare optional libraries javadoc + if (new File(targetPaths[PATH_ANDROID_OPT_DOCS]).equals( + new File(cachedPaths[PATH_ANDROID_OPT_DOCS])) == false) { + // different paths, force resolve again. + i++; + continue; + } + + // testing the optional jar files is a little bit trickier. + // The order is not guaranteed to be identical. + // From a previous test, we do know however that there is the same number. + // The number of libraries should be low enough that we can simply go through the + // lists manually. + targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { + String targetPath = targetPaths[tpi]; + + // look for a match in the other array + for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { + if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { + // found a match. Try the next targetPath + continue targetLoop; + } + } + + // if we stop here, we haven't found a match, which means there's a + // discrepancy in the libraries. We force a resolve. + i++; + continue projectLoop; + } + } + + // at the point the check passes, and we can remove the project from the list. + // we do not increment i in this case. + projects.remove(i); + } + } + + /** + * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. + * <p/>The paths are always in the same order. + * <ul> + * <li>Path to android.jar</li> + * <li>Path to the source code for android.jar</li> + * <li>Path to the javadoc for the android platform</li> + * </ul> + * Additionally, if there are optional libraries, the array will contain: + * <ul> + * <li>Path to the librairies javadoc</li> + * <li>Path to the first .jar file</li> + * <li>(more .jar as needed)</li> + * </ul> + */ + private static String[] getTargetPaths(IAndroidTarget target) { + ArrayList<String> paths = new ArrayList<String>(); + + // first, we get the path for android.jar + // The order is: android.jar, source folder, docs folder + paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); + paths.add(target.getPath(IAndroidTarget.SOURCES)); + paths.add(AdtPlugin.getUrlDoc()); + + // now deal with optional libraries. + IOptionalLibrary[] libraries = target.getOptionalLibraries(); + if (libraries != null) { + // all the optional libraries use the same javadoc, so we start with this + String targetDocPath = target.getPath(IAndroidTarget.DOCS); + if (targetDocPath != null) { + paths.add(targetDocPath); + } else { + // we add an empty string, to always have the same count. + paths.add(""); + } + + // because different libraries could use the same jar file, we make sure we add + // each jar file only once. + HashSet<String> visitedJars = new HashSet<String>(); + for (IOptionalLibrary library : libraries) { + String jarPath = library.getJarPath(); + if (visitedJars.contains(jarPath) == false) { + visitedJars.add(jarPath); + paths.add(jarPath); + } + } + } + + return paths.toArray(new String[paths.size()]); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java new file mode 100644 index 0000000..a4c019f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project.properties; + +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; +import com.android.sdkuilib.ApkConfigWidget; +import com.android.sdkuilib.SdkTargetSelector; + +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.IWorkbenchPropertyPage; +import org.eclipse.ui.dialogs.PropertyPage; + +import java.util.Map; + +/** + * Property page for "Android" project. + * This is accessible from the Package Explorer when right clicking a project and choosing + * "Properties". + * + */ +public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPropertyPage { + + private IProject mProject; + private SdkTargetSelector mSelector; + private ApkConfigWidget mApkConfigWidget; + + public AndroidPropertyPage() { + // pass + } + + @Override + protected Control createContents(Composite parent) { + // get the element (this is not yet valid in the constructor). + mProject = (IProject)getElement(); + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + + // build the UI. + Composite top = new Composite(parent, SWT.NONE); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + top.setLayout(new GridLayout(1, false)); + + Label l = new Label(top, SWT.NONE); + l.setText("Project Target"); + + mSelector = new SdkTargetSelector(top, targets, false /*allowMultipleSelection*/); + + l = new Label(top, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + l = new Label(top, SWT.NONE); + l.setText("Project APK Configurations"); + + mApkConfigWidget = new ApkConfigWidget(top); + + // fill the ui + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null && mProject.isOpen()) { + // get the target + IAndroidTarget target = currentSdk.getTarget(mProject); + if (target != null) { + mSelector.setSelection(target); + } + + // get the apk configurations + Map<String, String> configs = currentSdk.getProjectApkConfigs(mProject); + mApkConfigWidget.fillTable(configs); + } + + mSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // look for the selection and validate the page if there is a selection + IAndroidTarget target = mSelector.getFirstSelected(); + setValid(target != null); + } + }); + + if (mProject.isOpen() == false) { + // disable the ui. + } + + return top; + } + + @Override + public boolean performOk() { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + currentSdk.setProject(mProject, mSelector.getFirstSelected(), + mApkConfigWidget.getApkConfigs()); + } + + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java new file mode 100644 index 0000000..1f6ebf1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; + +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.management.InvalidAttributeValueException; + +/** + * Custom class loader able to load a class from the SDK jar file. + */ +public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader { + + /** + * Wrapper around a {@link Class} to provide the methods of + * {@link IAndroidClassLoader.IClassDescriptor}. + */ + public final static class ClassWrapper implements IClassDescriptor { + private Class<?> mClass; + + public ClassWrapper(Class<?> clazz) { + mClass = clazz; + } + + public String getCanonicalName() { + return mClass.getCanonicalName(); + } + + public IClassDescriptor[] getDeclaredClasses() { + Class<?>[] classes = mClass.getDeclaredClasses(); + IClassDescriptor[] iclasses = new IClassDescriptor[classes.length]; + for (int i = 0 ; i < classes.length ; i++) { + iclasses[i] = new ClassWrapper(classes[i]); + } + + return iclasses; + } + + public IClassDescriptor getEnclosingClass() { + return new ClassWrapper(mClass.getEnclosingClass()); + } + + public String getSimpleName() { + return mClass.getSimpleName(); + } + + public IClassDescriptor getSuperclass() { + return new ClassWrapper(mClass.getSuperclass()); + } + + @Override + public boolean equals(Object clazz) { + if (clazz instanceof ClassWrapper) { + return mClass.equals(((ClassWrapper)clazz).mClass); + } + return super.equals(clazz); + } + + @Override + public int hashCode() { + return mClass.hashCode(); + } + + + public boolean isInstantiable() { + int modifiers = mClass.getModifiers(); + return Modifier.isAbstract(modifiers) == false && Modifier.isPublic(modifiers) == true; + } + + public Class<?> wrappedClass() { + return mClass; + } + + } + + private String mOsFrameworkLocation; + + /** A cache for binary data extracted from the zip */ + private final HashMap<String, byte[]> mEntryCache = new HashMap<String, byte[]>(); + /** A cache for already defined Classes */ + private final HashMap<String, Class<?> > mClassCache = new HashMap<String, Class<?> >(); + + /** + * Creates the class loader by providing the os path to the framework jar archive + * + * @param osFrameworkLocation OS Path of the framework JAR file + */ + public AndroidJarLoader(String osFrameworkLocation) { + super(); + mOsFrameworkLocation = osFrameworkLocation; + } + + public String getSource() { + return mOsFrameworkLocation; + } + + /** + * Pre-loads all class binary data that belong to the given package by reading the archive + * once and caching them internally. + * <p/> + * This does not actually preload "classes", it just reads the unzipped bytes for a given + * class. To obtain a class, one must call {@link #findClass(String)} later. + * <p/> + * All classes which package name starts with "packageFilter" will be included and can be + * found later. + * <p/> + * May throw some exceptions if the framework JAR cannot be read. + * + * @param packageFilter The package that contains all the class data to preload, using a fully + * qualified binary name (.e.g "com.my.package."). The matching algorithm + * is simple "startsWith". Use an empty string to include everything. + * @param taskLabel An optional task name for the sub monitor. Can be null. + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + public void preLoadClasses(String packageFilter, String taskLabel, IProgressMonitor monitor) + throws IOException, InvalidAttributeValueException, ClassFormatError { + // Transform the package name into a zip entry path + String pathFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + + SubMonitor progress = SubMonitor.convert(monitor, taskLabel == null ? "" : taskLabel, 100); + + // create streams to read the intermediary archive + FileInputStream fis = new FileInputStream(mOsFrameworkLocation); + ZipInputStream zis = new ZipInputStream(fis); + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry. + String entryPath = entry.getName(); + + if (!entryPath.endsWith(AndroidConstants.DOT_CLASS)) { + // only accept class files + continue; + } + + // check if it is part of the package to preload + if (pathFilter.length() > 0 && !entryPath.startsWith(pathFilter)) { + continue; + } + String className = entryPathToClassName(entryPath); + + if (!mEntryCache.containsKey(className)) { + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + byte[] data = readZipData(zis, (int)entrySize); + mEntryCache.put(className, data); + } + + // advance 5% of whatever is allocated on the progress bar + progress.setWorkRemaining(100); + progress.worked(5); + progress.subTask(String.format("Preload %1$s", className)); + } + } + + /** + * Finds and loads all classes that derive from a given set of super classes. + * <p/> + * As a side-effect this will load and cache most, if not all, classes in the input JAR file. + * + * @param packageFilter Base name of package of classes to find. + * Use an empty string to find everyting. + * @param superClasses The super classes of all the classes to find. + * @return An hash map which keys are the super classes looked for and which values are + * ArrayList of the classes found. The array lists are always created for all the + * valid keys, they are simply empty if no deriving class is found for a given + * super class. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom( + String packageFilter, + String[] superClasses) + throws IOException, InvalidAttributeValueException, ClassFormatError { + + packageFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + + HashMap<String, ArrayList<IClassDescriptor>> mClassesFound = + new HashMap<String, ArrayList<IClassDescriptor>>(); + + for (String className : superClasses) { + mClassesFound.put(className, new ArrayList<IClassDescriptor>()); + } + + // create streams to read the intermediary archive + FileInputStream fis = new FileInputStream(mOsFrameworkLocation); + ZipInputStream zis = new ZipInputStream(fis); + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry and convert to a class binary name + String entryPath = entry.getName(); + if (!entryPath.endsWith(AndroidConstants.DOT_CLASS)) { + // only accept class files + continue; + } + if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) { + // only accept stuff from the requested root package. + continue; + } + String className = entryPathToClassName(entryPath); + + Class<?> loaded_class = mClassCache.get(className); + if (loaded_class == null) { + byte[] data = mEntryCache.get(className); + if (data == null) { + // Get the class and cache it + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + data = readZipData(zis, (int)entrySize); + } + loaded_class = defineAndCacheClass(className, data); + } + + for (Class<?> superClass = loaded_class.getSuperclass(); + superClass != null; + superClass = superClass.getSuperclass()) { + String superName = superClass.getCanonicalName(); + if (mClassesFound.containsKey(superName)) { + mClassesFound.get(superName).add(new ClassWrapper(loaded_class)); + break; + } + } + } + + return mClassesFound; + } + + /** Helper method that converts a Zip entry path into a corresponding + * Java full qualified binary class name. + * <p/> + * F.ex, this converts "com/my/package/Foo.class" into "com.my.package.Foo". + */ + private String entryPathToClassName(String entryPath) { + return entryPath.replaceFirst("\\.class$", "").replaceAll("[/\\\\]", "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + /** + * Finds the class with the specified binary name. + * + * {@inheritDoc} + */ + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + try { + // try to find the class in the cache + Class<?> cached_class = mClassCache.get(name); + if (cached_class == ClassNotFoundException.class) { + // we already know we can't find this class, don't try again + throw new ClassNotFoundException(name); + } else if (cached_class != null) { + return cached_class; + } + + // if not found, look it up and cache it + byte[] data = loadClassData(name); + if (data != null) { + return defineAndCacheClass(name, data); + } else { + // if the class can't be found, record a CNFE class in the map so + // that we don't try to reload it next time + mClassCache.put(name, ClassNotFoundException.class); + throw new ClassNotFoundException(name); + } + } catch (ClassNotFoundException e) { + throw e; + } catch (Exception e) { + throw new ClassNotFoundException(e.getMessage()); + } + } + + /** + * Defines a class based on its binary data and caches the resulting class object. + * + * @param name The binary name of the class (i.e. package.class1$class2) + * @param data The binary data from the loader. + * @return The class defined + * @throws ClassFormatError if defineClass failed. + */ + private Class<?> defineAndCacheClass(String name, byte[] data) throws ClassFormatError { + Class<?> cached_class; + cached_class = defineClass(null, data, 0, data.length); + + if (cached_class != null) { + // Add new class to the cache class and remove it from the zip entry data cache + mClassCache.put(name, cached_class); + mEntryCache.remove(name); + } + return cached_class; + } + + /** + * Loads a class data from its binary name. + * <p/> + * This uses the class binary data that has been preloaded earlier by the preLoadClasses() + * method if possible. + * + * @param className the binary name + * @return an array of bytes representing the class data or null if not found + * @throws InvalidAttributeValueException + * @throws IOException + */ + private synchronized byte[] loadClassData(String className) + throws InvalidAttributeValueException, IOException { + + byte[] data = mEntryCache.get(className); + if (data != null) { + return data; + } + + // The name is a binary name. Something like "android.R", or "android.R$id". + // Make a path out of it. + String entryName = className.replaceAll("\\.", "/") + AndroidConstants.DOT_CLASS; //$NON-NLS-1$ //$NON-NLS-2$ + + // create streams to read the intermediary archive + FileInputStream fis = new FileInputStream(mOsFrameworkLocation); + ZipInputStream zis = new ZipInputStream(fis); + + // loop on the entries of the intermediary package and put them in the final package. + ZipEntry entry; + + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry. + String currEntryName = entry.getName(); + + if (currEntryName.equals(entryName)) { + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + + data = readZipData(zis, (int)entrySize); + return data; + } + } + + return null; + } + + /** + * Reads data for the <em>current</em> entry from the zip input stream. + * + * @param zis The Zip input stream + * @param entrySize The entry size. -1 if unknown. + * @return The new data for the <em>current</em> entry. + * @throws IOException If ZipInputStream.read() fails. + */ + private byte[] readZipData(ZipInputStream zis, int entrySize) throws IOException { + int block_size = 1024; + int data_size = entrySize < 1 ? block_size : entrySize; + int offset = 0; + byte[] data = new byte[data_size]; + + while(zis.available() != 0) { + int count = zis.read(data, offset, data_size - offset); + if (count < 0) { // read data is done + break; + } + offset += count; + + if (entrySize >= 1 && offset >= entrySize) { // we know the size and we're done + break; + } + + // if we don't know the entry size and we're not done reading, + // expand the data buffer some more. + if (offset >= data_size) { + byte[] temp = new byte[data_size + block_size]; + System.arraycopy(data, 0, temp, 0, data_size); + data_size += block_size; + data = temp; + block_size *= 2; + } + } + + if (offset < data_size) { + // buffer was allocated too large, trim it + byte[] temp = new byte[offset]; + if (offset > 0) { + System.arraycopy(data, 0, temp, 0, offset); + } + data = temp; + } + + return data; + } + + /** + * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name. + * @param className the fully-qualified name of the class to return. + * @throws ClassNotFoundException + */ + public IClassDescriptor getClass(String className) throws ClassNotFoundException { + try { + return new ClassWrapper(loadClass(className)); + } catch (ClassNotFoundException e) { + throw e; // useful for debugging + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java new file mode 100644 index 0000000..a8852e7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.adt.build.DexWrapper; +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors; +import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors; +import com.android.ide.eclipse.editors.resources.manager.ProjectResources; +import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors; +import com.android.layoutlib.api.ILayoutBridge; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.IOptionalLibrary; + +import java.util.Hashtable; +import java.util.Map; + +/** + * This class contains the data of an Android Target as loaded from the SDK. + */ +public class AndroidTargetData { + + public final static int DESCRIPTOR_MANIFEST = 1; + public final static int DESCRIPTOR_LAYOUT = 2; + public final static int DESCRIPTOR_MENU = 3; + public final static int DESCRIPTOR_XML = 4; + public final static int DESCRIPTOR_RESOURCES = 5; + public final static int DESCRIPTOR_SEARCHABLE = 6; + public final static int DESCRIPTOR_PREFERENCES = 7; + public final static int DESCRIPTOR_GADGET_PROVIDER = 8; + + public final static class LayoutBridge { + /** Link to the layout bridge */ + public ILayoutBridge bridge; + + public LoadStatus status = LoadStatus.LOADING; + + public ClassLoader classLoader; + + public int apiLevel; + } + + private final IAndroidTarget mTarget; + + private DexWrapper mDexWrapper; + + /** + * mAttributeValues is a map { key => list [ values ] }. + * The key for the map is "(element-xml-name,attribute-namespace:attribute-xml-local-name)". + * The attribute namespace prefix must be: + * - "android" for AndroidConstants.NS_RESOURCES + * - "xmlns" for the XMLNS URI. + * + * This is used for attributes that do not have a unique name, but still need to be populated + * with values in the UI. Uniquely named attributes have their values in {@link #mEnumValueMap}. + */ + private Hashtable<String, String[]> mAttributeValues = new Hashtable<String, String[]>(); + + private IResourceRepository mSystemResourceRepository; + + private AndroidManifestDescriptors mManifestDescriptors; + private LayoutDescriptors mLayoutDescriptors; + private MenuDescriptors mMenuDescriptors; + private XmlDescriptors mXmlDescriptors; + + private Map<String, Map<String, Integer>> mEnumValueMap; + + private ProjectResources mFrameworkResources; + private LayoutBridge mLayoutBridge; + + private boolean mLayoutBridgeInit = false; + + AndroidTargetData(IAndroidTarget androidTarget) { + mTarget = androidTarget; + } + + void setDexWrapper(DexWrapper wrapper) { + mDexWrapper = wrapper; + } + + /** + * Creates an AndroidTargetData object. + * @param optionalLibraries + */ + void setExtraData(IResourceRepository systemResourceRepository, + AndroidManifestDescriptors manifestDescriptors, + LayoutDescriptors layoutDescriptors, + MenuDescriptors menuDescriptors, + XmlDescriptors xmlDescriptors, + Map<String, Map<String, Integer>> enumValueMap, + String[] permissionValues, + String[] activityIntentActionValues, + String[] broadcastIntentActionValues, + String[] serviceIntentActionValues, + String[] intentCategoryValues, + IOptionalLibrary[] optionalLibraries, + ProjectResources resources, + LayoutBridge layoutBridge) { + + mSystemResourceRepository = systemResourceRepository; + mManifestDescriptors = manifestDescriptors; + mLayoutDescriptors = layoutDescriptors; + mMenuDescriptors = menuDescriptors; + mXmlDescriptors = xmlDescriptors; + mEnumValueMap = enumValueMap; + mFrameworkResources = resources; + mLayoutBridge = layoutBridge; + + setPermissions(permissionValues); + setIntentFilterActionsAndCategories(activityIntentActionValues, broadcastIntentActionValues, + serviceIntentActionValues, intentCategoryValues); + setOptionalLibraries(optionalLibraries); + } + + public DexWrapper getDexWrapper() { + return mDexWrapper; + } + + public IResourceRepository getSystemResources() { + return mSystemResourceRepository; + } + + /** + * Returns an {@link IDescriptorProvider} from a given Id. + * The Id can be one of {@link #DESCRIPTOR_MANIFEST}, {@link #DESCRIPTOR_LAYOUT}, + * {@link #DESCRIPTOR_MENU}, or {@link #DESCRIPTOR_XML}. + * All other values will throw an {@link IllegalArgumentException}. + */ + public IDescriptorProvider getDescriptorProvider(int descriptorId) { + switch (descriptorId) { + case DESCRIPTOR_MANIFEST: + return mManifestDescriptors; + case DESCRIPTOR_LAYOUT: + return mLayoutDescriptors; + case DESCRIPTOR_MENU: + return mMenuDescriptors; + case DESCRIPTOR_XML: + return mXmlDescriptors; + case DESCRIPTOR_RESOURCES: + // FIXME: since it's hard-coded the Resources Descriptors are not platform dependent. + return ResourcesDescriptors.getInstance(); + case DESCRIPTOR_PREFERENCES: + return mXmlDescriptors.getPreferencesProvider(); + case DESCRIPTOR_GADGET_PROVIDER: + return mXmlDescriptors.getGadgetProvider(); + case DESCRIPTOR_SEARCHABLE: + return mXmlDescriptors.getSearchableProvider(); + default : + throw new IllegalArgumentException(); + } + } + + /** + * Returns the manifest descriptors. + */ + public AndroidManifestDescriptors getManifestDescriptors() { + return mManifestDescriptors; + } + + /** + * Returns the layout Descriptors. + */ + public LayoutDescriptors getLayoutDescriptors() { + return mLayoutDescriptors; + } + + /** + * Returns the menu descriptors. + */ + public MenuDescriptors getMenuDescriptors() { + return mMenuDescriptors; + } + + /** + * Returns the XML descriptors + */ + public XmlDescriptors getXmlDescriptors() { + return mXmlDescriptors; + } + + /** + * Returns this list of possible values for an XML attribute. + * <p/>This should only be called for attributes for which possible values depend on the + * parent element node. + * <p/>For attributes that have the same values no matter the parent node, use + * {@link #getEnumValueMap()}. + * @param elementName the name of the element containing the attribute. + * @param attributeName the name of the attribute + * @return an array of String with the possible values, or <code>null</code> if no values were + * found. + */ + public String[] getAttributeValues(String elementName, String attributeName) { + String key = String.format("(%1$s,%2$s)", elementName, attributeName); //$NON-NLS-1$ + return mAttributeValues.get(key); + } + + /** + * Returns this list of possible values for an XML attribute. + * <p/>This should only be called for attributes for which possible values depend on the + * parent and great-grand-parent element node. + * <p/>The typical example of this is for the 'name' attribute under + * activity/intent-filter/action + * <p/>For attributes that have the same values no matter the parent node, use + * {@link #getEnumValueMap()}. + * @param elementName the name of the element containing the attribute. + * @param attributeName the name of the attribute + * @param greatGrandParentElementName the great-grand-parent node. + * @return an array of String with the possible values, or <code>null</code> if no values were + * found. + */ + public String[] getAttributeValues(String elementName, String attributeName, + String greatGrandParentElementName) { + if (greatGrandParentElementName != null) { + String key = String.format("(%1$s,%2$s,%3$s)", //$NON-NLS-1$ + greatGrandParentElementName, elementName, attributeName); + String[] values = mAttributeValues.get(key); + if (values != null) { + return values; + } + } + + return getAttributeValues(elementName, attributeName); + } + + /** + * Returns the enum values map. + * <p/>The map defines the possible values for XML attributes. The key is the attribute name + * and the value is a map of (string, integer) in which the key (string) is the name of + * the value, and the Integer is the numerical value in the compiled binary XML files. + */ + public Map<String, Map<String, Integer>> getEnumValueMap() { + return mEnumValueMap; + } + + /** + * Returns the {@link ProjectResources} containing the Framework Resources. + */ + public ProjectResources getFrameworkResources() { + return mFrameworkResources; + } + + /** + * Returns a {@link LayoutBridge} object possibly containing a {@link ILayoutBridge} object. + * <p/>If {@link LayoutBridge#bridge} is <code>null</code>, {@link LayoutBridge#status} will + * contain the reason (either {@link LoadStatus#LOADING} or {@link LoadStatus#FAILED}). + * <p/>Valid {@link ILayoutBridge} objects are always initialized before being returned. + */ + public synchronized LayoutBridge getLayoutBridge() { + if (mLayoutBridgeInit == false && mLayoutBridge.bridge != null) { + mLayoutBridge.bridge.init(mTarget.getPath(IAndroidTarget.FONTS), + getEnumValueMap()); + mLayoutBridgeInit = true; + } + return mLayoutBridge; + } + + /** + * Sets the permission values + * @param permissionValues the list of permissions + */ + private void setPermissions(String[] permissionValues) { + setValues("(uses-permission,android:name)", permissionValues); //$NON-NLS-1$ + setValues("(application,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(activity,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(receiver,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(service,android:permission)", permissionValues); //$NON-NLS-1$ + setValues("(provider,android:permission)", permissionValues); //$NON-NLS-1$ + } + + private void setIntentFilterActionsAndCategories(String[] activityIntentActions, + String[] broadcastIntentActions, String[] serviceIntentActions, + String[] intentCategoryValues) { + setValues("(activity,action,android:name)", activityIntentActions); //$NON-NLS-1$ + setValues("(receiver,action,android:name)", broadcastIntentActions); //$NON-NLS-1$ + setValues("(service,action,android:name)", serviceIntentActions); //$NON-NLS-1$ + setValues("(category,android:name)", intentCategoryValues); //$NON-NLS-1$ + } + + private void setOptionalLibraries(IOptionalLibrary[] optionalLibraries) { + String[] values; + + if (optionalLibraries == null) { + values = new String[0]; + } else { + values = new String[optionalLibraries.length]; + for (int i = 0; i < optionalLibraries.length; i++) { + values[i] = optionalLibraries[i].getName(); + } + } + setValues("(uses-library,android:name)", values); + } + + /** + * Sets a (name, values) pair in the hash map. + * <p/> + * If the name is already present in the map, it is first removed. + * @param name the name associated with the values. + * @param values The values to add. + */ + private void setValues(String name, String[] values) { + mAttributeValues.remove(name); + mAttributeValues.put(name, values); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java new file mode 100644 index 0000000..04baeba --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java @@ -0,0 +1,704 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.build.DexWrapper; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.AttrsXmlParser; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo; +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.common.resources.ViewClassInfo; +import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors; +import com.android.ide.eclipse.editors.resources.manager.ProjectResources; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors; +import com.android.layoutlib.api.ILayoutBridge; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.InvalidAttributeValueException; + +/** + * Parser for the platform data in an SDK. + * <p/> + * This gather the following information: + * <ul> + * <li>Resource ID from <code>android.R</code></li> + * <li>The list of permissions values from <code>android.Manifest$permission</code></li> + * <li></li> + * </ul> + */ +public final class AndroidTargetParser { + + private static final String TAG = "Framework Resource Parser"; + private final IAndroidTarget mAndroidTarget; + + /** + * Creates a platform data parser. + */ + public AndroidTargetParser(IAndroidTarget platformTarget) { + mAndroidTarget = platformTarget; + } + + /** + * Parses the framework, collects all interesting information and stores them in the + * {@link IAndroidTarget} given to the constructor. + * + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + * @return True if the SDK path was valid and parsing has been attempted. + */ + public IStatus run(IProgressMonitor monitor) { + try { + SubMonitor progress = SubMonitor.convert(monitor, + String.format("Parsing SDK %1$s", mAndroidTarget.getName()), + 14); + + AndroidTargetData targetData = new AndroidTargetData(mAndroidTarget); + + // load DX. + DexWrapper dexWrapper = new DexWrapper(); + IStatus res = dexWrapper.loadDex(mAndroidTarget.getPath(IAndroidTarget.DX_JAR)); + if (res != Status.OK_STATUS) { + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + String.format("dx.jar loading failed for target '%1$s'", + mAndroidTarget.getFullName())); + } + + // we have loaded dx. + targetData.setDexWrapper(dexWrapper); + progress.worked(1); + + // parse the rest of the data. + + AndroidJarLoader classLoader = + new AndroidJarLoader(mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR)); + + preload(classLoader, progress.newChild(40, SubMonitor.SUPPRESS_NONE)); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // get the resource Ids. + progress.subTask("Resource IDs"); + IResourceRepository frameworkRepository = collectResourceIds(classLoader); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // get the permissions + progress.subTask("Permissions"); + String[] permissionValues = collectPermissions(classLoader); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // get the action and category values for the Intents. + progress.subTask("Intents"); + ArrayList<String> activity_actions = new ArrayList<String>(); + ArrayList<String> broadcast_actions = new ArrayList<String>(); + ArrayList<String> service_actions = new ArrayList<String>(); + ArrayList<String> categories = new ArrayList<String>(); + collectIntentFilterActionsAndCategories(activity_actions, broadcast_actions, + service_actions, categories); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // gather the attribute definition + progress.subTask("Attributes definitions"); + AttrsXmlParser attrsXmlParser = new AttrsXmlParser( + mAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES)); + attrsXmlParser.preload(); + progress.worked(1); + + progress.subTask("Manifest definitions"); + AttrsXmlParser attrsManifestXmlParser = new AttrsXmlParser( + mAndroidTarget.getPath(IAndroidTarget.MANIFEST_ATTRIBUTES), + attrsXmlParser); + attrsManifestXmlParser.preload(); + progress.worked(1); + + Collection<ViewClassInfo> mainList = new ArrayList<ViewClassInfo>(); + Collection<ViewClassInfo> groupList = new ArrayList<ViewClassInfo>(); + + // collect the layout/widgets classes + progress.subTask("Widgets and layouts"); + collectLayoutClasses(classLoader, attrsXmlParser, mainList, groupList, + progress.newChild(1)); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + ViewClassInfo[] layoutViewsInfo = mainList.toArray(new ViewClassInfo[mainList.size()]); + ViewClassInfo[] layoutGroupsInfo = groupList.toArray( + new ViewClassInfo[groupList.size()]); + + // collect the preferences classes. + mainList.clear(); + groupList.clear(); + collectPreferenceClasses(classLoader, attrsXmlParser, mainList, groupList, + progress.newChild(1)); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + ViewClassInfo[] preferencesInfo = mainList.toArray(new ViewClassInfo[mainList.size()]); + ViewClassInfo[] preferenceGroupsInfo = groupList.toArray( + new ViewClassInfo[groupList.size()]); + + Map<String, DeclareStyleableInfo> xmlMenuMap = collectMenuDefinitions(attrsXmlParser); + Map<String, DeclareStyleableInfo> xmlSearchableMap = collectSearchableDefinitions( + attrsXmlParser); + Map<String, DeclareStyleableInfo> manifestMap = collectManifestDefinitions( + attrsManifestXmlParser); + Map<String, Map<String, Integer>> enumValueMap = attrsXmlParser.getEnumFlagValues(); + + Map<String, DeclareStyleableInfo> xmlGadgetMap = null; + if (mAndroidTarget.getApiVersionNumber() >= 3) { + xmlGadgetMap = collectGadgetDefinitions(attrsXmlParser); + } + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + // From the information that was collected, create the pieces that will be put in + // the PlatformData object. + AndroidManifestDescriptors manifestDescriptors = new AndroidManifestDescriptors(); + manifestDescriptors.updateDescriptors(manifestMap); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + LayoutDescriptors layoutDescriptors = new LayoutDescriptors(); + layoutDescriptors.updateDescriptors(layoutViewsInfo, layoutGroupsInfo); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + MenuDescriptors menuDescriptors = new MenuDescriptors(); + menuDescriptors.updateDescriptors(xmlMenuMap); + progress.worked(1); + + if (progress.isCanceled()) { + return Status.CANCEL_STATUS; + } + + XmlDescriptors xmlDescriptors = new XmlDescriptors(); + xmlDescriptors.updateDescriptors( + xmlSearchableMap, + xmlGadgetMap, + preferencesInfo, + preferenceGroupsInfo); + progress.worked(1); + + // load the framework resources. + ProjectResources resources = ResourceManager.getInstance().loadFrameworkResources( + mAndroidTarget); + progress.worked(1); + + // now load the layout lib bridge + LayoutBridge layoutBridge = loadLayoutBridge(); + progress.worked(1); + + // and finally create the PlatformData with all that we loaded. + targetData.setExtraData(frameworkRepository, + manifestDescriptors, + layoutDescriptors, + menuDescriptors, + xmlDescriptors, + enumValueMap, + permissionValues, + activity_actions.toArray(new String[activity_actions.size()]), + broadcast_actions.toArray(new String[broadcast_actions.size()]), + service_actions.toArray(new String[service_actions.size()]), + categories.toArray(new String[categories.size()]), + mAndroidTarget.getOptionalLibraries(), + resources, + layoutBridge); + + Sdk.getCurrent().setTargetData(mAndroidTarget, targetData); + + return Status.OK_STATUS; + } catch (Exception e) { + AdtPlugin.logAndPrintError(e, TAG, "SDK parser failed"); //$NON-NLS-1$ + AdtPlugin.printToConsole("SDK parser failed", e.getMessage()); + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "SDK parser failed", e); + } + } + + /** + * Preloads all "interesting" classes from the framework SDK jar. + * <p/> + * Currently this preloads all classes from the framework jar + * + * @param classLoader The framework SDK jar classloader + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + private void preload(AndroidJarLoader classLoader, IProgressMonitor monitor) { + try { + classLoader.preLoadClasses("" /* all classes */, //$NON-NLS-1$ + mAndroidTarget.getName(), // monitor task label + monitor); + } catch (InvalidAttributeValueException e) { + AdtPlugin.log(e, "Problem preloading classes"); //$NON-NLS-1$ + } catch (IOException e) { + AdtPlugin.log(e, "Problem preloading classes"); //$NON-NLS-1$ + } + } + + /** + * Creates an IResourceRepository for the framework resources. + * + * @param classLoader The framework SDK jar classloader + * @return a map of the resources, or null if it failed. + */ + private IResourceRepository collectResourceIds( + AndroidJarLoader classLoader) { + try { + Class<?> r = classLoader.loadClass(AndroidConstants.CLASS_R); + + if (r != null) { + Map<ResourceType, List<ResourceItem>> map = parseRClass(r); + if (map != null) { + return new FrameworkResourceRepository(map); + } + } + } catch (ClassNotFoundException e) { + AdtPlugin.logAndPrintError(e, TAG, + "Collect resource IDs failed, class %1$s not found in %2$s", //$NON-NLS-1$ + AndroidConstants.CLASS_R, + mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR)); + } + + return null; + } + + /** + * Parse the R class and build the resource map. + * + * @param rClass the Class object representing the Resources. + * @return a map of the resource or null + */ + private Map<ResourceType, List<ResourceItem>> parseRClass(Class<?> rClass) { + // get the sub classes. + Class<?>[] classes = rClass.getClasses(); + + if (classes.length > 0) { + HashMap<ResourceType, List<ResourceItem>> map = + new HashMap<ResourceType, List<ResourceItem>>(); + + // get the fields of each class. + for (int c = 0 ; c < classes.length ; c++) { + Class<?> subClass = classes[c]; + String name = subClass.getSimpleName(); + + // get the matching ResourceType + ResourceType type = ResourceType.getEnum(name); + if (type != null) { + List<ResourceItem> list = new ArrayList<ResourceItem>(); + map.put(type, list); + + Field[] fields = subClass.getFields(); + + for (Field f : fields) { + list.add(new ResourceItem(f.getName())); + } + } + } + + return map; + } + + return null; + } + + /** + * Loads, collects and returns the list of default permissions from the framework. + * + * @param classLoader The framework SDK jar classloader + * @return a non null (but possibly empty) array containing the permission values. + */ + private String[] collectPermissions(AndroidJarLoader classLoader) { + try { + Class<?> permissionClass = + classLoader.loadClass(AndroidConstants.CLASS_MANIFEST_PERMISSION); + + if (permissionClass != null) { + ArrayList<String> list = new ArrayList<String>(); + + Field[] fields = permissionClass.getFields(); + + for (Field f : fields) { + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) && + Modifier.isPublic(modifiers)) { + try { + Object value = f.get(null); + if (value instanceof String) { + list.add((String)value); + } + } catch (IllegalArgumentException e) { + // since we provide null this should not happen + } catch (IllegalAccessException e) { + // if the field is inaccessible we ignore it. + } catch (NullPointerException npe) { + // looks like this is not a static field. we can ignore. + } catch (ExceptionInInitializerError eiie) { + // lets just ignore the field again + } + } + } + + return list.toArray(new String[list.size()]); + } + } catch (ClassNotFoundException e) { + AdtPlugin.logAndPrintError(e, TAG, + "Collect permissions failed, class %1$s not found in %2$s", //$NON-NLS-1$ + AndroidConstants.CLASS_MANIFEST_PERMISSION, + mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR)); + } + + return new String[0]; + } + + /** + * Loads and collects the action and category default values from the framework. + * The values are added to the <code>actions</code> and <code>categories</code> lists. + * + * @param activityActions the list which will receive the activity action values. + * @param broadcastActions the list which will receive the broadcast action values. + * @param serviceActions the list which will receive the service action values. + * @param categories the list which will receive the category values. + */ + private void collectIntentFilterActionsAndCategories(ArrayList<String> activityActions, + ArrayList<String> broadcastActions, + ArrayList<String> serviceActions, ArrayList<String> categories) { + collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_ACTIVITY), + activityActions); + collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_BROADCAST), + broadcastActions); + collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_SERVICE), + serviceActions); + collectValues(mAndroidTarget.getPath(IAndroidTarget.CATEGORIES), + categories); + } + + /** + * Collects values from a text file located in the SDK + * @param osFilePath The path to the text file. + * @param values the {@link ArrayList} to fill with the values. + */ + private void collectValues(String osFilePath, ArrayList<String> values) { + FileReader fr = null; + BufferedReader reader = null; + try { + fr = new FileReader(osFilePath); + reader = new BufferedReader(fr); + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() > 0 && line.startsWith("#") == false) { //$NON-NLS-1$ + values.add(line); + } + } + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$ + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$ + } + + try { + if (fr != null) { + fr.close(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$ + } + } + } + + /** + * Collects all layout classes information from the class loader and the + * attrs.xml and sets the corresponding structures in the resource manager. + * + * @param classLoader The framework SDK jar classloader in case we cannot get the widget from + * the platform directly + * @param attrsXmlParser The parser of the attrs.xml file + * @param mainList the Collection to receive the main list of {@link ViewClassInfo}. + * @param groupList the Collection to receive the group list of {@link ViewClassInfo}. + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + private void collectLayoutClasses(AndroidJarLoader classLoader, + AttrsXmlParser attrsXmlParser, + Collection<ViewClassInfo> mainList, Collection<ViewClassInfo> groupList, + IProgressMonitor monitor) { + LayoutParamsParser ldp = null; + try { + WidgetClassLoader loader = new WidgetClassLoader( + mAndroidTarget.getPath(IAndroidTarget.WIDGETS)); + if (loader.parseWidgetList(monitor)) { + ldp = new LayoutParamsParser(loader, attrsXmlParser); + } + // if the parsing failed, we'll use the old loader below. + } catch (FileNotFoundException e) { + AdtPlugin.log(e, "Android Framework Parser"); //$NON-NLS-1$ + // the file does not exist, we'll use the old loader below. + } + + if (ldp == null) { + ldp = new LayoutParamsParser(classLoader, attrsXmlParser); + } + ldp.parseLayoutClasses(monitor); + + List<ViewClassInfo> views = ldp.getViews(); + List<ViewClassInfo> groups = ldp.getGroups(); + + if (views != null && groups != null) { + mainList.addAll(views); + groupList.addAll(groups); + } + } + + /** + * Collects all preferences definition information from the attrs.xml and + * sets the corresponding structures in the resource manager. + * + * @param classLoader The framework SDK jar classloader + * @param attrsXmlParser The parser of the attrs.xml file + * @param mainList the Collection to receive the main list of {@link ViewClassInfo}. + * @param groupList the Collection to receive the group list of {@link ViewClassInfo}. + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + private void collectPreferenceClasses(AndroidJarLoader classLoader, + AttrsXmlParser attrsXmlParser, Collection<ViewClassInfo> mainList, + Collection<ViewClassInfo> groupList, IProgressMonitor monitor) { + LayoutParamsParser ldp = new LayoutParamsParser(classLoader, attrsXmlParser); + + try { + ldp.parsePreferencesClasses(monitor); + + List<ViewClassInfo> prefs = ldp.getViews(); + List<ViewClassInfo> groups = ldp.getGroups(); + + if (prefs != null && groups != null) { + mainList.addAll(prefs); + groupList.addAll(groups); + } + } catch (NoClassDefFoundError e) { + AdtPlugin.logAndPrintError(e, TAG, + "Collect preferences failed, class %1$s not found in %2$s", + e.getMessage(), + classLoader.getSource()); + } catch (Throwable e) { + AdtPlugin.log(e, "Android Framework Parser: failed to collect preference classes"); //$NON-NLS-1$ + AdtPlugin.printErrorToConsole("Android Framework Parser", + "failed to collect preference classes"); + } + } + + /** + * Collects all menu definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map<String, DeclareStyleableInfo> collectMenuDefinitions( + AttrsXmlParser attrsXmlParser) { + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>(); + for (String key : new String[] { "Menu", //$NON-NLS-1$ + "MenuItem", //$NON-NLS-1$ + "MenuGroup" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "Menu declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("Menu declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + + /** + * Collects all searchable definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map<String, DeclareStyleableInfo> collectSearchableDefinitions( + AttrsXmlParser attrsXmlParser) { + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>(); + for (String key : new String[] { "Searchable", //$NON-NLS-1$ + "SearchableActionKey" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "Searchable declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("Searchable declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + + /** + * Collects all gadgetProviderInfo definition information from the attrs.xml and returns it. + * + * @param attrsXmlParser The parser of the attrs.xml file + */ + private Map<String, DeclareStyleableInfo> collectGadgetDefinitions( + AttrsXmlParser attrsXmlParser) { + Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList(); + Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>(); + for (String key : new String[] { "GadgetProviderInfo" }) { //$NON-NLS-1$ + if (map.containsKey(key)) { + map2.put(key, map.get(key)); + } else { + AdtPlugin.log(IStatus.WARNING, + "Gadget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath()); + AdtPlugin.printErrorToConsole("Android Framework Parser", + String.format("Gadget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$ + key, attrsXmlParser.getOsAttrsXmlPath())); + } + } + + return Collections.unmodifiableMap(map2); + } + + /** + * Collects all manifest definition information from the attrs_manifest.xml and returns it. + */ + private Map<String, DeclareStyleableInfo> collectManifestDefinitions( + AttrsXmlParser attrsXmlParser) { + + return attrsXmlParser.getDeclareStyleableList(); + } + + /** + * Loads the layout bridge from the dynamically loaded layoutlib.jar + */ + private LayoutBridge loadLayoutBridge() { + LayoutBridge layoutBridge = new LayoutBridge(); + + try { + // get the URL for the file. + File f = new File(mAndroidTarget.getPath(IAndroidTarget.LAYOUT_LIB)); + if (f.isFile() == false) { + AdtPlugin.log(IStatus.ERROR, "layoutlib.jar is missing!"); //$NON-NLS-1$ + } else { + URL url = f.toURL(); + + // create a class loader. Because this jar reference interfaces + // that are in the editors plugin, it's important to provide + // a parent class loader. + layoutBridge.classLoader = new URLClassLoader(new URL[] { url }, + this.getClass().getClassLoader()); + + // load the class + Class<?> clazz = layoutBridge.classLoader.loadClass(AndroidConstants.CLASS_BRIDGE); + if (clazz != null) { + // instantiate an object of the class. + Constructor<?> constructor = clazz.getConstructor(); + if (constructor != null) { + Object bridge = constructor.newInstance(); + if (bridge instanceof ILayoutBridge) { + layoutBridge.bridge = (ILayoutBridge)bridge; + } + } + } + + if (layoutBridge.bridge == null) { + layoutBridge.status = LoadStatus.FAILED; + AdtPlugin.log(IStatus.ERROR, "Failed to load " + AndroidConstants.CLASS_BRIDGE); //$NON-NLS-1$ + } else { + // get the api level + try { + layoutBridge.apiLevel = layoutBridge.bridge.getApiLevel(); + } catch (AbstractMethodError e) { + // the first version of the api did not have this method + layoutBridge.apiLevel = 1; + } + + // and mark the lib as loaded. + layoutBridge.status = LoadStatus.LOADED; + } + } + } catch (Throwable t) { + layoutBridge.status = LoadStatus.FAILED; + // log the error. + AdtPlugin.log(t, "Failed to load the LayoutLib"); + } + + return layoutBridge; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java new file mode 100644 index 0000000..f4b10df --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of the {@link IResourceRepository} interface to hold the system resource Ids + * parsed by {@link AndroidTargetParser}. + */ +final class FrameworkResourceRepository implements IResourceRepository { + + private Map<ResourceType, List<ResourceItem>> mResourcesMap; + + public FrameworkResourceRepository(Map<ResourceType, List<ResourceItem>> systemResourcesMap) { + mResourcesMap = systemResourcesMap; + } + + public ResourceType[] getAvailableResourceTypes() { + if (mResourcesMap != null) { + Set<ResourceType> types = mResourcesMap.keySet(); + + if (types != null) { + return types.toArray(new ResourceType[types.size()]); + } + } + + return null; + } + + public ResourceItem[] getResources(ResourceType type) { + if (mResourcesMap != null) { + List<ResourceItem> items = mResourcesMap.get(type); + + if (items != null) { + return items.toArray(new ResourceItem[items.size()]); + } + } + + return null; + } + + public boolean hasResources(ResourceType type) { + if (mResourcesMap != null) { + List<ResourceItem> items = mResourcesMap.get(type); + + return (items != null && items.size() > 0); + } + + return false; + } + + public boolean isSystemRepository() { + return true; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java new file mode 100644 index 0000000..35057d1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.management.InvalidAttributeValueException; + +/** + * Classes which implements this interface provide methods to access framework resource + * data loaded from the SDK. + */ +public interface IAndroidClassLoader { + + /** + * Classes which implement this interface provide methods to describe a class. + */ + public interface IClassDescriptor { + + String getCanonicalName(); + + IClassDescriptor getSuperclass(); + + String getSimpleName(); + + IClassDescriptor getEnclosingClass(); + + IClassDescriptor[] getDeclaredClasses(); + + boolean isInstantiable(); + } + + /** + * Finds and loads all classes that derive from a given set of super classes. + * + * @param rootPackage Root package of classes to find. Use an empty string to find everyting. + * @param superClasses The super classes of all the classes to find. + * @return An hash map which keys are the super classes looked for and which values are + * ArrayList of the classes found. The array lists are always created for all the + * valid keys, they are simply empty if no deriving class is found for a given + * super class. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom( + String rootPackage, String[] superClasses) + throws IOException, InvalidAttributeValueException, ClassFormatError; + + /** + * Returns a {@link IClassDescriptor} by its fully-qualified name. + * @param className the fully-qualified name of the class to return. + * @throws ClassNotFoundException + */ + public IClassDescriptor getClass(String className) throws ClassNotFoundException; + + /** + * Returns a string indicating the source of the classes, typically for debugging + * or in error messages. This would typically be a JAR file name or some kind of + * identifier that would mean something to the user when looking at error messages. + * + * @return An informal string representing the source of the classes. + */ + public String getSource(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java new file mode 100644 index 0000000..dc600d7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.IAndroidClassLoader.IClassDescriptor; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.AttrsXmlParser; +import com.android.ide.eclipse.common.resources.ViewClassInfo; +import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.management.InvalidAttributeValueException; + +/* + * TODO: refactor this. Could use some cleanup. + */ + +/** + * Parser for the framework library. + * <p/> + * This gather the following information: + * <ul> + * <li>Resource ID from <code>android.R</code></li> + * <li>The list of permissions values from <code>android.Manifest$permission</code></li> + * <li></li> + * </ul> + */ +public class LayoutParamsParser { + + /** + * Class extending {@link ViewClassInfo} by adding the notion of instantiability. + * {@link LayoutParamsParser#getViews()} and {@link LayoutParamsParser#getGroups()} should + * only return classes that can be instantiated. + */ + final static class ExtViewClassInfo extends ViewClassInfo { + + private boolean mIsInstantiable; + + ExtViewClassInfo(boolean instantiable, boolean isLayout, String canonicalClassName, + String shortClassName) { + super(isLayout, canonicalClassName, shortClassName); + mIsInstantiable = instantiable; + } + + boolean isInstantiable() { + return mIsInstantiable; + } + } + + /* Note: protected members/methods are overridden in unit tests */ + + /** Reference to android.view.View */ + protected IClassDescriptor mTopViewClass; + /** Reference to android.view.ViewGroup */ + protected IClassDescriptor mTopGroupClass; + /** Reference to android.view.ViewGroup$LayoutParams */ + protected IClassDescriptor mTopLayoutParamsClass; + + /** Input list of all classes deriving from android.view.View */ + protected ArrayList<IClassDescriptor> mViewList; + /** Input list of all classes deriving from android.view.ViewGroup */ + protected ArrayList<IClassDescriptor> mGroupList; + + /** Output map of FQCN => info on View classes */ + protected TreeMap<String, ExtViewClassInfo> mViewMap; + /** Output map of FQCN => info on ViewGroup classes */ + protected TreeMap<String, ExtViewClassInfo> mGroupMap; + /** Output map of FQCN => info on LayoutParams classes */ + protected HashMap<String, LayoutParamsInfo> mLayoutParamsMap; + + /** The attrs.xml parser */ + protected AttrsXmlParser mAttrsXmlParser; + + /** The android.jar class loader */ + protected IAndroidClassLoader mClassLoader; + + /** + * Instantiate a new LayoutParamsParser. + * @param classLoader The android.jar class loader + * @param attrsXmlParser The parser of the attrs.xml file + */ + public LayoutParamsParser(IAndroidClassLoader classLoader, + AttrsXmlParser attrsXmlParser) { + mClassLoader = classLoader; + mAttrsXmlParser = attrsXmlParser; + } + + /** Returns the map of FQCN => info on View classes */ + public List<ViewClassInfo> getViews() { + return getInstantiables(mViewMap); + } + + /** Returns the map of FQCN => info on ViewGroup classes */ + public List<ViewClassInfo> getGroups() { + return getInstantiables(mGroupMap); + } + + /** + * TODO: doc here. + * <p/> + * Note: on output we should have NO dependency on {@link IClassDescriptor}, + * otherwise we wouldn't be able to unload the class loader later. + * <p/> + * Note on Vocabulary: FQCN=Fully Qualified Class Name (e.g. "my.package.class$innerClass") + * @param monitor A progress monitor. Can be null. Caller is responsible for calling done. + */ + public void parseLayoutClasses(IProgressMonitor monitor) { + parseClasses(monitor, + AndroidConstants.CLASS_VIEW, + AndroidConstants.CLASS_VIEWGROUP, + AndroidConstants.CLASS_VIEWGROUP_LAYOUTPARAMS); + } + + public void parsePreferencesClasses(IProgressMonitor monitor) { + parseClasses(monitor, + AndroidConstants.CLASS_PREFERENCE, + AndroidConstants.CLASS_PREFERENCEGROUP, + null /* paramsClassName */ ); + } + + private void parseClasses(IProgressMonitor monitor, + String rootClassName, + String groupClassName, + String paramsClassName) { + try { + SubMonitor progress = SubMonitor.convert(monitor, 100); + + String[] superClasses = new String[2 + (paramsClassName == null ? 0 : 1)]; + superClasses[0] = groupClassName; + superClasses[1] = rootClassName; + if (paramsClassName != null) { + superClasses[2] = paramsClassName; + } + HashMap<String, ArrayList<IClassDescriptor>> found = + mClassLoader.findClassesDerivingFrom("android.", superClasses); + mTopViewClass = mClassLoader.getClass(rootClassName); + mTopGroupClass = mClassLoader.getClass(groupClassName); + if (paramsClassName != null) { + mTopLayoutParamsClass = mClassLoader.getClass(paramsClassName); + } + + mViewList = found.get(rootClassName); + mGroupList = found.get(groupClassName); + + mViewMap = new TreeMap<String, ExtViewClassInfo>(); + mGroupMap = new TreeMap<String, ExtViewClassInfo>(); + if (mTopLayoutParamsClass != null) { + mLayoutParamsMap = new HashMap<String, LayoutParamsInfo>(); + } + + // Add top classes to the maps since by design they are not listed in classes deriving + // from themselves. + addGroup(mTopGroupClass); + addView(mTopViewClass); + + // ViewGroup derives from View + mGroupMap.get(groupClassName).setSuperClass( + mViewMap.get(rootClassName)); + + progress.setWorkRemaining(mGroupList.size() + mViewList.size()); + + for (IClassDescriptor groupChild : mGroupList) { + addGroup(groupChild); + progress.worked(1); + } + + for (IClassDescriptor viewChild : mViewList) { + if (viewChild != mTopGroupClass) { + addView(viewChild); + } + progress.worked(1); + } + } catch (ClassNotFoundException e) { + AdtPlugin.log(e, "Problem loading class %1$s or %2$s", //$NON-NLS-1$ + rootClassName, groupClassName); + } catch (InvalidAttributeValueException e) { + AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$ + } catch (ClassFormatError e) { + AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$ + } catch (IOException e) { + AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$ + } + } + + /** + * Parses a View class and adds a ExtViewClassInfo for it in mViewMap. + * It calls itself recursively to handle super classes which are also Views. + */ + private ExtViewClassInfo addView(IClassDescriptor viewClass) { + String fqcn = viewClass.getCanonicalName(); + if (mViewMap.containsKey(fqcn)) { + return mViewMap.get(fqcn); + } else if (mGroupMap.containsKey(fqcn)) { + return mGroupMap.get(fqcn); + } + + ExtViewClassInfo info = new ExtViewClassInfo(viewClass.isInstantiable(), + false /* layout */, fqcn, viewClass.getSimpleName()); + mViewMap.put(fqcn, info); + + // All view classes derive from mTopViewClass by design. + // Do not lookup the super class for mTopViewClass itself. + if (viewClass.equals(mTopViewClass) == false) { + IClassDescriptor superClass = viewClass.getSuperclass(); + ExtViewClassInfo superClassInfo = addView(superClass); + info.setSuperClass(superClassInfo); + } + + mAttrsXmlParser.loadViewAttributes(info); + return info; + } + + /** + * Parses a ViewGroup class and adds a ExtViewClassInfo for it in mGroupMap. + * It calls itself recursively to handle super classes which are also ViewGroups. + */ + private ExtViewClassInfo addGroup(IClassDescriptor groupClass) { + String fqcn = groupClass.getCanonicalName(); + if (mGroupMap.containsKey(fqcn)) { + return mGroupMap.get(fqcn); + } + + ExtViewClassInfo info = new ExtViewClassInfo(groupClass.isInstantiable(), + true /* layout */, fqcn, groupClass.getSimpleName()); + mGroupMap.put(fqcn, info); + + // All groups derive from android.view.ViewGroup, which in turns derives from + // android.view.View (i.e. mTopViewClass here). So the only group that can have View as + // its super class is the ViewGroup base class and we don't try to resolve it since groups + // are loaded before views. + IClassDescriptor superClass = groupClass.getSuperclass(); + + // Assertion: at this point, we should have + // superClass != mTopViewClass || fqcn.equals(AndroidConstants.CLASS_VIEWGROUP); + + if (superClass != null && superClass.equals(mTopViewClass) == false) { + ExtViewClassInfo superClassInfo = addGroup(superClass); + + // Assertion: we should have superClassInfo != null && superClassInfo != info; + if (superClassInfo != null && superClassInfo != info) { + info.setSuperClass(superClassInfo); + } + } + + mAttrsXmlParser.loadViewAttributes(info); + if (mTopLayoutParamsClass != null) { + info.setLayoutParams(addLayoutParams(groupClass)); + } + return info; + } + + /** + * Parses a ViewGroup class and returns an info object on its inner LayoutParams. + * + * @return The {@link LayoutParamsInfo} for the ViewGroup class or null. + */ + private LayoutParamsInfo addLayoutParams(IClassDescriptor groupClass) { + + // Is there a LayoutParams in this group class? + IClassDescriptor layoutParamsClass = findLayoutParams(groupClass); + + // if there's no layout data in the group class, link to the one from the + // super class. + if (layoutParamsClass == null) { + for (IClassDescriptor superClass = groupClass.getSuperclass(); + layoutParamsClass == null && + superClass != null && + superClass.equals(mTopViewClass) == false; + superClass = superClass.getSuperclass()) { + layoutParamsClass = findLayoutParams(superClass); + } + } + + if (layoutParamsClass != null) { + return getLayoutParamsInfo(layoutParamsClass); + } + + return null; + } + + /** + * Parses a LayoutParams class and returns a LayoutParamsInfo object for it. + * It calls itself recursively to handle the super class of the LayoutParams. + */ + private LayoutParamsInfo getLayoutParamsInfo(IClassDescriptor layoutParamsClass) { + String fqcn = layoutParamsClass.getCanonicalName(); + LayoutParamsInfo layoutParamsInfo = mLayoutParamsMap.get(fqcn); + + if (layoutParamsInfo != null) { + return layoutParamsInfo; + } + + // Find the link on the LayoutParams super class + LayoutParamsInfo superClassInfo = null; + if (layoutParamsClass.equals(mTopLayoutParamsClass) == false) { + IClassDescriptor superClass = layoutParamsClass.getSuperclass(); + superClassInfo = getLayoutParamsInfo(superClass); + } + + // Find the link on the enclosing ViewGroup + ExtViewClassInfo enclosingGroupInfo = addGroup(layoutParamsClass.getEnclosingClass()); + + layoutParamsInfo = new ExtViewClassInfo.LayoutParamsInfo( + enclosingGroupInfo, layoutParamsClass.getSimpleName(), superClassInfo); + mLayoutParamsMap.put(fqcn, layoutParamsInfo); + + mAttrsXmlParser.loadLayoutParamsAttributes(layoutParamsInfo); + + return layoutParamsInfo; + } + + /** + * Given a ViewGroup-derived class, looks for an inner class named LayoutParams + * and if found returns its class definition. + * <p/> + * This uses the actual defined inner classes and does not look at inherited classes. + * + * @param groupClass The ViewGroup derived class + * @return The Class of the inner LayoutParams or null if none is declared. + */ + private IClassDescriptor findLayoutParams(IClassDescriptor groupClass) { + IClassDescriptor[] innerClasses = groupClass.getDeclaredClasses(); + for (IClassDescriptor innerClass : innerClasses) { + if (innerClass.getSimpleName().equals(AndroidConstants.CLASS_LAYOUTPARAMS)) { + return innerClass; + } + } + return null; + } + + /** + * Computes and return a list of ViewClassInfo from a map by filtering out the class that + * cannot be instantiated. + */ + private List<ViewClassInfo> getInstantiables(SortedMap<String, ExtViewClassInfo> map) { + Collection<ExtViewClassInfo> values = map.values(); + ArrayList<ViewClassInfo> list = new ArrayList<ViewClassInfo>(); + + for (ExtViewClassInfo info : values) { + if (info.isInstantiable()) { + list.add(info); + } + } + + return list; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java new file mode 100644 index 0000000..6bf0272 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +/** + * Enum for loading status of various SDK parts. + */ +public enum LoadStatus { + LOADING, LOADED, FAILED; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java new file mode 100644 index 0000000..ba0b568 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISdkLog; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.SdkManager; +import com.android.sdklib.avd.AvdManager; +import com.android.sdklib.project.ApkConfigurationHelper; +import com.android.sdklib.project.ProjectProperties; +import com.android.sdklib.project.ProjectProperties.PropertyType; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used + * at the same time. + * + * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of + * the Sdk object. + * + * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}. + */ +public class Sdk implements IProjectListener { + private static Sdk sCurrentSdk = null; + + private final SdkManager mManager; + private final AvdManager mAvdManager; + + private final HashMap<IProject, IAndroidTarget> mProjectTargetMap = + new HashMap<IProject, IAndroidTarget>(); + private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap = + new HashMap<IAndroidTarget, AndroidTargetData>(); + private final HashMap<IProject, Map<String, String>> mProjectApkConfigMap = + new HashMap<IProject, Map<String, String>>(); + private final String mDocBaseUrl; + + /** + * Classes implementing this interface will receive notification when targets are changed. + */ + public interface ITargetChangeListener { + /** + * Sent when project has its target changed. + */ + void onProjectTargetChange(IProject changedProject); + + /** + * Called when the targets are loaded (either the SDK finished loading when Eclipse starts, + * or the SDK is changed). + */ + void onTargetsLoaded(); + } + + /** + * Loads an SDK and returns an {@link Sdk} object if success. + * @param sdkLocation the OS path to the SDK. + */ + public static Sdk loadSdk(String sdkLocation) { + if (sCurrentSdk != null) { + sCurrentSdk.dispose(); + sCurrentSdk = null; + } + + final ArrayList<String> logMessages = new ArrayList<String>(); + ISdkLog log = new ISdkLog() { + public void error(Throwable throwable, String errorFormat, Object... arg) { + if (errorFormat != null) { + logMessages.add(String.format(errorFormat, arg)); + } + + if (throwable != null) { + logMessages.add(throwable.getMessage()); + } + } + + public void warning(String warningFormat, Object... arg) { + logMessages.add(String.format(warningFormat, arg)); + } + + public void printf(String msgFormat, Object... arg) { + logMessages.add(String.format(msgFormat, arg)); + } + }; + + // get an SdkManager object for the location + SdkManager manager = SdkManager.createManager(sdkLocation, log); + if (manager != null) { + AvdManager avdManager = null; + try { + avdManager = new AvdManager(manager, log); + } catch (AndroidLocationException e) { + log.error(e, "Error parsing the AVDs"); + } + sCurrentSdk = new Sdk(manager, avdManager); + return sCurrentSdk; + } else { + StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); + for (String msg : logMessages) { + sb.append('\n'); + sb.append(msg); + } + AdtPlugin.displayError("Android SDK", sb.toString()); + } + return null; + } + + /** + * Returns the current {@link Sdk} object. + */ + public static Sdk getCurrent() { + return sCurrentSdk; + } + + /** + * Returns the location (OS path) of the current SDK. + */ + public String getSdkLocation() { + return mManager.getLocation(); + } + + /** + * Returns the URL to the local documentation. + * Can return null if no documentation is found in the current SDK. + * + * @return A file:// URL on the local documentation folder if it exists or null. + */ + public String getDocumentationBaseUrl() { + return mDocBaseUrl; + } + + /** + * Returns the list of targets that are available in the SDK. + */ + public IAndroidTarget[] getTargets() { + return mManager.getTargets(); + } + + /** + * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. + * + * @param hash the {@link IAndroidTarget} hash string. + * @return The matching {@link IAndroidTarget} or null. + */ + public IAndroidTarget getTargetFromHashString(String hash) { + return mManager.getTargetFromHashString(hash); + } + + /** + * Sets a new target and a new list of Apk configuration for a given project. + * + * @param project the project to receive the new apk configurations + * @param target The new target to set, or <code>null</code> to not change the current target. + * @param apkConfigMap a map of apk configurations. The map contains (name, filter) where name + * is the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of + * resource configuration to include in the apk (see aapt -c). Can be <code>null</code> if the + * apk configurations should not be updated. + */ + public void setProject(IProject project, IAndroidTarget target, + Map<String, String> apkConfigMap) { + synchronized (mProjectTargetMap) { + boolean resolveProject = false; + boolean compileProject = false; + boolean cleanProject = false; + + ProjectProperties properties = ProjectProperties.load( + project.getLocation().toOSString(), PropertyType.DEFAULT); + if (properties == null) { + // doesn't exist yet? we create it. + properties = ProjectProperties.create(project.getLocation().toOSString(), + PropertyType.DEFAULT); + } + + if (target != null) { + // look for the current target of the project + IAndroidTarget previousTarget = mProjectTargetMap.get(project); + + if (target != previousTarget) { + // save the target hash string in the project persistent property + properties.setAndroidTarget(target); + + // put it in a local map for easy access. + mProjectTargetMap.put(project, target); + + resolveProject = true; + } + } + + if (apkConfigMap != null) { + // save the apk configs in the project persistent property + cleanProject = ApkConfigurationHelper.setConfigs(properties, apkConfigMap); + + // put it in a local map for easy access. + mProjectApkConfigMap.put(project, apkConfigMap); + + compileProject = true; + } + + // we are done with the modification. Save the property file. + try { + properties.save(); + } catch (IOException e) { + AdtPlugin.log(e, "Failed to save default.properties for project '%s'", + project.getName()); + } + + if (resolveProject) { + // force a resolve of the project by updating the classpath container. + IJavaProject javaProject = JavaCore.create(project); + AndroidClasspathContainerInitializer.updateProjects( + new IJavaProject[] { javaProject }); + } else if (compileProject) { + // If there was removed configs, we clean instead of build + // (to remove the obsolete ap_ and apk file from removed configs). + try { + project.build(cleanProject ? + IncrementalProjectBuilder.CLEAN_BUILD : + IncrementalProjectBuilder.FULL_BUILD, + null); + } catch (CoreException e) { + // failed to build? force resolve instead. + IJavaProject javaProject = JavaCore.create(project); + AndroidClasspathContainerInitializer.updateProjects( + new IJavaProject[] { javaProject }); + } + } + + // finally, update the opened editors. + if (resolveProject) { + AdtPlugin.getDefault().updateTargetListener(project); + } + } + } + + /** + * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}. + */ + public IAndroidTarget getTarget(IProject project) { + synchronized (mProjectTargetMap) { + IAndroidTarget target = mProjectTargetMap.get(project); + if (target == null) { + // get the value from the project persistent property. + String targetHashString = loadProjectProperties(project, this); + + if (targetHashString != null) { + target = mManager.getTargetFromHashString(targetHashString); + } + } + + return target; + } + } + + + /** + * Parses the project properties and returns the hash string uniquely identifying the + * target of the given project. + * <p/> + * This methods reads the content of the <code>default.properties</code> file present in + * the root folder of the project. + * <p/>The returned string is equivalent to the return of {@link IAndroidTarget#hashString()}. + * @param project The project for which to return the target hash string. + * @param sdkStorage The sdk in which to store the Apk Configs. Can be null. + * @return the hash string or null if the project does not have a target set. + */ + private static String loadProjectProperties(IProject project, Sdk sdkStorage) { + // load the default.properties from the project folder. + IPath location = project.getLocation(); + if (location == null) { // can return null when the project is being deleted. + // do nothing and return null; + return null; + } + ProjectProperties properties = ProjectProperties.load(location.toOSString(), + PropertyType.DEFAULT); + if (properties == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'", + project.getName()); + return null; + } + + if (sdkStorage != null) { + Map<String, String> configMap = ApkConfigurationHelper.getConfigs(properties); + + if (configMap != null) { + sdkStorage.mProjectApkConfigMap.put(project, configMap); + } + } + + return properties.getProperty(ProjectProperties.PROPERTY_TARGET); + } + + /** + * Returns the hash string uniquely identifying the target of a project. + * <p/> + * This methods reads the content of the <code>default.properties</code> file present in + * the root folder of the project. + * <p/>The string is equivalent to the return of {@link IAndroidTarget#hashString()}. + * @param project The project for which to return the target hash string. + * @return the hash string or null if the project does not have a target set. + */ + public static String getProjectTargetHashString(IProject project) { + return loadProjectProperties(project, null /*storeConfigs*/); + } + + /** + * Sets a target hash string in given project's <code>default.properties</code> file. + * @param project The project in which to save the hash string. + * @param targetHashString The target hash string to save. This must be the result from + * {@link IAndroidTarget#hashString()}. + */ + public static void setProjectTargetHashString(IProject project, String targetHashString) { + // because we don't want to erase other properties from default.properties, we first load + // them + ProjectProperties properties = ProjectProperties.load(project.getLocation().toOSString(), + PropertyType.DEFAULT); + if (properties == null) { + // doesn't exist yet? we create it. + properties = ProjectProperties.create(project.getLocation().toOSString(), + PropertyType.DEFAULT); + } + + // add/change the target hash string. + properties.setProperty(ProjectProperties.PROPERTY_TARGET, targetHashString); + + // and rewrite the file. + try { + properties.save(); + } catch (IOException e) { + AdtPlugin.log(e, "Failed to save default.properties for project '%s'", + project.getName()); + } + } + /** + * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}. + */ + public AndroidTargetData getTargetData(IAndroidTarget target) { + synchronized (mTargetDataMap) { + return mTargetDataMap.get(target); + } + } + + /** + * Returns the configuration map for a given project. + * <p/>The Map key are name to be used in the apk filename, while the values are comma separated + * config values. The config value can be passed directly to aapt through the -c option. + */ + public Map<String, String> getProjectApkConfigs(IProject project) { + return mProjectApkConfigMap.get(project); + } + + /** + * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could + * be <code>null</code>. + */ + public AvdManager getAvdManager() { + return mAvdManager; + } + + private Sdk(SdkManager manager, AvdManager avdManager) { + mManager = manager; + mAvdManager = avdManager; + + // listen to projects closing + ResourceMonitor monitor = ResourceMonitor.getMonitor(); + monitor.addProjectListener(this); + + // pre-compute some paths + mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + + SdkConstants.OS_SDK_DOCS_FOLDER); + } + + /** + * Cleans and unloads the SDK. + */ + private void dispose() { + ResourceMonitor.getMonitor().removeProjectListener(this); + } + + void setTargetData(IAndroidTarget target, AndroidTargetData data) { + synchronized (mTargetDataMap) { + mTargetDataMap.put(target, data); + } + } + + /** + * Returns the URL to the local documentation. + * Can return null if no documentation is found in the current SDK. + * + * @param osDocsPath Path to the documentation folder in the current SDK. + * The folder may not actually exist. + * @return A file:// URL on the local documentation folder if it exists or null. + */ + private String getDocumentationBaseUrl(String osDocsPath) { + File f = new File(osDocsPath); + + if (f.isDirectory()) { + try { + // Note: to create a file:// URL, one would typically use something like + // f.toURI().toURL().toString(). However this generates a broken path on + // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of + // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll + // do the correct thing manually. + + String path = f.getAbsolutePath(); + if (File.separatorChar != '/') { + path = path.replace(File.separatorChar, '/'); + } + + // For some reason the URL class doesn't add the mandatory "//" after + // the "file:" protocol name, so it has to be hacked into the path. + URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$ + String result = url.toString(); + return result; + } catch (MalformedURLException e) { + // ignore malformed URLs + } + } + + return null; + } + + public void projectClosed(IProject project) { + // get the target project + synchronized (mProjectTargetMap) { + IAndroidTarget target = mProjectTargetMap.get(project); + if (target != null) { + // get the bridge for the target, and clear the cache for this project. + AndroidTargetData data = mTargetDataMap.get(target); + if (data != null) { + LayoutBridge bridge = data.getLayoutBridge(); + if (bridge != null && bridge.status == LoadStatus.LOADED) { + bridge.bridge.clearCaches(project); + } + } + } + + // now remove the project for the maps. + mProjectTargetMap.remove(project); + mProjectApkConfigMap.remove(project); + } + } + + public void projectDeleted(IProject project) { + projectClosed(project); + } + + public void projectOpened(IProject project) { + // ignore this. The project will be added to the map the first time the target needs + // to be resolved. + } + + public void projectOpenedWithWorkspace(IProject project) { + // ignore this. The project will be added to the map the first time the target needs + // to be resolved. + } +} + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java new file mode 100644 index 0000000..0e60f8a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.runtime.IProgressMonitor; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import javax.management.InvalidAttributeValueException; + +/** + * Parser for the text file containing the list of widgets, layouts and layout params. + * <p/> + * The file is a straight text file containing one class per line.<br> + * Each line is in the following format<br> + * <code>[code][class name] [super class name] [super class name]...</code> + * where code is a single letter (W for widget, L for layout, P for layout params), and class names + * are the fully qualified name of the classes. + */ +public final class WidgetClassLoader implements IAndroidClassLoader { + + /** + * Basic class containing the class descriptions found in the text file. + */ + private final static class ClassDescriptor implements IClassDescriptor { + + private String mName; + private String mSimpleName; + private ClassDescriptor mSuperClass; + private ClassDescriptor mEnclosingClass; + private final ArrayList<IClassDescriptor> mDeclaredClasses = + new ArrayList<IClassDescriptor>(); + private boolean mIsInstantiable = false; + + ClassDescriptor(String fqcn) { + mName = fqcn; + mSimpleName = getSimpleName(fqcn); + } + + public String getCanonicalName() { + return mName; + } + + public String getSimpleName() { + return mSimpleName; + } + + public IClassDescriptor[] getDeclaredClasses() { + return mDeclaredClasses.toArray(new IClassDescriptor[mDeclaredClasses.size()]); + } + + private void addDeclaredClass(ClassDescriptor declaredClass) { + mDeclaredClasses.add(declaredClass); + } + + public IClassDescriptor getEnclosingClass() { + return mEnclosingClass; + } + + void setEnclosingClass(ClassDescriptor enclosingClass) { + // set the enclosing class. + mEnclosingClass = enclosingClass; + + // add this to the list of declared class in the enclosing class. + mEnclosingClass.addDeclaredClass(this); + + // finally change the name of declared class to make sure it uses the + // convention: package.enclosing$declared instead of package.enclosing.declared + mName = enclosingClass.mName + "$" + mName.substring(enclosingClass.mName.length() + 1); + } + + public IClassDescriptor getSuperclass() { + return mSuperClass; + } + + void setSuperClass(ClassDescriptor superClass) { + mSuperClass = superClass; + } + + @Override + public boolean equals(Object clazz) { + if (clazz instanceof ClassDescriptor) { + return mName.equals(((ClassDescriptor)clazz).mName); + } + return super.equals(clazz); + } + + @Override + public int hashCode() { + return mName.hashCode(); + } + + public boolean isInstantiable() { + return mIsInstantiable; + } + + void setInstantiable(boolean state) { + mIsInstantiable = state; + } + + private String getSimpleName(String fqcn) { + String[] segments = fqcn.split("\\."); + return segments[segments.length-1]; + } + } + + private BufferedReader mReader; + + /** Output map of FQCN => descriptor on all classes */ + private final Map<String, ClassDescriptor> mMap = new TreeMap<String, ClassDescriptor>(); + /** Output map of FQCN => descriptor on View classes */ + private final Map<String, ClassDescriptor> mWidgetMap = new TreeMap<String, ClassDescriptor>(); + /** Output map of FQCN => descriptor on ViewGroup classes */ + private final Map<String, ClassDescriptor> mLayoutMap = new TreeMap<String, ClassDescriptor>(); + /** Output map of FQCN => descriptor on LayoutParams classes */ + private final Map<String, ClassDescriptor> mLayoutParamsMap = + new HashMap<String, ClassDescriptor>(); + /** File path of the source text file */ + private String mOsFilePath; + + /** + * Creates a loader with a given file path. + * @param osFilePath the OS path of the file to load. + * @throws FileNotFoundException if the file is not found. + */ + WidgetClassLoader(String osFilePath) throws FileNotFoundException { + mOsFilePath = osFilePath; + mReader = new BufferedReader(new FileReader(osFilePath)); + } + + public String getSource() { + return mOsFilePath; + } + + /** + * Parses the text file and return true if the file was successfully parsed. + * @param monitor + */ + boolean parseWidgetList(IProgressMonitor monitor) { + try { + String line; + while ((line = mReader.readLine()) != null) { + if (line.length() > 0) { + char prefix = line.charAt(0); + String[] classes = null; + ClassDescriptor clazz = null; + switch (prefix) { + case 'W': + classes = line.substring(1).split(" "); + clazz = processClass(classes, 0, null /* map */); + if (clazz != null) { + clazz.setInstantiable(true); + mWidgetMap.put(classes[0], clazz); + } + break; + case 'L': + classes = line.substring(1).split(" "); + clazz = processClass(classes, 0, null /* map */); + if (clazz != null) { + clazz.setInstantiable(true); + mLayoutMap.put(classes[0], clazz); + } + break; + case 'P': + classes = line.substring(1).split(" "); + clazz = processClass(classes, 0, mLayoutParamsMap); + if (clazz != null) { + clazz.setInstantiable(true); + } + break; + case '#': + // comment, do nothing + break; + default: + throw new IllegalArgumentException(); + } + } + } + + // reconciliate the layout and their layout params + postProcess(); + + return true; + } catch (IOException e) { + } finally { + try { + mReader.close(); + } catch (IOException e) { + } + } + + return false; + } + + /** + * Parses a View class and adds a ViewClassInfo for it in mWidgetMap. + * It calls itself recursively to handle super classes which are also Views. + * @param classes the inheritance list of the class to process. + * @param index the index of the class to process in the <code>classes</code> array. + * @param map an optional map in which to put every {@link ClassDescriptor} created. + */ + private ClassDescriptor processClass(String[] classes, int index, + Map<String, ClassDescriptor> map) { + if (index >= classes.length) { + return null; + } + + String fqcn = classes[index]; + + if ("java.lang.Object".equals(fqcn)) { //$NON-NLS-1$ + return null; + } + + // check if the ViewInfoClass has not yet been created. + if (mMap.containsKey(fqcn)) { + return mMap.get(fqcn); + } + + // create the custom class. + ClassDescriptor clazz = new ClassDescriptor(fqcn); + mMap.put(fqcn, clazz); + if (map != null) { + map.put(fqcn, clazz); + } + + // get the super class + ClassDescriptor superClass = processClass(classes, index+1, map); + if (superClass != null) { + clazz.setSuperClass(superClass); + } + + return clazz; + } + + /** + * Goes through the layout params and look for the enclosed class. If the layout params + * has no known enclosed type it is dropped. + */ + private void postProcess() { + Collection<ClassDescriptor> params = mLayoutParamsMap.values(); + + for (ClassDescriptor param : params) { + String fqcn = param.getCanonicalName(); + + // get the enclosed name. + String enclosed = getEnclosedName(fqcn); + + // look for a match in the layouts. We don't use the layout map as it only contains the + // end classes, but in this case we also need to process the layout params for the base + // layout classes. + ClassDescriptor enclosingType = mMap.get(enclosed); + if (enclosingType != null) { + param.setEnclosingClass(enclosingType); + + // remove the class from the map, and put it back with the fixed name + mMap.remove(fqcn); + mMap.put(param.getCanonicalName(), param); + } + } + } + + private String getEnclosedName(String fqcn) { + int index = fqcn.lastIndexOf('.'); + return fqcn.substring(0, index); + } + + /** + * Finds and loads all classes that derive from a given set of super classes. + * + * @param rootPackage Root package of classes to find. Use an empty string to find everyting. + * @param superClasses The super classes of all the classes to find. + * @return An hash map which keys are the super classes looked for and which values are + * ArrayList of the classes found. The array lists are always created for all the + * valid keys, they are simply empty if no deriving class is found for a given + * super class. + * @throws IOException + * @throws InvalidAttributeValueException + * @throws ClassFormatError + */ + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(String rootPackage, + String[] superClasses) throws IOException, InvalidAttributeValueException, + ClassFormatError { + HashMap<String, ArrayList<IClassDescriptor>> map = + new HashMap<String, ArrayList<IClassDescriptor>>(); + + ArrayList<IClassDescriptor> list = new ArrayList<IClassDescriptor>(); + list.addAll(mWidgetMap.values()); + map.put(AndroidConstants.CLASS_VIEW, list); + + list = new ArrayList<IClassDescriptor>(); + list.addAll(mLayoutMap.values()); + map.put(AndroidConstants.CLASS_VIEWGROUP, list); + + list = new ArrayList<IClassDescriptor>(); + list.addAll(mLayoutParamsMap.values()); + map.put(AndroidConstants.CLASS_VIEWGROUP_LAYOUTPARAMS, list); + + return map; + } + + /** + * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name. + * @param className the fully-qualified name of the class to return. + * @throws ClassNotFoundException + */ + public IClassDescriptor getClass(String className) throws ClassNotFoundException { + return mMap.get(className); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java new file mode 100644 index 0000000..e0d0d5e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.wizards.actions; + +import com.android.ide.eclipse.adt.wizards.newproject.NewProjectWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Project". + * It displays the Android New Project wizard. + */ +public class NewProjectAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewProjectWizard(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java new file mode 100644 index 0000000..8c4a115 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.wizards.actions; + +import com.android.ide.eclipse.editors.wizards.NewXmlFileWizard; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IWorkbenchWizard; + +/** + * Delegate for the toolbar action "Android Project". + * It displays the Android New XML file wizard. + */ +public class NewXmlFileAction extends OpenWizardAction { + + @Override + protected IWorkbenchWizard instanciateWizard(IAction action) { + return new NewXmlFileWizard(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java new file mode 100644 index 0000000..4fc9dee --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.wizards.actions; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.IWorkbenchWizard; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.IWorkbenchHelpContextIds; +import org.eclipse.ui.internal.LegacyResourceSupport; +import org.eclipse.ui.internal.actions.NewWizardShortcutAction; +import org.eclipse.ui.internal.util.Util; + +/** + * An abstract action that displays one of our wizards. + * Derived classes must provide the actual wizard to display. + */ +/*package*/ abstract class OpenWizardAction implements IWorkbenchWindowActionDelegate { + + /** + * The wizard dialog width, extracted from {@link NewWizardShortcutAction} + */ + private static final int SIZING_WIZARD_WIDTH = 500; + + /** + * The wizard dialog height, extracted from {@link NewWizardShortcutAction} + */ + private static final int SIZING_WIZARD_HEIGHT = 500; + + + /* (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#dispose() + */ + public void dispose() { + // pass + } + + /* (non-Javadoc) + * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#init(org.eclipse.ui.IWorkbenchWindow) + */ + public void init(IWorkbenchWindow window) { + // pass + } + + /** + * Opens and display the Android New Project Wizard. + * <p/> + * Most of this implementation is extracted from {@link NewWizardShortcutAction#run()}. + * + * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction) + */ + public void run(IAction action) { + + // get the workbench and the current window + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + + // This code from NewWizardShortcutAction#run() gets the current window selection + // and converts it to a workbench structured selection for the wizard, if possible. + ISelection selection = window.getSelectionService().getSelection(); + IStructuredSelection selectionToPass = StructuredSelection.EMPTY; + if (selection instanceof IStructuredSelection) { + selectionToPass = (IStructuredSelection) selection; + } else { + // Build the selection from the IFile of the editor + IWorkbenchPart part = window.getPartService().getActivePart(); + if (part instanceof IEditorPart) { + IEditorInput input = ((IEditorPart) part).getEditorInput(); + Class<?> fileClass = LegacyResourceSupport.getFileClass(); + if (input != null && fileClass != null) { + Object file = Util.getAdapter(input, fileClass); + if (file != null) { + selectionToPass = new StructuredSelection(file); + } + } + } + } + + // Create the wizard and initialize it with the selection + IWorkbenchWizard wizard = instanciateWizard(action); + wizard.init(workbench, selectionToPass); + + // It's not visible yet until a dialog is created and opened + Shell parent = window.getShell(); + WizardDialog dialog = new WizardDialog(parent, wizard); + dialog.create(); + + // This code comes straight from NewWizardShortcutAction#run() + Point defaultSize = dialog.getShell().getSize(); + dialog.getShell().setSize( + Math.max(SIZING_WIZARD_WIDTH, defaultSize.x), + Math.max(SIZING_WIZARD_HEIGHT, defaultSize.y)); + window.getWorkbench().getHelpSystem().setHelp(dialog.getShell(), + IWorkbenchHelpContextIds.NEW_WIZARD_SHORTCUT); + + dialog.open(); + } + + /** + * Called by {@link #run(IAction)} to instantiate the actual wizard. + * + * @param action The action parameter from {@link #run(IAction)}. + * @return A new wizard instance. Must not be null. + */ + protected abstract IWorkbenchWizard instanciateWizard(IAction action); + + /* (non-Javadoc) + * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction, org.eclipse.jface.viewers.ISelection) + */ + public void selectionChanged(IAction action, ISelection selection) { + // pass + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java new file mode 100644 index 0000000..33ec2bc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java @@ -0,0 +1,1319 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * References: + * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard + * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage + */ + +package com.android.ide.eclipse.adt.wizards.newproject; + +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; +import com.android.sdklib.project.ProjectProperties; +import com.android.sdklib.project.ProjectProperties.PropertyType; +import com.android.sdkuilib.SdkTargetSelector; + +import org.eclipse.core.filesystem.URIUtil; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jdt.core.JavaConventions; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.osgi.util.TextProcessor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.io.FileFilter; +import java.net.URI; +import java.util.regex.Pattern; + +/** + * NewAndroidProjectCreationPage is a project creation page that provides the + * following fields: + * <ul> + * <li> Project name + * <li> SDK Target + * <li> Application name + * <li> Package name + * <li> Activity name + * </ul> + * Note: this class is public so that it can be accessed from unit tests. + * It is however an internal class. Its API may change without notice. + * It should semantically be considered as a private final class. + * Do not derive from this class. + */ +public class NewProjectCreationPage extends WizardPage { + + // constants + /** Initial value for all name fields (project, activity, application, package). Used + * whenever a value is requested before controls are created. */ + private static final String INITIAL_NAME = ""; //$NON-NLS-1$ + /** Initial value for the Create New Project radio; False means Create From Existing would be + * the default.*/ + private static final boolean INITIAL_CREATE_NEW_PROJECT = true; + /** Initial value for the Use Default Location check box. */ + private static final boolean INITIAL_USE_DEFAULT_LOCATION = true; + /** Initial value for the Create Activity check box. */ + private static final boolean INITIAL_CREATE_ACTIVITY = true; + + + /** Pattern for characters accepted in a project name. Since this will be used as a + * directory name, we're being a bit conservative on purpose. It cannot start with a space. */ + private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$"); //$NON-NLS-1$ + /** Last user-browsed location, static so that it be remembered for the whole session */ + private static String sCustomLocationOsPath = ""; //$NON-NLS-1$ + private static boolean sAutoComputeCustomLocation = true; + + private final int MSG_NONE = 0; + private final int MSG_WARNING = 1; + private final int MSG_ERROR = 2; + + private String mUserPackageName = ""; //$NON-NLS-1$ + private String mUserActivityName = ""; //$NON-NLS-1$ + private boolean mUserCreateActivityCheck = INITIAL_CREATE_ACTIVITY; + private String mSourceFolder = ""; //$NON-NLS-1$ + + // widgets + private Text mProjectNameField; + private Text mPackageNameField; + private Text mActivityNameField; + private Text mApplicationNameField; + private Button mCreateNewProjectRadio; + private Button mUseDefaultLocation; + private Label mLocationLabel; + private Text mLocationPathField; + private Button mBrowseButton; + private Button mCreateActivityCheck; + private Text mMinSdkVersionField; + private SdkTargetSelector mSdkTargetSelector; + + private boolean mInternalLocationPathUpdate; + protected boolean mInternalProjectNameUpdate; + protected boolean mInternalApplicationNameUpdate; + private boolean mInternalCreateActivityUpdate; + private boolean mInternalActivityNameUpdate; + protected boolean mProjectNameModifiedByUser; + protected boolean mApplicationNameModifiedByUser; + private boolean mInternalMinSdkVersionUpdate; + private boolean mMinSdkVersionModifiedByUser; + + + /** + * Creates a new project creation wizard page. + * + * @param pageName the name of this page + */ + public NewProjectCreationPage(String pageName) { + super(pageName); + setPageComplete(false); + } + + // --- Getters used by NewProjectWizard --- + + /** + * Returns the current project location path as entered by the user, or its + * anticipated initial value. Note that if the default has been returned the + * path in a project description used to create a project should not be set. + * + * @return the project location path or its anticipated initial value. + */ + public IPath getLocationPath() { + return new Path(getProjectLocation()); + } + + /** Returns the value of the project name field with leading and trailing spaces removed. */ + public String getProjectName() { + return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim(); + } + + /** Returns the value of the package name field with spaces trimmed. */ + public String getPackageName() { + return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim(); + } + + /** Returns the value of the activity name field with spaces trimmed. */ + public String getActivityName() { + return mActivityNameField == null ? INITIAL_NAME : mActivityNameField.getText().trim(); + } + + /** Returns the value of the min sdk version field with spaces trimmed. */ + public String getMinSdkVersion() { + return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim(); + } + + /** Returns the value of the application name field with spaces trimmed. */ + public String getApplicationName() { + // Return the name of the activity as default application name. + return mApplicationNameField == null ? getActivityName() + : mApplicationNameField.getText().trim(); + + } + + /** Returns the value of the "Create New Project" radio. */ + public boolean isNewProject() { + return mCreateNewProjectRadio == null ? INITIAL_CREATE_NEW_PROJECT + : mCreateNewProjectRadio.getSelection(); + } + + /** Returns the value of the "Create Activity" checkbox. */ + public boolean isCreateActivity() { + return mCreateActivityCheck == null ? INITIAL_CREATE_ACTIVITY + : mCreateActivityCheck.getSelection(); + } + + /** Returns the value of the Use Default Location field. */ + public boolean useDefaultLocation() { + return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION + : mUseDefaultLocation.getSelection(); + } + + /** Returns the internal source folder (for the "existing project" mode) or the default + * "src" constant. */ + public String getSourceFolder() { + if (isNewProject() || mSourceFolder == null || mSourceFolder.length() == 0) { + return SdkConstants.FD_SOURCES; + } else { + return mSourceFolder; + } + } + + /** Returns the current sdk target or null if none has been selected yet. */ + public IAndroidTarget getSdkTarget() { + return mSdkTargetSelector == null ? null : mSdkTargetSelector.getFirstSelected(); + } + + /** + * Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when + * the dialog is made visible. + */ + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + mProjectNameField.setFocus(); + } + } + + // --- UI creation --- + + /** + * Creates the top level control for this dialog page under the given parent + * composite. + * + * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) + */ + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setFont(parent.getFont()); + + initializeDialogUnits(parent); + + composite.setLayout(new GridLayout()); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createProjectNameGroup(composite); + createLocationGroup(composite); + createTargetGroup(composite); + createPropertiesGroup(composite); + + // Update state the first time + enableLocationWidgets(); + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + // Validate. This will complain about the first empty field. + setPageComplete(validatePage()); + } + + /** + * Creates the group for the project name: + * [label: "Project Name"] [text field] + * + * @param parent the parent composite + */ + private final void createProjectNameGroup(Composite parent) { + Composite group = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // new project label + Label label = new Label(group, SWT.NONE); + label.setText("Project name:"); + label.setFont(parent.getFont()); + label.setToolTipText("Name of the Eclipse project to create. It cannot be empty."); + + // new project name entry field + mProjectNameField = new Text(group, SWT.BORDER); + GridData data = new GridData(GridData.FILL_HORIZONTAL); + mProjectNameField.setToolTipText("Name of the Eclipse project to create. It cannot be empty."); + mProjectNameField.setLayoutData(data); + mProjectNameField.setFont(parent.getFont()); + mProjectNameField.addListener(SWT.Modify, new Listener() { + public void handleEvent(Event event) { + if (!mInternalProjectNameUpdate) { + mProjectNameModifiedByUser = true; + } + updateLocationPathField(null); + } + }); + } + + + /** + * Creates the group for the Project options: + * [radio] Create new project + * [radio] Create project from existing sources + * [check] Use default location + * Location [text field] [browse button] + * + * @param parent the parent composite + */ + private final void createLocationGroup(Composite parent) { + Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); + // Layout has 4 columns of non-equal size + group.setLayout(new GridLayout()); + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + group.setText("Contents"); + + mCreateNewProjectRadio = new Button(group, SWT.RADIO); + mCreateNewProjectRadio.setText("Create new project in workspace"); + mCreateNewProjectRadio.setSelection(INITIAL_CREATE_NEW_PROJECT); + Button existing_project_radio = new Button(group, SWT.RADIO); + existing_project_radio.setText("Create project from existing source"); + existing_project_radio.setSelection(!INITIAL_CREATE_NEW_PROJECT); + + mUseDefaultLocation = new Button(group, SWT.CHECK); + mUseDefaultLocation.setText("Use default location"); + mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION); + + SelectionListener location_listener = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + enableLocationWidgets(); + extractNamesFromAndroidManifest(); + setPageComplete(validatePage()); + } + }; + + mCreateNewProjectRadio.addSelectionListener(location_listener); + existing_project_radio.addSelectionListener(location_listener); + mUseDefaultLocation.addSelectionListener(location_listener); + + Composite location_group = new Composite(group, SWT.NONE); + location_group.setLayout(new GridLayout(4, /* num columns */ + false /* columns of not equal size */)); + location_group.setLayoutData(new GridData(GridData.FILL_BOTH)); + location_group.setFont(parent.getFont()); + + mLocationLabel = new Label(location_group, SWT.NONE); + mLocationLabel.setText("Location:"); + + mLocationPathField = new Text(location_group, SWT.BORDER); + GridData data = new GridData(GridData.FILL, /* horizontal alignment */ + GridData.BEGINNING, /* vertical alignment */ + true, /* grabExcessHorizontalSpace */ + false, /* grabExcessVerticalSpace */ + 2, /* horizontalSpan */ + 1); /* verticalSpan */ + mLocationPathField.setLayoutData(data); + mLocationPathField.setFont(parent.getFont()); + mLocationPathField.addListener(SWT.Modify, new Listener() { + public void handleEvent(Event event) { + onLocationPathFieldModified(); + } + }); + + mBrowseButton = new Button(location_group, SWT.PUSH); + mBrowseButton.setText("Browse..."); + setButtonLayoutData(mBrowseButton); + mBrowseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + openDirectoryBrowser(); + } + }); + } + + /** + * Creates the target group. + * It only contains an SdkTargetSelector. + */ + private void createTargetGroup(Composite parent) { + Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); + // Layout has 1 column + group.setLayout(new GridLayout()); + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + group.setText("Target"); + + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (Sdk.getCurrent() != null) { + targets = Sdk.getCurrent().getTargets(); + } + + mSdkTargetSelector = new SdkTargetSelector(group, targets, false /*multi-selection*/); + + // If there's only one target, select it + if (targets != null && targets.length == 1) { + mSdkTargetSelector.setSelection(targets[0]); + } + + mSdkTargetSelector.setSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onSdkTargetModified(); + updateLocationPathField(null); + setPageComplete(validatePage()); + } + }); + } + + /** + * Display a directory browser and update the location path field with the selected path + */ + private void openDirectoryBrowser() { + + String existing_dir = getLocationPathFieldValue(); + + // Disable the path if it doesn't exist + if (existing_dir.length() == 0) { + existing_dir = null; + } else { + File f = new File(existing_dir); + if (!f.exists()) { + existing_dir = null; + } + } + + DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell()); + dd.setMessage("Browse for folder"); + dd.setFilterPath(existing_dir); + String abs_dir = dd.open(); + + if (abs_dir != null) { + updateLocationPathField(abs_dir); + extractNamesFromAndroidManifest(); + setPageComplete(validatePage()); + } + } + + /** + * Creates the group for the project properties: + * - Package name [text field] + * - Activity name [text field] + * - Application name [text field] + * + * @param parent the parent composite + */ + private final void createPropertiesGroup(Composite parent) { + // package specification group + Group group = new Group(parent, SWT.SHADOW_ETCHED_IN); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + group.setFont(parent.getFont()); + group.setText("Properties"); + + // new application label + Label label = new Label(group, SWT.NONE); + label.setText("Application name:"); + label.setFont(parent.getFont()); + label.setToolTipText("Name of the Application. This is a free string. It can be empty."); + + // new application name entry field + mApplicationNameField = new Text(group, SWT.BORDER); + GridData data = new GridData(GridData.FILL_HORIZONTAL); + mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty."); + mApplicationNameField.setLayoutData(data); + mApplicationNameField.setFont(parent.getFont()); + mApplicationNameField.addListener(SWT.Modify, new Listener() { + public void handleEvent(Event event) { + if (!mInternalApplicationNameUpdate) { + mApplicationNameModifiedByUser = true; + } + } + }); + + // new package label + label = new Label(group, SWT.NONE); + label.setText("Package name:"); + label.setFont(parent.getFont()); + label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components."); + + // new package name entry field + mPackageNameField = new Text(group, SWT.BORDER); + data = new GridData(GridData.FILL_HORIZONTAL); + mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components."); + mPackageNameField.setLayoutData(data); + mPackageNameField.setFont(parent.getFont()); + mPackageNameField.addListener(SWT.Modify, new Listener() { + public void handleEvent(Event event) { + onPackageNameFieldModified(); + } + }); + + // new activity label + mCreateActivityCheck = new Button(group, SWT.CHECK); + mCreateActivityCheck.setText("Create Activity:"); + mCreateActivityCheck.setToolTipText("Specifies if you want to create a default Activity."); + mCreateActivityCheck.setFont(parent.getFont()); + mCreateActivityCheck.setSelection(INITIAL_CREATE_ACTIVITY); + mCreateActivityCheck.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event event) { + onCreateActivityCheckModified(); + enableLocationWidgets(); + } + }); + + // new activity name entry field + mActivityNameField = new Text(group, SWT.BORDER); + data = new GridData(GridData.FILL_HORIZONTAL); + mActivityNameField.setToolTipText("Name of the Activity class to create. Must be a valid Java identifier."); + mActivityNameField.setLayoutData(data); + mActivityNameField.setFont(parent.getFont()); + mActivityNameField.addListener(SWT.Modify, new Listener() { + public void handleEvent(Event event) { + onActivityNameFieldModified(); + } + }); + + // min sdk version label + label = new Label(group, SWT.NONE); + label.setText("Min SDK Version:"); + label.setFont(parent.getFont()); + label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty."); + + // min sdk version entry field + mMinSdkVersionField = new Text(group, SWT.BORDER); + data = new GridData(GridData.FILL_HORIZONTAL); + label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty."); + mMinSdkVersionField.setLayoutData(data); + mMinSdkVersionField.setFont(parent.getFont()); + mMinSdkVersionField.addListener(SWT.Modify, new Listener() { + public void handleEvent(Event event) { + onMinSdkVersionFieldModified(); + setPageComplete(validatePage()); + } + }); + } + + + //--- Internal getters & setters ------------------ + + /** Returns the location path field value with spaces trimmed. */ + private String getLocationPathFieldValue() { + return mLocationPathField == null ? "" : mLocationPathField.getText().trim(); + } + + /** Returns the current project location, depending on the Use Default Location check box. */ + public String getProjectLocation() { + if (isNewProject() && useDefaultLocation()) { + return Platform.getLocation().toString(); + } else { + return getLocationPathFieldValue(); + } + } + + /** + * Creates a project resource handle for the current project name field + * value. + * <p> + * This method does not create the project resource; this is the + * responsibility of <code>IProject::create</code> invoked by the new + * project resource wizard. + * </p> + * + * @return the new project resource handle + */ + private IProject getProjectHandle() { + return ResourcesPlugin.getWorkspace().getRoot().getProject(getProjectName()); + } + + // --- UI Callbacks ---- + + /** + * Enables or disable the location widgets depending on the user selection: + * the location path is enabled when using the "existing source" mode (i.e. not new project) + * or in new project mode with the "use default location" turned off. + */ + private void enableLocationWidgets() { + boolean is_new_project = isNewProject(); + boolean use_default = useDefaultLocation(); + boolean location_enabled = !is_new_project || !use_default; + boolean create_activity = isCreateActivity(); + + mUseDefaultLocation.setEnabled(is_new_project); + + mLocationLabel.setEnabled(location_enabled); + mLocationPathField.setEnabled(location_enabled); + mBrowseButton.setEnabled(location_enabled); + + mPackageNameField.setEnabled(is_new_project); + mCreateActivityCheck.setEnabled(is_new_project); + mActivityNameField.setEnabled(is_new_project & create_activity); + + updateLocationPathField(null); + updatePackageAndActivityFields(); + } + + /** + * Updates the location directory path field. + * <br/> + * When custom user selection is enabled, use the abs_dir argument if not null and also + * save it internally. If abs_dir is null, restore the last saved abs_dir. This allows the + * user selection to be remembered when the user switches from default to custom. + * <br/> + * When custom user selection is disabled, use the workspace default location with the + * current project name. This does not change the internally cached abs_dir. + * + * @param abs_dir A new absolute directory path or null to use the default. + */ + private void updateLocationPathField(String abs_dir) { + boolean is_new_project = isNewProject(); + boolean use_default = useDefaultLocation(); + boolean custom_location = !is_new_project || !use_default; + + if (!mInternalLocationPathUpdate) { + mInternalLocationPathUpdate = true; + if (custom_location) { + if (abs_dir != null) { + // We get here if the user selected a directory with the "Browse" button. + // Disable auto-compute of the custom location unless the user selected + // the exact same path. + sAutoComputeCustomLocation = sAutoComputeCustomLocation && + abs_dir.equals(sCustomLocationOsPath); + sCustomLocationOsPath = TextProcessor.process(abs_dir); + } else if (sAutoComputeCustomLocation || + !new File(sCustomLocationOsPath).isDirectory()) { + // By default select the samples directory of the current target + IAndroidTarget target = getSdkTarget(); + if (target != null) { + sCustomLocationOsPath = target.getPath(IAndroidTarget.SAMPLES); + } + + // If we don't have a target, select the base directory of the + // "universal sdk". If we don't even have that, use a root drive. + if (sCustomLocationOsPath == null || sCustomLocationOsPath.length() == 0) { + if (Sdk.getCurrent() != null) { + sCustomLocationOsPath = Sdk.getCurrent().getSdkLocation(); + } else { + sCustomLocationOsPath = File.listRoots()[0].getAbsolutePath(); + } + } + } + if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) { + mLocationPathField.setText(sCustomLocationOsPath); + } + } else { + String value = Platform.getLocation().append(getProjectName()).toString(); + value = TextProcessor.process(value); + if (!mLocationPathField.getText().equals(value)) { + mLocationPathField.setText(value); + } + } + setPageComplete(validatePage()); + mInternalLocationPathUpdate = false; + } + } + + /** + * The location path field is either modified internally (from updateLocationPathField) + * or manually by the user when the custom_location mode is not set. + * + * Ignore the internal modification. When modified by the user, memorize the choice and + * validate the page. + */ + private void onLocationPathFieldModified() { + if (!mInternalLocationPathUpdate) { + // When the updates doesn't come from updateLocationPathField, it must be the user + // editing the field manually, in which case we want to save the value internally + // and we disable auto-compute of the custom location (to avoid overriding the user + // value) + String newPath = getLocationPathFieldValue(); + sAutoComputeCustomLocation = sAutoComputeCustomLocation && + newPath.equals(sCustomLocationOsPath); + sCustomLocationOsPath = newPath; + extractNamesFromAndroidManifest(); + setPageComplete(validatePage()); + } + } + + /** + * The package name field is either modified internally (from extractNamesFromAndroidManifest) + * or manually by the user when the custom_location mode is not set. + * + * Ignore the internal modification. When modified by the user, memorize the choice and + * validate the page. + */ + private void onPackageNameFieldModified() { + if (isNewProject()) { + mUserPackageName = getPackageName(); + setPageComplete(validatePage()); + } + } + + /** + * The create activity checkbox is either modified internally (from + * extractNamesFromAndroidManifest) or manually by the user. + * + * Ignore the internal modification. When modified by the user, memorize the choice and + * validate the page. + */ + private void onCreateActivityCheckModified() { + if (isNewProject() && !mInternalCreateActivityUpdate) { + mUserCreateActivityCheck = isCreateActivity(); + } + setPageComplete(validatePage()); + } + + /** + * The activity name field is either modified internally (from extractNamesFromAndroidManifest) + * or manually by the user when the custom_location mode is not set. + * + * Ignore the internal modification. When modified by the user, memorize the choice and + * validate the page. + */ + private void onActivityNameFieldModified() { + if (isNewProject() && !mInternalActivityNameUpdate) { + mUserActivityName = getActivityName(); + setPageComplete(validatePage()); + } + } + + /** + * Called when the min sdk version field has been modified. + * + * Ignore the internal modifications. When modified by the user, try to match + * a target with the same API level. + */ + private void onMinSdkVersionFieldModified() { + if (mInternalMinSdkVersionUpdate) { + return; + } + + try { + int version = Integer.parseInt(getMinSdkVersion()); + + // Before changing, compare with the currently selected one, if any. + // There can be multiple targets with the same sdk api version, so don't change + // it if it's already at the right version. + IAndroidTarget curr_target = getSdkTarget(); + if (curr_target != null && curr_target.getApiVersionNumber() == version) { + return; + } + + for (IAndroidTarget target : mSdkTargetSelector.getTargets()) { + if (target.getApiVersionNumber() == version) { + mSdkTargetSelector.setSelection(target); + break; + } + } + } catch (NumberFormatException e) { + // ignore + } + + mMinSdkVersionModifiedByUser = true; + } + + /** + * Called when an SDK target is modified. + * + * If the minSdkVersion field hasn't been modified by the user yet, we change it + * to reflect the sdk api level that has just been selected. + */ + private void onSdkTargetModified() { + IAndroidTarget target = getSdkTarget(); + + if (target != null && !mMinSdkVersionModifiedByUser) { + mInternalMinSdkVersionUpdate = true; + mMinSdkVersionField.setText(Integer.toString(target.getApiVersionNumber())); + mInternalMinSdkVersionUpdate = false; + } + } + + /** + * Called when the radio buttons are changed between the "create new project" and the + * "use existing source" mode. This reverts the fields to whatever the user manually + * entered before. + */ + private void updatePackageAndActivityFields() { + if (isNewProject()) { + if (mUserPackageName.length() > 0 && + !mPackageNameField.getText().equals(mUserPackageName)) { + mPackageNameField.setText(mUserPackageName); + } + + if (mUserActivityName.length() > 0 && + !mActivityNameField.getText().equals(mUserActivityName)) { + mInternalActivityNameUpdate = true; + mActivityNameField.setText(mUserActivityName); + mInternalActivityNameUpdate = false; + } + + if (mUserCreateActivityCheck != mCreateActivityCheck.getSelection()) { + mInternalCreateActivityUpdate = true; + mCreateActivityCheck.setSelection(mUserCreateActivityCheck); + mInternalCreateActivityUpdate = false; + } + } + } + + /** + * Extract names from an android manifest. + * This is done only if the user selected the "use existing source" and a manifest xml file + * can actually be found in the custom user directory. + */ + private void extractNamesFromAndroidManifest() { + if (isNewProject()) { + return; + } + + String projectLocation = getProjectLocation(); + File f = new File(projectLocation); + if (!f.isDirectory()) { + return; + } + + Path path = new Path(f.getPath()); + String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString(); + AndroidManifestHelper manifest = new AndroidManifestHelper(osPath); + if (!manifest.exists()) { + return; + } + + String packageName = null; + String activityName = null; + String minSdkVersion = null; + try { + packageName = manifest.getPackageName(); + activityName = manifest.getActivityName(1); + minSdkVersion = manifest.getMinSdkVersion(); + } catch (Exception e) { + // ignore exceptions + } + + + if (packageName != null && packageName.length() > 0) { + mPackageNameField.setText(packageName); + } + + if (activityName != null && activityName.length() > 0) { + mInternalActivityNameUpdate = true; + mInternalCreateActivityUpdate = true; + mActivityNameField.setText(activityName); + mCreateActivityCheck.setSelection(true); + mInternalCreateActivityUpdate = false; + mInternalActivityNameUpdate = false; + + // If project name and application names are empty, use the activity + // name as a default. If the activity name has dots, it's a part of a + // package specification and only the last identifier must be used. + if (activityName.indexOf('.') != -1) { + String[] ids = activityName.split(AndroidConstants.RE_DOT); + activityName = ids[ids.length - 1]; + } + if (mProjectNameField.getText().length() == 0 || + !mProjectNameModifiedByUser) { + mInternalProjectNameUpdate = true; + mProjectNameField.setText(activityName); + mInternalProjectNameUpdate = false; + } + if (mApplicationNameField.getText().length() == 0 || + !mApplicationNameModifiedByUser) { + mInternalApplicationNameUpdate = true; + mApplicationNameField.setText(activityName); + mInternalApplicationNameUpdate = false; + } + } else { + mInternalActivityNameUpdate = true; + mInternalCreateActivityUpdate = true; + mActivityNameField.setText(""); //$NON-NLS-1$ + mCreateActivityCheck.setSelection(false); + mInternalCreateActivityUpdate = false; + mInternalActivityNameUpdate = false; + + // There is no activity name to use to fill in the project and application + // name. However if there's a package name, we can use this as a base. + if (packageName != null && packageName.length() > 0) { + // Package name is a java identifier, so it's most suitable for + // an application name. + + if (mApplicationNameField.getText().length() == 0 || + !mApplicationNameModifiedByUser) { + mInternalApplicationNameUpdate = true; + mApplicationNameField.setText(packageName); + mInternalApplicationNameUpdate = false; + } + + // For the project name, remove any dots + packageName = packageName.replace('.', '_'); + if (mProjectNameField.getText().length() == 0 || + !mProjectNameModifiedByUser) { + mInternalProjectNameUpdate = true; + mProjectNameField.setText(packageName); + mInternalProjectNameUpdate = false; + } + + } + } + + // Select the target matching the manifest's sdk or build properties, if any + boolean foundTarget = false; + + ProjectProperties p = ProjectProperties.create(projectLocation, null); + if (p != null) { + // Check the {build|default}.properties files if present + p.merge(PropertyType.BUILD).merge(PropertyType.DEFAULT); + String v = p.getProperty(ProjectProperties.PROPERTY_TARGET); + IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(v); + if (target != null) { + mSdkTargetSelector.setSelection(target); + foundTarget = true; + } + } + + if (!foundTarget && minSdkVersion != null) { + try { + int sdkVersion = Integer.parseInt(minSdkVersion); + + for (IAndroidTarget target : mSdkTargetSelector.getTargets()) { + if (target.getApiVersionNumber() == sdkVersion) { + mSdkTargetSelector.setSelection(target); + foundTarget = true; + break; + } + } + } catch(NumberFormatException e) { + // ignore + } + } + + if (!foundTarget) { + for (IAndroidTarget target : mSdkTargetSelector.getTargets()) { + if (projectLocation.startsWith(target.getLocation())) { + mSdkTargetSelector.setSelection(target); + foundTarget = true; + break; + } + } + } + + if (!foundTarget) { + mInternalMinSdkVersionUpdate = true; + mMinSdkVersionField.setText(minSdkVersion == null ? "" : minSdkVersion); //$NON-NLS-1$ + mInternalMinSdkVersionUpdate = false; + } + } + + /** + * Returns whether this page's controls currently all contain valid values. + * + * @return <code>true</code> if all controls are valid, and + * <code>false</code> if at least one is invalid + */ + protected boolean validatePage() { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + + int status = validateProjectField(workspace); + if ((status & MSG_ERROR) == 0) { + status |= validateLocationPath(workspace); + } + if ((status & MSG_ERROR) == 0) { + status |= validateSdkTarget(); + } + if ((status & MSG_ERROR) == 0) { + status |= validatePackageField(); + } + if ((status & MSG_ERROR) == 0) { + status |= validateActivityField(); + } + if ((status & MSG_ERROR) == 0) { + status |= validateMinSdkVersionField(); + } + if ((status & MSG_ERROR) == 0) { + status |= validateSourceFolder(); + } + if (status == MSG_NONE) { + setStatus(null, MSG_NONE); + } + + // Return false if there's an error so that the finish button be disabled. + return (status & MSG_ERROR) == 0; + } + + /** + * Validates the project name field. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validateProjectField(IWorkspace workspace) { + // Validate project field + String projectFieldContents = getProjectName(); + if (projectFieldContents.length() == 0) { + return setStatus("Project name must be specified", MSG_ERROR); + } + + // Limit the project name to shell-agnostic characters since it will be used to + // generate the final package + if (!sProjectNamePattern.matcher(projectFieldContents).matches()) { + return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.", + MSG_ERROR); + } + + IStatus nameStatus = workspace.validateName(projectFieldContents, IResource.PROJECT); + if (!nameStatus.isOK()) { + return setStatus(nameStatus.getMessage(), MSG_ERROR); + } + + if (getProjectHandle().exists()) { + return setStatus("A project with that name already exists in the workspace", + MSG_ERROR); + } + + return MSG_NONE; + } + + /** + * Validates the location path field. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validateLocationPath(IWorkspace workspace) { + Path path = new Path(getProjectLocation()); + if (isNewProject()) { + if (!useDefaultLocation()) { + // If not using the default value validate the location. + URI uri = URIUtil.toURI(path.toOSString()); + IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(), + uri); + if (!locationStatus.isOK()) { + return setStatus(locationStatus.getMessage(), MSG_ERROR); + } else { + // The location is valid as far as Eclipse is concerned (i.e. mostly not + // an existing workspace project.) Check it either doesn't exist or is + // a directory that is empty. + File f = path.toFile(); + if (f.exists() && !f.isDirectory()) { + return setStatus("A directory name must be specified.", MSG_ERROR); + } else if (f.isDirectory()) { + // However if the directory exists, we should put a warning if it is not + // empty. We don't put an error (we'll ask the user again for confirmation + // before using the directory.) + String[] l = f.list(); + if (l.length != 0) { + return setStatus("The selected output directory is not empty.", + MSG_WARNING); + } + } + } + } else { + // Otherwise validate the path string is not empty + if (getProjectLocation().length() == 0) { + return setStatus("A directory name must be specified.", MSG_ERROR); + } + + File dest = path.append(getProjectName()).toFile(); + if (dest.exists()) { + return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.", + getProjectName()), MSG_ERROR); + } + } + } else { + // Must be an existing directory + File f = path.toFile(); + if (!f.isDirectory()) { + return setStatus("An existing directory name must be specified.", MSG_ERROR); + } + + // Check there's an android manifest in the directory + String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString(); + AndroidManifestHelper manifest = new AndroidManifestHelper(osPath); + if (!manifest.exists()) { + return setStatus( + String.format("File %1$s not found in %2$s.", + AndroidConstants.FN_ANDROID_MANIFEST, f.getName()), + MSG_ERROR); + } + + // Parse it and check the important fields. + String packageName = manifest.getPackageName(); + if (packageName == null || packageName.length() == 0) { + return setStatus( + String.format("No package name defined in %1$s.", osPath), + MSG_ERROR); + } + + String activityName = manifest.getActivityName(1); + if (activityName == null || activityName.length() == 0) { + // This is acceptable now as long as no activity needs to be created + if (isCreateActivity()) { + return setStatus( + String.format("No activity name defined in %1$s.", osPath), + MSG_ERROR); + } + } + + // If there's already a .project, tell the user to use import instead. + if (path.append(".project").toFile().exists()) { //$NON-NLS-1$ + return setStatus("An Eclipse project already exists in this directory. Consider using File > Import > Existing Project instead.", + MSG_WARNING); + } + } + + return MSG_NONE; + } + + /** + * Validates the sdk target choice. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validateSdkTarget() { + if (getSdkTarget() == null) { + return setStatus("An SDK Target must be specified.", MSG_ERROR); + } + return MSG_NONE; + } + + /** + * Validates the sdk target choice. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validateMinSdkVersionField() { + + // If the min sdk version is empty, it is always accepted. + if (getMinSdkVersion().length() == 0) { + return MSG_NONE; + } + + int version = -1; + try { + // If not empty, it must be a valid integer > 0 + version = Integer.parseInt(getMinSdkVersion()); + } catch (NumberFormatException e) { + // ignore + } + + if (version < 1) { + return setStatus("Min SDK Version must be an integer > 0.", MSG_ERROR); + } + + if (getSdkTarget() != null && getSdkTarget().getApiVersionNumber() != version) { + return setStatus("The API level for the selected SDK target does not match the Min SDK version.", + MSG_WARNING); + } + + return MSG_NONE; + } + + /** + * Validates the activity name field. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validateActivityField() { + // Disregard if not creating an activity + if (!isCreateActivity()) { + return MSG_NONE; + } + + // Validate activity field + String activityFieldContents = getActivityName(); + if (activityFieldContents.length() == 0) { + return setStatus("Activity name must be specified.", MSG_ERROR); + } + + // The activity field can actually contain part of a sub-package name + // or it can start with a dot "." to indicates it comes from the parent package name. + String packageName = ""; + int pos = activityFieldContents.lastIndexOf('.'); + if (pos >= 0) { + packageName = activityFieldContents.substring(0, pos); + if (packageName.startsWith(".")) { //$NON-NLS-1$ + packageName = packageName.substring(1); + } + + activityFieldContents = activityFieldContents.substring(pos + 1); + } + + // the activity field can contain a simple java identifier, or a + // package name or one that starts with a dot. So if it starts with a dot, + // ignore this dot -- the rest must look like a package name. + if (activityFieldContents.charAt(0) == '.') { + activityFieldContents = activityFieldContents.substring(1); + } + + // Check it's a valid activity string + int result = MSG_NONE; + IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents, + "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$ + if (!status.isOK()) { + result = setStatus(status.getMessage(), + status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING); + } + + // Check it's a valid package string + if (result != MSG_ERROR && packageName.length() > 0) { + status = JavaConventions.validatePackageName(packageName, + "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$ + if (!status.isOK()) { + result = setStatus(status.getMessage() + " (in the activity name)", + status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING); + } + } + + + return result; + } + + /** + * Validates the package name field. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validatePackageField() { + // Validate package field + String packageFieldContents = getPackageName(); + if (packageFieldContents.length() == 0) { + return setStatus("Package name must be specified.", MSG_ERROR); + } + + // Check it's a valid package string + int result = MSG_NONE; + IStatus status = JavaConventions.validatePackageName(packageFieldContents, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$ + if (!status.isOK()) { + result = setStatus(status.getMessage(), + status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING); + } + + // The Android Activity Manager does not accept packages names with only one + // identifier. Check the package name has at least one dot in them (the previous rule + // validated that if such a dot exist, it's not the first nor last characters of the + // string.) + if (result != MSG_ERROR && packageFieldContents.indexOf('.') == -1) { + return setStatus("Package name must have at least two identifiers.", MSG_ERROR); + } + + return result; + } + + /** + * Validates that an existing project actually has a source folder. + * + * For project in "use existing source" mode, this tries to find the source folder. + * A source folder should be just under the project directory and it should have all + * the directories composing the package+activity name. + * + * As a side effect, it memorizes the source folder in mSourceFolder. + * + * TODO: support multiple source folders for multiple activities. + * + * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE. + */ + private int validateSourceFolder() { + // This check does nothing when creating a new project. + // This check is also useless when no activity is present or created. + if (isNewProject() || !isCreateActivity()) { + return MSG_NONE; + } + + String osTarget = getActivityName(); + + if (osTarget.indexOf('.') == -1) { + osTarget = getPackageName() + File.separator + osTarget; + } else if (osTarget.indexOf('.') == 0) { + osTarget = getPackageName() + osTarget; + } + osTarget = osTarget.replace('.', File.separatorChar) + AndroidConstants.DOT_JAVA; + + String projectPath = getProjectLocation(); + File projectDir = new File(projectPath); + File[] all_dirs = projectDir.listFiles(new FileFilter() { + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + for (File f : all_dirs) { + Path path = new Path(f.getAbsolutePath()); + File java_activity = path.append(osTarget).toFile(); + if (java_activity.isFile()) { + mSourceFolder = f.getName(); + return MSG_NONE; + } + } + + if (all_dirs.length > 0) { + return setStatus( + String.format("%1$s can not be found under %2$s.", osTarget, projectPath), + MSG_ERROR); + } else { + return setStatus( + String.format("No source folders can be found in %1$s.", projectPath), + MSG_ERROR); + } + } + + /** + * Sets the error message for the wizard with the given message icon. + * + * @param message The wizard message type, one of MSG_ERROR or MSG_WARNING. + * @return As a convenience, always returns messageType so that the caller can return + * immediately. + */ + private int setStatus(String message, int messageType) { + if (message == null) { + setErrorMessage(null); + setMessage(null); + } else if (!message.equals(getMessage())) { + setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR); + } + return messageType; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java new file mode 100644 index 0000000..cb79796 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java @@ -0,0 +1,737 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.wizards.newproject; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.project.AndroidNature; +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceStatus; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.actions.WorkspaceModifyOperation; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * A "New Android Project" Wizard. + * <p/> + * Note: this class is public so that it can be accessed from unit tests. + * It is however an internal class. Its API may change without notice. + * It should semantically be considered as a private final class. + * Do not derive from this class. + + */ +public class NewProjectWizard extends Wizard implements INewWizard { + + private static final String PARAM_SDK_TOOLS_DIR = "ANDROID_SDK_TOOLS"; //$NON-NLS-1$ + private static final String PARAM_ACTIVITY = "ACTIVITY_NAME"; //$NON-NLS-1$ + private static final String PARAM_APPLICATION = "APPLICATION_NAME"; //$NON-NLS-1$ + private static final String PARAM_PACKAGE = "PACKAGE"; //$NON-NLS-1$ + private static final String PARAM_PROJECT = "PROJECT_NAME"; //$NON-NLS-1$ + private static final String PARAM_STRING_NAME = "STRING_NAME"; //$NON-NLS-1$ + private static final String PARAM_STRING_CONTENT = "STRING_CONTENT"; //$NON-NLS-1$ + private static final String PARAM_IS_NEW_PROJECT = "IS_NEW_PROJECT"; //$NON-NLS-1$ + private static final String PARAM_SRC_FOLDER = "SRC_FOLDER"; //$NON-NLS-1$ + private static final String PARAM_SDK_TARGET = "SDK_TARGET"; //$NON-NLS-1$ + private static final String PARAM_MIN_SDK_VERSION = "MIN_SDK_VERSION"; //$NON-NLS-1$ + + private static final String PH_ACTIVITIES = "ACTIVITIES"; //$NON-NLS-1$ + private static final String PH_USES_SDK = "USES-SDK"; //$NON-NLS-1$ + private static final String PH_INTENT_FILTERS = "INTENT_FILTERS"; //$NON-NLS-1$ + private static final String PH_STRINGS = "STRINGS"; //$NON-NLS-1$ + + private static final String BIN_DIRECTORY = + SdkConstants.FD_OUTPUT + AndroidConstants.WS_SEP; + private static final String RES_DIRECTORY = + SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP; + private static final String ASSETS_DIRECTORY = + SdkConstants.FD_ASSETS + AndroidConstants.WS_SEP; + private static final String DRAWABLE_DIRECTORY = + SdkConstants.FD_DRAWABLE + AndroidConstants.WS_SEP; + private static final String LAYOUT_DIRECTORY = + SdkConstants.FD_LAYOUT + AndroidConstants.WS_SEP; + private static final String VALUES_DIRECTORY = + SdkConstants.FD_VALUES + AndroidConstants.WS_SEP; + private static final String GEN_SRC_DIRECTORY = + SdkConstants.FD_GEN_SOURCES + AndroidConstants.WS_SEP; + + private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$ + private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY + + "AndroidManifest.template"; //$NON-NLS-1$ + private static final String TEMPLATE_ACTIVITIES = TEMPLATES_DIRECTORY + + "activity.template"; //$NON-NLS-1$ + private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY + + "uses-sdk.template"; //$NON-NLS-1$ + private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY + + "launcher_intent_filter.template"; //$NON-NLS-1$ + + private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY + + "strings.template"; //$NON-NLS-1$ + private static final String TEMPLATE_STRING = TEMPLATES_DIRECTORY + + "string.template"; //$NON-NLS-1$ + private static final String ICON = "icon.png"; //$NON-NLS-1$ + + private static final String STRINGS_FILE = "strings.xml"; //$NON-NLS-1$ + + private static final String STRING_RSRC_PREFIX = "@string/"; //$NON-NLS-1$ + private static final String STRING_APP_NAME = "app_name"; //$NON-NLS-1$ + private static final String STRING_HELLO_WORLD = "hello"; //$NON-NLS-1$ + + private static final String[] DEFAULT_DIRECTORIES = new String[] { + BIN_DIRECTORY, RES_DIRECTORY, ASSETS_DIRECTORY }; + private static final String[] RES_DIRECTORIES = new String[] { + DRAWABLE_DIRECTORY, LAYOUT_DIRECTORY, VALUES_DIRECTORY}; + + private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$ + private static final String JAVA_ACTIVITY_TEMPLATE = "java_file.template"; //$NON-NLS-1$ + private static final String LAYOUT_TEMPLATE = "layout.template"; //$NON-NLS-1$ + private static final String MAIN_LAYOUT_XML = "main.xml"; //$NON-NLS-1$ + + protected static final String MAIN_PAGE_NAME = "newAndroidProjectPage"; //$NON-NLS-1$ + + private NewProjectCreationPage mMainPage; + + /** + * Initializes this creation wizard using the passed workbench and object + * selection. Inherited from org.eclipse.ui.IWorkbenchWizard + */ + public void init(IWorkbench workbench, IStructuredSelection selection) { + setHelpAvailable(false); // TODO have help + setWindowTitle("New Android Project"); + setImageDescriptor(); + + mMainPage = createMainPage(); + mMainPage.setTitle("New Android Project"); + mMainPage.setDescription("Creates a new Android Project resource."); + } + + /** + * Creates the wizard page. + * <p/> + * Please do NOT override this method. + * <p/> + * This is protected so that it can be overridden by unit tests. + * However the contract of this class is private and NO ATTEMPT will be made + * to maintain compatibility between different versions of the plugin. + */ + protected NewProjectCreationPage createMainPage() { + return new NewProjectCreationPage(MAIN_PAGE_NAME); + } + + // -- Methods inherited from org.eclipse.jface.wizard.Wizard -- + // The Wizard class implements most defaults and boilerplate code needed by + // IWizard + + /** + * Adds pages to this wizard. + */ + @Override + public void addPages() { + addPage(mMainPage); + } + + /** + * Performs any actions appropriate in response to the user having pressed + * the Finish button, or refuse if finishing now is not permitted: here, it + * actually creates the workspace project and then switch to the Java + * perspective. + * + * @return True + */ + @Override + public boolean performFinish() { + if (!createAndroidProject()) { + return false; + } + + // Open the default Java Perspective + OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction(); + action.run(); + return true; + } + + // -- Custom Methods -- + + /** + * Before actually creating the project for a new project (as opposed to using an + * existing project), we check if the target location is a directory that either does + * not exist or is empty. + * + * If it's not empty, ask the user for confirmation. + * + * @param destination The destination folder where the new project is to be created. + * @return True if the destination doesn't exist yet or is an empty directory or is + * accepted by the user. + */ + private boolean validateNewProjectLocationIsEmpty(IPath destination) { + File f = new File(destination.toOSString()); + if (f.isDirectory() && f.list().length > 0) { + return AdtPlugin.displayPrompt("New Android Project", + "You are going to create a new Android Project in an existing, non-empty, directory. Are you sure you want to proceed?"); + } + return true; + } + + /** + * Creates the android project. + * @return True if the project could be created. + */ + private boolean createAndroidProject() { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + final IProject project = workspace.getRoot().getProject(mMainPage.getProjectName()); + final IProjectDescription description = workspace.newProjectDescription(project.getName()); + + final Map<String, Object> parameters = new HashMap<String, Object>(); + parameters.put(PARAM_PROJECT, mMainPage.getProjectName()); + parameters.put(PARAM_PACKAGE, mMainPage.getPackageName()); + parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME); + parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder()); + parameters.put(PARAM_IS_NEW_PROJECT, mMainPage.isNewProject()); + parameters.put(PARAM_SRC_FOLDER, mMainPage.getSourceFolder()); + parameters.put(PARAM_SDK_TARGET, mMainPage.getSdkTarget()); + parameters.put(PARAM_MIN_SDK_VERSION, mMainPage.getMinSdkVersion()); + + if (mMainPage.isCreateActivity()) { + // An activity name can be of the form ".package.Class" or ".Class". + // The initial dot is ignored, as it is always added later in the templates. + String activityName = mMainPage.getActivityName(); + if (activityName.startsWith(".")) { //$NON-NLS-1$ + activityName = activityName.substring(1); + } + parameters.put(PARAM_ACTIVITY, activityName); + } + + // create a dictionary of string that will contain name+content. + // we'll put all the strings into values/strings.xml + final HashMap<String, String> stringDictionary = new HashMap<String, String>(); + stringDictionary.put(STRING_APP_NAME, mMainPage.getApplicationName()); + + IPath path = mMainPage.getLocationPath(); + IPath defaultLocation = Platform.getLocation(); + if (!path.equals(defaultLocation)) { + description.setLocation(path); + } + + if (mMainPage.isNewProject() && !mMainPage.useDefaultLocation() && + !validateNewProjectLocationIsEmpty(path)) { + return false; + } + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(project, description, monitor, parameters, stringDictionary); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Runs the operation in a different thread and display generated + * exceptions. + * + * @param op The asynchronous operation to run. + */ + private void runAsyncOperation(WorkspaceModifyOperation op) { + try { + getContainer().run(true /* fork */, true /* cancelable */, op); + } catch (InvocationTargetException e) { + // The runnable threw an exception + Throwable t = e.getTargetException(); + if (t instanceof CoreException) { + CoreException core = (CoreException) t; + if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) { + // The error indicates the file system is not case sensitive + // and there's a resource with a similar name. + MessageDialog.openError(getShell(), "Error", "Error: Case Variant Exists"); + } else { + ErrorDialog.openError(getShell(), "Error", null, core.getStatus()); + } + } else { + // Some other kind of exception + MessageDialog.openError(getShell(), "Error", t.getMessage()); + } + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Creates the actual project, sets its nature and adds the required folders + * and files to it. This is run asynchronously in a different thread. + * + * @param project The project to create. + * @param description A description of the project. + * @param monitor An existing monitor. + * @param parameters Template parameters. + * @param stringDictionary String definition. + * @throws InvocationTargetException to wrap any unmanaged exception and + * return it to the calling thread. The method can fail if it fails + * to create or modify the project or if it is canceled by the user. + */ + private void createProjectAsync(IProject project, IProjectDescription description, + IProgressMonitor monitor, Map<String, Object> parameters, + Map<String, String> stringDictionary) + throws InvocationTargetException { + monitor.beginTask("Create Android Project", 100); + try { + // Create project and open it + project.create(description, new SubProgressMonitor(monitor, 10)); + if (monitor.isCanceled()) throw new OperationCanceledException(); + project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10)); + + // Add the Java and android nature to the project + AndroidNature.setupProjectNatures(project, monitor); + + // Create folders in the project if they don't already exist + addDefaultDirectories(project, AndroidConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor); + String[] sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER), + GEN_SRC_DIRECTORY + }; + addDefaultDirectories(project, AndroidConstants.WS_ROOT, sourceFolders, monitor); + + // Create the resource folders in the project if they don't already exist. + addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor); + + // Setup class path + IJavaProject javaProject = JavaCore.create(project); + for (String sourceFolder : sourceFolders) { + setupSourceFolder(javaProject, sourceFolder, monitor); + } + + if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) { + // Create files in the project if they don't already exist + addManifest(project, parameters, stringDictionary, monitor); + + // add the default app icon + addIcon(project, monitor); + + // Create the default package components + addSampleCode(project, sourceFolders[0], parameters, stringDictionary, monitor); + + // add the string definition file if needed + if (stringDictionary.size() > 0) { + addStringDictionaryFile(project, stringDictionary, monitor); + } + + // Set output location + javaProject.setOutputLocation(project.getFolder(BIN_DIRECTORY).getFullPath(), + monitor); + } + + Sdk.getCurrent().setProject(project, (IAndroidTarget) parameters.get(PARAM_SDK_TARGET), + null /* apkConfigMap*/); + + // Fix the project to make sure all properties are as expected. + // Necessary for existing projects and good for new ones to. + ProjectHelper.fixProject(project); + + } catch (CoreException e) { + throw new InvocationTargetException(e); + } catch (IOException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + + /** + * Adds default directories to the project. + * + * @param project The Java Project to update. + * @param parentFolder The path of the parent folder. Must end with a + * separator. + * @param folders Folders to be added. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to create the directories in + * the project. + */ + private void addDefaultDirectories(IProject project, String parentFolder, + String[] folders, IProgressMonitor monitor) throws CoreException { + for (String name : folders) { + if (name.length() > 0) { + IFolder folder = project.getFolder(parentFolder + name); + if (!folder.exists()) { + folder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + } + } + } + + /** + * Adds the manifest to the project. + * + * @param project The Java Project to update. + * @param parameters Template Parameters. + * @param stringDictionary String List to be added to a string definition + * file. This map will be filled by this method. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addManifest(IProject project, Map<String, Object> parameters, + Map<String, String> stringDictionary, IProgressMonitor monitor) + throws CoreException, IOException { + + // get IFile to the manifest and check if it's not already there. + IFile file = project.getFile(AndroidConstants.FN_ANDROID_MANIFEST); + if (!file.exists()) { + + // Read manifest template + String manifestTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_MANIFEST); + + // Replace all keyword parameters + manifestTemplate = replaceParameters(manifestTemplate, parameters); + + if (parameters.containsKey(PARAM_ACTIVITY)) { + // now get the activity template + String activityTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_ACTIVITIES); + + // Replace all keyword parameters to make main activity. + String activities = replaceParameters(activityTemplate, parameters); + + // set the intent. + String intent = AdtPlugin.readEmbeddedTextFile(TEMPLATE_INTENT_LAUNCHER); + + // set the intent to the main activity + activities = activities.replaceAll(PH_INTENT_FILTERS, intent); + + // set the activity(ies) in the manifest + manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, activities); + } else { + // remove the activity(ies) from the manifest + manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, ""); + } + + String minSdkVersion = (String) parameters.get(PARAM_MIN_SDK_VERSION); + if (minSdkVersion != null && minSdkVersion.length() > 0) { + String usesSdkTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_USES_SDK); + String usesSdk = replaceParameters(usesSdkTemplate, parameters); + manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, usesSdk); + } else { + manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, ""); + } + + // Save in the project as UTF-8 + InputStream stream = new ByteArrayInputStream( + manifestTemplate.getBytes("UTF-8")); //$NON-NLS-1$ + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + /** + * Adds the string resource file. + * + * @param project The Java Project to update. + * @param strings The list of strings to be added to the string file. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addStringDictionaryFile(IProject project, + Map<String, String> strings, IProgressMonitor monitor) + throws CoreException, IOException { + + // create the IFile object and check if the file doesn't already exist. + IFile file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP + + VALUES_DIRECTORY + AndroidConstants.WS_SEP + STRINGS_FILE); + if (!file.exists()) { + // get the Strings.xml template + String stringDefinitionTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRINGS); + + // get the template for one string + String stringTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRING); + + // get all the string names + Set<String> stringNames = strings.keySet(); + + // loop on it and create the string definitions + StringBuilder stringNodes = new StringBuilder(); + for (String key : stringNames) { + // get the value from the key + String value = strings.get(key); + + // place them in the template + String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key); + stringDef = stringDef.replace(PARAM_STRING_CONTENT, value); + + // append to the other string + if (stringNodes.length() > 0) { + stringNodes.append("\n"); + } + stringNodes.append(stringDef); + } + + // put the string nodes in the Strings.xml template + stringDefinitionTemplate = stringDefinitionTemplate.replace(PH_STRINGS, + stringNodes.toString()); + + // write the file as UTF-8 + InputStream stream = new ByteArrayInputStream( + stringDefinitionTemplate.getBytes("UTF-8")); //$NON-NLS-1$ + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + + + /** + * Adds default application icon to the project. + * + * @param project The Java Project to update. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + */ + private void addIcon(IProject project, IProgressMonitor monitor) + throws CoreException { + IFile file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP + + DRAWABLE_DIRECTORY + AndroidConstants.WS_SEP + ICON); + if (!file.exists()) { + // read the content from the template + byte[] buffer = AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON); + + // if valid + if (buffer != null) { + // Save in the project + InputStream stream = new ByteArrayInputStream(buffer); + file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + } + } + + /** + * Creates the package folder and copies the sample code in the project. + * + * @param project The Java Project to update. + * @param parameters Template Parameters. + * @param stringDictionary String List to be added to a string definition + * file. This map will be filled by this method. + * @param monitor An existing monitor. + * @throws CoreException if the method fails to update the project. + * @throws IOException if the method fails to create the files in the + * project. + */ + private void addSampleCode(IProject project, String sourceFolder, + Map<String, Object> parameters, Map<String, String> stringDictionary, + IProgressMonitor monitor) throws CoreException, IOException { + // create the java package directories. + IFolder pkgFolder = project.getFolder(sourceFolder); + String packageName = (String) parameters.get(PARAM_PACKAGE); + + // The PARAM_ACTIVITY key will be absent if no activity should be created, + // in which case activityName will be null. + String activityName = (String) parameters.get(PARAM_ACTIVITY); + Map<String, Object> java_activity_parameters = parameters; + if (activityName != null) { + if (activityName.indexOf('.') >= 0) { + // There are package names in the activity name. Transform packageName to add + // those sub packages and remove them from activityName. + packageName += "." + activityName; //$NON-NLS-1$ + int pos = packageName.lastIndexOf('.'); + activityName = packageName.substring(pos + 1); + packageName = packageName.substring(0, pos); + + // Also update the values used in the JAVA_FILE_TEMPLATE below + // (but not the ones from the manifest so don't change the caller's dictionary) + java_activity_parameters = new HashMap<String, Object>(parameters); + java_activity_parameters.put(PARAM_PACKAGE, packageName); + java_activity_parameters.put(PARAM_ACTIVITY, activityName); + } + } + + String[] components = packageName.split(AndroidConstants.RE_DOT); + for (String component : components) { + pkgFolder = pkgFolder.getFolder(component); + if (!pkgFolder.exists()) { + pkgFolder.create(true /* force */, true /* local */, + new SubProgressMonitor(monitor, 10)); + } + } + + if (activityName != null) { + // create the main activity Java file + String activityJava = activityName + AndroidConstants.DOT_JAVA; + IFile file = pkgFolder.getFile(activityJava); + if (!file.exists()) { + copyFile(JAVA_ACTIVITY_TEMPLATE, file, java_activity_parameters, monitor); + } + } + + // create the layout file + IFolder layoutfolder = project.getFolder(RES_DIRECTORY).getFolder(LAYOUT_DIRECTORY); + IFile file = layoutfolder.getFile(MAIN_LAYOUT_XML); + if (!file.exists()) { + copyFile(LAYOUT_TEMPLATE, file, parameters, monitor); + if (activityName != null) { + stringDictionary.put(STRING_HELLO_WORLD, "Hello World, " + activityName + "!"); + } else { + stringDictionary.put(STRING_HELLO_WORLD, "Hello World!"); + } + } + } + + /** + * Adds the given folder to the project's class path. + * + * @param javaProject The Java Project to update. + * @param sourceFolder Template Parameters. + * @param monitor An existing monitor. + * @throws JavaModelException if the classpath could not be set. + */ + private void setupSourceFolder(IJavaProject javaProject, String sourceFolder, + IProgressMonitor monitor) throws JavaModelException { + IProject project = javaProject.getProject(); + + // Add "src" to class path + IFolder srcFolder = project.getFolder(sourceFolder); + + IClasspathEntry[] entries = javaProject.getRawClasspath(); + entries = removeSourceClasspath(entries, srcFolder); + entries = removeSourceClasspath(entries, srcFolder.getParent()); + + entries = ProjectHelper.addEntryToClasspath(entries, + JavaCore.newSourceEntry(srcFolder.getFullPath())); + + javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10)); + } + + + /** + * Removes the corresponding source folder from the class path entries if + * found. + * + * @param entries The class path entries to read. A copy will be returned. + * @param folder The parent source folder to remove. + * @return A new class path entries array. + */ + private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries, IContainer folder) { + if (folder == null) { + return entries; + } + IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath()); + int n = entries.length; + for (int i = n - 1; i >= 0; i--) { + if (entries[i].equals(source)) { + IClasspathEntry[] newEntries = new IClasspathEntry[n - 1]; + if (i > 0) System.arraycopy(entries, 0, newEntries, 0, i); + if (i < n - 1) System.arraycopy(entries, i + 1, newEntries, i, n - i - 1); + n--; + entries = newEntries; + } + } + return entries; + } + + + /** + * Copies the given file from our resource folder to the new project. + * Expects the file to the US-ASCII or UTF-8 encoded. + * + * @throws CoreException from IFile if failing to create the new file. + * @throws MalformedURLException from URL if failing to interpret the URL. + * @throws FileNotFoundException from RandomAccessFile. + * @throws IOException from RandomAccessFile.length() if can't determine the + * length. + */ + private void copyFile(String resourceFilename, IFile destFile, + Map<String, Object> parameters, IProgressMonitor monitor) + throws CoreException, IOException { + + // Read existing file. + String template = AdtPlugin.readEmbeddedTextFile( + TEMPLATES_DIRECTORY + resourceFilename); + + // Replace all keyword parameters + template = replaceParameters(template, parameters); + + // Save in the project as UTF-8 + InputStream stream = new ByteArrayInputStream(template.getBytes("UTF-8")); //$NON-NLS-1$ + destFile.create(stream, false /* force */, new SubProgressMonitor(monitor, 10)); + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } + + /** + * Replaces placeholders found in a string with values. + * + * @param str the string to search for placeholders. + * @param parameters a map of <placeholder, Value> to search for in the string + * @return A new String object with the placeholder replaced by the values. + */ + private String replaceParameters(String str, Map<String, Object> parameters) { + for (Entry<String, Object> entry : parameters.entrySet()) { + if (entry.getValue() instanceof String) { + str = str.replaceAll(entry.getKey(), (String) entry.getValue()); + } + } + + return str; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java new file mode 100644 index 0000000..e201132 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common; + +import com.android.sdklib.SdkConstants; + +import java.io.File; +import java.util.regex.Pattern; + +/** + * Constant definition class.<br> + * <br> + * Most constants have a prefix defining the content. + * <ul> + * <li><code>WS_</code> Workspace path constant. Those are absolute paths, + * from the project root.</li> + * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li> + * <li><code>FN_</code> File name constant.</li> + * <li><code>FD_</code> Folder name constant.</li> + * <li><code>MARKER_</code> Resource Marker Ids constant.</li> + * <li><code>EXT_</code> File extension constant. This does NOT include a dot.</li> + * <li><code>DOT_</code> File extension constant. This start with a dot.</li> + * <li><code>RE_</code> Regexp constant.</li> + * <li><code>NS_</code> Namespace constant.</li> + * <li><code>CLASS_</code> Fully qualified class name.</li> + * </ul> + * + */ +public class AndroidConstants { + /** + * The old Editors Plugin ID. It is still used in some places for compatibility. + * Please do not use for new features. + */ + public static final String EDITORS_NAMESPACE = "com.android.ide.eclipse.editors"; // $NON-NLS-1$ + + /** Nature of android projects */ + public final static String NATURE = "com.android.ide.eclipse.adt.AndroidNature"; //$NON-NLS-1$ + + /** Separator for workspace path, i.e. "/". */ + public final static String WS_SEP = "/"; //$NON-NLS-1$ + /** Separator character for workspace path, i.e. '/'. */ + public final static char WS_SEP_CHAR = '/'; + + /** Extension of the Application package Files, i.e. "apk". */ + public final static String EXT_ANDROID_PACKAGE = "apk"; //$NON-NLS-1$ + /** Extension of java files, i.e. "java" */ + public final static String EXT_JAVA = "java"; //$NON-NLS-1$ + /** Extension of compiled java files, i.e. "class" */ + public final static String EXT_CLASS = "class"; //$NON-NLS-1$ + /** Extension of xml files, i.e. "xml" */ + public final static String EXT_XML = "xml"; //$NON-NLS-1$ + /** Extension of jar files, i.e. "jar" */ + public final static String EXT_JAR = "jar"; //$NON-NLS-1$ + /** Extension of aidl files, i.e. "aidl" */ + public final static String EXT_AIDL = "aidl"; //$NON-NLS-1$ + /** Extension of native libraries, i.e. "so" */ + public final static String EXT_NATIVE_LIB = "so"; //$NON-NLS-1$ + + private final static String DOT = "."; //$NON-NLS-1$ + + /** Dot-Extension of the Application package Files, i.e. ".apk". */ + public final static String DOT_ANDROID_PACKAGE = DOT + EXT_ANDROID_PACKAGE; + /** Dot-Extension of java files, i.e. ".java" */ + public final static String DOT_JAVA = DOT + EXT_JAVA; + /** Dot-Extension of compiled java files, i.e. ".class" */ + public final static String DOT_CLASS = DOT + EXT_CLASS; + /** Dot-Extension of xml files, i.e. ".xml" */ + public final static String DOT_XML = DOT + EXT_XML; + /** Dot-Extension of jar files, i.e. ".jar" */ + public final static String DOT_JAR = DOT + EXT_JAR; + /** Dot-Extension of aidl files, i.e. ".aidl" */ + public final static String DOT_AIDL = DOT + EXT_AIDL; + + /** Name of the manifest file, i.e. "AndroidManifest.xml". */ + public static final String FN_ANDROID_MANIFEST = "AndroidManifest.xml"; //$NON-NLS-1$ + public static final String FN_PROJECT_AIDL = "project.aidl"; //$NON-NLS-1$ + + /** Name of the android sources directory */ + public static final String FD_ANDROID_SOURCES = "sources"; //$NON-NLS-1$ + + /** Resource java class filename, i.e. "R.java" */ + public final static String FN_RESOURCE_CLASS = "R.java"; //$NON-NLS-1$ + /** Resource class file filename, i.e. "R.class" */ + public final static String FN_COMPILED_RESOURCE_CLASS = "R.class"; //$NON-NLS-1$ + /** Manifest java class filename, i.e. "Manifest.java" */ + public final static String FN_MANIFEST_CLASS = "Manifest.java"; //$NON-NLS-1$ + /** Dex conversion output filname, i.e. "classes.dex" */ + public final static String FN_CLASSES_DEX = "classes.dex"; //$NON-NLS-1$ + /** Temporary packaged resources file name, i.e. "resources.ap_" */ + public final static String FN_RESOURCES_AP_ = "resources.ap_"; //$NON-NLS-1$ + /** Temporary packaged resources file name for a specific set of configuration */ + public final static String FN_RESOURCES_S_AP_ = "resources-%s.ap_"; //$NON-NLS-1$ + public final static Pattern PATTERN_RESOURCES_S_AP_ = + Pattern.compile("resources-.*\\.ap_", Pattern.CASE_INSENSITIVE); + + public final static String FN_ADB = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? + "adb.exe" : "adb"; //$NON-NLS-1$ //$NON-NLS-2$ + + public final static String FN_EMULATOR = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? + "emulator.exe" : "emulator"; //$NON-NLS-1$ //$NON-NLS-2$ + + public final static String FN_TRACEVIEW = + (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ? + "traceview.exe" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$ + + /** Absolute path of the workspace root, i.e. "/" */ + public final static String WS_ROOT = WS_SEP; + + /** Absolute path of the resource folder, eg "/res".<br> This is a workspace path. */ + public final static String WS_RESOURCES = WS_SEP + SdkConstants.FD_RESOURCES; + + /** Absolute path of the resource folder, eg "/assets".<br> This is a workspace path. */ + public final static String WS_ASSETS = WS_SEP + SdkConstants.FD_ASSETS; + + /** Leaf of the javaDoc folder. Does not start with a separator. */ + public final static String WS_JAVADOC_FOLDER_LEAF = SdkConstants.FD_DOCS + "/reference"; //$NON-NLS-1$ + + /** Path of the samples directory relative to the sdk folder. + * This is an OS path, ending with a separator. + * FIXME: remove once the NPW is fixed. */ + public final static String OS_SDK_SAMPLES_FOLDER = SdkConstants.FD_SAMPLES + File.separator; + + public final static String RE_DOT = "\\."; //$NON-NLS-1$ + /** Regexp for java extension, i.e. "\.java$" */ + public final static String RE_JAVA_EXT = "\\.java$"; //$NON-NLS-1$ + /** Regexp for aidl extension, i.e. "\.aidl$" */ + public final static String RE_AIDL_EXT = "\\.aidl$"; //$NON-NLS-1$ + + /** Namespace pattern for the custom resource XML, i.e. "http://schemas.android.com/apk/res/%s" */ + public final static String NS_CUSTOM_RESOURCES = "http://schemas.android.com/apk/res/%1$s"; //$NON-NLS-1$ + + /** The old common plug-in ID. Please do not use for new features. */ + public static final String COMMON_PLUGIN_ID = "com.android.ide.eclipse.common"; //$NON-NLS-1$ + + /** aapt marker error when running the compile command */ + public final static String MARKER_AAPT_COMPILE = COMMON_PLUGIN_ID + ".aaptProblem"; //$NON-NLS-1$ + + /** aapt marker error when running the package command */ + public final static String MARKER_AAPT_PACKAGE = COMMON_PLUGIN_ID + ".aapt2Problem"; //$NON-NLS-1$ + + /** XML marker error. */ + public final static String MARKER_XML = COMMON_PLUGIN_ID + ".xmlProblem"; //$NON-NLS-1$ + + /** aidl marker error. */ + public final static String MARKER_AIDL = COMMON_PLUGIN_ID + ".aidlProblem"; //$NON-NLS-1$ + + /** android marker error */ + public final static String MARKER_ANDROID = COMMON_PLUGIN_ID + ".androidProblem"; //$NON-NLS-1$ + + /** Name for the "type" marker attribute */ + public final static String MARKER_ATTR_TYPE = "android.type"; //$NON-NLS-1$ + /** Name for the "class" marker attribute */ + public final static String MARKER_ATTR_CLASS = "android.class"; //$NON-NLS-1$ + /** activity value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_ACTIVITY = "activity"; //$NON-NLS-1$ + /** service value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_SERVICE = "service"; //$NON-NLS-1$ + /** receiver value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_RECEIVER = "receiver"; //$NON-NLS-1$ + /** provider value for marker attribute "type" */ + public final static String MARKER_ATTR_TYPE_PROVIDER = "provider"; //$NON-NLS-1$ + + public final static String CLASS_ACTIVITY = "android.app.Activity"; //$NON-NLS-1$ + public final static String CLASS_SERVICE = "android.app.Service"; //$NON-NLS-1$ + public final static String CLASS_BROADCASTRECEIVER = "android.content.BroadcastReceiver"; //$NON-NLS-1$ + public final static String CLASS_CONTENTPROVIDER = "android.content.ContentProvider"; //$NON-NLS-1$ + public final static String CLASS_INSTRUMENTATION = "android.app.Instrumentation"; //$NON-NLS-1$ + public final static String CLASS_BUNDLE = "android.os.Bundle"; //$NON-NLS-1$ + public final static String CLASS_R = "android.R"; //$NON-NLS-1$ + public final static String CLASS_MANIFEST_PERMISSION = "android.Manifest$permission"; //$NON-NLS-1$ + public final static String CLASS_INTENT = "android.content.Intent"; //$NON-NLS-1$ + public final static String CLASS_CONTEXT = "android.content.Context"; //$NON-NLS-1$ + public final static String CLASS_VIEW = "android.view.View"; //$NON-NLS-1$ + public final static String CLASS_VIEWGROUP = "android.view.ViewGroup"; //$NON-NLS-1$ + public final static String CLASS_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$ + public final static String CLASS_VIEWGROUP_LAYOUTPARAMS = + CLASS_VIEWGROUP + "$" + CLASS_LAYOUTPARAMS; //$NON-NLS-1$ + public final static String CLASS_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$ + public final static String CLASS_PREFERENCE = "android.preference.Preference"; //$NON-NLS-1$ + public final static String CLASS_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$ + public final static String CLASS_PREFERENCES = + "android.preference." + CLASS_PREFERENCE_SCREEN; //$NON-NLS-1$ + public final static String CLASS_PREFERENCEGROUP = "android.preference.PreferenceGroup"; //$NON-NLS-1$ + public final static String CLASS_PARCELABLE = "android.os.Parcelable"; //$NON-NLS-1$ + + public final static String CLASS_BRIDGE = "com.android.layoutlib.bridge.Bridge"; //$NON-NLS-1$ + + /** + * Prefered compiler level, i.e. "1.5". + */ + public final static String COMPILER_COMPLIANCE_PREFERRED = "1.5"; //$NON-NLS-1$ + /** + * List of valid compiler level, i.e. "1.5" and "1.6" + */ + public final static String[] COMPILER_COMPLIANCE = { + "1.5", //$NON-NLS-1$ + "1.6", //$NON-NLS-1$ + }; + + /** The base URL where to find the Android class & manifest documentation */ + public static final String CODESITE_BASE_URL = "http://code.google.com/android"; //$NON-NLS-1$ + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java new file mode 100644 index 0000000..6dc8562 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common; + +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; + +/** + * Helpers for Eclipse UI related stuff. + */ +public final class EclipseUiHelper { + + /** View Id for the default Eclipse Content Outline view. */ + public static final String CONTENT_OUTLINE_VIEW_ID = "org.eclipse.ui.views.ContentOutline"; + /** View Id for the default Eclipse Property Sheet view. */ + public static final String PROPERTY_SHEET_VIEW_ID = "org.eclipse.ui.views.PropertySheet"; + + /** This class never gets instantiated. */ + private EclipseUiHelper() { + } + + /** + * Shows the corresponding view. + * <p/> + * Silently fails in case of error. + * + * @param viewId One of {@link #CONTENT_OUTLINE_VIEW_ID}, {@link #PROPERTY_SHEET_VIEW_ID}. + * @param activate True to force activate (i.e. takes focus), false to just make visible (i.e. + * does not steal focus.) + */ + public static void showView(String viewId, boolean activate) { + IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (win != null) { + IWorkbenchPage page = win.getActivePage(); + if (page != null) { + try { + IViewPart part = page.showView(viewId, + null /* secondaryId */, + activate ? IWorkbenchPage.VIEW_ACTIVATE : IWorkbenchPage.VIEW_VISIBLE); + } catch (PartInitException e) { + // ignore + } + } + } + + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java new file mode 100644 index 0000000..3f1bde4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java @@ -0,0 +1,21 @@ + +package com.android.ide.eclipse.common; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "com.android.ide.eclipse.common.messages"; //$NON-NLS-1$ + + public static String Console_Data_Project_Tag; + + public static String Console_Date_Tag; + + + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java new file mode 100644 index 0000000..345c663 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common; + +import com.android.sdkstats.SdkStatsService; + +import org.osgi.framework.Version; + +/** + * Helper class to access the ping usage stat server. + */ +public class SdkStatsHelper { + + /** + * Pings the usage start server. + * @param pluginName the name of the plugin to appear in the stats + * @param pluginVersion the {@link Version} of the plugin. + */ + public static void pingUsageServer(String pluginName, Version pluginVersion) { + String versionString = String.format("%1$d.%2$d.%3$d", pluginVersion.getMajor(), + pluginVersion.getMinor(), pluginVersion.getMicro()); + + SdkStatsService.ping(pluginName, versionString); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java new file mode 100644 index 0000000..6ccf4f2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common; + +import org.eclipse.ui.console.MessageConsoleStream; + +import java.util.Calendar; + +/** + * Stream helper class. + */ +public class StreamHelper { + + /** + * Prints messages, associated with a project to the specified stream + * @param stream The stream to write to + * @param tag The tag associated to the message. Can be null + * @param objects The objects to print through their toString() method (or directly for + * {@link String} objects. + */ + public static synchronized void printToStream(MessageConsoleStream stream, String tag, + Object... objects) { + String dateTag = getMessageTag(tag); + + for (Object obj : objects) { + stream.print(dateTag); + if (obj instanceof String) { + stream.println((String)obj); + } else { + stream.println(obj.toString()); + } + } + } + + /** + * Creates a string containing the current date/time, and the tag + * @param tag The tag associated to the message. Can be null + * @return The dateTag + */ + public static String getMessageTag(String tag) { + Calendar c = Calendar.getInstance(); + + if (tag == null) { + return String.format(Messages.Console_Date_Tag, c); + } + + return String.format(Messages.Console_Data_Project_Tag, c, tag); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties new file mode 100644 index 0000000..dba6edc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties @@ -0,0 +1,2 @@ +Console_Date_Tag=[%1$tF %1$tT] +Console_Data_Project_Tag=[%1$tF %1$tT - %2$s] diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java new file mode 100644 index 0000000..58c2f40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.preferences; + +import com.android.sdkstats.SdkStatsService; + +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Link; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +import java.io.IOException; + +public class UsagePreferencePage extends PreferencePage implements IWorkbenchPreferencePage { + + private BooleanFieldEditor mOptInCheckBox; + + public UsagePreferencePage() { + } + + public void init(IWorkbench workbench) { + // pass + } + + @Override + protected Control createContents(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + top.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Link text = new Link(top, SWT.WRAP); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.widthHint = 200; + text.setLayoutData(gd); + text.setText(SdkStatsService.BODY_TEXT); + + text.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + SdkStatsService.openUrl(event.text); + } + }); + + mOptInCheckBox = new BooleanFieldEditor(SdkStatsService.PING_OPT_IN, + SdkStatsService.CHECKBOX_TEXT, top); + mOptInCheckBox.setPage(this); + mOptInCheckBox.setPreferenceStore(SdkStatsService.getPreferenceStore()); + mOptInCheckBox.load(); + + return top; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.preference.PreferencePage#performCancel() + */ + @Override + public boolean performCancel() { + mOptInCheckBox.load(); + return super.performCancel(); + } + + /* (non-Javadoc) + * @see org.eclipse.jface.preference.PreferencePage#performDefaults() + */ + @Override + protected void performDefaults() { + mOptInCheckBox.loadDefault(); + super.performDefaults(); + } + + /* (non-Javadoc) + * @see org.eclipse.jface.preference.PreferencePage#performOk() + */ + @Override + public boolean performOk() { + save(); + return super.performOk(); + } + + /* (non-Javadoc) + * @see org.eclipse.jface.preference.PreferencePage#performApply() + */ + @Override + protected void performApply() { + save(); + super.performApply(); + } + + private void save() { + try { + PreferenceStore store = SdkStatsService.getPreferenceStore(); + if (store != null) { + store.setValue(SdkStatsService.PING_OPT_IN, mOptInCheckBox.getBooleanValue()); + store.save(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java new file mode 100644 index 0000000..cd238d2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.xml.sax.InputSource; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; + +/** + * Utility class that manages the AndroidManifest.xml file. + * <p/> + * All the get method work by XPath. Repeated calls to those may warrant using + * {@link AndroidManifestParser} instead. + */ +public class AndroidManifestHelper { + private IFile mManifestIFile; + private File mManifestFile; + private XPath mXPath; + + /** + * Creates an AndroidManifest based on an existing Eclipse {@link IProject} object. + * </p> + * Use {@link #exists()} to check if the manifest file really exists in the project. + * + * @param project The project to search for the manifest. + */ + public AndroidManifestHelper(IProject project) { + mXPath = AndroidXPathFactory.newXPath(); + mManifestIFile = getManifest(project); + } + + /** + * Creates an AndroidManifest based on a file path. + * <p/> + * Use {@link #exists()} to check if the manifest file really exists. + * + * @param osManifestFilePath the os path to the AndroidManifest.xml file. + */ + public AndroidManifestHelper(String osManifestFilePath) { + mXPath = AndroidXPathFactory.newXPath(); + mManifestFile = new File(osManifestFilePath); + } + + + /** + * Returns the underlying {@link IFile} for the android manifest XML file, if found in the + * given Eclipse project. + * + * Always return null if the constructor that takes an {@link IProject} was NOT called. + * + * @return The IFile for the androidManifest.xml or null if no such file could be found. + */ + public IFile getManifestIFile() { + return mManifestIFile; + } + + /** + * Returns the underlying {@link File} for the android manifest XML file. + */ + public File getManifestFile() { + if (mManifestIFile != null) { + return mManifestIFile.getLocation().toFile(); + } + + return mManifestFile; + } + + /** + * Returns the package name defined in the manifest file. + * + * @return A String object with the package or null if any error happened. + */ + public String getPackageName() { + try { + return mXPath.evaluate("/manifest/@package", getSource()); //$NON-NLS-1$ + } catch (XPathExpressionException e1) { + // If the XPath failed to evaluate, we'll return null. + } catch (Exception e) { + // if this happens this is due to the resource being out of sync. + // so we must refresh it and do it again + + // for any other kind of exception we must return null as well; + } + + return null; + } + + /** + * Returns the minSdkVersion defined in the manifest file. + * + * @return A String object with the package or null if any error happened. + */ + public String getMinSdkVersion() { + try { + return mXPath.evaluate("/manifest/uses-sdk/@" //$NON-NLS-1$ + + AndroidXPathFactory.DEFAULT_NS_PREFIX + + ":minSdkVersion", getSource()); //$NON-NLS-1$ + } catch (XPathExpressionException e1) { + // If the XPath failed to evaluate, we'll return null. + } catch (Exception e) { + // if this happens this is due to the resource being out of sync. + // so we must refresh it and do it again + + // for any other kind of exception we must return null as well; + } + + return null; + } + /** + * Returns the i-th activity defined in the manifest file. + * + * @param index The 1-based index of the activity to return. + * @return A String object with the activity or null if any error happened. + */ + public String getActivityName(int index) { + try { + return mXPath.evaluate("/manifest/application/activity[" //$NON-NLS-1$ + + index + + "]/@" //$NON-NLS-1$ + + AndroidXPathFactory.DEFAULT_NS_PREFIX +":name", //$NON-NLS-1$ + getSource()); + } catch (XPathExpressionException e1) { + // If the XPath failed to evaluate, we'll return null. + } catch (Exception e) { + // if this happens this is due to the resource being out of sync. + // so we must refresh it and do it again + + // for any other kind of exception we must return null as well; + } + return null; + } + + /** + * Returns an IFile object representing the manifest for the specified + * project. + * + * @param project The project containing the manifest file. + * @return An IFile object pointing to the manifest or null if the manifest + * is missing. + */ + public static IFile getManifest(IProject project) { + IResource r = project.findMember(AndroidConstants.WS_SEP + + AndroidConstants.FN_ANDROID_MANIFEST); + + if (r == null || r.exists() == false || (r instanceof IFile) == false) { + return null; + } + return (IFile) r; + } + + /** + * Combines a java package, with a class value from the manifest to make a fully qualified + * class name + * @param javaPackage the java package from the manifest. + * @param className the class name from the manifest. + * @return the fully qualified class name. + */ + public static String combinePackageAndClassName(String javaPackage, String className) { + if (className == null || className.length() == 0) { + return javaPackage; + } + if (javaPackage == null || javaPackage.length() == 0) { + return className; + } + + // the class name can be a subpackage (starts with a '.' + // char), a simple class name (no dot), or a full java package + boolean startWithDot = (className.charAt(0) == '.'); + boolean hasDot = (className.indexOf('.') != -1); + if (startWithDot || hasDot == false) { + + // add the concatenation of the package and class name + if (startWithDot) { + return javaPackage + className; + } else { + return javaPackage + '.' + className; + } + } else { + // just add the class as it should be a fully qualified java name. + return className; + } + } + + + + /** + * Returns true either if an androidManifest.xml file was found in the project + * or if the given file path exists. + */ + public boolean exists() { + if (mManifestIFile != null) { + return mManifestIFile.exists(); + } else if (mManifestFile != null) { + return mManifestFile.exists(); + } + + return false; + } + + /** + * Returns an InputSource for XPath. + * + * @throws FileNotFoundException if file does not exist. + * @throws CoreException if the {@link IFile} does not exist. + */ + private InputSource getSource() throws FileNotFoundException, CoreException { + if (mManifestIFile != null) { + return new InputSource(mManifestIFile.getContents()); + } else if (mManifestFile != null) { + return new InputSource(new FileReader(mManifestFile)); + } + + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java new file mode 100644 index 0000000..850c59d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java @@ -0,0 +1,661 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaProject; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Set; +import java.util.TreeSet; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +public class AndroidManifestParser { + + private final static String ATTRIBUTE_PACKAGE = "package"; //$NON-NLS-1$ + private final static String ATTRIBUTE_NAME = "name"; //$NON-NLS-1$ + private final static String ATTRIBUTE_PROCESS = "process"; //$NON-NLS-$ + private final static String ATTRIBUTE_DEBUGGABLE = "debuggable"; //$NON-NLS-$ + private final static String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion"; //$NON-NLS-$ + private final static String NODE_MANIFEST = "manifest"; //$NON-NLS-1$ + private final static String NODE_APPLICATION = "application"; //$NON-NLS-1$ + private final static String NODE_ACTIVITY = "activity"; //$NON-NLS-1$ + private final static String NODE_SERVICE = "service"; //$NON-NLS-1$ + private final static String NODE_RECEIVER = "receiver"; //$NON-NLS-1$ + private final static String NODE_PROVIDER = "provider"; //$NON-NLS-1$ + private final static String NODE_INTENT = "intent-filter"; //$NON-NLS-1$ + private final static String NODE_ACTION = "action"; //$NON-NLS-1$ + private final static String NODE_CATEGORY = "category"; //$NON-NLS-1$ + private final static String NODE_USES_SDK = "uses-sdk"; //$NON-NLS-1$ + + private final static int LEVEL_MANIFEST = 0; + private final static int LEVEL_APPLICATION = 1; + private final static int LEVEL_ACTIVITY = 2; + private final static int LEVEL_INTENT_FILTER = 3; + private final static int LEVEL_CATEGORY = 4; + + private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$ + private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$ + + private static class ManifestHandler extends XmlErrorHandler { + + //--- data read from the parsing + + /** Application package */ + private String mPackage; + /** List of all activities */ + private final ArrayList<String> mActivities = new ArrayList<String>(); + /** Launcher activity */ + private String mLauncherActivity = null; + /** list of process names declared by the manifest */ + private Set<String> mProcesses = null; + /** debuggable attribute value. If null, the attribute is not present. */ + private Boolean mDebuggable = null; + /** API level requirement. if 0 the attribute was not present. */ + private int mApiLevelRequirement = 0; + + //--- temporary data/flags used during parsing + private IJavaProject mJavaProject; + private boolean mGatherData = false; + private boolean mMarkErrors = false; + private int mCurrentLevel = 0; + private int mValidLevel = 0; + private boolean mFoundMainAction = false; + private boolean mFoundLauncherCategory = false; + private String mCurrentActivity = null; + private Locator mLocator; + + /** + * + * @param manifestFile + * @param errorListener + * @param gatherData + * @param javaProject + * @param markErrors + */ + ManifestHandler(IFile manifestFile, XmlErrorListener errorListener, + boolean gatherData, IJavaProject javaProject, boolean markErrors) { + super(manifestFile, errorListener); + mGatherData = gatherData; + mJavaProject = javaProject; + mMarkErrors = markErrors; + } + + /** + * Returns the package defined in the manifest, if found. + * @return The package name or null if not found. + */ + String getPackage() { + return mPackage; + } + + /** + * Returns the list of activities found in the manifest. + * @return An array of fully qualified class names, or empty if no activity were found. + */ + String[] getActivities() { + return mActivities.toArray(new String[mActivities.size()]); + } + + /** + * Returns the name of one activity found in the manifest, that is configured to show + * up in the HOME screen. + * @return the fully qualified name of a HOME activity or null if none were found. + */ + String getLauncherActivity() { + return mLauncherActivity; + } + + /** + * Returns the list of process names declared by the manifest. + */ + String[] getProcesses() { + if (mProcesses != null) { + return mProcesses.toArray(new String[mProcesses.size()]); + } + + return new String[0]; + } + + /** + * Returns the <code>debuggable</code> attribute value or null if it is not set. + */ + Boolean getDebuggable() { + return mDebuggable; + } + + /** + * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set. + */ + int getApiLevelRequirement() { + return mApiLevelRequirement; + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator) + */ + @Override + public void setDocumentLocator(Locator locator) { + mLocator = locator; + super.setDocumentLocator(locator); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, + * java.lang.String, org.xml.sax.Attributes) + */ + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + try { + if (mGatherData == false) { + return; + } + + // if we're at a valid level + if (mValidLevel == mCurrentLevel) { + String value; + switch (mValidLevel) { + case LEVEL_MANIFEST: + if (NODE_MANIFEST.equals(localName)) { + // lets get the package name. + mPackage = getAttributeValue(attributes, ATTRIBUTE_PACKAGE, + false /* hasNamespace */); + mValidLevel++; + } + break; + case LEVEL_APPLICATION: + if (NODE_APPLICATION.equals(localName)) { + value = getAttributeValue(attributes, ATTRIBUTE_PROCESS, + true /* hasNamespace */); + if (value != null) { + addProcessName(value); + } + + value = getAttributeValue(attributes, ATTRIBUTE_DEBUGGABLE, + true /* hasNamespace*/); + if (value != null) { + mDebuggable = Boolean.parseBoolean(value); + } + + mValidLevel++; + } else if (NODE_USES_SDK.equals(localName)) { + value = getAttributeValue(attributes, ATTRIBUTE_MIN_SDK_VERSION, + true /* hasNamespace */); + + try { + mApiLevelRequirement = Integer.parseInt(value); + } catch (NumberFormatException e) { + handleError(e, -1 /* lineNumber */); + } + } + break; + case LEVEL_ACTIVITY: + if (NODE_ACTIVITY.equals(localName)) { + processActivityNode(attributes); + mValidLevel++; + } else if (NODE_SERVICE.equals(localName)) { + processNode(attributes, AndroidConstants.CLASS_SERVICE); + mValidLevel++; + } else if (NODE_RECEIVER.equals(localName)) { + processNode(attributes, AndroidConstants.CLASS_BROADCASTRECEIVER); + mValidLevel++; + } else if (NODE_PROVIDER.equals(localName)) { + processNode(attributes, AndroidConstants.CLASS_CONTENTPROVIDER); + mValidLevel++; + } + break; + case LEVEL_INTENT_FILTER: + // only process this level if we are in an activity + if (mCurrentActivity != null && NODE_INTENT.equals(localName)) { + // if we're at the intent level, lets reset some flag to + // be used when parsing the children + mFoundMainAction = false; + mFoundLauncherCategory = false; + mValidLevel++; + } + break; + case LEVEL_CATEGORY: + if (mCurrentActivity != null && mLauncherActivity == null) { + if (NODE_ACTION.equals(localName)) { + // get the name attribute + if (ACTION_MAIN.equals( + getAttributeValue(attributes, ATTRIBUTE_NAME, + true /* hasNamespace */))) { + mFoundMainAction = true; + } + } else if (NODE_CATEGORY.equals(localName)) { + if (CATEGORY_LAUNCHER.equals( + getAttributeValue(attributes, ATTRIBUTE_NAME, + true /* hasNamespace */))) { + mFoundLauncherCategory = true; + } + } + + // no need to increase mValidLevel as we don't process anything + // below this level. + } + break; + } + } + + mCurrentLevel++; + } finally { + super.startElement(uri, localName, name, attributes); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, + * java.lang.String) + */ + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + try { + if (mGatherData == false) { + return; + } + + // decrement the levels. + if (mValidLevel == mCurrentLevel) { + mValidLevel--; + } + mCurrentLevel--; + + // if we're at a valid level + // process the end of the element + if (mValidLevel == mCurrentLevel) { + switch (mValidLevel) { + case LEVEL_ACTIVITY: + mCurrentActivity = null; + break; + case LEVEL_INTENT_FILTER: + // if we found both a main action and a launcher category, this is our + // launcher activity! + if (mCurrentActivity != null && + mFoundMainAction && mFoundLauncherCategory) { + mLauncherActivity = mCurrentActivity; + } + break; + default: + break; + } + + } + } finally { + super.endElement(uri, localName, name); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException) + */ + @Override + public void error(SAXParseException e) { + if (mMarkErrors) { + handleError(e, e.getLineNumber()); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException) + */ + @Override + public void fatalError(SAXParseException e) { + if (mMarkErrors) { + handleError(e, e.getLineNumber()); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException) + */ + @Override + public void warning(SAXParseException e) throws SAXException { + if (mMarkErrors) { + super.warning(e); + } + } + + /** + * Processes the activity node. + * @param attributes the attributes for the activity node. + */ + private void processActivityNode(Attributes attributes) { + // lets get the activity name, and add it to the list + String activityName = getAttributeValue(attributes, ATTRIBUTE_NAME, + true /* hasNamespace */); + if (activityName != null) { + mCurrentActivity = AndroidManifestHelper.combinePackageAndClassName(mPackage, + activityName); + mActivities.add(mCurrentActivity); + + if (mMarkErrors) { + checkClass(mCurrentActivity, AndroidConstants.CLASS_ACTIVITY, + true /* testVisibility */); + } + } else { + // no activity found! Aapt will output an error, + // so we don't have to do anything + mCurrentActivity = activityName; + } + + String processName = getAttributeValue(attributes, ATTRIBUTE_PROCESS, + true /* hasNamespace */); + if (processName != null) { + addProcessName(processName); + } + } + + /** + * Processes the service/receiver/provider nodes. + * @param attributes the attributes for the activity node. + * @param superClassName the fully qualified name of the super class that this + * node is representing + */ + private void processNode(Attributes attributes, String superClassName) { + // lets get the class name, and check it if required. + String serviceName = getAttributeValue(attributes, ATTRIBUTE_NAME, + true /* hasNamespace */); + if (serviceName != null) { + serviceName = AndroidManifestHelper.combinePackageAndClassName(mPackage, + serviceName); + + if (mMarkErrors) { + checkClass(serviceName, superClassName, false /* testVisibility */); + } + } + + String processName = getAttributeValue(attributes, ATTRIBUTE_PROCESS, + true /* hasNamespace */); + if (processName != null) { + addProcessName(processName); + } + } + + /** + * Checks that a class is valid and can be used in the Android Manifest. + * <p/> + * Errors are put as {@link IMarker} on the manifest file. + * @param className the fully qualified name of the class to test. + * @param superClassName the fully qualified name of the class it is supposed to extend. + * @param testVisibility if <code>true</code>, the method will check the visibility of + * the class or of its constructors. + */ + private void checkClass(String className, String superClassName, boolean testVisibility) { + // we need to check the validity of the activity. + String result = BaseProjectHelper.testClassForManifest(mJavaProject, + className, superClassName, testVisibility); + if (result != BaseProjectHelper.TEST_CLASS_OK) { + // get the line number + int line = mLocator.getLineNumber(); + + // mark the file + IMarker marker = BaseProjectHelper.addMarker(getFile(), + AndroidConstants.MARKER_ANDROID, + result, line, IMarker.SEVERITY_ERROR); + + // add custom attributes to be used by the manifest editor. + if (marker != null) { + try { + marker.setAttribute(AndroidConstants.MARKER_ATTR_TYPE, + AndroidConstants.MARKER_ATTR_TYPE_ACTIVITY); + marker.setAttribute(AndroidConstants.MARKER_ATTR_CLASS, className); + } catch (CoreException e) { + } + } + } + + } + + /** + * Searches through the attributes list for a particular one and returns its value. + * @param attributes the attribute list to search through + * @param attributeName the name of the attribute to look for. + * @param hasNamespace Indicates whether the attribute has an android namespace. + * @return a String with the value or null if the attribute was not found. + * @see SdkConstants#NS_RESOURCES + */ + private String getAttributeValue(Attributes attributes, String attributeName, + boolean hasNamespace) { + int count = attributes.getLength(); + for (int i = 0 ; i < count ; i++) { + if (attributeName.equals(attributes.getLocalName(i)) && + ((hasNamespace && + SdkConstants.NS_RESOURCES.equals(attributes.getURI(i))) || + (hasNamespace == false && attributes.getURI(i).length() == 0))) { + return attributes.getValue(i); + } + } + + return null; + } + + private void addProcessName(String processName) { + if (mProcesses == null) { + mProcesses = new TreeSet<String>(); + } + + mProcesses.add(processName); + } + } + + private static SAXParserFactory sParserFactory; + + private final String mJavaPackage; + private final String[] mActivities; + private final String mLauncherActivity; + private final String[] mProcesses; + private final Boolean mDebuggable; + private final int mApiLevelRequirement; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + /** + * Parses the Android Manifest, and returns an object containing + * the result of the parsing. + * @param javaProject The java project. + * @param manifestFile the {@link IFile} representing the manifest file. + * @param errorListener + * @param gatherData indicates whether the parsing will extract data from the manifest. + * @param markErrors indicates whether the error found during parsing should put a + * marker on the file. For class validation errors to put a marker, <code>gatherData</code> + * must be set to <code>true</code> + * @return an {@link AndroidManifestParser} or null if the parsing failed. + * @throws CoreException + */ + public static AndroidManifestParser parse(IJavaProject javaProject, IFile manifestFile, + XmlErrorListener errorListener, boolean gatherData, boolean markErrors) + throws CoreException { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + ManifestHandler manifestHandler = new ManifestHandler(manifestFile, + errorListener, gatherData, javaProject, markErrors); + + parser.parse(new InputSource(manifestFile.getContents()), manifestHandler); + + // get the result from the handler + + return new AndroidManifestParser(manifestHandler.getPackage(), + manifestHandler.getActivities(), manifestHandler.getLauncherActivity(), + manifestHandler.getProcesses(), manifestHandler.getDebuggable(), + manifestHandler.getApiLevelRequirement()); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return null; + } + + /** + * Parses the Android Manifest for the specified project, and returns an object containing + * the result of the parsing. + * @param javaProject The java project. Required if <var>markErrors</var> is <code>true</code> + * @param errorListener the {@link XmlErrorListener} object being notified of the presence + * of errors. Optional. + * @param gatherData indicates whether the parsing will extract data from the manifest. + * @param markErrors indicates whether the error found during parsing should put a + * marker on the file. For class validation errors to put a marker, <code>gatherData</code> + * must be set to <code>true</code> + * @return an {@link AndroidManifestParser} or null if the parsing failed. + * @throws CoreException + */ + public static AndroidManifestParser parse(IJavaProject javaProject, + XmlErrorListener errorListener, boolean gatherData, boolean markErrors) + throws CoreException { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + IFile manifestFile = AndroidManifestHelper.getManifest(javaProject.getProject()); + if (manifestFile != null) { + ManifestHandler manifestHandler = new ManifestHandler(manifestFile, + errorListener, gatherData, javaProject, markErrors); + + parser.parse(new InputSource(manifestFile.getContents()), manifestHandler); + + // get the result from the handler + return new AndroidManifestParser(manifestHandler.getPackage(), + manifestHandler.getActivities(), manifestHandler.getLauncherActivity(), + manifestHandler.getProcesses(), manifestHandler.getDebuggable(), + manifestHandler.getApiLevelRequirement()); + } + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return null; + } + + /** + * Parses the manifest file, collects data, and checks for errors. + * @param javaProject The java project. Required. + * @param manifestFile The manifest file to parse. + * @param errorListener the {@link XmlErrorListener} object being notified of the presence + * of errors. Optional. + * @return an {@link AndroidManifestParser} or null if the parsing failed. + * @throws CoreException + */ + public static AndroidManifestParser parseForError(IJavaProject javaProject, IFile manifestFile, + XmlErrorListener errorListener) throws CoreException { + return parse(javaProject, manifestFile, errorListener, true, true); + } + + /** + * Parses the manifest file, and collects data. + * @param manifestFile The manifest file to parse. + * @return an {@link AndroidManifestParser} or null if the parsing failed. + * @throws CoreException + */ + public static AndroidManifestParser parseForData(IFile manifestFile) throws CoreException { + return parse(null /* javaProject */, manifestFile, null /* errorListener */, + true /* gatherData */, false /* markErrors */); + } + + /** + * Returns the package defined in the manifest, if found. + * @return The package name or null if not found. + */ + public String getPackage() { + return mJavaPackage; + } + + /** + * Returns the list of activities found in the manifest. + * @return An array of fully qualified class names, or empty if no activity were found. + */ + public String[] getActivities() { + return mActivities; + } + + /** + * Returns the name of one activity found in the manifest, that is configured to show + * up in the HOME screen. + * @return the fully qualified name of a HOME activity or null if none were found. + */ + public String getLauncherActivity() { + return mLauncherActivity; + } + + /** + * Returns the list of process names declared by the manifest. + */ + public String[] getProcesses() { + return mProcesses; + } + + /** + * Returns the debuggable attribute value or <code>null</code> if it is not set. + */ + public Boolean getDebuggable() { + return mDebuggable; + } + + /** + * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set. + */ + public int getApiLevelRequirement() { + return mApiLevelRequirement; + } + + + /** + * Private constructor to enforce using + * {@link #parse(IJavaProject, XmlErrorListener, boolean, boolean)}, + * {@link #parse(IJavaProject, IFile, XmlErrorListener, boolean, boolean)}, + * or {@link #parseForError(IJavaProject, IFile, XmlErrorListener)} to get an + * {@link AndroidManifestParser} object. + * @param javaPackage the package parsed from the manifest. + * @param activities the list of activities parsed from the manifest. + * @param launcherActivity the launcher activity parser from the manifest. + * @param processes the list of custom processes declared in the manifest. + * @param debuggable the debuggable attribute, or null if not set. + * @param apiLevelRequirement the minSdkVersion attribute value or 0 if not set. + */ + private AndroidManifestParser(String javaPackage, String[] activities, + String launcherActivity, String[] processes, Boolean debuggable, + int apiLevelRequirement) { + mJavaPackage = javaPackage; + mActivities = activities; + mLauncherActivity = launcherActivity; + mProcesses = processes; + mDebuggable = debuggable; + mApiLevelRequirement = apiLevelRequirement; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java new file mode 100644 index 0000000..0f1e255 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.sdklib.SdkConstants; + +import java.util.Iterator; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathFactory; + +/** + * XPath factory with automatic support for the android namespace. + */ +public class AndroidXPathFactory { + public final static String DEFAULT_NS_PREFIX = "android"; //$NON-NLS-1$ + + private final static XPathFactory sFactory = XPathFactory.newInstance(); + + /** Namespace context for Android resource XML files. */ + private static class AndroidNamespaceContext implements NamespaceContext { + private String mAndroidPrefix; + + /** + * Construct the context with the prefix associated with the android namespace. + * @param androidPrefix the Prefix + */ + public AndroidNamespaceContext(String androidPrefix) { + mAndroidPrefix = androidPrefix; + } + + public String getNamespaceURI(String prefix) { + if (prefix != null) { + if (prefix.equals(mAndroidPrefix)) { + return SdkConstants.NS_RESOURCES; + } + } + + return XMLConstants.NULL_NS_URI; + } + + public String getPrefix(String namespaceURI) { + // This isn't necessary for our use. + assert false; + return null; + } + + public Iterator<?> getPrefixes(String namespaceURI) { + // This isn't necessary for our use. + assert false; + return null; + } + } + + /** + * Creates a new XPath object, specifying which prefix in the query is used for the + * android namespace. + * @param androidPrefix The namespace prefix. + */ + public static XPath newXPath(String androidPrefix) { + XPath xpath = sFactory.newXPath(); + xpath.setNamespaceContext(new AndroidNamespaceContext(androidPrefix)); + return xpath; + } + + /** + * Creates a new XPath object using the default prefix for the android namespace. + * @see #DEFAULT_NS_PREFIX + */ + public static XPath newXPath() { + return newXPath(DEFAULT_NS_PREFIX); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java new file mode 100644 index 0000000..bd8b444 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; + +import java.util.ArrayList; + +/** + * Utility methods to manipulate projects. + */ +public final class BaseProjectHelper { + + public static final String TEST_CLASS_OK = null; + + /** + * returns a list of source classpath for a specified project + * @param javaProject + * @return a list of path relative to the workspace root. + */ + public static ArrayList<IPath> getSourceClasspaths(IJavaProject javaProject) { + ArrayList<IPath> sourceList = new ArrayList<IPath>(); + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + sourceList.add(e.getPath()); + } + } + } + return sourceList; + } + + /** + * Adds a marker to a file on a specific line. This methods catches thrown + * {@link CoreException}, and returns null instead. + * @param file the file to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param lineNumber the line number where to put the mark. If line is < 1, it puts the marker + * on line 1. + * @param severity the severity of the marker. + * @return the IMarker that was added or null if it failed to add one. + */ + public final static IMarker addMarker(IResource file, String markerId, + String message, int lineNumber, int severity) { + try { + IMarker marker = file.createMarker(markerId); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.SEVERITY, severity); + if (lineNumber < 1) { + lineNumber = 1; + } + marker.setAttribute(IMarker.LINE_NUMBER, lineNumber); + + // on Windows, when adding a marker to a project, it takes a refresh for the marker + // to show. In order to fix this we're forcing a refresh of elements receiving + // markers (and only the element, not its children), to force the marker display. + file.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + + return marker; + } catch (CoreException e) { + AdtPlugin.log(e, "Failed to add marker '%1$s' to '%2$s'", //$NON-NLS-1$ + markerId, file.getFullPath()); + } + + return null; + } + + /** + * Adds a marker to a resource. This methods catches thrown {@link CoreException}, + * and returns null instead. + * @param resource the file to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param severity the severity of the marker. + * @return the IMarker that was added or null if it failed to add one. + */ + public final static IMarker addMarker(IResource resource, String markerId, + String message, int severity) { + try { + IMarker marker = resource.createMarker(markerId); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.SEVERITY, severity); + + // on Windows, when adding a marker to a project, it takes a refresh for the marker + // to show. In order to fix this we're forcing a refresh of elements receiving + // markers (and only the element, not its children), to force the marker display. + resource.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + + return marker; + } catch (CoreException e) { + AdtPlugin.log(e, "Failed to add marker '%1$s' to '%2$s'", //$NON-NLS-1$ + markerId, resource.getFullPath()); + } + + return null; + } + + /** + * Adds a marker to a resource. This method does not catch {@link CoreException} and instead + * throw them. + * @param resource the file to be marked + * @param markerId The id of the marker to add. + * @param message the message associated with the mark + * @param lineNumber the line number where to put the mark if != -1. + * @param severity the severity of the marker. + * @param priority the priority of the marker + * @return the IMarker that was added. + * @throws CoreException + */ + public final static IMarker addMarker(IResource resource, String markerId, + String message, int lineNumber, int severity, int priority) throws CoreException { + IMarker marker = resource.createMarker(markerId); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.SEVERITY, severity); + if (lineNumber != -1) { + marker.setAttribute(IMarker.LINE_NUMBER, lineNumber); + } + marker.setAttribute(IMarker.PRIORITY, priority); + + // on Windows, when adding a marker to a project, it takes a refresh for the marker + // to show. In order to fix this we're forcing a refresh of elements receiving + // markers (and only the element, not its children), to force the marker display. + resource.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + + return marker; + } + + /** + * Tests that a class name is valid for usage in the manifest. + * <p/> + * This tests the class existence, that it can be instantiated (ie it must not be abstract, + * nor non static if enclosed), and that it extends the proper super class (not necessarily + * directly) + * @param javaProject the {@link IJavaProject} containing the class. + * @param className the fully qualified name of the class to test. + * @param superClassName the fully qualified name of the expected super class. + * @param testVisibility if <code>true</code>, the method will check the visibility of the class + * or of its constructors. + * @return {@link #TEST_CLASS_OK} or an error message. + */ + public final static String testClassForManifest(IJavaProject javaProject, String className, + String superClassName, boolean testVisibility) { + try { + // replace $ by . + String javaClassName = className.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$ + + // look for the IType object for this class + IType type = javaProject.findType(javaClassName); + if (type != null && type.exists()) { + // test that the class is not abstract + int flags = type.getFlags(); + if (Flags.isAbstract(flags)) { + return String.format("%1$s is abstract", className); + } + + // test whether the class is public or not. + if (testVisibility && Flags.isPublic(flags) == false) { + // if its not public, it may have a public default constructor, + // which would then be fine. + IMethod basicConstructor = type.getMethod(type.getElementName(), new String[0]); + if (basicConstructor != null && basicConstructor.exists()) { + int constructFlags = basicConstructor.getFlags(); + if (Flags.isPublic(constructFlags) == false) { + return String.format( + "%1$s or its default constructor must be public for the system to be able to instantiate it", + className); + } + } else { + return String.format( + "%1$s must be public, or the system will not be able to instantiate it.", + className); + } + } + + // If it's enclosed, test that it's static. If its declaring class is enclosed + // as well, test that it is also static, and public. + IType declaringType = type; + do { + IType tmpType = declaringType.getDeclaringType(); + if (tmpType != null) { + if (tmpType.exists()) { + flags = declaringType.getFlags(); + if (Flags.isStatic(flags) == false) { + return String.format("%1$s is enclosed, but not static", + declaringType.getFullyQualifiedName()); + } + + flags = tmpType.getFlags(); + if (testVisibility && Flags.isPublic(flags) == false) { + return String.format("%1$s is not public", + tmpType.getFullyQualifiedName()); + } + } else { + // if it doesn't exist, we need to exit so we may as well mark it null. + tmpType = null; + } + } + declaringType = tmpType; + } while (declaringType != null); + + // test the class inherit from the specified super class. + // get the type hierarchy + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + + // if the super class is not the reference class, it may inherit from + // it so we get its supertype. At some point it will be null and we + // will stop + IType superType = type; + boolean foundProperSuperClass = false; + while ((superType = hierarchy.getSuperclass(superType)) != null && + superType.exists()) { + if (superClassName.equals(superType.getFullyQualifiedName())) { + foundProperSuperClass = true; + } + } + + // didn't find the proper superclass? return false. + if (foundProperSuperClass == false) { + return String.format("%1$s does not extend %2$s", className, superClassName); + } + + return TEST_CLASS_OK; + } else { + return String.format("Class %1$s does not exist", className); + } + } catch (JavaModelException e) { + return String.format("%1$s: %2$s", className, e.getMessage()); + } + } + + /** + * Parses the manifest file for errors. + * <p/> + * This starts by removing the current XML marker, and then parses the xml for errors, both + * of XML type and of Android type (checking validity of class files). + * @param manifestFile + * @param errorListener + * @throws CoreException + */ + public static AndroidManifestParser parseManifestForError(IFile manifestFile, + XmlErrorListener errorListener) throws CoreException { + // remove previous markers + if (manifestFile.exists()) { + manifestFile.deleteMarkers(AndroidConstants.MARKER_XML, true, IResource.DEPTH_ZERO); + manifestFile.deleteMarkers(AndroidConstants.MARKER_ANDROID, true, IResource.DEPTH_ZERO); + } + + // and parse + return AndroidManifestParser.parseForError( + BaseProjectHelper.getJavaProject(manifestFile.getProject()), + manifestFile, errorListener); + } + + /** + * Returns the {@link IJavaProject} for a {@link IProject} object. + * <p/> + * This checks if the project has the Java Nature first. + * @param project + * @return the IJavaProject or null if the project couldn't be created or if the project + * does not have the Java Nature. + * @throws CoreException + */ + public static IJavaProject getJavaProject(IProject project) throws CoreException { + if (project != null && project.hasNature(JavaCore.NATURE_ID)) { + return JavaCore.create(project); + } + return null; + } + + /** + * Reveals a specific line in the source file defining a specified class, + * for a specific project. + * @param project + * @param className + * @param line + */ + public static void revealSource(IProject project, String className, int line) { + // in case the type is enclosed, we need to replace the $ with . + className = className.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$ + + // get the java project + IJavaProject javaProject = JavaCore.create(project); + + try { + // look for the IType matching the class name. + IType result = javaProject.findType(className); + if (result != null && result.exists()) { + // before we show the type in an editor window, we make sure the current + // workbench page has an editor area (typically the ddms perspective doesn't). + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + IWorkbenchPage page = window.getActivePage(); + if (page.isEditorAreaVisible() == false) { + // no editor area? we open the java perspective. + new OpenJavaPerspectiveAction().run(); + } + + IEditorPart editor = JavaUI.openInEditor(result); + if (editor instanceof ITextEditor) { + // get the text editor that was just opened. + ITextEditor textEditor = (ITextEditor)editor; + + IEditorInput input = textEditor.getEditorInput(); + + // get the location of the line to show. + IDocumentProvider documentProvider = textEditor.getDocumentProvider(); + IDocument document = documentProvider.getDocument(input); + IRegion lineInfo = document.getLineInformation(line - 1); + + // select and reveal the line. + textEditor.selectAndReveal(lineInfo.getOffset(), lineInfo.getLength()); + } + } + } catch (JavaModelException e) { + } catch (PartInitException e) { + } catch (BadLocationException e) { + } + } + + /** + * Returns the list of android-flagged projects. This list contains projects that are opened + * in the workspace and that are flagged as android project (through the android nature) + * @return an array of IJavaProject, which can be empty if no projects match. + */ + public static IJavaProject[] getAndroidProjects() { + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + + return getAndroidProjects(javaModel); + } + + /** + * Returns the list of android-flagged projects for the specified java Model. + * This list contains projects that are opened in the workspace and that are flagged as android + * project (through the android nature) + * @param javaModel the Java Model object corresponding for the current workspace root. + * @return an array of IJavaProject, which can be empty if no projects match. + */ + public static IJavaProject[] getAndroidProjects(IJavaModel javaModel) { + // get the java projects + IJavaProject[] javaProjectList = null; + try { + javaProjectList = javaModel.getJavaProjects(); + } + catch (JavaModelException jme) { + return new IJavaProject[0]; + } + + // temp list to build the android project array + ArrayList<IJavaProject> androidProjectList = new ArrayList<IJavaProject>(); + + // loop through the projects and add the android flagged projects to the temp list. + for (IJavaProject javaProject : javaProjectList) { + // get the workspace project object + IProject project = javaProject.getProject(); + + // check if it's an android project based on its nature + try { + if (project.hasNature(AndroidConstants.NATURE)) { + androidProjectList.add(javaProject); + } + } catch (CoreException e) { + // this exception, thrown by IProject.hasNature(), means the project either doesn't + // exist or isn't opened. So, in any case we just skip it (the exception will + // bypass the ArrayList.add() + } + } + + // return the android projects list. + return androidProjectList.toArray(new IJavaProject[androidProjectList.size()]); + } + + /** + * Returns the {@link IFolder} representing the output for the project. + * <p> + * The project must be a java project and be opened, or the method will return null. + * @param project the {@link IProject} + * @return an IFolder item or null. + */ + public final static IFolder getOutputFolder(IProject project) { + try { + if (project.isOpen() && project.hasNature(JavaCore.NATURE_ID)) { + // get a java project from the normal project object + IJavaProject javaProject = JavaCore.create(project); + + IPath path = javaProject.getOutputLocation(); + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + IResource outputResource = wsRoot.findMember(path); + if (outputResource != null && outputResource.getType() == IResource.FOLDER) { + return (IFolder)outputResource; + } + } + } catch (JavaModelException e) { + // Let's do nothing and return null + } catch (CoreException e) { + // Let's do nothing and return null + } + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java new file mode 100644 index 0000000..4b169a1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * Export helper for project. + */ +public final class ExportHelper { + + private static IExportCallback sCallback; + + public interface IExportCallback { + void startExportWizard(IProject project); + } + + public static void setCallback(IExportCallback callback) { + sCallback = callback; + } + + public static void startExportWizard(IProject project) { + if (sCallback != null) { + sCallback.startExportWizard(project); + } + } + + /** + * Exports an <b>unsigned</b> version of the application created by the given project. + * @param project the project to export + */ + public static void exportProject(IProject project) { + Shell shell = Display.getCurrent().getActiveShell(); + + // get the java project to get the output directory + IFolder outputFolder = BaseProjectHelper.getOutputFolder(project); + if (outputFolder != null) { + IPath binLocation = outputFolder.getLocation(); + + // make the full path to the package + String fileName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE; + + File file = new File(binLocation.toOSString() + File.separator + fileName); + + if (file.exists() == false || file.isFile() == false) { + MessageDialog.openInformation(Display.getCurrent().getActiveShell(), + "Android IDE Plug-in", + String.format("Failed to export %1$s: %2$s doesn't exist!", + project.getName(), file.getPath())); + return; + } + + // ok now pop up the file save window + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + + fileDialog.setText("Export Project"); + fileDialog.setFileName(fileName); + + String saveLocation = fileDialog.open(); + if (saveLocation != null) { + // get the stream from the original file + + ZipInputStream zis = null; + ZipOutputStream zos = null; + FileInputStream input = null; + FileOutputStream output = null; + + try { + input = new FileInputStream(file); + zis = new ZipInputStream(input); + + // get an output stream into the new file + File saveFile = new File(saveLocation); + output = new FileOutputStream(saveFile); + zos = new ZipOutputStream(output); + } catch (FileNotFoundException e) { + // only the input/output stream are throwing this exception. + // so we only have to close zis if output is the one that threw. + if (zis != null) { + try { + zis.close(); + } catch (IOException e1) { + // pass + } + } + + MessageDialog.openInformation(shell, "Android IDE Plug-in", + String.format("Failed to export %1$s: %2$s doesn't exist!", + project.getName(), file.getPath())); + return; + } + + try { + ZipEntry entry; + + byte[] buffer = new byte[4096]; + + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + + // do not take directories or anything inside the META-INF folder since + // we want to strip the signature. + if (entry.isDirectory() || name.startsWith("META-INF/")) { //$NON-NL1$ + continue; + } + + ZipEntry newEntry; + + // Preserve the STORED method of the input entry. + if (entry.getMethod() == JarEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + // Create a new entry so that the compressed len is recomputed. + newEntry = new JarEntry(name); + } + + // add the entry to the jar archive + zos.putNextEntry(newEntry); + + // read the content of the entry from the input stream, and write it into the archive. + int count; + while ((count = zis.read(buffer)) != -1) { + zos.write(buffer, 0, count); + } + + // close the entry for this file + zos.closeEntry(); + zis.closeEntry(); + + } + + } catch (IOException e) { + MessageDialog.openInformation(shell, "Android IDE Plug-in", + String.format("Failed to export %1$s: %2$s", + project.getName(), e.getMessage())); + } finally { + try { + zos.close(); + } catch (IOException e) { + // pass + } + try { + zis.close(); + } catch (IOException e) { + // pass + } + } + } + } else { + MessageDialog.openInformation(shell, "Android IDE Plug-in", + String.format("Failed to export %1$s: Could not get project output location", + project.getName())); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java new file mode 100644 index 0000000..0c43499 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.ide.eclipse.common.project.BaseProjectHelper; + +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ui.JavaElementLabelProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.ElementListSelectionDialog; + +/** + * Helper class to deal with displaying a project choosing dialog that lists only the + * projects with the Android nature. + */ +public class ProjectChooserHelper { + + private final Shell mParentShell; + + /** + * List of current android projects. Since the dialog is modal, we'll just get + * the list once on-demand. + */ + private IJavaProject[] mAndroidProjects; + + public ProjectChooserHelper(Shell parentShell) { + mParentShell = parentShell; + } + /** + * Displays a project chooser dialog which lists all available projects with the Android nature. + * <p/> + * The list of project is built from Android flagged projects currently opened in the workspace. + * + * @param projectName If non null and not empty, represents the name of an Android project + * that will be selected by default. + * @return the project chosen by the user in the dialog, or null if the dialog was canceled. + */ + public IJavaProject chooseJavaProject(String projectName) { + ILabelProvider labelProvider = new JavaElementLabelProvider( + JavaElementLabelProvider.SHOW_DEFAULT); + ElementListSelectionDialog dialog = new ElementListSelectionDialog( + mParentShell, labelProvider); + dialog.setTitle("Project Selection"); + dialog.setMessage("Select a project to constrain your search."); + + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + + // set the elements in the dialog. These are opened android projects. + dialog.setElements(getAndroidProjects(javaModel)); + + // look for the project matching the given project name + IJavaProject javaProject = null; + if (projectName != null && projectName.length() > 0) { + javaProject = javaModel.getJavaProject(projectName); + } + + // if we found it, we set the initial selection in the dialog to this one. + if (javaProject != null) { + dialog.setInitialSelections(new Object[] { javaProject }); + } + + // open the dialog and return the object selected if OK was clicked, or null otherwise + if (dialog.open() == Window.OK) { + return (IJavaProject)dialog.getFirstResult(); + } + return null; + } + + /** + * Returns the list of Android projects. + * <p/> + * Because this list can be time consuming, this class caches the list of project. + * It is recommended to call this method instead of + * {@link BaseProjectHelper#getAndroidProjects()}. + * + * @param javaModel the java model. Can be null. + */ + public IJavaProject[] getAndroidProjects(IJavaModel javaModel) { + if (mAndroidProjects == null) { + if (javaModel == null) { + mAndroidProjects = BaseProjectHelper.getAndroidProjects(); + } else { + mAndroidProjects = BaseProjectHelper.getAndroidProjects(javaModel); + } + } + + return mAndroidProjects; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java new file mode 100644 index 0000000..fda55c4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * XML error handler used by the parser to report errors/warnings. + */ +public class XmlErrorHandler extends DefaultHandler { + + /** file being parsed */ + private IFile mFile; + + /** link to the delta visitor, to set the xml error flag */ + private XmlErrorListener mErrorListener; + + /** + * Classes which implement this interface provide a method that deals + * with XML errors. + */ + public interface XmlErrorListener { + /** + * Sent when an XML error is detected. + */ + public void errorFound(); + } + + public static class BasicXmlErrorListener implements XmlErrorListener { + public boolean mHasXmlError = false; + + public void errorFound() { + mHasXmlError = true; + } + } + + public XmlErrorHandler(IFile file, XmlErrorListener errorListener) { + mFile = file; + mErrorListener = errorListener; + } + + /** + * Xml Error call back + * @param exception the parsing exception + * @throws SAXException + */ + @Override + public void error(SAXParseException exception) throws SAXException { + handleError(exception, exception.getLineNumber()); + } + + /** + * Xml Fatal Error call back + * @param exception the parsing exception + * @throws SAXException + */ + @Override + public void fatalError(SAXParseException exception) throws SAXException { + handleError(exception, exception.getLineNumber()); + } + + /** + * Xml Warning call back + * @param exception the parsing exception + * @throws SAXException + */ + @Override + public void warning(SAXParseException exception) throws SAXException { + BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(), + exception.getLineNumber(), IMarker.SEVERITY_WARNING); + } + + protected final IFile getFile() { + return mFile; + } + + /** + * Handles a parsing error and an optional line number. + * @param exception + * @param lineNumber + */ + protected void handleError(Exception exception, int lineNumber) { + if (mErrorListener != null) { + mErrorListener.errorFound(); + } + + if (lineNumber != -1) { + BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(), + lineNumber, IMarker.SEVERITY_ERROR); + } else { + BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(), + IMarker.SEVERITY_ERROR); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java new file mode 100644 index 0000000..3875e81 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo.Format; +import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; + +import org.eclipse.core.runtime.IStatus; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * Parser for attributes description files. + */ +public final class AttrsXmlParser { + + private Document mDocument; + private String mOsAttrsXmlPath; + // all attributes that have the same name are supposed to have the same + // parameters so we'll keep a cache of them to avoid processing them twice. + private HashMap<String, AttributeInfo> mAttributeMap; + + /** Map of all attribute names for a given element */ + private HashMap<String, DeclareStyleableInfo> mStyleMap; + + /** Map of all (constant, value) pairs for attributes of format enum or flag. + * E.g. for attribute name=gravity, this tells us there's an enum/flag called "center" + * with value 0x11. + */ + private Map<String, Map<String, Integer>> mEnumFlagValues; + + + /** + * Creates a new {@link AttrsXmlParser}, set to load things from the given + * XML file. Nothing has been parsed yet. Callers should call {@link #preload()} + * next. + */ + public AttrsXmlParser(String osAttrsXmlPath) { + this(osAttrsXmlPath, null /* inheritableAttributes */); + } + + /** + * Creates a new {@link AttrsXmlParser} set to load things from the given + * XML file. If inheritableAttributes is non-null, it must point to a preloaded + * {@link AttrsXmlParser} which attributes will be used for this one. Since + * already defined attributes are not modifiable, they are thus "inherited". + */ + public AttrsXmlParser(String osAttrsXmlPath, AttrsXmlParser inheritableAttributes) { + mOsAttrsXmlPath = osAttrsXmlPath; + + // styles are not inheritable. + mStyleMap = new HashMap<String, DeclareStyleableInfo>(); + + if (inheritableAttributes == null) { + mAttributeMap = new HashMap<String, AttributeInfo>(); + mEnumFlagValues = new HashMap<String, Map<String,Integer>>(); + } else { + mAttributeMap = new HashMap<String, AttributeInfo>(inheritableAttributes.mAttributeMap); + mEnumFlagValues = new HashMap<String, Map<String,Integer>>( + inheritableAttributes.mEnumFlagValues); + } + } + + /** + * @return The OS path of the attrs.xml file parsed + */ + public String getOsAttrsXmlPath() { + return mOsAttrsXmlPath; + } + + /** + * Preloads the document, parsing all attributes and declared styles. + * + * @return Self, for command chaining. + */ + public AttrsXmlParser preload() { + Document doc = getDocument(); + + if (doc == null) { + AdtPlugin.log(IStatus.WARNING, "Failed to find %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + return this; + } + + Node res = doc.getFirstChild(); + while (res != null && + res.getNodeType() != Node.ELEMENT_NODE && + !res.getNodeName().equals("resources")) { //$NON-NLS-1$ + res = res.getNextSibling(); + } + + if (res == null) { + AdtPlugin.log(IStatus.WARNING, "Failed to find a <resources> node in %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + return this; + } + + parseResources(res); + return this; + } + + /** + * Loads all attributes & javadoc for the view class info based on the class name. + */ + public void loadViewAttributes(ViewClassInfo info) { + if (getDocument() != null) { + String xmlName = info.getShortClassName(); + DeclareStyleableInfo style = mStyleMap.get(xmlName); + if (style != null) { + info.setAttributes(style.getAttributes()); + info.setJavaDoc(style.getJavaDoc()); + } + } + } + + /** + * Loads all attributes for the layout data info based on the class name. + */ + public void loadLayoutParamsAttributes(LayoutParamsInfo info) { + if (getDocument() != null) { + // Transforms "LinearLayout" and "LayoutParams" into "LinearLayout_Layout". + String xmlName = String.format("%1$s_%2$s", //$NON-NLS-1$ + info.getViewLayoutClass().getShortClassName(), + info.getShortClassName()); + xmlName = xmlName.replaceFirst("Params$", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + DeclareStyleableInfo style = mStyleMap.get(xmlName); + if (style != null) { + info.setAttributes(style.getAttributes()); + } + } + } + + /** + * Returns a list of all decleare-styleable found in the xml file. + */ + public Map<String, DeclareStyleableInfo> getDeclareStyleableList() { + return Collections.unmodifiableMap(mStyleMap); + } + + /** + * Returns a map of all enum and flag constants sorted by parent attribute name. + * The map is attribute_name => (constant_name => integer_value). + */ + public Map<String, Map<String, Integer>> getEnumFlagValues() { + return mEnumFlagValues; + } + + //------------------------- + + /** + * Creates an XML document from the attrs.xml OS path. + * May return null if the file doesn't exist or cannot be parsed. + */ + private Document getDocument() { + if (mDocument == null) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + mDocument = builder.parse(new File(mOsAttrsXmlPath)); + } catch (ParserConfigurationException e) { + AdtPlugin.log(e, "Failed to create XML document builder for %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + } catch (SAXException e) { + AdtPlugin.log(e, "Failed to parse XML document %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + } catch (IOException e) { + AdtPlugin.log(e, "Failed to read XML document %1$s", //$NON-NLS-1$ + mOsAttrsXmlPath); + } + } + return mDocument; + } + + /** + * Finds all the <declare-styleable> and <attr> nodes in the top <resources> node. + */ + private void parseResources(Node res) { + Node lastComment = null; + for (Node node = res.getFirstChild(); node != null; node = node.getNextSibling()) { + switch (node.getNodeType()) { + case Node.COMMENT_NODE: + lastComment = node; + break; + case Node.ELEMENT_NODE: + if (node.getNodeName().equals("declare-styleable")) { //$NON-NLS-1$ + Node nameNode = node.getAttributes().getNamedItem("name"); //$NON-NLS-1$ + if (nameNode != null) { + String name = nameNode.getNodeValue(); + + Node parentNode = node.getAttributes().getNamedItem("parent"); //$NON-NLS-1$ + String parents = parentNode == null ? null : parentNode.getNodeValue(); + + if (name != null && !mStyleMap.containsKey(name)) { + DeclareStyleableInfo style = parseDeclaredStyleable(name, node); + if (parents != null) { + style.setParents(parents.split("[ ,|]")); //$NON-NLS-1$ + } + mStyleMap.put(name, style); + if (lastComment != null) { + style.setJavaDoc(parseJavadoc(lastComment.getNodeValue())); + } + } + } + } else if (node.getNodeName().equals("attr")) { //$NON-NLS-1$ + parseAttr(node, lastComment); + } + lastComment = null; + break; + } + } + } + + /** + * Parses an <attr> node and convert it into an {@link AttributeInfo} if it is valid. + */ + private AttributeInfo parseAttr(Node attrNode, Node lastComment) { + AttributeInfo info = null; + Node nameNode = attrNode.getAttributes().getNamedItem("name"); //$NON-NLS-1$ + if (nameNode != null) { + String name = nameNode.getNodeValue(); + if (name != null) { + info = mAttributeMap.get(name); + // If the attribute is unknown yet, parse it. + // If the attribute is know but its format is unknown, parse it too. + if (info == null || info.getFormats().length == 0) { + info = parseAttributeTypes(attrNode, name); + if (info != null) { + mAttributeMap.put(name, info); + } + } else if (lastComment != null) { + info = new AttributeInfo(info); + } + if (info != null) { + if (lastComment != null) { + info.setJavaDoc(parseJavadoc(lastComment.getNodeValue())); + info.setDeprecatedDoc(parseDeprecatedDoc(lastComment.getNodeValue())); + } + } + } + } + return info; + } + + /** + * Finds all the attributes for a particular style node, + * e.g. a declare-styleable of name "TextView" or "LinearLayout_Layout". + * + * @param styleName The name of the declare-styleable node + * @param declareStyleableNode The declare-styleable node itself + */ + private DeclareStyleableInfo parseDeclaredStyleable(String styleName, + Node declareStyleableNode) { + ArrayList<AttributeInfo> attrs = new ArrayList<AttributeInfo>(); + Node lastComment = null; + for (Node node = declareStyleableNode.getFirstChild(); + node != null; + node = node.getNextSibling()) { + + switch (node.getNodeType()) { + case Node.COMMENT_NODE: + lastComment = node; + break; + case Node.ELEMENT_NODE: + if (node.getNodeName().equals("attr")) { //$NON-NLS-1$ + AttributeInfo info = parseAttr(node, lastComment); + if (info != null) { + attrs.add(info); + } + } + lastComment = null; + break; + } + + } + + return new DeclareStyleableInfo(styleName, attrs.toArray(new AttributeInfo[attrs.size()])); + } + + /** + * Returns the {@link AttributeInfo} for a specific <attr> XML node. + * This gets the javadoc, the type, the name and the enum/flag values if any. + * <p/> + * The XML node is expected to have the following attributes: + * <ul> + * <li>"name", which is mandatory. The node is skipped if this is missing.</li> + * <li>"format".</li> + * </ul> + * The format may be one type or two types (e.g. "reference|color"). + * An extra format can be implied: "enum" or "flag" are not specified in the "format" attribute, + * they are implicitely stated by the presence of sub-nodes <enum> or <flag>. + * <p/> + * By design, <attr> nodes of the same name MUST have the same type. + * Attribute nodes are thus cached by name and reused as much as possible. + * When reusing a node, it is duplicated and its javadoc reassigned. + */ + private AttributeInfo parseAttributeTypes(Node attrNode, String name) { + TreeSet<AttributeInfo.Format> formats = new TreeSet<AttributeInfo.Format>(); + String[] enumValues = null; + String[] flagValues = null; + + Node attrFormat = attrNode.getAttributes().getNamedItem("format"); //$NON-NLS-1$ + if (attrFormat != null) { + for (String f : attrFormat.getNodeValue().split("\\|")) { //$NON-NLS-1$ + try { + Format format = AttributeInfo.Format.valueOf(f.toUpperCase()); + // enum and flags are handled differently right below + if (format != null && + format != AttributeInfo.Format.ENUM && + format != AttributeInfo.Format.FLAG) { + formats.add(format); + } + } catch (IllegalArgumentException e) { + AdtPlugin.log(e, "Unknown format name '%s' in <attr name=\"%s\">, file '%s'.", //$NON-NLS-1$ + f, name, getOsAttrsXmlPath()); + } + } + } + + // does this <attr> have <enum> children? + enumValues = parseEnumFlagValues(attrNode, "enum", name); //$NON-NLS-1$ + if (enumValues != null) { + formats.add(AttributeInfo.Format.ENUM); + } + + // does this <attr> have <flag> children? + flagValues = parseEnumFlagValues(attrNode, "flag", name); //$NON-NLS-1$ + if (flagValues != null) { + formats.add(AttributeInfo.Format.FLAG); + } + + AttributeInfo info = new AttributeInfo(name, + formats.toArray(new AttributeInfo.Format[formats.size()])); + info.setEnumValues(enumValues); + info.setFlagValues(flagValues); + return info; + } + + /** + * Given an XML node that represents an <attr> node, this method searches + * if the node has any children nodes named "target" (e.g. "enum" or "flag"). + * Such nodes must have a "name" attribute. + * <p/> + * If "attrNode" is null, look for any <attr> that has the given attrNode + * and the requested children nodes. + * <p/> + * This method collects all the possible names of these children nodes and + * return them. + * + * @param attrNode The <attr> XML node + * @param filter The child node to look for, either "enum" or "flag". + * @param attrName The value of the name attribute of <attr> + * + * @return Null if there are no such children nodes, otherwise an array of length >= 1 + * of all the names of these children nodes. + */ + private String[] parseEnumFlagValues(Node attrNode, String filter, String attrName) { + ArrayList<String> names = null; + for (Node child = attrNode.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(filter)) { + Node nameNode = child.getAttributes().getNamedItem("name"); //$NON-NLS-1$ + if (nameNode == null) { + AdtPlugin.log(IStatus.WARNING, + "Missing name attribute in <attr name=\"%s\"><%s></attr>", //$NON-NLS-1$ + attrName, filter); + } else { + if (names == null) { + names = new ArrayList<String>(); + } + String name = nameNode.getNodeValue(); + names.add(name); + + Node valueNode = child.getAttributes().getNamedItem("value"); //$NON-NLS-1$ + if (valueNode == null) { + AdtPlugin.log(IStatus.WARNING, + "Missing value attribute in <attr name=\"%s\"><%s name=\"%s\"></attr>", //$NON-NLS-1$ + attrName, filter, name); + } else { + String value = valueNode.getNodeValue(); + try { + int i = value.startsWith("0x") ? + Integer.parseInt(value.substring(2), 16 /* radix */) : + Integer.parseInt(value); + + Map<String, Integer> map = mEnumFlagValues.get(attrName); + if (map == null) { + map = new HashMap<String, Integer>(); + mEnumFlagValues.put(attrName, map); + } + map.put(name, Integer.valueOf(i)); + + } catch(NumberFormatException e) { + AdtPlugin.log(e, + "Value in <attr name=\"%s\"><%s name=\"%s\" value=\"%s\"></attr> is not a valid decimal or hexadecimal", //$NON-NLS-1$ + attrName, filter, name, value); + } + } + } + } + } + return names == null ? null : names.toArray(new String[names.size()]); + } + + /** + * Parses the javadoc comment. + * Only keeps the first sentence. + * <p/> + * This does not remove nor simplify links and references. Such a transformation + * is done later at "display" time in {@link DescriptorsUtils#formatTooltip(String)} and co. + */ + private String parseJavadoc(String comment) { + if (comment == null) { + return null; + } + + // sanitize & collapse whitespace + comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + // Explicitly remove any @deprecated tags since they are handled separately. + comment = comment.replaceAll("(?:\\{@deprecated[^}]*\\}|@deprecated[^@}]*)", ""); + + // take everything up to the first dot that is followed by a space or the end of the line. + // I love regexps :-). For the curious, the regexp is: + // - start of line + // - ignore whitespace + // - group: + // - everything, not greedy + // - non-capturing group (?: ) + // - end of string + // or + // - not preceded by a letter, a dot and another letter (for "i.e" and "e.g" ) + // (<! non-capturing zero-width negative look-behind) + // - a dot + // - followed by a space (?= non-capturing zero-width positive look-ahead) + // - anything else is ignored + comment = comment.replaceFirst("^\\s*(.*?(?:$|(?<![a-zA-Z]\\.[a-zA-Z])\\.(?=\\s))).*", "$1"); //$NON-NLS-1$ //$NON-NLS-2$ + + return comment; + } + + + /** + * Parses the javadoc and extract the first @deprecated tag, if any. + * Returns null if there's no @deprecated tag. + * The deprecated tag can be of two forms: + * - {+@deprecated ...text till the next bracket } + * Note: there should be no space or + between { and @. I need one in this comment otherwise + * this method will be tagged as deprecated ;-) + * - @deprecated ...text till the next @tag or end of the comment. + * In both cases the comment can be multi-line. + */ + private String parseDeprecatedDoc(String comment) { + // Skip if we can't even find the tag in the comment. + if (comment == null) { + return null; + } + + // sanitize & collapse whitespace + comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + int pos = comment.indexOf("{@deprecated"); + if (pos >= 0) { + comment = comment.substring(pos + 12 /* len of {@deprecated */); + comment = comment.replaceFirst("^([^}]*).*", "$1"); + } else if ((pos = comment.indexOf("@deprecated")) >= 0) { + comment = comment.substring(pos + 11 /* len of @deprecated */); + comment = comment.replaceFirst("^(.*?)(?:@.*|$)", "$1"); + } else { + return null; + } + + return comment.trim(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java new file mode 100644 index 0000000..efa5981 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + + +/** + * Information needed to represent a View or ViewGroup (aka Layout) item + * in the layout hierarchy, as extracted from the main android.jar and the + * associated attrs.xml. + */ +public class DeclareStyleableInfo { + /** The style name, never null. */ + private String mStyleName; + /** Attributes for this view or view group. Can be empty but never null. */ + private AttributeInfo[] mAttributes; + /** Short javadoc. Can be null. */ + private String mJavaDoc; + /** Optional name of the parents stylable. Can be null. */ + private String[] mParents; + + public static class AttributeInfo { + /** XML Name of the attribute */ + private String mName; + + public enum Format { + STRING, + BOOLEAN, + INTEGER, + FLOAT, + REFERENCE, + COLOR, + DIMENSION, + FRACTION, + ENUM, + FLAG, + } + + /** Formats of the attribute. Cannot be null. Should have at least one format. */ + private Format[] mFormats; + /** Values for enum. null for other types. */ + private String[] mEnumValues; + /** Values for flag. null for other types. */ + private String[] mFlagValues; + /** Short javadoc (i.e. the first sentence). */ + private String mJavaDoc; + /** Documentation for deprecated attributes. Null if not deprecated. */ + private String mDeprecatedDoc; + + /** + * @param name The XML Name of the attribute + * @param formats The formats of the attribute. Cannot be null. + * Should have at least one format. + */ + public AttributeInfo(String name, Format[] formats) { + mName = name; + mFormats = formats; + } + + public AttributeInfo(AttributeInfo info) { + mName = info.mName; + mFormats = info.mFormats; + mEnumValues = info.mEnumValues; + mFlagValues = info.mFlagValues; + mJavaDoc = info.mJavaDoc; + mDeprecatedDoc = info.mDeprecatedDoc; + } + + /** Returns the XML Name of the attribute */ + public String getName() { + return mName; + } + /** Returns the formats of the attribute. Cannot be null. + * Should have at least one format. */ + public Format[] getFormats() { + return mFormats; + } + /** Returns the values for enums. null for other types. */ + public String[] getEnumValues() { + return mEnumValues; + } + /** Returns the values for flags. null for other types. */ + public String[] getFlagValues() { + return mFlagValues; + } + /** Returns a short javadoc, .i.e. the first sentence. */ + public String getJavaDoc() { + return mJavaDoc; + } + /** Returns the documentation for deprecated attributes. Null if not deprecated. */ + public String getDeprecatedDoc() { + return mDeprecatedDoc; + } + + /** Sets the values for enums. null for other types. */ + public void setEnumValues(String[] values) { + mEnumValues = values; + } + /** Sets the values for flags. null for other types. */ + public void setFlagValues(String[] values) { + mFlagValues = values; + } + /** Sets a short javadoc, .i.e. the first sentence. */ + public void setJavaDoc(String javaDoc) { + mJavaDoc = javaDoc; + } + /** Sets the documentation for deprecated attributes. Null if not deprecated. */ + public void setDeprecatedDoc(String deprecatedDoc) { + mDeprecatedDoc = deprecatedDoc; + } + + } + + // -------- + + /** + * Creates a new {@link DeclareStyleableInfo}. + * + * @param styleName The name of the style. Should not be empty nor null. + * @param attributes The initial list of attributes. Can be null. + */ + public DeclareStyleableInfo(String styleName, AttributeInfo[] attributes) { + mStyleName = styleName; + mAttributes = attributes == null ? new AttributeInfo[0] : attributes; + } + + /** Returns style name */ + public String getStyleName() { + return mStyleName; + } + + /** Returns the attributes for this view or view group. Maybe empty but not null. */ + public AttributeInfo[] getAttributes() { + return mAttributes; + } + + /** Sets the list of attributes for this View or ViewGroup. */ + public void setAttributes(AttributeInfo[] attributes) { + mAttributes = attributes; + } + + /** Returns a short javadoc */ + public String getJavaDoc() { + return mJavaDoc; + } + + /** Sets the javadoc. */ + public void setJavaDoc(String javaDoc) { + mJavaDoc = javaDoc; + } + + /** Sets the name of the parents styleable. Can be null. */ + public void setParents(String[] parents) { + mParents = parents; + } + + /** Returns the name of the parents styleable. Can be null. */ + public String[] getParents() { + return mParents; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java new file mode 100644 index 0000000..38b7e03 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +/** + * Classes which implements this interface provides a method indicating the state of a resource of + * type {@link ResourceType#ID}. + */ +public interface IIdResourceItem { + /** + * Returns whether the ID resource has been declared inline inside another resource XML file. + */ + public boolean isDeclaredInline(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java new file mode 100644 index 0000000..53d9077 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +/** + * Classes which implement this interface provide a method that deals with + * a path change. + */ +public interface IPathChangedListener { + /** + * Sent when the location of the android sdk directory changed. + * @param osPath The new android sdk directory location. + */ + public void pathChanged(String osPath); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java new file mode 100644 index 0000000..3819997 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +/** + * A repository of resources. This allows access to the resource by {@link ResourceType}. + */ +public interface IResourceRepository { + + /** + * Returns the present {@link ResourceType}s in the project. + * @return an array containing all the type of resources existing in the project. + */ + public abstract ResourceType[] getAvailableResourceTypes(); + + /** + * Returns an array of the existing resource for the specified type. + * @param type the type of the resources to return + */ + public abstract ResourceItem[] getResources(ResourceType type); + + /** + * Returns whether resources of the specified type are present. + * @param type the type of the resources to check. + */ + public abstract boolean hasResources(ResourceType type); + + /** + * Returns whether the repository is a system repository. + */ + public abstract boolean isSystemRepository(); + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java new file mode 100644 index 0000000..83527f3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +/** + * Base class representing a Resource Item, as returned by a {@link IResourceRepository}. + */ +public class ResourceItem implements Comparable<ResourceItem> { + + private final String mName; + + /** + * Constructs a new ResourceItem + * @param name the name of the resource as it appears in the XML and R.java files. + */ + public ResourceItem(String name) { + mName = name; + } + + /** + * Returns the name of the resource item. + */ + public final String getName() { + return mName; + } + + /** + * Compares the {@link ResourceItem} to another. + * @param other the ResourceItem to be compared to. + */ + public int compareTo(ResourceItem other) { + return mName.compareTo(other.mName); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java new file mode 100644 index 0000000..60c471e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +/** + * Enum representing a type of compiled resource. + */ +public enum ResourceType { + ANIM("anim", "Animation"), //$NON-NLS-1$ + ARRAY("array", "Array", "string-array", "integer-array"), //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$ + ATTR("attr", "Attr"), //$NON-NLS-1$ + COLOR("color", "Color"), //$NON-NLS-1$ + DIMEN("dimen", "Dimension"), //$NON-NLS-1$ + DRAWABLE("drawable", "Drawable"), //$NON-NLS-1$ + ID("id", "ID"), //$NON-NLS-1$ + LAYOUT("layout", "Layout"), //$NON-NLS-1$ + MENU("menu", "Menu"), //$NON-NLS-1$ + RAW("raw", "Raw"), //$NON-NLS-1$ + STRING("string", "String"), //$NON-NLS-1$ + STYLE("style", "Style"), //$NON-NLS-1$ + STYLEABLE("styleable", "Styleable"), //$NON-NLS-1$ + XML("xml", "XML"); //$NON-NLS-1$ + + private final String mName; + private final String mDisplayName; + private final String[] mAlternateXmlNames; + + ResourceType(String name, String displayName, String... alternateXmlNames) { + mName = name; + mDisplayName = displayName; + mAlternateXmlNames = alternateXmlNames; + } + + /** + * Returns the resource type name, as used by XML files. + */ + public String getName() { + return mName; + } + + /** + * Returns a translated display name for the resource type. + */ + public String getDisplayName() { + return mDisplayName; + } + + /** + * Returns the enum by its name as it appears in the XML or the R class. + * @param name name of the resource + * @return the matching {@link ResourceType} or <code>null</code> if no match was found. + */ + public static ResourceType getEnum(String name) { + for (ResourceType rType : values()) { + if (rType.mName.equals(name)) { + return rType; + } else if (rType.mAlternateXmlNames != null) { + // if there are alternate Xml Names, we test those too + for (String alternate : rType.mAlternateXmlNames) { + if (alternate.equals(name)) { + return rType; + } + } + } + } + return null; + } + + /** + * Returns a formatted string usable in an XML to use the specified {@link ResourceItem}. + * @param resourceItem The resource item. + * @param system Whether this is a system resource or a project resource. + * @return a string in the format @[type]/[name] + */ + public String getXmlString(ResourceItem resourceItem, boolean system) { + if (this == ID && resourceItem instanceof IIdResourceItem) { + IIdResourceItem idResource = (IIdResourceItem)resourceItem; + if (idResource.isDeclaredInline()) { + return (system?"@android:":"@+") + mName + "/" + resourceItem.getName(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return (system?"@android:":"@") + mName + "/" + resourceItem.getName(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + /** + * Returns an array with all the names defined by this enum. + */ + public static String[] getNames() { + ResourceType[] values = values(); + String[] names = new String[values.length]; + for (int i = values.length - 1; i >= 0; --i) { + names[i] = values[i].getName(); + } + return names; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java new file mode 100644 index 0000000..619e3cc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; + +/** + * Information needed to represent a View or ViewGroup (aka Layout) item + * in the layout hierarchy, as extracted from the main android.jar and the + * associated attrs.xml. + */ +public class ViewClassInfo { + /** Is this a layout class (i.e. ViewGroup) or just a view? */ + private boolean mIsLayout; + /** FQCN e.g. android.view.View, never null. */ + private String mCanonicalClassName; + /** Short class name, e.g. View, never null. */ + private String mShortClassName; + /** Super class. Can be null. */ + private ViewClassInfo mSuperClass; + /** Short javadoc. Can be null. */ + private String mJavaDoc; + /** Attributes for this view or view group. Can be empty but never null. */ + private AttributeInfo[] mAttributes; + + public static class LayoutParamsInfo { + /** Short class name, e.g. LayoutData, never null. */ + private String mShortClassName; + /** ViewLayout class info owning this layout data */ + private ViewClassInfo mViewLayoutClass; + /** Super class. Can be null. */ + private LayoutParamsInfo mSuperClass; + /** Layout Data Attributes for layout classes. Can be empty but not null. */ + private AttributeInfo[] mAttributes; + + public LayoutParamsInfo(ViewClassInfo enclosingViewClassInfo, + String shortClassName, LayoutParamsInfo superClassInfo) { + mShortClassName = shortClassName; + mViewLayoutClass = enclosingViewClassInfo; + mSuperClass = superClassInfo; + mAttributes = new AttributeInfo[0]; + } + + /** Returns short class name, e.g. "LayoutData" */ + public String getShortClassName() { + return mShortClassName; + } + /** Returns the ViewLayout class info enclosing this layout data. Cannot null. */ + public ViewClassInfo getViewLayoutClass() { + return mViewLayoutClass; + } + /** Returns the super class info. Can be null. */ + public LayoutParamsInfo getSuperClass() { + return mSuperClass; + } + /** Returns the LayoutData attributes. Can be empty but not null. */ + public AttributeInfo[] getAttributes() { + return mAttributes; + } + /** Sets the LayoutData attributes. Can be empty but not null. */ + public void setAttributes(AttributeInfo[] attributes) { + mAttributes = attributes; + } + } + + /** Layout data info for a layout class. Null for all non-layout classes and always + * non-null for a layout class. */ + public LayoutParamsInfo mLayoutData; + + // -------- + + public ViewClassInfo(boolean isLayout, String canonicalClassName, String shortClassName) { + mIsLayout = isLayout; + mCanonicalClassName = canonicalClassName; + mShortClassName = shortClassName; + mAttributes = new AttributeInfo[0]; + } + + /** Returns whether this is a layout class (i.e. ViewGroup) or just a View */ + public boolean isLayout() { + return mIsLayout; + } + + /** Returns FQCN e.g. "android.view.View" */ + public String getCanonicalClassName() { + return mCanonicalClassName; + } + + /** Returns short class name, e.g. "View" */ + public String getShortClassName() { + return mShortClassName; + } + + /** Returns the super class. Can be null. */ + public ViewClassInfo getSuperClass() { + return mSuperClass; + } + + /** Returns a short javadoc */ + public String getJavaDoc() { + return mJavaDoc; + } + + /** Returns the attributes for this view or view group. Maybe empty but not null. */ + public AttributeInfo[] getAttributes() { + return mAttributes; + } + + /** Returns the LayoutData info for layout classes. Null for non-layout view classes. */ + public LayoutParamsInfo getLayoutData() { + return mLayoutData; + } + + /** + * Sets a link on the info of the super class of this View or ViewGroup. + * <p/> + * The super class info must be of the same kind (i.e. group to group or view to view) + * except for the top ViewGroup which links to the View info. + * <p/> + * The super class cannot be null except for the top View info. + */ + public void setSuperClass(ViewClassInfo superClass) { + mSuperClass = superClass; + } + + /** Sets the javadoc for this View or ViewGroup. */ + public void setJavaDoc(String javaDoc) { + mJavaDoc = javaDoc; + } + + /** Sets the list of attributes for this View or ViewGroup. */ + public void setAttributes(AttributeInfo[] attributes) { + mAttributes = attributes; + } + + /** + * Sets the {@link LayoutParamsInfo} for layout classes. + * Does nothing for non-layout view classes. + */ + public void setLayoutParams(LayoutParamsInfo layoutData) { + if (mIsLayout) { + mLayoutData = layoutData; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java new file mode 100644 index 0000000..a6db786 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java @@ -0,0 +1,791 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.jface.text.contentassist.CompletionProposal; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.IContentAssistProcessor; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.jface.text.contentassist.IContextInformationValidator; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.regex.Pattern; + +/** + * Content Assist Processor for Android XML files + */ +public abstract class AndroidContentAssist implements IContentAssistProcessor { + + /** Regexp to detect a full attribute after an element tag. + * <pre>Syntax: + * name = "..." quoted string with all but < and " + * or: + * name = '...' quoted string with all but < and ' + * </pre> + */ + private static Pattern sFirstAttribute = Pattern.compile( + "^ *[a-zA-Z_:]+ *= *(?:\"[^<\"]*\"|'[^<']*')"); //$NON-NLS-1$ + + /** Regexp to detect an element tag name */ + private static Pattern sFirstElementWord = Pattern.compile("^[a-zA-Z0-9_:]+"); //$NON-NLS-1$ + + /** Regexp to detect whitespace */ + private static Pattern sWhitespace = Pattern.compile("\\s+"); //$NON-NLS-1$ + + protected final static String ROOT_ELEMENT = ""; + + /** Descriptor of the root of the XML hierarchy. This a "fake" ElementDescriptor which + * is used to list all the possible roots given by actual implementations. + * DO NOT USE DIRECTLY. Call {@link #getRootDescriptor()} instead. */ + private ElementDescriptor mRootDescriptor; + + private final int mDescriptorId; + + private AndroidEditor mEditor; + + /** + * Constructor for AndroidContentAssist + * @param descriptorId An id for {@link AndroidTargetData#getDescriptorProvider(int)}. + * The Id can be one of {@link AndroidTargetData#DESCRIPTOR_MANIFEST}, + * {@link AndroidTargetData#DESCRIPTOR_LAYOUT}, + * {@link AndroidTargetData#DESCRIPTOR_MENU}, + * or {@link AndroidTargetData#DESCRIPTOR_XML}. + * All other values will throw an {@link IllegalArgumentException} later at runtime. + */ + public AndroidContentAssist(int descriptorId) { + mDescriptorId = descriptorId; + } + + /** + * Returns a list of completion proposals based on the + * specified location within the document that corresponds + * to the current cursor position within the text viewer. + * + * @param viewer the viewer whose document is used to compute the proposals + * @param offset an offset within the document for which completions should be computed + * @return an array of completion proposals or <code>null</code> if no proposals are possible + * + * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int) + */ + public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { + + if (mEditor == null) { + mEditor = getAndroidEditor(viewer); + } + + UiElementNode rootUiNode = mEditor.getUiRootNode(); + + Object[] choices = null; /* An array of ElementDescriptor, or AttributeDescriptor + or String or null */ + String parent = ""; //$NON-NLS-1$ + String wordPrefix = extractElementPrefix(viewer, offset); + char needTag = 0; + boolean isElement = false; + boolean isAttribute = false; + + Node currentNode = getNode(viewer, offset); + if (currentNode == null) + return null; + + // check to see if we can find a UiElementNode matching this XML node + UiElementNode currentUiNode = + rootUiNode == null ? null : rootUiNode.findXmlNode(currentNode); + + if (currentNode.getNodeType() == Node.ELEMENT_NODE) { + parent = currentNode.getNodeName(); + + if (wordPrefix.equals(parent)) { + // We are still editing the element's tag name, not the attributes + // (the element's tag name may not even be complete) + isElement = true; + choices = getChoicesForElement(parent, currentNode); + } else { + // We're not editing the current node name, so we might be editing its + // attributes instead... + isAttribute = true; + AttribInfo info = parseAttributeInfo(viewer, offset); + if (info != null) { + // We're editing attributes in an element node (either the attributes' names + // or their values). + choices = getChoicesForAttribute(parent, currentNode, currentUiNode, info); + + if (info.correctedPrefix != null) { + wordPrefix = info.correctedPrefix; + } + needTag = info.needTag; + } + } + } else if (currentNode.getNodeType() == Node.TEXT_NODE) { + isElement = true; + // Examine the parent of the text node. + choices = getChoicesForTextNode(currentNode); + } + + // Abort if we can't recognize the context or there are no completion choices + if (choices == null || choices.length == 0) return null; + + if (isElement) { + // If we found some suggestions, do we need to add an opening "<" bracket + // for the element? We don't if the cursor is right after "<" or "</". + // Per XML Spec, there's no whitespace between "<" or "</" and the tag name. + int offset2 = offset - wordPrefix.length() - 1; + int c1 = extractChar(viewer, offset2); + if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) { + needTag = '<'; + } + } + + // get the selection length + int selectionLength = 0; + ISelection selection = viewer.getSelectionProvider().getSelection(); + if (selection instanceof TextSelection) { + TextSelection textSelection = (TextSelection)selection; + selectionLength = textSelection.getLength(); + } + + return computeProposals(offset, currentNode, choices, wordPrefix, needTag, + isAttribute, selectionLength); + } + + /** + * Returns the namespace prefix matching the Android Resource URI. + * If no such declaration is found, returns the default "android" prefix. + * + * @param node The current node. Must not be null. + * @param nsUri The namespace URI of which the prefix is to be found, + * e.g. {@link SdkConstants#NS_RESOURCES} + * @return The first prefix declared or the default "android" prefix. + */ + private String lookupNamespacePrefix(Node node, String nsUri) { + // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java + // The following emulates this: + // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES); + + if (XmlnsAttributeDescriptor.XMLNS_URI.equals(nsUri)) { + return "xmlns"; //$NON-NLS-1$ + } + + HashSet<String> visited = new HashSet<String>(); + + String prefix = null; + for (; prefix == null && + node != null && + node.getNodeType() == Node.ELEMENT_NODE; + node = node.getParentNode()) { + NamedNodeMap attrs = node.getAttributes(); + for (int n = attrs.getLength() - 1; n >= 0; --n) { + Node attr = attrs.item(n); + if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$ + String uri = attr.getNodeValue(); + if (SdkConstants.NS_RESOURCES.equals(uri)) { + return attr.getLocalName(); + } + visited.add(uri); + } + } + } + + // Use a sensible default prefix if we can't find one. + // We need to make sure the prefix is not one that was declared in the scope + // visited above. + prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$ + String base = prefix; + for (int i = 1; visited.contains(prefix); i++) { + prefix = base + Integer.toString(i); + } + return prefix; + } + + /** + * Gets the choices when the user is editing the name of an XML element. + * <p/> + * The user is editing the name of an element (the "parent"). + * Find the grand-parent and if one is found, return its children element list. + * The name which is being edited should be one of those. + * <p/> + * Example: <manifest><applic*cursor* => returns the list of all elements that + * can be found under <manifest>, of which <application> is one of the choices. + * + * @return an ElementDescriptor[] or null if no valid element was found. + */ + private Object[] getChoicesForElement(String parent, Node current_node) { + ElementDescriptor grandparent = null; + if (current_node.getParentNode().getNodeType() == Node.ELEMENT_NODE) { + grandparent = getDescriptor(current_node.getParentNode().getNodeName()); + } else if (current_node.getParentNode().getNodeType() == Node.DOCUMENT_NODE) { + grandparent = getRootDescriptor(); + } + if (grandparent != null) { + for (ElementDescriptor e : grandparent.getChildren()) { + if (e.getXmlName().startsWith(parent)) { + return grandparent.getChildren(); + } + } + } + + return null; + } + + /** + * Gets the choices when the user is editing an XML attribute. + * <p/> + * In input, attrInfo contains details on the analyzed context, namely whether the + * user is editing an attribute value (isInValue) or an attribute name. + * <p/> + * In output, attrInfo also contains two possible new values (this is a hack to circumvent + * the lack of out-parameters in Java): + * - AttribInfo.correctedPrefix if the user has been editing an attribute value and it has + * been detected that what the user typed is different from what extractElementPrefix() + * predicted. This happens because extractElementPrefix() stops when a character that + * cannot be an element name appears whereas parseAttributeInfo() uses a grammar more + * lenient as suitable for attribute values. + * - AttribInfo.needTag will be non-zero if we find that the attribute completion proposal + * must be double-quoted. + * @param currentUiNode + * + * @return an AttributeDescriptor[] if the user is editing an attribute name. + * a String[] if the user is editing an attribute value with some known values, + * or null if nothing is known about the context. + */ + private Object[] getChoicesForAttribute(String parent, + Node currentNode, UiElementNode currentUiNode, AttribInfo attrInfo) { + Object[] choices = null; + if (attrInfo.isInValue) { + // Editing an attribute's value... Get the attribute name and then the + // possible choice for the tuple(parent,attribute) + String value = attrInfo.value; + if (value.startsWith("'") || value.startsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$ + value = value.substring(1); + // The prefix that was found at the beginning only scan for characters + // valid of tag name. We now know the real prefix for this attribute's + // value, which is needed to generate the completion choices below. + attrInfo.correctedPrefix = value; + } else { + attrInfo.needTag = '"'; + } + + if (currentUiNode != null) { + // look for an UI attribute matching the current attribute name + String attrName = attrInfo.name; + // remove any namespace prefix from the attribute name + int pos = attrName.indexOf(':'); + if (pos >= 0) { + attrName = attrName.substring(pos + 1); + } + + UiAttributeNode currAttrNode = null; + for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) { + if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) { + currAttrNode = attrNode; + break; + } + } + + if (currAttrNode != null) { + choices = currAttrNode.getPossibleValues(); + + if (currAttrNode instanceof UiFlagAttributeNode) { + // A "flag" can consist of several values separated by "or" (|). + // If the correct prefix contains such a pipe character, we change + // it so that only the currently edited value is completed. + pos = value.indexOf('|'); + if (pos >= 0) { + attrInfo.correctedPrefix = value = value.substring(pos + 1); + attrInfo.needTag = 0; + } + } + } + } + + if (choices == null) { + // fallback on the older descriptor-only based lookup. + + // in order to properly handle the special case of the name attribute in + // the action tag, we need the grandparent of the action node, to know + // what type of actions we need. + // e.g. activity -> intent-filter -> action[@name] + String greatGrandParentName = null; + Node grandParent = currentNode.getParentNode(); + if (grandParent != null) { + Node greatGrandParent = grandParent.getParentNode(); + if (greatGrandParent != null) { + greatGrandParentName = greatGrandParent.getLocalName(); + } + } + + AndroidTargetData data = mEditor.getTargetData(); + if (data != null) { + choices = data.getAttributeValues(parent, attrInfo.name, greatGrandParentName); + } + } + } else { + // Editing an attribute's name... Get attributes valid for the parent node. + if (currentUiNode != null) { + choices = currentUiNode.getAttributeDescriptors(); + } else { + ElementDescriptor parent_desc = getDescriptor(parent); + choices = parent_desc.getAttributes(); + } + } + return choices; + } + + /** + * Gets the choices when the user is editing an XML text node. + * <p/> + * This means the user is editing outside of any XML element or attribute. + * Simply return the list of XML elements that can be present there, based on the + * parent of the current node. + * + * @return An ElementDescriptor[] or null. + */ + private Object[] getChoicesForTextNode(Node currentNode) { + Object[] choices = null; + String parent; + Node parent_node = currentNode.getParentNode(); + if (parent_node.getNodeType() == Node.ELEMENT_NODE) { + // We're editing a text node which parent is an element node. Limit + // content assist to elements valid for the parent. + parent = parent_node.getNodeName(); + ElementDescriptor desc = getDescriptor(parent); + if (desc != null) { + choices = desc.getChildren(); + } + } else if (parent_node.getNodeType() == Node.DOCUMENT_NODE) { + // We're editing a text node at the first level (i.e. root node). + // Limit content assist to the only valid root elements. + choices = getRootDescriptor().getChildren(); + } + return choices; + } + + /** + * Given a list of choices found, generates the proposals to be displayed to the user. + * <p/> + * Choices is an object array. Items of the array can be: + * - ElementDescriptor: a possible element descriptor which XML name should be completed. + * - AttributeDescriptor: a possible attribute descriptor which XML name should be completed. + * - String: string values to display as-is to the user. Typically those are possible + * values for a given attribute. + * + * @return The ICompletionProposal[] to display to the user. + */ + private ICompletionProposal[] computeProposals(int offset, Node currentNode, + Object[] choices, String wordPrefix, char need_tag, + boolean is_attribute, int selectionLength) { + ArrayList<CompletionProposal> proposals = new ArrayList<CompletionProposal>(); + HashMap<String, String> nsUriMap = new HashMap<String, String>(); + + for (Object choice : choices) { + String keyword = null; + String nsPrefix = null; + Image icon = null; + String tooltip = null; + if (choice instanceof ElementDescriptor) { + keyword = ((ElementDescriptor)choice).getXmlName(); + icon = ((ElementDescriptor)choice).getIcon(); + tooltip = DescriptorsUtils.formatTooltip(((ElementDescriptor)choice).getTooltip()); + } else if (choice instanceof TextValueDescriptor) { + continue; // Value nodes are not part of the completion choices + } else if (choice instanceof SeparatorAttributeDescriptor) { + continue; // not real attribute descriptors + } else if (choice instanceof AttributeDescriptor) { + keyword = ((AttributeDescriptor)choice).getXmlLocalName(); + icon = ((AttributeDescriptor)choice).getIcon(); + if (choice instanceof TextAttributeDescriptor) { + tooltip = ((TextAttributeDescriptor) choice).getTooltip(); + } + + String nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); + nsPrefix = nsUriMap.get(nsUri); + if (nsPrefix == null) { + nsPrefix = lookupNamespacePrefix(currentNode, nsUri); + nsUriMap.put(nsUri, nsPrefix); + } + if (nsPrefix != null) { + nsPrefix += ":"; //$NON-NLS-1$ + } + + } else if (choice instanceof String) { + keyword = (String) choice; + } else { + continue; // discard unknown choice + } + + String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword); + + if (keyword.startsWith(wordPrefix) || + (nsPrefix != null && keyword.startsWith(nsPrefix)) || + (nsPrefix != null && nsKeyword.startsWith(wordPrefix))) { + if (nsPrefix != null) { + keyword = nsPrefix + keyword; + } + String end_tag = ""; //$NON-NLS-1$ + if (need_tag != 0) { + if (need_tag == '"') { + keyword = need_tag + keyword; + end_tag = String.valueOf(need_tag); + } else if (need_tag == '<') { + if (elementCanHaveChildren(choice)) { + end_tag = String.format("></%1$s>", keyword); //$NON-NLS-1$ + keyword = need_tag + keyword; + } else { + keyword = need_tag + keyword; + end_tag = "/>"; //$NON-NLS-1$ + } + } + } + CompletionProposal proposal = new CompletionProposal( + keyword + end_tag, // String replacementString + offset - wordPrefix.length(), // int replacementOffset + wordPrefix.length() + selectionLength, // int replacementLength + keyword.length(), // int cursorPosition (rel. to rplcmntOffset) + icon, // Image image + null, // String displayString + null, // IContextInformation contextInformation + tooltip // String additionalProposalInfo + ); + + proposals.add(proposal); + } + } + + return proposals.toArray(new ICompletionProposal[proposals.size()]); + } + + /** + * Indicates whether this descriptor describes an element that can potentially + * have children (either sub-elements or text value). If an element can have children, + * we want to explicitly write an opening and a separate closing tag. + * <p/> + * Elements can have children if the descriptor has children element descriptors + * or if one of the attributes is a TextValueDescriptor. + * + * @param descriptor An ElementDescriptor or an AttributeDescriptor + * @return True if the descriptor is an ElementDescriptor that can have children or a text value + */ + private boolean elementCanHaveChildren(Object descriptor) { + if (descriptor instanceof ElementDescriptor) { + ElementDescriptor desc = (ElementDescriptor) descriptor; + if (desc.hasChildren()) { + return true; + } + for (AttributeDescriptor attr_desc : desc.getAttributes()) { + if (attr_desc instanceof TextValueDescriptor) { + return true; + } + } + } + return false; + } + + /** + * Returns the element descriptor matching a given XML node name or null if it can't be + * found. + * <p/> + * This is simplistic; ideally we should consider the parent's chain to make sure we + * can differentiate between different hierarchy trees. Right now the first match found + * is returned. + */ + private ElementDescriptor getDescriptor(String nodeName) { + return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */); + } + + public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { + return null; + } + + /** + * Returns the characters which when entered by the user should + * automatically trigger the presentation of possible completions. + * + * In our case, we auto-activate on opening tags and attributes namespace. + * + * @return the auto activation characters for completion proposal or <code>null</code> + * if no auto activation is desired + */ + public char[] getCompletionProposalAutoActivationCharacters() { + return new char[]{ '<', ':', '=' }; + } + + public char[] getContextInformationAutoActivationCharacters() { + return null; + } + + public IContextInformationValidator getContextInformationValidator() { + return null; + } + + public String getErrorMessage() { + return null; + } + + /** + * Heuristically extracts the prefix used for determining template relevance + * from the viewer's document. The default implementation returns the String from + * offset backwards that forms a potential XML element name, attribute name or + * attribute value. + * + * The part were we access the docment was extracted from + * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs. + * + * @param viewer the viewer + * @param offset offset into document + * @return the prefix to consider + */ + protected String extractElementPrefix(ITextViewer viewer, int offset) { + int i = offset; + IDocument document = viewer.getDocument(); + if (i > document.getLength()) return ""; //$NON-NLS-1$ + + try { + for (; i > 0; --i) { + char ch = document.getChar(i - 1); + + // We want all characters that can form a valid: + // - element name, e.g. anything that is a valid Java class/variable literal. + // - attribute name, including : for the namespace + // - attribute value. + // Before we were inclusive and that made the code fragile. So now we're + // going to be exclusive: take everything till we get one of: + // - any form of whitespace + // - any xml separator, e.g. < > ' " and = + if (Character.isWhitespace(ch) || + ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') { + break; + } + } + + return document.get(i, offset - i); + } catch (BadLocationException e) { + return ""; //$NON-NLS-1$ + } + } + + /** + * Extracts the character at the given offset. + * Returns 0 if the offset is invalid. + */ + protected char extractChar(ITextViewer viewer, int offset) { + IDocument document = viewer.getDocument(); + if (offset > document.getLength()) return 0; + + try { + return document.getChar(offset); + } catch (BadLocationException e) { + return 0; + } + } + + /** + * Information about the current edit of an attribute as reported by parseAttributeInfo. + */ + private class AttribInfo { + /** True if the cursor is located in an attribute's value, false if in an attribute name */ + public boolean isInValue = false; + /** The attribute name. Null when not set. */ + public String name = null; + /** The attribute value. Null when not set. The value *may* start with a quote + * (' or "), in which case we know we don't need to quote the string for the user */ + public String value = null; + /** String typed by the user so far (i.e. right before requesting code completion), + * which will be corrected if we find a possible completion for an attribute value. + * See the long comment in getChoicesForAttribute(). */ + public String correctedPrefix = null; + /** Non-zero if an attribute value need a start/end tag (i.e. quotes or brackets) */ + public char needTag = 0; + } + + + /** + * Try to guess if the cursor is editing an element's name or an attribute following an + * element. If it's an attribute, try to find if an attribute name is being defined or + * its value. + * <br/> + * This is currently *only* called when we know the cursor is after a complete element + * tag name, so it should never return null. + * <br/> + * Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags + * <br/> + * @return An AttribInfo describing which attribute is being edited or null if the cursor is + * not editing an attribute (in which case it must be an element's name). + */ + private AttribInfo parseAttributeInfo(ITextViewer viewer, int offset) { + AttribInfo info = new AttribInfo(); + + IDocument document = viewer.getDocument(); + int n = document.getLength(); + if (offset <= n) { + try { + n = offset; + for (;offset > 0; --offset) { + char ch = document.getChar(offset - 1); + if (ch == '<') break; + } + + // text will contain the full string of the current element, + // i.e. whatever is after the "<" to the current cursor + String text = document.get(offset, n - offset); + + // Normalize whitespace to single spaces + text = sWhitespace.matcher(text).replaceAll(" "); //$NON-NLS-1$ + + // Remove the leading element name. By spec, it must be after the < without + // any whitespace. If there's nothing left, no attribute has been defined yet. + // Be sure to keep any whitespace after the initial word if any, as it matters. + text = sFirstElementWord.matcher(text).replaceFirst(""); //$NON-NLS-1$ + + // There MUST be space after the element name. If not, the cursor is still + // defining the element name. + if (!text.startsWith(" ")) { //$NON-NLS-1$ + return null; + } + + // Remove full attributes: + // Syntax: + // name = "..." quoted string with all but < and " + // or: + // name = '...' quoted string with all but < and ' + String temp; + do { + temp = text; + text = sFirstAttribute.matcher(temp).replaceFirst(""); //$NON-NLS-1$ + } while(!temp.equals(text)); + + // Now we're left with 3 cases: + // - nothing: either there is no attribute definition or the cursor located after + // a completed attribute definition. + // - a string with no =: the user is writing an attribute name. This case can be + // merged with the previous one. + // - string with an = sign, optionally followed by a quote (' or "): the user is + // writing the value of the attribute. + int pos_equal = text.indexOf('='); + if (pos_equal == -1) { + info.isInValue = false; + info.name = text.trim(); + } else { + info.isInValue = true; + info.name = text.substring(0, pos_equal).trim(); + info.value = text.substring(pos_equal + 1).trim(); + } + return info; + } catch (BadLocationException e) { + // pass + } + } + + return null; + } + + + /** + * Returns the XML DOM node corresponding to the given offset of the given document. + */ + protected Node getNode(ITextViewer viewer, int offset) { + Node node = null; + try { + IModelManager mm = StructuredModelManager.getModelManager(); + if (mm != null) { + IStructuredModel model = mm.getExistingModelForRead(viewer.getDocument()); + if (model != null) { + for(; offset >= 0 && node == null; --offset) { + node = (Node) model.getIndexedRegion(offset); + } + } + } + } catch (Exception e) { + // Ignore exceptions. + } + + return node; + } + + /** + * Computes (if needed) and returns the root descriptor. + */ + private ElementDescriptor getRootDescriptor() { + if (mRootDescriptor == null) { + AndroidTargetData data = mEditor.getTargetData(); + if (data != null) { + IDescriptorProvider descriptorProvider = data.getDescriptorProvider(mDescriptorId); + + if (descriptorProvider != null) { + mRootDescriptor = new ElementDescriptor("", + descriptorProvider.getRootElementDescriptors()); + } + } + } + + return mRootDescriptor; + } + + /** + * Returns the active {@link AndroidEditor} matching this source viewer. + */ + private AndroidEditor getAndroidEditor(ITextViewer viewer) { + IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (wwin != null) { + IWorkbenchPage page = wwin.getActivePage(); + if (page != null) { + IEditorPart editor = page.getActiveEditor(); + if (editor instanceof AndroidEditor) { + ISourceViewer ssviewer = ((AndroidEditor) editor).getStructuredSourceViewer(); + if (ssviewer == viewer) { + return (AndroidEditor) editor; + } + } + } + } + + return null; + } + + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java new file mode 100644 index 0000000..c7541e9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java @@ -0,0 +1,828 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.browser.IWorkbenchBrowserSupport; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormEditor; +import org.eclipse.ui.forms.editor.IFormPage; +import org.eclipse.ui.forms.events.HyperlinkAdapter; +import org.eclipse.ui.forms.events.HyperlinkEvent; +import org.eclipse.ui.forms.events.IHyperlinkListener; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.part.MultiPageEditorPart; +import org.eclipse.ui.part.WorkbenchPart; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.sse.ui.StructuredTextEditor; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Document; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Multi-page form editor for Android XML files. + * <p/> + * It is designed to work with a {@link StructuredTextEditor} that will display an XML file. + * <br/> + * Derived classes must implement createFormPages to create the forms before the + * source editor. This can be a no-op if desired. + */ +public abstract class AndroidEditor extends FormEditor implements IResourceChangeListener { + + /** Preference name for the current page of this file */ + private static final String PREF_CURRENT_PAGE = "_current_page"; + + /** Id string used to create the Android SDK browser */ + private static String BROWSER_ID = "android"; // $NON-NLS-1$ + + /** Page id of the XML source editor, used for switching tabs programmatically */ + public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$ + + /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */ + public static final int TEXT_WIDTH_HINT = 50; + + /** Page index of the text editor (always the last page) */ + private int mTextPageIndex; + /** The text editor */ + private StructuredTextEditor mTextEditor; + /** Listener for the XML model from the StructuredEditor */ + private XmlModelStateListener mXmlModelStateListener; + /** Listener to update the root node if the target of the file is changed because of a + * SDK location change or a project target change */ + private ITargetChangeListener mTargetListener; + + /** + * Creates a form editor. + */ + public AndroidEditor() { + super(); + ResourcesPlugin.getWorkspace().addResourceChangeListener(this); + + mTargetListener = new ITargetChangeListener() { + public void onProjectTargetChange(IProject changedProject) { + if (changedProject == getProject()) { + onTargetsLoaded(); + } + } + + public void onTargetsLoaded() { + commitPages(false /* onSave */); + + // recreate the ui root node always + initUiRootNode(true /*force*/); + } + }; + AdtPlugin.getDefault().addTargetListener(mTargetListener); + } + + // ---- Abstract Methods ---- + + /** + * Returns the root node of the UI element hierarchy manipulated by the current + * UI node editor. + */ + abstract public UiElementNode getUiRootNode(); + + /** + * Creates the various form pages. + * <p/> + * Derived classes must implement this to add their own specific tabs. + */ + abstract protected void createFormPages(); + + /** + * Creates the initial UI Root Node, including the known mandatory elements. + * @param force if true, a new UiManifestNode is recreated even if it already exists. + */ + abstract protected void initUiRootNode(boolean force); + + /** + * Subclasses should override this method to process the new XML Model, which XML + * root node is given. + * + * The base implementation is empty. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + protected void xmlModelChanged(Document xml_doc) { + // pass + } + + // ---- Base Class Overrides, Interfaces Implemented ---- + + /** + * Creates the pages of the multi-page editor. + */ + @Override + protected void addPages() { + createAndroidPages(); + selectDefaultPage(null /* defaultPageId */); + } + + /** + * Creates the page for the Android Editors + */ + protected void createAndroidPages() { + createFormPages(); + createTextEditor(); + + createUndoRedoActions(); + } + + /** + * Creates undo redo actions for the editor site (so that it works for any page of this + * multi-page editor) by re-using the actions defined by the {@link StructuredTextEditor} + * (aka the XML text editor.) + */ + private void createUndoRedoActions() { + IActionBars bars = getEditorSite().getActionBars(); + if (bars != null) { + IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId()); + bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action); + + action = mTextEditor.getAction(ActionFactory.REDO.getId()); + bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action); + + bars.updateActionBars(); + } + } + + /** + * Selects the default active page. + * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to + * find the default page in the properties of the {@link IResource} object being edited. + */ + protected void selectDefaultPage(String defaultPageId) { + if (defaultPageId == null) { + if (getEditorInput() instanceof IFileEditorInput) { + IFile file = ((IFileEditorInput) getEditorInput()).getFile(); + + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, + getClass().getSimpleName() + PREF_CURRENT_PAGE); + String pageId; + try { + pageId = file.getPersistentProperty(qname); + if (pageId != null) { + defaultPageId = pageId; + } + } catch (CoreException e) { + // ignored + } + } + } + + if (defaultPageId != null) { + try { + setActivePage(Integer.parseInt(defaultPageId)); + } catch (Exception e) { + // We can get NumberFormatException from parseInt but also + // AssertionError from setActivePage when the index is out of bounds. + // Generally speaking we just want to ignore any exception and fall back on the + // first page rather than crash the editor load. Logging the error is enough. + AdtPlugin.log(e, "Selecting page '%s' in AndroidEditor failed", defaultPageId); + } + } + } + + /** + * Removes all the pages from the editor. + */ + protected void removePages() { + int count = getPageCount(); + for (int i = count - 1 ; i >= 0 ; i--) { + removePage(i); + } + } + + /** + * Overrides the parent's setActivePage to be able to switch to the xml editor. + * + * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page. + * This is needed because the editor doesn't actually derive from IFormPage and thus + * doesn't have the get-by-page-id method. In this case, the method returns null since + * IEditorPart does not implement IFormPage. + */ + @Override + public IFormPage setActivePage(String pageId) { + if (pageId.equals(TEXT_EDITOR_ID)) { + super.setActivePage(mTextPageIndex); + return null; + } else { + return super.setActivePage(pageId); + } + } + + + /** + * Notifies this multi-page editor that the page with the given id has been + * activated. This method is called when the user selects a different tab. + * + * @see MultiPageEditorPart#pageChange(int) + */ + @Override + protected void pageChange(int newPageIndex) { + super.pageChange(newPageIndex); + + if (getEditorInput() instanceof IFileEditorInput) { + IFile file = ((IFileEditorInput) getEditorInput()).getFile(); + + QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, + getClass().getSimpleName() + PREF_CURRENT_PAGE); + try { + file.setPersistentProperty(qname, Integer.toString(newPageIndex)); + } catch (CoreException e) { + // ignore + } + } + } + + /** + * Notifies this listener that some resource changes + * are happening, or have already happened. + * + * Closes all project files on project close. + * @see IResourceChangeListener + */ + public void resourceChanged(final IResourceChangeEvent event) { + if (event.getType() == IResourceChangeEvent.PRE_CLOSE) { + Display.getDefault().asyncExec(new Runnable() { + public void run() { + IWorkbenchPage[] pages = getSite().getWorkbenchWindow() + .getPages(); + for (int i = 0; i < pages.length; i++) { + if (((FileEditorInput)mTextEditor.getEditorInput()) + .getFile().getProject().equals( + event.getResource())) { + IEditorPart editorPart = pages[i].findEditor(mTextEditor + .getEditorInput()); + pages[i].closeEditor(editorPart, true); + } + } + } + }); + } + } + + /** + * Initializes the editor part with a site and input. + * <p/> + * Checks that the input is an instance of {@link IFileEditorInput}. + * + * @see FormEditor + */ + @Override + public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException { + if (!(editorInput instanceof IFileEditorInput)) + throw new PartInitException("Invalid Input: Must be IFileEditorInput"); + super.init(site, editorInput); + } + + /** + * Removes attached listeners. + * + * @see WorkbenchPart + */ + @Override + public void dispose() { + IStructuredModel xml_model = getModelForRead(); + if (xml_model != null) { + try { + if (mXmlModelStateListener != null) { + xml_model.removeModelStateListener(mXmlModelStateListener); + } + + } finally { + xml_model.releaseFromRead(); + } + } + ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); + + if (mTargetListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetListener); + mTargetListener = null; + } + + super.dispose(); + } + + /** + * Commit all dirty pages then saves the contents of the text editor. + * <p/> + * This works by committing all data to the XML model and then + * asking the Structured XML Editor to save the XML. + * + * @see IEditorPart + */ + @Override + public void doSave(IProgressMonitor monitor) { + commitPages(true /* onSave */); + + // The actual "save" operation is done by the Structured XML Editor + getEditor(mTextPageIndex).doSave(monitor); + } + + /* (non-Javadoc) + * Saves the contents of this editor to another object. + * <p> + * Subclasses must override this method to implement the open-save-close lifecycle + * for an editor. For greater details, see <code>IEditorPart</code> + * </p> + * + * @see IEditorPart + */ + @Override + public void doSaveAs() { + commitPages(true /* onSave */); + + IEditorPart editor = getEditor(mTextPageIndex); + editor.doSaveAs(); + setPageText(mTextPageIndex, editor.getTitle()); + setInput(editor.getEditorInput()); + } + + /** + * Commits all dirty pages in the editor. This method should + * be called as a first step of a 'save' operation. + * <p/> + * This is the same implementation as in {@link FormEditor} + * except it fixes two bugs: a cast to IFormPage is done + * from page.get(i) <em>before</em> being tested with instanceof. + * Another bug is that the last page might be a null pointer. + * <p/> + * The incorrect casting makes the original implementation crash due + * to our {@link StructuredTextEditor} not being an {@link IFormPage} + * so we have to override and duplicate to fix it. + * + * @param onSave <code>true</code> if commit is performed as part + * of the 'save' operation, <code>false</code> otherwise. + * @since 3.3 + */ + @Override + public void commitPages(boolean onSave) { + if (pages != null) { + for (int i = 0; i < pages.size(); i++) { + Object page = pages.get(i); + if (page != null && page instanceof IFormPage) { + IFormPage form_page = (IFormPage) page; + IManagedForm managed_form = form_page.getManagedForm(); + if (managed_form != null && managed_form.isDirty()) { + managed_form.commit(onSave); + } + } + } + } + } + + /* (non-Javadoc) + * Returns whether the "save as" operation is supported by this editor. + * <p> + * Subclasses must override this method to implement the open-save-close lifecycle + * for an editor. For greater details, see <code>IEditorPart</code> + * </p> + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return false; + } + + // ---- Local methods ---- + + + /** + * Helper method that creates a new hyper-link Listener. + * Used by derived classes which need active links in {@link FormText}. + * <p/> + * This link listener handles two kinds of URLs: + * <ul> + * <li> Links starting with "http" are simply sent to a local browser. + * <li> Links starting with "file:/" are simply sent to a local browser. + * <li> Links starting with "page:" are expected to be an editor page id to switch to. + * <li> Other links are ignored. + * </ul> + * + * @return A new hyper-link listener for FormText to use. + */ + public final IHyperlinkListener createHyperlinkListener() { + return new HyperlinkAdapter() { + /** + * Switch to the page corresponding to the link that has just been clicked. + * For this purpose, the HREF of the <a> tags above is the page ID to switch to. + */ + @Override + public void linkActivated(HyperlinkEvent e) { + super.linkActivated(e); + String link = e.data.toString(); + if (link.startsWith("http") || //$NON-NLS-1$ + link.startsWith("file:/")) { //$NON-NLS-1$ + openLinkInBrowser(link); + } else if (link.startsWith("page:")) { //$NON-NLS-1$ + // Switch to an internal page + setActivePage(link.substring(5 /* strlen("page:") */)); + } + } + }; + } + + /** + * Open the http link into a browser + * + * @param link The URL to open in a browser + */ + private void openLinkInBrowser(String link) { + try { + IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance(); + wbs.createBrowser(BROWSER_ID).openURL(new URL(link)); + } catch (PartInitException e1) { + // pass + } catch (MalformedURLException e1) { + // pass + } + } + + /** + * Creates the XML source editor. + * <p/> + * Memorizes the index page of the source editor (it's always the last page, but the number + * of pages before can change.) + * <br/> + * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it. + * Finally triggers modelChanged() on the model listener -- derived classes can use this + * to initialize the model the first time. + * <p/> + * Called only once <em>after</em> createFormPages. + */ + private void createTextEditor() { + try { + mTextEditor = new StructuredTextEditor(); + int index = addPage(mTextEditor, getEditorInput()); + mTextPageIndex = index; + setPageText(index, mTextEditor.getTitle()); + + if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) { + Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Error opening the Android XML editor. Is the document an XML file?"); + throw new RuntimeException("Android XML Editor Error", new CoreException(status)); + } + + IStructuredModel xml_model = getModelForRead(); + if (xml_model != null) { + try { + mXmlModelStateListener = new XmlModelStateListener(); + xml_model.addModelStateListener(mXmlModelStateListener); + mXmlModelStateListener.modelChanged(xml_model); + } catch (Exception e) { + AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$ + } finally { + xml_model.releaseFromRead(); + } + } + } catch (PartInitException e) { + ErrorDialog.openError(getSite().getShell(), + "Android XML Editor Error", null, e.getStatus()); + } + } + + /** + * Returns the ISourceViewer associated with the Structured Text editor. + */ + public final ISourceViewer getStructuredSourceViewer() { + if (mTextEditor != null) { + // We can't access mEditor.getSourceViewer() because it is protected, + // however getTextViewer simply returns the SourceViewer casted, so we + // can use it instead. + return mTextEditor.getTextViewer(); + } + return null; + } + + /** + * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source + * Editor) or null if not available. + */ + public final IStructuredDocument getStructuredDocument() { + if (mTextEditor != null && mTextEditor.getTextViewer() != null) { + return (IStructuredDocument) mTextEditor.getTextViewer().getDocument(); + } + return null; + } + + /** + * Returns a version of the model that has been shared for read. + * <p/> + * Callers <em>must</em> call model.releaseFromRead() when done, typically + * in a try..finally clause. + * + * @return The model for the XML document or null if cannot be obtained from the editor + */ + public final IStructuredModel getModelForRead() { + IStructuredDocument document = getStructuredDocument(); + if (document != null) { + IModelManager mm = StructuredModelManager.getModelManager(); + if (mm != null) { + return mm.getModelForRead(document); + } + } + return null; + } + + /** + * Returns a version of the model that has been shared for edit. + * <p/> + * Callers <em>must</em> call model.releaseFromEdit() when done, typically + * in a try..finally clause. + * + * @return The model for the XML document or null if cannot be obtained from the editor + */ + public final IStructuredModel getModelForEdit() { + IStructuredDocument document = getStructuredDocument(); + if (document != null) { + IModelManager mm = StructuredModelManager.getModelManager(); + if (mm != null) { + return mm.getModelForEdit(document); + } + } + return null; + } + + /** + * Helper class to perform edits on the XML model whilst making sure the + * model has been prepared to be changed. + * <p/> + * It first gets a model for edition using {@link #getModelForEdit()}, + * then calls {@link IStructuredModel#aboutToChangeModel()}, + * then performs the requested action + * and finally calls {@link IStructuredModel#changedModel()} + * and {@link IStructuredModel#releaseFromEdit()}. + * <p/> + * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method + * is called, XML model listeners will be triggered. + * + * @param edit_action Something that will change the XML. + */ + public final void editXmlModel(Runnable edit_action) { + IStructuredModel model = getModelForEdit(); + try { + model.aboutToChangeModel(); + edit_action.run(); + } finally { + // Notify the model we're done modifying it. This must *always* be executed. + model.changedModel(); + model.releaseFromEdit(); + } + } + + /** + * Starts an "undo recording" session. This is managed by the underlying undo manager + * associated to the structured XML model. + * <p/> + * There <em>must</em> be a corresponding call to {@link #endUndoRecording()}. + * <p/> + * beginUndoRecording/endUndoRecording calls can be nested (inner calls are ignored, only one + * undo operation is recorded.) + * + * @param label The label for the undo operation. Can be null but we should really try to put + * something meaningful if possible. + * @return True if the undo recording actually started, false if any kind of error occured. + * {@link #endUndoRecording()} should only be called if True is returned. + */ + private final boolean beginUndoRecording(String label) { + IStructuredDocument document = getStructuredDocument(); + if (document != null) { + IModelManager mm = StructuredModelManager.getModelManager(); + if (mm != null) { + IStructuredModel model = mm.getModelForEdit(document); + if (model != null) { + model.beginRecording(this, label); + return true; + } + } + } + return false; + } + + /** + * Ends an "undo recording" session. + * <p/> + * This is the counterpart call to {@link #beginUndoRecording(String)} and should only be + * used if the initial call returned true. + */ + private final void endUndoRecording() { + IStructuredDocument document = getStructuredDocument(); + if (document != null) { + IModelManager mm = StructuredModelManager.getModelManager(); + if (mm != null) { + IStructuredModel model = mm.getModelForEdit(document); + if (model != null) { + model.endRecording(this); + } + } + } + } + + /** + * Creates an "undo recording" session by calling the undoableAction runnable + * using {@link #beginUndoRecording(String)} and {@link #endUndoRecording()}. + * <p> + * You can nest several calls to {@link #wrapUndoRecording(String, Runnable)}, only one + * recording session will be created. + * + * @param label The label for the undo operation. Can be null. Ideally we should really try + * to put something meaningful if possible. + */ + public void wrapUndoRecording(String label, Runnable undoableAction) { + boolean recording = false; + try { + recording = beginUndoRecording(label); + undoableAction.run(); + } finally { + if (recording) { + endUndoRecording(); + } + } + } + + /** + * Returns the XML {@link Document} or null if we can't get it + */ + protected final Document getXmlDocument(IStructuredModel model) { + if (model == null) { + AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$ + return null; + } + + if (model instanceof IDOMModel) { + IDOMModel dom_model = (IDOMModel) model; + return dom_model.getDocument(); + } + return null; + } + + /** + * Returns the {@link IProject} for the edited file. + */ + public IProject getProject() { + if (mTextEditor != null) { + IEditorInput input = mTextEditor.getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + IFile inputFile = fileInput.getFile(); + + if (inputFile != null) { + return inputFile.getProject(); + } + } + } + + return null; + } + + /** + * Returns the {@link AndroidTargetData} for the edited file. + */ + public AndroidTargetData getTargetData() { + IProject project = getProject(); + if (project != null) { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(project); + + if (target != null) { + return currentSdk.getTargetData(target); + } + } + } + + return null; + } + + + /** + * Listen to changes in the underlying XML model in the structured editor. + */ + private class XmlModelStateListener implements IModelStateListener { + + /** + * A model is about to be changed. This typically is initiated by one + * client of the model, to signal a large change and/or a change to the + * model's ID or base Location. A typical use might be if a client might + * want to suspend processing until all changes have been made. + * <p/> + * This AndroidEditor implementation of IModelChangedListener is empty. + */ + public void modelAboutToBeChanged(IStructuredModel model) { + // pass + } + + /** + * Signals that the changes foretold by modelAboutToBeChanged have been + * made. A typical use might be to refresh, or to resume processing that + * was suspended as a result of modelAboutToBeChanged. + * <p/> + * This AndroidEditor implementation calls the xmlModelChanged callback. + */ + public void modelChanged(IStructuredModel model) { + xmlModelChanged(getXmlDocument(model)); + } + + /** + * Notifies that a model's dirty state has changed, and passes that state + * in isDirty. A model becomes dirty when any change is made, and becomes + * not-dirty when the model is saved. + * <p/> + * This AndroidEditor implementation of IModelChangedListener is empty. + */ + public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) { + // pass + } + + /** + * A modelDeleted means the underlying resource has been deleted. The + * model itself is not removed from model management until all have + * released it. Note: baseLocation is not (necessarily) changed in this + * event, but may not be accurate. + * <p/> + * This AndroidEditor implementation of IModelChangedListener is empty. + */ + public void modelResourceDeleted(IStructuredModel model) { + // pass + } + + /** + * A model has been renamed or copied (as in saveAs..). In the renamed + * case, the two paramenters are the same instance, and only contain the + * new info for id and base location. + * <p/> + * This AndroidEditor implementation of IModelChangedListener is empty. + */ + public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) { + // pass + } + + /** + * This AndroidEditor implementation of IModelChangedListener is empty. + */ + public void modelAboutToBeReinitialized(IStructuredModel structuredModel) { + // pass + } + + /** + * This AndroidEditor implementation of IModelChangedListener is empty. + */ + public void modelReinitialized(IStructuredModel structuredModel) { + // pass + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java new file mode 100644 index 0000000..ab17bef --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors; + + +import org.eclipse.jface.text.IAutoEditStrategy; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextHover; +import org.eclipse.jface.text.contentassist.IContentAssistProcessor; +import org.eclipse.jface.text.contentassist.IContentAssistant; +import org.eclipse.jface.text.formatter.IContentFormatter; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.viewers.IInputProvider; +import org.eclipse.wst.sse.core.text.IStructuredPartitions; +import org.eclipse.wst.xml.core.text.IXMLPartitions; +import org.eclipse.wst.xml.ui.StructuredTextViewerConfigurationXML; + +import java.util.ArrayList; + +/** + * Base Source Viewer Configuration for Android resources. + */ +public class AndroidSourceViewerConfig extends StructuredTextViewerConfigurationXML { + + /** Content Assist Processor to use for all handled partitions. */ + private IContentAssistProcessor mProcessor; + + public AndroidSourceViewerConfig(IContentAssistProcessor processor) { + super(); + mProcessor = processor; + } + + @Override + public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) { + return super.getContentAssistant(sourceViewer); + } + + /** + * Returns the content assist processors that will be used for content + * assist in the given source viewer and for the given partition type. + * + * @param sourceViewer the source viewer to be configured by this + * configuration + * @param partitionType the partition type for which the content assist + * processors are applicable + * @return IContentAssistProcessors or null if should not be supported + */ + @Override + protected IContentAssistProcessor[] getContentAssistProcessors( + ISourceViewer sourceViewer, String partitionType) { + ArrayList<IContentAssistProcessor> processors = new ArrayList<IContentAssistProcessor>(); + if (partitionType == IStructuredPartitions.UNKNOWN_PARTITION || + partitionType == IStructuredPartitions.DEFAULT_PARTITION || + partitionType == IXMLPartitions.XML_DEFAULT) { + if (sourceViewer instanceof IInputProvider) { + IInputProvider input = (IInputProvider) sourceViewer; + Object a = input.getInput(); + if (a != null) + a.toString(); + } + + IDocument doc = sourceViewer.getDocument(); + if (doc != null) + doc.toString(); + + processors.add(mProcessor); + } + + IContentAssistProcessor[] others = super.getContentAssistProcessors(sourceViewer, + partitionType); + if (others != null && others.length > 0) { + for (IContentAssistProcessor p : others) { + processors.add(p); + } + } + + if (processors.size() > 0) { + return processors.toArray(new IContentAssistProcessor[processors.size()]); + } else { + return null; + } + } + + @Override + public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) { + // TODO text hover for android xml + return super.getTextHover(sourceViewer, contentType); + } + + @Override + public IAutoEditStrategy[] getAutoEditStrategies( + ISourceViewer sourceViewer, String contentType) { + // TODO auto edit strategies for android xml + return super.getAutoEditStrategies(sourceViewer, contentType); + } + + @Override + public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) { + // TODO content formatter for android xml + return super.getContentFormatter(sourceViewer); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java new file mode 100644 index 0000000..bb0996b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Quickly parses a (potential) XML file to extract its first element (i.e. the root element) + * and namespace, if any. + * <p/> + * This is used to determine if a file is an XML document that the XmlEditor can process. + * <p/> + * TODO use this to remove the hardcoded "android" namespace prefix limitation. + */ +public final class FirstElementParser { + + private static SAXParserFactory sSaxfactory; + + /** + * Result from the XML parsing. <br/> + * Contains the name of the root XML element. <br/> + * If an XMLNS URI was specified and found, the XMLNS prefix is recorded. Otherwise it is null. + */ + public static final class Result { + private String mElement; + private String mXmlnsPrefix; + private String mXmlnsUri; + + public String getElement() { + return mElement; + } + + public String getXmlnsPrefix() { + return mXmlnsPrefix; + } + + public String getXmlnsUri() { + return mXmlnsUri; + } + + void setElement(String element) { + mElement = element; + } + + void setXmlnsPrefix(String xmlnsPrefix) { + mXmlnsPrefix = xmlnsPrefix; + } + + void setXmlnsUri(String xmlnsUri) { + mXmlnsUri = xmlnsUri; + } + } + + private static class ResultFoundException extends SAXException { } + + /** + * Parses the given filename. + * + * @param osFilename The file to parse. + * @param xmlnsUri An optional URL of which we want to know the prefix. + * @return The element details found or null if not found. + */ + public static Result parse(String osFilename, String xmlnsUri) { + if (sSaxfactory == null) { + // TODO just create a single factory in CommonPlugin and reuse it + sSaxfactory = SAXParserFactory.newInstance(); + sSaxfactory.setNamespaceAware(true); + } + + Result result = new Result(); + if (xmlnsUri != null && xmlnsUri.length() > 0) { + result.setXmlnsUri(xmlnsUri); + } + + try { + SAXParser parser = sSaxfactory.newSAXParser(); + XmlHandler handler = new XmlHandler(result); + parser.parse(new InputSource(new FileReader(osFilename)), handler); + + } catch(ResultFoundException e) { + // XML handling was aborted because the required element was found. + // Simply return the result. + return result; + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (FileNotFoundException e) { + } catch (IOException e) { + } + + return null; + } + + /** + * Private constructor. Use the static parse() method instead. + */ + private FirstElementParser() { + // pass + } + + /** + * A specialized SAX handler that captures the arguments of the very first element + * (i.e. the root element) + */ + private static class XmlHandler extends DefaultHandler { + private final Result mResult; + + public XmlHandler(Result result) { + mResult = result; + } + + /** + * Processes a namespace prefix mapping. + * I.e. for xmlns:android="some-uri", this received prefix="android" and uri="some-uri". + * <p/> + * The prefix is recorded in the result structure if the URI is the one searched for. + * <p/> + * This event happens <em>before</em> the corresponding startElement event. + */ + @Override + public void startPrefixMapping(String prefix, String uri) { + if (uri.equals(mResult.getXmlnsUri())) { + mResult.setXmlnsPrefix(prefix); + } + } + + /** + * Processes a new element start. + * <p/> + * This simply records the element name and abort processing by throwing an exception. + */ + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + mResult.setElement(localName); + throw new ResultFoundException(); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java new file mode 100644 index 0000000..2c24772 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.sdklib.SdkConstants; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Display; + +import java.util.HashMap; + +/** + * Factory to generate icons for Android Editors. + * <p/> + * Icons are kept here and reused. + */ +public class IconFactory { + + public static final int COLOR_RED = SWT.COLOR_DARK_RED; + public static final int COLOR_GREEN = SWT.COLOR_DARK_GREEN; + public static final int COLOR_BLUE = SWT.COLOR_DARK_BLUE; + public static final int COLOR_DEFAULT = SWT.COLOR_BLACK; + + public static final int SHAPE_CIRCLE = 'C'; + public static final int SHAPE_RECT = 'R'; + public static final int SHAPE_DEFAULT = SHAPE_CIRCLE; + + private static IconFactory sInstance; + + private HashMap<String, Image> mIconMap = new HashMap<String, Image>(); + private HashMap<String, ImageDescriptor> mImageDescMap = new HashMap<String, ImageDescriptor>(); + + private IconFactory() { + } + + public static synchronized IconFactory getInstance() { + if (sInstance == null) { + sInstance = new IconFactory(); + } + return sInstance; + } + + public void Dispose() { + // Dispose icons + for (Image icon : mIconMap.values()) { + // The map can contain null values + if (icon != null) { + icon.dispose(); + } + } + mIconMap.clear(); + } + + /** + * Returns an Image for a given icon name. + * <p/> + * Callers should not dispose it. + * + * @param osName The leaf name, without the extension, of an existing icon in the + * editor's "icons" directory. If it doesn't exists, a default icon will be + * generated automatically based on the name. + */ + public Image getIcon(String osName) { + return getIcon(osName, COLOR_DEFAULT, SHAPE_DEFAULT); + } + + /** + * Returns an Image for a given icon name. + * <p/> + * Callers should not dispose it. + * + * @param osName The leaf name, without the extension, of an existing icon in the + * editor's "icons" directory. If it doesn't exists, a default icon will be + * generated automatically based on the name. + * @param color The color of the text in the automatically generated icons, + * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED. + * @param shape The shape of the icon in the automatically generated icons, + * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT. + */ + public Image getIcon(String osName, int color, int shape) { + String key = Character.toString((char) shape) + Integer.toString(color) + osName; + Image icon = mIconMap.get(key); + if (icon == null && !mIconMap.containsKey(key)) { + ImageDescriptor id = getImageDescriptor(osName, color, shape); + if (id != null) { + icon = id.createImage(); + } + // Note that we store null references in the icon map, to avoid looking them + // up every time. If it didn't exist once, it will not exist later. + mIconMap.put(key, icon); + } + return icon; + } + + /** + * Returns an ImageDescriptor for a given icon name. + * <p/> + * Callers should not dispose it. + * + * @param osName The leaf name, without the extension, of an existing icon in the + * editor's "icons" directory. If it doesn't exists, a default icon will be + * generated automatically based on the name. + */ + public ImageDescriptor getImageDescriptor(String osName) { + return getImageDescriptor(osName, COLOR_DEFAULT, SHAPE_DEFAULT); + } + + /** + * Returns an ImageDescriptor for a given icon name. + * <p/> + * Callers should not dispose it. + * + * @param osName The leaf name, without the extension, of an existing icon in the + * editor's "icons" directory. If it doesn't exists, a default icon will be + * generated automatically based on the name. + * @param color The color of the text in the automatically generated icons. + * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED. + * @param shape The shape of the icon in the automatically generated icons, + * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT. + */ + public ImageDescriptor getImageDescriptor(String osName, int color, int shape) { + String key = Character.toString((char) shape) + Integer.toString(color) + osName; + ImageDescriptor id = mImageDescMap.get(key); + if (id == null && !mImageDescMap.containsKey(key)) { + id = AdtPlugin.imageDescriptorFromPlugin( + AdtPlugin.PLUGIN_ID, + String.format("/icons/%1$s.png", osName)); //$NON-NLS-1$ + + if (id == null) { + id = new LetterImageDescriptor(osName.charAt(0), color, shape); + } + + // Note that we store null references in the icon map, to avoid looking them + // up every time. If it didn't exist once, it will not exist later. + mImageDescMap.put(key, id); + } + return id; + } + + /** + * A simple image description that generates a 16x16 image which consists + * of a colored letter inside a black & white circle. + */ + private static class LetterImageDescriptor extends ImageDescriptor { + + private final char mLetter; + private final int mColor; + private final int mShape; + + public LetterImageDescriptor(char letter, int color, int shape) { + mLetter = letter; + mColor = color; + mShape = shape; + } + + @Override + public ImageData getImageData() { + + final int SX = 15; + final int SY = 15; + final int RX = 4; + final int RY = 4; + + Display display = Display.getCurrent(); + if (display == null) { + return null; + } + + Image image = new Image(display, SX, SY); + + image.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); + + GC gc = new GC(image); + gc.setAdvanced(true); + gc.setAntialias(SWT.ON); + gc.setTextAntialias(SWT.ON); + + gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); + if (mShape == SHAPE_CIRCLE) { + gc.fillOval(0, 0, SX - 1, SY - 1); + } else if (mShape == SHAPE_RECT) { + gc.fillRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY); + } + + gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK)); + gc.setLineWidth(1); + if (mShape == SHAPE_CIRCLE) { + gc.drawOval(0, 0, SX - 1, SY - 1); + } else if (mShape == SHAPE_RECT) { + gc.drawRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY); + } + + // Get a bold version of the default system font, if possible. + Font font = display.getSystemFont(); + FontData[] fds = font.getFontData(); + fds[0].setStyle(SWT.BOLD); + // use 3/4th of the circle diameter for the font size (in pixels) + // and convert it to "font points" (font points in SWT are hardcoded in an + // arbitrary 72 dpi and then converted in real pixels using whatever is + // indicated by getDPI -- at least that's how it works under Win32). + fds[0].setHeight((int) ((SY + 1) * 3./4. * 72./display.getDPI().y)); + // Note: win32 implementation always uses fds[0] so we change just that one. + // getFontData indicates that the array of fd is really an unusual thing for X11. + font = new Font(display, fds); + gc.setFont(font); + gc.setForeground(display.getSystemColor(mColor)); + + // Text measurement varies so slightly depending on the platform + int ofx = 0; + int ofy = 0; + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { + ofx = +1; + ofy = -1; + } + + String s = Character.toString(mLetter).toUpperCase(); + Point p = gc.textExtent(s); + int tx = (SX + ofx - p.x) / 2; + int ty = (SY + ofy - p.y) / 2; + gc.drawText(s, tx, ty, true /* isTransparent */); + + font.dispose(); + gc.dispose(); + + ImageData data = image.getImageData(); + image.dispose(); + return data; + } + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java new file mode 100644 index 0000000..e0ec86b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.swt.graphics.Image; + +/** + * {@link AttributeDescriptor} describes an XML attribute with its XML attribute name. + * <p/> + * An attribute descriptor also knows which UI node should be instantiated to represent + * this particular attribute (e.g. text field, icon chooser, class selector, etc.) + * Some attributes may be hidden and have no user interface at all. + * <p/> + * This is an abstract class. Derived classes must implement data description and return + * the correct UiAttributeNode-derived class. + */ +public abstract class AttributeDescriptor { + private String mXmlLocalName; + private ElementDescriptor mParent; + private final String mNsUri; + private boolean mDeprecated; + + /** + * Creates a new {@link AttributeDescriptor} + * + * @param xmlLocalName The XML name of the attribute (case sensitive) + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + */ + public AttributeDescriptor(String xmlLocalName, String nsUri) { + mXmlLocalName = xmlLocalName; + mNsUri = nsUri; + } + + /** + * Returns the XML local name of the attribute (case sensitive) + */ + public final String getXmlLocalName() { + return mXmlLocalName; + } + + public final String getNamespaceUri() { + return mNsUri; + } + + final void setParent(ElementDescriptor parent) { + mParent = parent; + } + + public final ElementDescriptor getParent() { + return mParent; + } + + public void setDeprecated(boolean isDeprecated) { + mDeprecated = isDeprecated; + } + + public boolean isDeprecated() { + return mDeprecated; + } + + /** + * Returns an optional icon for the attribute. + * <p/> + * By default this tries to return an icon based on the XML name of the attribute. + * If this fails, it tries to return the default Android logo as defined in the + * plugin. If all fails, it returns null. + * + * @return An icon for this element or null. + */ + public Image getIcon() { + IconFactory factory = IconFactory.getInstance(); + Image icon; + icon = factory.getIcon(getXmlLocalName(), IconFactory.COLOR_RED, IconFactory.SHAPE_CIRCLE); + return icon != null ? icon : AdtPlugin.getAndroidLogo(); + } + + /** + * @param uiParent The {@link UiElementNode} parent of this UI attribute. + * @return A new {@link UiAttributeNode} linked to this descriptor or null if this + * attribute has no user interface. + */ + public abstract UiAttributeNode createUiNode(UiElementNode uiParent); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java new file mode 100644 index 0000000..2729565 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.uimodel.UiAbstractTextAttributeNode; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.swt.graphics.Image; + +/** + * Label provider for {@link UiAbstractTextAttributeNode}. + */ +public class AttributeDescriptorLabelProvider implements ILabelProvider { + + private final static AttributeDescriptorLabelProvider sThis = + new AttributeDescriptorLabelProvider(); + + public static ILabelProvider getProvider() { + return sThis; + } + + public Image getImage(Object element) { + if (element instanceof UiAbstractTextAttributeNode) { + UiAbstractTextAttributeNode node = (UiAbstractTextAttributeNode) element; + if (node.getDescriptor().isDeprecated()) { + String v = node.getCurrentValue(); + if (v != null && v.length() > 0) { + IconFactory factory = IconFactory.getInstance(); + return factory.getIcon("warning"); //$NON-NLS-1$ + } + } + } + + return null; + } + + public String getText(Object element) { + if (element instanceof UiAbstractTextAttributeNode) { + return ((UiAbstractTextAttributeNode)element).getCurrentValue(); + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // TODO Auto-generated method stub + + } + + public void dispose() { + // TODO Auto-generated method stub + + } + + public boolean isLabelProperty(Object element, String property) { + // TODO Auto-generated method stub + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // TODO Auto-generated method stub + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java new file mode 100644 index 0000000..9ebf5f1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode; + +/** + * Describes a text attribute that can only contain boolean values. + * It is displayed by a {@link UiListAttributeNode}. + */ +public class BooleanAttributeDescriptor extends ListAttributeDescriptor { + + public BooleanAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip, + new String[] { "true", "false" } ); + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java new file mode 100644 index 0000000..f1d62a1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java @@ -0,0 +1,845 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo.Format; +import com.android.ide.eclipse.editors.layout.LayoutConstants; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.swt.graphics.Image; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Utility methods related to descriptors handling. + */ +public final class DescriptorsUtils { + + private static final String DEFAULT_WIDGET_PREFIX = "widget"; + + private static final int JAVADOC_BREAK_LENGTH = 60; + + /** + * The path in the online documentation for the manifest description. + * <p/> + * This is NOT a complete URL. To be used, it needs to be appended + * to {@link AndroidConstants#CODESITE_BASE_URL} or to the local SDK + * documentation. + */ + public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#"; //$NON-NLS-1$ + + public static final String IMAGE_KEY = "image"; //$NON-NLS-1$ + + private static final String CODE = "$code"; //$NON-NLS-1$ + private static final String LINK = "$link"; //$NON-NLS-1$ + private static final String ELEM = "$elem"; //$NON-NLS-1$ + private static final String BREAK = "$break"; //$NON-NLS-1$ + + /** + * The {@link ITextAttributeCreator} interface is used by the appendAttribute() method + * to provide a way for caller to override the kind of {@link TextAttributeDescriptor} + * created for a give XML attribute name. + */ + public interface ITextAttributeCreator { + /** + * Creates a new {@link TextAttributeDescriptor} instance for the given XML name, + * UI name and tooltip. + * + * @param xmlName The XML attribute name. + * @param uiName The UI attribute name. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param tooltip An optional tooltip. + * @return A new {@link TextAttributeDescriptor} (or derived) instance. + */ + public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri, + String tooltip); + } + + /** + * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}. + * + * @param attributes The list of {@link AttributeDescriptor} to append to + * @param elementXmlName Optional XML local name of the element to which attributes are + * being added. When not null, this is used to filter overrides. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param infos The array of {@link AttributeInfo} to read and append to attributes + * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append + * a "*" to their UI name as a hint for the user.) If not null, must contains + * entries in the form "elem-name/attr-name". Elem-name can be "*". + * @param overrides A map [attribute name => TextAttributeDescriptor creator]. A creator + * can either by a Class<? extends TextAttributeDescriptor> or an instance of + * {@link ITextAttributeCreator} that instantiates the right TextAttributeDescriptor. + */ + public static void appendAttributes(ArrayList<AttributeDescriptor> attributes, + String elementXmlName, + String nsUri, AttributeInfo[] infos, + Set<String> requiredAttributes, + Map<String, Object> overrides) { + for (AttributeInfo info : infos) { + boolean required = false; + if (requiredAttributes != null) { + String attr_name = info.getName(); + if (requiredAttributes.contains("*/" + attr_name) || + requiredAttributes.contains(elementXmlName + "/" + attr_name)) { + required = true; + } + } + appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides); + } + } + + /** + * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}. + * + * @param attributes The list of {@link AttributeDescriptor} to append to + * @param elementXmlName Optional XML local name of the element to which attributes are + * being added. When not null, this is used to filter overrides. + * @param info The {@link AttributeInfo} to append to attributes + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param required True if the attribute is to be marked as "required" (i.e. append + * a "*" to its UI name as a hint for the user.) + * @param overrides A map [attribute name => TextAttributeDescriptor creator]. A creator + * can either by a Class<? extends TextAttributeDescriptor> or an instance of + * {@link ITextAttributeCreator} that instantiates the right TextAttributeDescriptor. + */ + public static void appendAttribute(ArrayList<AttributeDescriptor> attributes, + String elementXmlName, + String nsUri, + AttributeInfo info, boolean required, + Map<String, Object> overrides) { + AttributeDescriptor attr = null; + + String xmlLocalName = info.getName(); + String uiName = prettyAttributeUiName(info.getName()); // ui_name + if (required) { + uiName += "*"; //$NON-NLS-1$ + } + + String tooltip = null; + String rawTooltip = info.getJavaDoc(); + if (rawTooltip == null) { + rawTooltip = ""; + } + + String deprecated = info.getDeprecatedDoc(); + if (deprecated != null) { + if (rawTooltip.length() > 0) { + rawTooltip += "@@"; //$NON-NLS-1$ insert a break + } + rawTooltip += "* Deprecated"; + if (deprecated.length() != 0) { + rawTooltip += ": " + deprecated; //$NON-NLS-1$ + } + if (deprecated.length() == 0 || !deprecated.endsWith(".")) { //$NON-NLS-1$ + rawTooltip += "."; //$NON-NLS-1$ + } + } + + // Add the known types to the tooltip + Format[] formats_list = info.getFormats(); + int flen = formats_list.length; + if (flen > 0) { + // Fill the formats in a set for faster access + HashSet<Format> formats_set = new HashSet<Format>(); + + StringBuilder sb = new StringBuilder(); + if (rawTooltip != null && rawTooltip.length() > 0) { + sb.append(rawTooltip); + sb.append(" "); //$NON-NLS-1$ + } + if (sb.length() > 0) { + sb.append("@@"); //$NON-NLS-1$ @@ inserts a break before the types + } + sb.append("["); //$NON-NLS-1$ + for (int i = 0; i < flen; i++) { + Format f = formats_list[i]; + formats_set.add(f); + + sb.append(f.toString().toLowerCase()); + if (i < flen - 1) { + sb.append(", "); //$NON-NLS-1$ + } + } + // The extra space at the end makes the tooltip more readable on Windows. + sb.append("]"); //$NON-NLS-1$ + + if (required) { + sb.append(".@@* "); //$NON-NLS-1$ @@ inserts a break. + sb.append("Required."); + } + + // The extra space at the end makes the tooltip more readable on Windows. + sb.append(" "); //$NON-NLS-1$ + + rawTooltip = sb.toString(); + tooltip = formatTooltip(rawTooltip); + + // Create a specialized attribute if we can + if (overrides != null) { + for (Entry<String, Object> entry: overrides.entrySet()) { + String key = entry.getKey(); + String elements[] = key.split("/"); //$NON-NLS-1$ + String overrideAttrLocalName = null; + if (elements.length < 1) { + continue; + } else if (elements.length == 1) { + overrideAttrLocalName = elements[0]; + elements = null; + } else { + overrideAttrLocalName = elements[elements.length - 1]; + elements = elements[0].split(","); //$NON-NLS-1$ + } + + if (overrideAttrLocalName == null || + !overrideAttrLocalName.equals(xmlLocalName)) { + continue; + } + + boolean ok_element = elements.length < 1; + if (!ok_element) { + for (String element : elements) { + if (element.equals("*") //$NON-NLS-1$ + || element.equals(elementXmlName)) { + ok_element = true; + break; + } + } + } + + if (!ok_element) { + continue; + } + + Object override = entry.getValue(); + if (override instanceof Class) { + try { + // The override is instance of the class to create, which must + // have a constructor compatible with TextAttributeDescriptor. + @SuppressWarnings("unchecked") //$NON-NLS-1$ + Class<? extends TextAttributeDescriptor> clazz = + (Class<? extends TextAttributeDescriptor>) override; + Constructor<? extends TextAttributeDescriptor> cons; + cons = clazz.getConstructor(new Class<?>[] { + String.class, String.class, String.class, String.class } ); + attr = cons.newInstance( + new Object[] { xmlLocalName, uiName, nsUri, tooltip }); + } catch (SecurityException e) { + // ignore + } catch (NoSuchMethodException e) { + // ignore + } catch (IllegalArgumentException e) { + // ignore + } catch (InstantiationException e) { + // ignore + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } else if (override instanceof ITextAttributeCreator) { + attr = ((ITextAttributeCreator) override).create( + xmlLocalName, uiName, nsUri, tooltip); + } + } + } // if overrides + + // Create a specialized descriptor if we can, based on type + if (attr == null) { + if (formats_set.contains(Format.REFERENCE)) { + // This is either a multi-type reference or a generic reference. + attr = new ReferenceAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip); + } else if (formats_set.contains(Format.ENUM)) { + attr = new ListAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip, + info.getEnumValues()); + } else if (formats_set.contains(Format.FLAG)) { + attr = new FlagAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip, + info.getFlagValues()); + } else if (formats_set.contains(Format.BOOLEAN)) { + attr = new BooleanAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip); + } else if (formats_set.contains(Format.STRING)) { + attr = new ReferenceAttributeDescriptor(ResourceType.STRING, + xmlLocalName, uiName, nsUri, + tooltip); + } + } + } + + // By default a simple text field is used + if (attr == null) { + if (tooltip == null) { + tooltip = formatTooltip(rawTooltip); + } + attr = new TextAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip); + } + attr.setDeprecated(info.getDeprecatedDoc() != null); + attributes.add(attr); + } + + /** + * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of + * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same + * XML name. + * + * @param attributes The list of {@link AttributeDescriptor} to compare to. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param info The {@link AttributeInfo} to know whether it is included in the above list. + * @return True if this {@link AttributeInfo} is already present in + * the {@link AttributeDescriptor} list. + */ + public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes, + String nsUri, + AttributeInfo info) { + String xmlLocalName = info.getName(); + for (AttributeDescriptor desc : attributes) { + if (desc.getXmlLocalName().equals(xmlLocalName)) { + if (nsUri == desc.getNamespaceUri() || + (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) { + return true; + } + } + } + return false; + } + + /** + * Create a pretty attribute UI name from an XML name. + * <p/> + * The original xml name starts with a lower case and is camel-case, + * e.g. "maxWidthForView". The pretty name starts with an upper case + * and has space separators, e.g. "Max width for view". + */ + public static String prettyAttributeUiName(String name) { + if (name.length() < 1) { + return name; + } + StringBuffer buf = new StringBuffer(); + + char c = name.charAt(0); + // Use upper case initial letter + buf.append((char)(c >= 'a' && c <= 'z' ? c + 'A' - 'a' : c)); + int len = name.length(); + for (int i = 1; i < len; i++) { + c = name.charAt(i); + if (c >= 'A' && c <= 'Z') { + // Break camel case into separate words + buf.append(' '); + // Use a lower case initial letter for the next word, except if the + // word is solely X, Y or Z. + if (c >= 'X' && c <= 'Z' && + (i == len-1 || + (i < len-1 && name.charAt(i+1) >= 'A' && name.charAt(i+1) <= 'Z'))) { + buf.append(c); + } else { + buf.append((char)(c - 'A' + 'a')); + } + } else if (c == '_') { + buf.append(' '); + } else { + buf.append(c); + } + } + + name = buf.toString(); + + // Replace these acronyms by upper-case versions + // - (?<=^| ) means "if preceded by a space or beginning of string" + // - (?=$| ) means "if followed by a space or end of string" + name = name.replaceAll("(?<=^| )sdk(?=$| )", "SDK"); + name = name.replaceAll("(?<=^| )uri(?=$| )", "URI"); + + return name; + } + + /** + * Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z]. + * Returns the string unmodified if the first character is not [a-z]. + * + * @param str The string to capitalize. + * @return The capitalized string + */ + public static String capitalize(String str) { + if (str == null || str.length() < 1 || str.charAt(0) < 'a' || str.charAt(0) > 'z') { + return str; + } + + StringBuilder sb = new StringBuilder(); + sb.append((char)(str.charAt(0) + 'A' - 'a')); + sb.append(str.substring(1)); + return sb.toString(); + } + + /** + * Formats the javadoc tooltip to be usable in a tooltip. + */ + public static String formatTooltip(String javadoc) { + ArrayList<String> spans = scanJavadoc(javadoc); + + StringBuilder sb = new StringBuilder(); + boolean needBreak = false; + + for (int n = spans.size(), i = 0; i < n; ++i) { + String s = spans.get(i); + if (CODE.equals(s)) { + s = spans.get(++i); + if (s != null) { + sb.append('"').append(s).append('"'); + } + } else if (LINK.equals(s)) { + String base = spans.get(++i); + String anchor = spans.get(++i); + String text = spans.get(++i); + + if (base != null) { + base = base.trim(); + } + if (anchor != null) { + anchor = anchor.trim(); + } + if (text != null) { + text = text.trim(); + } + + // If there's no text, use the anchor if there's one + if (text == null || text.length() == 0) { + text = anchor; + } + + if (base != null && base.length() > 0) { + if (text == null || text.length() == 0) { + // If we still have no text, use the base as text + text = base; + } + } + + if (text != null) { + sb.append(text); + } + + } else if (ELEM.equals(s)) { + s = spans.get(++i); + if (s != null) { + sb.append(s); + } + } else if (BREAK.equals(s)) { + needBreak = true; + } else if (s != null) { + if (needBreak && s.trim().length() > 0) { + sb.append('\r'); + } + sb.append(s); + needBreak = false; + } + } + + return sb.toString(); + } + + /** + * Formats the javadoc tooltip to be usable in a FormText. + * <p/> + * If the descriptor can provide an icon, the caller should provide + * elementsDescriptor.getIcon() as "image" to FormText, e.g.: + * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code> + * + * @param javadoc The javadoc to format. Cannot be null. + * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null. + * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be + * <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code> + */ + public static String formatFormText(String javadoc, + ElementDescriptor elementDescriptor, + String androidDocBaseUrl) { + ArrayList<String> spans = scanJavadoc(javadoc); + + String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL; + String sdkUrl = elementDescriptor.getSdkUrl(); + if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) { + fullSdkUrl = androidDocBaseUrl + sdkUrl; + } + + StringBuilder sb = new StringBuilder(); + + Image icon = elementDescriptor.getIcon(); + if (icon != null) { + sb.append("<form><li style=\"image\" value=\"" + //$NON-NLS-1$ + IMAGE_KEY + "\">"); //$NON-NLS-1$ + } else { + sb.append("<form><p>"); //$NON-NLS-1$ + } + + for (int n = spans.size(), i = 0; i < n; ++i) { + String s = spans.get(i); + if (CODE.equals(s)) { + s = spans.get(++i); + if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) { + sb.append("<a href=\""); //$NON-NLS-1$ + sb.append(fullSdkUrl); + sb.append("\">"); //$NON-NLS-1$ + sb.append(s); + sb.append("</a>"); //$NON-NLS-1$ + } else if (s != null) { + sb.append('"').append(s).append('"'); + } + } else if (LINK.equals(s)) { + String base = spans.get(++i); + String anchor = spans.get(++i); + String text = spans.get(++i); + + if (base != null) { + base = base.trim(); + } + if (anchor != null) { + anchor = anchor.trim(); + } + if (text != null) { + text = text.trim(); + } + + // If there's no text, use the anchor if there's one + if (text == null || text.length() == 0) { + text = anchor; + } + + // TODO specialize with a base URL for views, menus & other resources + // Base is empty for a local page anchor, in which case we'll replace it + // by the element SDK URL if it exists. + if ((base == null || base.length() == 0) && fullSdkUrl != null) { + base = fullSdkUrl; + } + + String url = null; + if (base != null && base.length() > 0) { + if (base.startsWith("http")) { //$NON-NLS-1$ + // If base looks an URL, use it, with the optional anchor + url = base; + if (anchor != null && anchor.length() > 0) { + // If the base URL already has an anchor, it needs to be + // removed first. If there's no anchor, we need to add "#" + int pos = url.lastIndexOf('#'); + if (pos < 0) { + url += "#"; //$NON-NLS-1$ + } else if (pos < url.length() - 1) { + url = url.substring(0, pos + 1); + } + + url += anchor; + } + } else if (text == null || text.length() == 0) { + // If we still have no text, use the base as text + text = base; + } + } + + if (url != null && text != null) { + sb.append("<a href=\""); //$NON-NLS-1$ + sb.append(url); + sb.append("\">"); //$NON-NLS-1$ + sb.append(text); + sb.append("</a>"); //$NON-NLS-1$ + } else if (text != null) { + sb.append("<b>").append(text).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + } else if (ELEM.equals(s)) { + s = spans.get(++i); + if (sdkUrl != null && s != null) { + sb.append("<a href=\""); //$NON-NLS-1$ + sb.append(sdkUrl); + sb.append("\">"); //$NON-NLS-1$ + sb.append(s); + sb.append("</a>"); //$NON-NLS-1$ + } else if (s != null) { + sb.append("<b>").append(s).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } else if (BREAK.equals(s)) { + // ignore line breaks in pseudo-HTML rendering + } else if (s != null) { + sb.append(s); + } + } + + if (icon != null) { + sb.append("</li></form>"); //$NON-NLS-1$ + } else { + sb.append("</p></form>"); //$NON-NLS-1$ + } + return sb.toString(); + } + + private static ArrayList<String> scanJavadoc(String javadoc) { + ArrayList<String> spans = new ArrayList<String>(); + + // Standardize all whitespace in the javadoc to single spaces. + if (javadoc != null) { + javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + } + + // Detects {@link <base>#<name> <text>} where all 3 are optional + Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$ + // Detects <code>blah</code> + Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)"); //$NON-NLS-1$ + // Detects @blah@, used in hard-coded tooltip descriptors + Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)"); //$NON-NLS-1$ + // Detects a buffer that starts by @@ (request for a break) + Pattern p_break = Pattern.compile("@@(.*)"); //$NON-NLS-1$ + // Detects a buffer that starts by @ < or { (one that was not matched above) + Pattern p_open = Pattern.compile("([@<\\{])(.*)"); //$NON-NLS-1$ + // Detects everything till the next potential separator, i.e. @ < or { + Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)"); //$NON-NLS-1$ + + int currentLength = 0; + String text = null; + + while(javadoc != null && javadoc.length() > 0) { + Matcher m; + String s = null; + if ((m = p_code.matcher(javadoc)).matches()) { + spans.add(CODE); + spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text + javadoc = m.group(2); + if (text != null) { + currentLength += text.length(); + } + } else if ((m = p_link.matcher(javadoc)).matches()) { + spans.add(LINK); + spans.add(m.group(1)); // @link base + spans.add(m.group(2)); // @link anchor + spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text + javadoc = m.group(4); + if (text != null) { + currentLength += text.length(); + } + } else if ((m = p_elem.matcher(javadoc)).matches()) { + spans.add(ELEM); + spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@ + javadoc = m.group(2); + if (text != null) { + currentLength += text.length() - 2; + } + } else if ((m = p_break.matcher(javadoc)).matches()) { + spans.add(BREAK); + currentLength = 0; + javadoc = m.group(1); + } else if ((m = p_open.matcher(javadoc)).matches()) { + s = m.group(1); + javadoc = m.group(2); + } else if ((m = p_text.matcher(javadoc)).matches()) { + s = m.group(1); + javadoc = m.group(2); + } else { + // This is not supposed to happen. In case of, just use everything. + s = javadoc; + javadoc = null; + } + if (s != null && s.length() > 0) { + s = cleanupJavadocHtml(s); + + if (currentLength >= JAVADOC_BREAK_LENGTH) { + spans.add(BREAK); + currentLength = 0; + } + while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) { + int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength); + if (pos <= 0) { + break; + } + spans.add(s.substring(0, pos + 1)); + spans.add(BREAK); + currentLength = 0; + s = s.substring(pos + 1); + } + + spans.add(s); + currentLength += s.length(); + } + } + + return spans; + } + + /** + * Remove anything that looks like HTML from a javadoc snippet, as it is supported + * neither by FormText nor a standard text tooltip. + */ + private static String cleanupJavadocHtml(String s) { + if (s != null) { + s = s.replaceAll("<", "\""); //$NON-NLS-1$ $NON-NLS-2$ + s = s.replaceAll(">", "\""); //$NON-NLS-1$ $NON-NLS-2$ + s = s.replaceAll("<[^>]+>", ""); //$NON-NLS-1$ $NON-NLS-2$ + } + return s; + } + + /** + * Sets the default layout attributes for the a new UiElementNode. + * <p/> + * Note that ideally the node should already be part of a hierarchy so that its + * parent layout and previous sibling can be determined, if any. + * <p/> + * This does not override attributes which are not empty. + */ + public static void setDefaultLayoutAttributes(UiElementNode ui_node, boolean updateLayout) { + // if this ui_node is a layout and we're adding it to a document, use fill_parent for + // both W/H. Otherwise default to wrap_layout. + boolean fill = ui_node.getDescriptor().hasChildren() && + ui_node.getUiParent() instanceof UiDocumentNode; + ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_WIDTH, + fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT, + false /* override */); + ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_HEIGHT, + fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT, + false /* override */); + + String widget_id = getFreeWidgetId(ui_node.getUiRoot(), + new Object[] { ui_node.getDescriptor().getXmlLocalName(), null, null, null }); + if (widget_id != null) { + ui_node.setAttributeValue(LayoutConstants.ATTR_ID, "@+id/" + widget_id, //$NON-NLS-1$ + false /* override */); + } + + ui_node.setAttributeValue(LayoutConstants.ATTR_TEXT, widget_id, false /*override*/); + + if (updateLayout) { + UiElementNode ui_parent = ui_node.getUiParent(); + if (ui_parent != null && + ui_parent.getDescriptor().getXmlLocalName().equals( + LayoutConstants.RELATIVE_LAYOUT)) { + UiElementNode ui_previous = ui_node.getUiPreviousSibling(); + if (ui_previous != null) { + String id = ui_previous.getAttributeValue(LayoutConstants.ATTR_ID); + if (id != null && id.length() > 0) { + id = id.replace("@+", "@"); //$NON-NLS-1$ //$NON-NLS-2$ + ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_BELOW, id, + false /* override */); + } + } + } + } + } + + /** + * Given a UI root node, returns the first available id that matches the + * pattern "prefix%02d". + * + * @param uiNode The UI node that gives the prefix to match. + * @return A suitable generated id + */ + public static String getFreeWidgetId(UiElementNode uiNode) { + return getFreeWidgetId(uiNode.getUiRoot(), + new Object[] { uiNode.getDescriptor().getXmlLocalName(), null, null, null }); + } + + /** + * Given a UI root node, returns the first available id that matches the + * pattern "prefix%02d". + * + * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters + * in methods and we're not going to do a dedicated type, we just use an object array which + * must contain one initial item and several are built on the fly just for internal storage: + * <ul> + * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null. + * <li> index(Integer): The minimum index of the generated id. Must start with null. + * <li> generated(String): The generated widget currently being searched. Must start with null. + * <li> map(Set<String>): A set of the ids collected so far when walking through the widget + * hierarchy. Must start with null. + * </ul> + * + * @param uiRoot The Ui root node where to start searching recursively. For the initial call + * you want to pass the document root. + * @param params An in-out context of parameters used during recursion, as explained above. + * @return A suitable generated id + */ + @SuppressWarnings("unchecked") + private static String getFreeWidgetId(UiElementNode uiRoot, + Object[] params) { + + Set<String> map = (Set<String>)params[3]; + if (map == null) { + params[3] = map = new HashSet<String>(); + } + + int num = params[1] == null ? 0 : ((Integer)params[1]).intValue(); + + String generated = (String) params[2]; + String prefix = (String) params[0]; + if (generated == null) { + int pos = prefix.indexOf('.'); + if (pos >= 0) { + prefix = prefix.substring(pos + 1); + } + pos = prefix.indexOf('$'); + if (pos >= 0) { + prefix = prefix.substring(pos + 1); + } + prefix = prefix.replaceAll("[^a-zA-Z]", ""); //$NON-NLS-1$ $NON-NLS-2$ + if (prefix.length() == 0) { + prefix = DEFAULT_WIDGET_PREFIX; + } + + do { + num++; + generated = String.format("%1$s%2$02d", prefix, num); //$NON-NLS-1$ + } while (map.contains(generated)); + + params[0] = prefix; + params[1] = num; + params[2] = generated; + } + + String id = uiRoot.getAttributeValue(LayoutConstants.ATTR_ID); + if (id != null) { + id = id.replace("@+id/", ""); //$NON-NLS-1$ $NON-NLS-2$ + id = id.replace("@id/", ""); //$NON-NLS-1$ $NON-NLS-2$ + if (map.add(id) && map.contains(generated)) { + + do { + num++; + generated = String.format("%1$s%2$02d", prefix, num); //$NON-NLS-1$ + } while (map.contains(generated)); + + params[1] = num; + params[2] = generated; + } + } + + for (UiElementNode uiChild : uiRoot.getUiChildren()) { + getFreeWidgetId(uiChild, params); + } + + // Note: return params[2] (not "generated") since it could have changed during recursion. + return (String) params[2]; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java new file mode 100644 index 0000000..7d296f7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * {@link DocumentDescriptor} describes the properties expected for an XML document node. + * + * Compared to ElementDescriptor, {@link DocumentDescriptor} does not have XML name nor UI name, + * tooltip, SDK url and attributes list. + * <p/> + * It has a children list which represent all the possible roots of the document. + * <p/> + * The document nodes are "mandatory", meaning the UI node is never deleted and it may lack + * an actual XML node attached. + */ +public class DocumentDescriptor extends ElementDescriptor { + + /** + * Constructs a new {@link DocumentDescriptor} based on its XML name and children list. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * <p/> + * The XML name is never shown in the UI directly. It is however used when an icon + * needs to be found for the node. + * + * @param xml_name The XML element node name. Case sensitive. + * @param children The list of allowed children. Can be null or empty. + */ + public DocumentDescriptor(String xml_name, ElementDescriptor[] children) { + super(xml_name, children, true /* mandatory */); + } + + /** + * @return A new {@link UiElementNode} linked to this descriptor. + */ + @Override + public UiElementNode createUiNode() { + return new UiDocumentNode(this); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java new file mode 100644 index 0000000..5550155 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; + +import java.util.HashSet; +import java.util.Set; + +/** + * {@link ElementDescriptor} describes the properties expected for a given XML element node. + * + * {@link ElementDescriptor} have an XML name, UI name, a tooltip, an SDK url, + * an attributes list and a children list. + * + * An UI node can be "mandatory", meaning the UI node is never deleted and it may lack + * an actual XML node attached. A non-mandatory UI node MUST have an XML node attached + * and it will cease to exist when the XML node ceases to exist. + */ +public class ElementDescriptor { + /** The XML element node name. Case sensitive. */ + private String mXmlName; + /** The XML element name for the user interface, typically capitalized. */ + private String mUiName; + /** The list of allowed attributes. */ + private AttributeDescriptor[] mAttributes; + /** The list of allowed children */ + private ElementDescriptor[] mChildren; + /* An optional tooltip. Can be empty. */ + private String mTooltip; + /** An optional SKD URL. Can be empty. */ + private String mSdkUrl; + /** Whether this UI node must always exist (even for empty models). */ + private boolean mMandatory; + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name, UI name, + * tooltip, SDK url, attributes list, children list and mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). A mandatory + * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory + * UI node MUST have an XML node attached and it will cease to exist when the XML node + * ceases to exist. + */ + public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url, + AttributeDescriptor[] attributes, + ElementDescriptor[] children, + boolean mandatory) { + mMandatory = mandatory; + mXmlName = xml_name; + mUiName = ui_name; + mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null; + mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null; + setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{}); + mChildren = children != null ? children : new ElementDescriptor[]{}; + } + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name and children list. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). A mandatory + * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory + * UI node MUST have an XML node attached and it will cease to exist when the XML node + * ceases to exist. + */ + public ElementDescriptor(String xml_name, ElementDescriptor[] children, boolean mandatory) { + this(xml_name, prettyName(xml_name), null, null, null, children, mandatory); + } + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name and children list. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param children The list of allowed children. Can be null or empty. + */ + public ElementDescriptor(String xml_name, ElementDescriptor[] children) { + this(xml_name, prettyName(xml_name), null, null, null, children, false); + } + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + */ + public ElementDescriptor(String xml_name) { + this(xml_name, prettyName(xml_name), null, null, null, null, false); + } + + /** Returns whether this node must always exist (even for empty models) */ + public boolean isMandatory() { + return mMandatory; + } + + /** + * Returns the XML element node local name (case sensitive) + */ + public final String getXmlLocalName() { + int pos = mXmlName.indexOf(':'); + if (pos != -1) { + return mXmlName.substring(pos+1); + } + return mXmlName; + } + + /** Returns the XML element node name. Case sensitive. */ + public String getXmlName() { + return mXmlName; + } + + /** + * Returns the namespace of the attribute. + */ + public final String getNamespace() { + // For now we hard-code the prefix as being "android" + if (mXmlName.startsWith("android:")) { //$NON-NLs-1$ + return SdkConstants.NS_RESOURCES; + } + + return ""; //$NON-NLs-1$ + } + + + /** Returns the XML element name for the user interface, typically capitalized. */ + public String getUiName() { + return mUiName; + } + + /** + * Returns an optional icon for the element. + * <p/> + * By default this tries to return an icon based on the XML name of the element. + * If this fails, it tries to return the default Android logo as defined in the + * plugin. If all fails, it returns null. + * + * @return An icon for this element or null. + */ + public Image getIcon() { + IconFactory factory = IconFactory.getInstance(); + int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN; + int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE; + Image icon = factory.getIcon(mXmlName, color, shape); + return icon != null ? icon : AdtPlugin.getAndroidLogo(); + } + + /** + * Returns an optional ImageDescriptor for the element. + * <p/> + * By default this tries to return an image based on the XML name of the element. + * If this fails, it tries to return the default Android logo as defined in the + * plugin. If all fails, it returns null. + * + * @return An ImageDescriptor for this element or null. + */ + public ImageDescriptor getImageDescriptor() { + IconFactory factory = IconFactory.getInstance(); + int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN; + int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE; + ImageDescriptor id = factory.getImageDescriptor(mXmlName, color, shape); + return id != null ? id : AdtPlugin.getAndroidLogoDesc(); + } + + /* Returns the list of allowed attributes. */ + public AttributeDescriptor[] getAttributes() { + return mAttributes; + } + + /* Sets the list of allowed attributes. */ + public void setAttributes(AttributeDescriptor[] attributes) { + mAttributes = attributes; + for (AttributeDescriptor attribute : attributes) { + attribute.setParent(this); + } + } + + /** Returns the list of allowed children */ + public ElementDescriptor[] getChildren() { + return mChildren; + } + + /** @return True if this descriptor has children available */ + public boolean hasChildren() { + return mChildren.length > 0; + } + + /** Sets the list of allowed children. */ + public void setChildren(ElementDescriptor[] newChildren) { + mChildren = newChildren; + } + + /** + * Returns an optional tooltip. Will be null if not present. + * <p/> + * The tooltip is based on the Javadoc of the element and already processed via + * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as + * a UI tooltip. + */ + public String getTooltip() { + return mTooltip; + } + + /** Returns an optional SKD URL. Will be null if not present. */ + public String getSdkUrl() { + return mSdkUrl; + } + + /** Sets the optional tooltip. Can be null or empty. */ + public void setTooltip(String tooltip) { + mTooltip = tooltip; + } + + /** Sets the optional SDK URL. Can be null or empty. */ + public void setSdkUrl(String sdkUrl) { + mSdkUrl = sdkUrl; + } + + /** + * @return A new {@link UiElementNode} linked to this descriptor. + */ + public UiElementNode createUiNode() { + return new UiElementNode(this); + } + + /** + * Returns the first children of this descriptor that describes the given XML element name. + * <p/> + * In recursive mode, searches the direct children first before descending in the hierarchy. + * + * @return The ElementDescriptor matching the requested XML node element name or null. + */ + public ElementDescriptor findChildrenDescriptor(String element_name, boolean recursive) { + return findChildrenDescriptorInternal(element_name, recursive, null); + } + + private ElementDescriptor findChildrenDescriptorInternal(String element_name, + boolean recursive, + Set<ElementDescriptor> visited) { + if (recursive && visited == null) { + visited = new HashSet<ElementDescriptor>(); + } + + for (ElementDescriptor e : getChildren()) { + if (e.getXmlName().equals(element_name)) { + return e; + } + } + + if (visited != null) { + visited.add(this); + } + + if (recursive) { + for (ElementDescriptor e : getChildren()) { + if (visited != null) { + if (!visited.add(e)) { // Set.add() returns false if element is already present + continue; + } + } + ElementDescriptor f = e.findChildrenDescriptorInternal(element_name, + recursive, visited); + if (f != null) { + return f; + } + } + } + + return null; + } + + /** + * Utility helper than pretty-formats an XML Name for the UI. + * This is used by the simplified constructor that takes only an XML element name. + * + * @param xml_name The XML name to convert. + * @return The XML name with dashes replaced by spaces and capitalized. + */ + private static String prettyName(String xml_name) { + char c[] = xml_name.toCharArray(); + if (c.length > 0) { + c[0] = Character.toUpperCase(c[0]); + } + return new String(c).replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java new file mode 100644 index 0000000..cab9883 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode; + +/** + * Describes a text attribute that can only contains some predefined values. + * It is displayed by a {@link UiListAttributeNode}. + */ +public class EnumAttributeDescriptor extends ListAttributeDescriptor { + + public EnumAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * @return A new {@link UiListAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiListAttributeNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java new file mode 100644 index 0000000..903417b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.ui.FlagValueCellEditor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode; + +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Describes a text attribute that can only contains some predefined values. + * It is displayed by a {@link UiListAttributeNode}. + * + * Note: in Android resources, a "flag" is a list of fixed values where one or + * more values can be selected using an "or", e.g. "align='left|top'". + * By contrast, an "enum" is a list of fixed values of which only one can be + * selected at a given time, e.g. "gravity='right'". + * <p/> + * This class handles the "flag" case. + * The "enum" case is done using {@link ListAttributeDescriptor}. + */ +public class FlagAttributeDescriptor extends TextAttributeDescriptor { + + private String[] mNames; + + /** + * Creates a new {@link FlagAttributeDescriptor} which automatically gets its + * values from the FrameworkResourceManager. + */ + public FlagAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * Creates a new {@link FlagAttributeDescriptor} which uses the provided values. + */ + public FlagAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip, String[] names) { + super(xmlLocalName, uiName, nsUri, tooltip); + mNames = names; + } + + /** + * @return The initial names of the flags. Can be null, in which case the Framework + * resource parser should be checked. + */ + public String[] getNames() { + return mNames; + } + + /** + * @return A new {@link UiListAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiFlagAttributeNode(this, uiParent); + } + + // ------- IPropertyDescriptor Methods + + @Override + public CellEditor createPropertyEditor(Composite parent) { + return new FlagValueCellEditor(parent); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java new file mode 100644 index 0000000..4c115e9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +public interface IDescriptorProvider { + + ElementDescriptor[] getRootElementDescriptors(); + + ElementDescriptor getDescriptor(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java new file mode 100644 index 0000000..93969e9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.ui.ListValueCellEditor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode; + +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Describes a text attribute that can contains some predefined values. + * It is displayed by a {@link UiListAttributeNode}. + */ +public class ListAttributeDescriptor extends TextAttributeDescriptor { + + private String[] mValues = null; + + /** + * Creates a new {@link ListAttributeDescriptor} which automatically gets its + * values from the FrameworkResourceManager. + */ + public ListAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * Creates a new {@link ListAttributeDescriptor} which uses the provided values. + */ + public ListAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip, String[] values) { + super(xmlLocalName, uiName, nsUri, tooltip); + mValues = values; + } + + public String[] getValues() { + return mValues; + } + + /** + * @return A new {@link UiListAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiListAttributeNode(this, uiParent); + } + + // ------- IPropertyDescriptor Methods + + @Override + public CellEditor createPropertyEditor(Composite parent) { + return new ListValueCellEditor(parent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java new file mode 100644 index 0000000..336dfe2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.ui.ResourceValueCellEditor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Describes an XML attribute displayed containing a value or a reference to a resource. + * It is displayed by a {@link UiResourceAttributeNode}. + */ +public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor { + + private ResourceType mResourceType; + + /** + * Creates a reference attributes that can contain any type of resources. + * @param xmlLocalName The XML name of the attribute (case sensitive) + * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param tooltip A non-empty tooltip string or null + */ + public ReferenceAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * Creates a reference attributes that can contain a reference to a specific + * {@link ResourceType}. + * @param resourceType The specific {@link ResourceType} that this reference attribute supports. + * It can be <code>null</code>, in which case, all resource types are supported. + * @param xmlLocalName The XML name of the attribute (case sensitive) + * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param tooltip A non-empty tooltip string or null + */ + public ReferenceAttributeDescriptor(ResourceType resourceType, + String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + mResourceType = resourceType; + } + + + /** + * @return A new {@link UiResourceAttributeNode} linked to this reference descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiResourceAttributeNode(mResourceType, this, uiParent); + } + + // ------- IPropertyDescriptor Methods + + @Override + public CellEditor createPropertyEditor(Composite parent) { + return new ResourceValueCellEditor(parent); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java new file mode 100644 index 0000000..8fb1c7c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiSeparatorAttributeNode; + +/** + * {@link SeparatorAttributeDescriptor} does not represent any real attribute. + * <p/> + * It is used to separate groups of attributes visually. + */ +public class SeparatorAttributeDescriptor extends AttributeDescriptor { + + /** + * Creates a new {@link SeparatorAttributeDescriptor} + */ + public SeparatorAttributeDescriptor(String label) { + super(label /* xmlLocalName */, null /* nsUri */); + } + + /** + * @return A new {@link UiAttributeNode} linked to this descriptor or null if this + * attribute has no user interface. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiSeparatorAttributeNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java new file mode 100644 index 0000000..77fc067 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.ui.TextValueCellEditor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiTextAttributeNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.views.properties.IPropertyDescriptor; + + +/** + * Describes a textual XML attribute. + * <p/> + * Such an attribute has a tooltip and would typically be displayed by + * {@link UiTextAttributeNode} using a label widget and text field. + * <p/> + * This is the "default" kind of attribute. If in doubt, use this. + */ +public class TextAttributeDescriptor extends AttributeDescriptor implements IPropertyDescriptor { + private String mUiName; + private String mTooltip; + + /** + * Creates a new {@link TextAttributeDescriptor} + * + * @param xmlLocalName The XML name of the attribute (case sensitive) + * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param tooltip A non-empty tooltip string or null + */ + public TextAttributeDescriptor(String xmlLocalName, String uiName, + String nsUri, String tooltip) { + super(xmlLocalName, nsUri); + mUiName = uiName; + mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null; + } + + /** + * @return The UI name of the attribute. Cannot be an empty string and cannot be null. + */ + public final String getUiName() { + return mUiName; + } + + /** + * The tooltip string is either null or a non-empty string. + * <p/> + * The tooltip is based on the Javadoc of the attribute and already processed via + * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as + * a UI tooltip. + * <p/> + * An empty string is converted to null, to match the behavior of setToolTipText() in + * {@link Control}. + * + * @return A non-empty tooltip string or null + */ + public final String getTooltip() { + return mTooltip; + } + + /** + * @return A new {@link UiTextAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiTextAttributeNode(this, uiParent); + } + + // ------- IPropertyDescriptor Methods + + public CellEditor createPropertyEditor(Composite parent) { + return new TextValueCellEditor(parent); + } + + public String getCategory() { + if (isDeprecated()) { + return "Deprecated"; + } + + ElementDescriptor parent = getParent(); + if (parent != null) { + return parent.getUiName(); + } + + return null; + } + + public String getDescription() { + return mTooltip; + } + + public String getDisplayName() { + return mUiName; + } + + public String[] getFilterFlags() { + return null; + } + + public Object getHelpContextIds() { + return null; + } + + public Object getId() { + return this; + } + + public ILabelProvider getLabelProvider() { + return AttributeDescriptorLabelProvider.getProvider(); + } + + public boolean isCompatibleWith(IPropertyDescriptor anotherProperty) { + return anotherProperty == this; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java new file mode 100644 index 0000000..2015d71 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiTextValueNode; + + +/** + * Describes the value of an XML element. + * <p/> + * The value is a simple text string, displayed by an {@link UiTextValueNode}. + */ +public class TextValueDescriptor extends TextAttributeDescriptor { + + /** + * Creates a new {@link TextValueDescriptor} + * + * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null. + * @param tooltip A non-empty tooltip string or null + */ + public TextValueDescriptor(String uiName, String tooltip) { + super("#text" /* xmlLocalName */, uiName, null /* nsUri */, tooltip); + } + + /** + * @return A new {@link UiTextValueNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiTextValueNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java new file mode 100644 index 0000000..ed9c897 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.descriptors; + +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + + +/** + * Describes an XMLNS attribute that is hidden. + * <p/> + * Such an attribute has no user interface and no corresponding {@link UiAttributeNode}. + * It also has a single constant default value. + * <p/> + * When loading an XML, we'll ignore this attribute. + * However when writing a new XML, we should always write this attribute. + * <p/> + * Currently this is used for the xmlns:android attribute in the manifest element. + */ +public final class XmlnsAttributeDescriptor extends AttributeDescriptor { + + /** + * URI of the reserved "xmlns" prefix, as described in + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/namespaces-algorithms.html#normalizeDocumentAlgo + */ + public final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; //$NON-NLS-1$ + + private String mValue; + + + public XmlnsAttributeDescriptor(String defaultPrefix, String value) { + super(defaultPrefix, XMLNS_URI); + mValue = value; + } + + /** + * Returns the value of this specialized attribute descriptor, which is the URI associated + * to the declared namespace prefix. + */ + public String getValue() { + return mValue; + } + + /** + * Returns the "xmlns" prefix that is always used by this node for its namespace URI. + * This is defined by the XML specification. + */ + public String getXmlNsPrefix() { + return "xmlns"; //$NON-NLS-1$ + } + + /** + * Returns the fully-qualified attribute name, namely "xmlns:xxx" where xxx is + * the defaultPrefix passed in the constructor. + */ + public String getXmlNsName() { + return getXmlNsPrefix() + ":" + getXmlLocalName(); //$NON-NLS-1$ + } + + /** + * @return Always returns null. {@link XmlnsAttributeDescriptor} has no user interface. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java new file mode 100644 index 0000000..381539b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.layoutlib.api.IXmlPullParser; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.InputStream; +import java.io.Reader; + +/** + * Base implementation of an {@link IXmlPullParser} for cases where the parser is not sitting + * on top of an actual XML file. + * <p/>It's designed to work on layout files, and will most likely not work on other resource + * files. + */ +public abstract class BasePullParser implements IXmlPullParser { + + protected int mParsingState = START_DOCUMENT; + + public BasePullParser() { + } + + // --- new methods to override --- + + public abstract void onNextFromStartDocument(); + public abstract void onNextFromStartTag(); + public abstract void onNextFromEndTag(); + + // --- basic implementation of IXmlPullParser --- + + public void setFeature(String name, boolean state) throws XmlPullParserException { + if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) { + return; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) { + return; + } + throw new XmlPullParserException("Unsupported feature: " + name); + } + + public boolean getFeature(String name) { + if (FEATURE_PROCESS_NAMESPACES.equals(name)) { + return true; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + return true; + } + return false; + } + + public void setProperty(String name, Object value) throws XmlPullParserException { + throw new XmlPullParserException("setProperty() not supported"); + } + + public Object getProperty(String name) { + return null; + } + + public void setInput(Reader in) throws XmlPullParserException { + throw new XmlPullParserException("setInput() not supported"); + } + + public void setInput(InputStream inputStream, String inputEncoding) + throws XmlPullParserException { + throw new XmlPullParserException("setInput() not supported"); + } + + public void defineEntityReplacementText(String entityName, String replacementText) + throws XmlPullParserException { + throw new XmlPullParserException("defineEntityReplacementText() not supported"); + } + + public String getNamespacePrefix(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespacePrefix() not supported"); + } + + public String getInputEncoding() { + return null; + } + + public String getNamespace(String prefix) { + throw new RuntimeException("getNamespace() not supported"); + } + + public int getNamespaceCount(int depth) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceCount() not supported"); + } + + public String getNamespaceUri(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceUri() not supported"); + } + + public int getColumnNumber() { + return -1; + } + + public int getLineNumber() { + return -1; + } + + public String getAttributeType(int arg0) { + return "CDATA"; + } + + public int getEventType() { + return mParsingState; + } + + public String getText() { + return null; + } + + public char[] getTextCharacters(int[] arg0) { + return null; + } + + public boolean isAttributeDefault(int arg0) { + return false; + } + + public boolean isWhitespace() { + return false; + } + + public int next() throws XmlPullParserException { + switch (mParsingState) { + case END_DOCUMENT: + throw new XmlPullParserException("Nothing after the end"); + case START_DOCUMENT: + onNextFromStartDocument(); + break; + case START_TAG: + onNextFromStartTag(); + break; + case END_TAG: + onNextFromEndTag(); + break; + case TEXT: + // not used + break; + case CDSECT: + // not used + break; + case ENTITY_REF: + // not used + break; + case IGNORABLE_WHITESPACE: + // not used + break; + case PROCESSING_INSTRUCTION: + // not used + break; + case COMMENT: + // not used + break; + case DOCDECL: + // not used + break; + } + + return mParsingState; + } + + public int nextTag() throws XmlPullParserException { + int eventType = next(); + if (eventType != START_TAG && eventType != END_TAG) { + throw new XmlPullParserException("expected start or end tag", this, null); + } + return eventType; + } + + public String nextText() throws XmlPullParserException { + if (getEventType() != START_TAG) { + throw new XmlPullParserException("parser must be on START_TAG to read next text", this, + null); + } + int eventType = next(); + if (eventType == TEXT) { + String result = getText(); + eventType = next(); + if (eventType != END_TAG) { + throw new XmlPullParserException( + "event TEXT it must be immediately followed by END_TAG", this, null); + } + return result; + } else if (eventType == END_TAG) { + return ""; + } else { + throw new XmlPullParserException("parser must be on START_TAG or TEXT to read text", + this, null); + } + } + + public int nextToken() throws XmlPullParserException { + return next(); + } + + public void require(int type, String namespace, String name) throws XmlPullParserException { + if (type != getEventType() || (namespace != null && !namespace.equals(getNamespace())) + || (name != null && !name.equals(getName()))) + throw new XmlPullParserException("expected " + TYPES[type] + getPositionDescription()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java new file mode 100644 index 0000000..eb7dee6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java @@ -0,0 +1,2402 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.LoadStatus; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions; +import com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener; +import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.editors.layout.parts.ElementCreateCommand; +import com.android.ide.eclipse.editors.layout.parts.UiElementEditPart; +import com.android.ide.eclipse.editors.layout.parts.UiElementsEditPartFactory; +import com.android.ide.eclipse.editors.resources.configurations.CountryCodeQualifier; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier; +import com.android.ide.eclipse.editors.resources.configurations.LanguageQualifier; +import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier; +import com.android.ide.eclipse.editors.resources.configurations.NetworkCodeQualifier; +import com.android.ide.eclipse.editors.resources.configurations.PixelDensityQualifier; +import com.android.ide.eclipse.editors.resources.configurations.RegionQualifier; +import com.android.ide.eclipse.editors.resources.configurations.ScreenDimensionQualifier; +import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier; +import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier; +import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier; +import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier.KeyboardState; +import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier.NavigationMethod; +import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier.ScreenOrientation; +import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier.TextInputMethod; +import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier.TouchScreenType; +import com.android.ide.eclipse.editors.resources.manager.ProjectResources; +import com.android.ide.eclipse.editors.resources.manager.ResourceFile; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.ui.tree.CopyCutAction; +import com.android.ide.eclipse.editors.ui.tree.PasteAction; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.DensityVerifier; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.DimensionVerifier; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.LanguageRegionVerifier; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.MobileCodeVerifier; +import com.android.layoutlib.api.ILayoutLog; +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.gef.DefaultEditDomain; +import org.eclipse.gef.EditPart; +import org.eclipse.gef.EditPartViewer; +import org.eclipse.gef.GraphicalViewer; +import org.eclipse.gef.SelectionManager; +import org.eclipse.gef.dnd.TemplateTransferDragSourceListener; +import org.eclipse.gef.dnd.TemplateTransferDropTargetListener; +import org.eclipse.gef.editparts.ScalableFreeformRootEditPart; +import org.eclipse.gef.palette.PaletteRoot; +import org.eclipse.gef.requests.CreationFactory; +import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette; +import org.eclipse.gef.ui.parts.SelectionSynchronizer; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.FileEditorInput; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.Raster; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Graphical layout editor, based on GEF. + * <p/> + * To understand GEF: http://www.ibm.com/developerworks/opensource/library/os-gef/ + * <p/> + * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html + */ +public class GraphicalLayoutEditor extends GraphicalEditorWithPalette + implements ILayoutReloadListener { + + private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$ + + /** Reference to the layout editor */ + private final LayoutEditor mLayoutEditor; + + /** reference to the file being edited. */ + private IFile mEditedFile; + + private Clipboard mClipboard; + private Composite mParent; + private PaletteRoot mPaletteRoot; + + private Text mCountry; + private Text mNetwork; + private Combo mLanguage; + private Combo mRegion; + private Combo mOrientation; + private Text mDensity; + private Combo mTouch; + private Combo mKeyboard; + private Combo mTextInput; + private Combo mNavigation; + private Text mSize1; + private Text mSize2; + private Combo mThemeCombo; + private Button mCreateButton; + + private Label mCountryIcon; + private Label mNetworkIcon; + private Label mLanguageIcon; + private Label mRegionIcon; + private Label mOrientationIcon; + private Label mDensityIcon; + private Label mTouchIcon; + private Label mKeyboardIcon; + private Label mTextInputIcon; + private Label mNavigationIcon; + private Label mSizeIcon; + + private Label mCurrentLayoutLabel; + + private Image mWarningImage; + private Image mMatchImage; + private Image mErrorImage; + + /** The {@link FolderConfiguration} representing the state of the UI controls */ + private FolderConfiguration mCurrentConfig = new FolderConfiguration(); + /** The {@link FolderConfiguration} being edited. */ + private FolderConfiguration mEditedConfig; + + private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes; + private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes; + private ProjectCallback mProjectCallback; + private ILayoutLog mLogger; + + private boolean mNeedsXmlReload = false; + private boolean mNeedsRecompute = false; + private int mPlatformThemeCount = 0; + private boolean mDisableUpdates = false; + + /** Listener to update the root node if the target of the file is changed because of a + * SDK location change or a project target change */ + private ITargetChangeListener mTargetListener = new ITargetChangeListener() { + public void onProjectTargetChange(IProject changedProject) { + if (changedProject == getLayoutEditor().getProject()) { + onTargetsLoaded(); + } + } + + public void onTargetsLoaded() { + // because the SDK changed we must reset the configured framework resource. + mConfiguredFrameworkRes = null; + + updateUIFromResources(); + + mThemeCombo.getParent().layout(); + + // updateUiFromFramework will reset language/region combo, so we must call + // setConfiguration after, or the settext on language/region will be lost. + if (mEditedConfig != null) { + setConfiguration(mEditedConfig); + } + + // make sure we remove the custom view loader, since its parent class loader is the + // bridge class loader. + mProjectCallback = null; + + recomputeLayout(); + } + }; + + private final Runnable mConditionalRecomputeRunnable = new Runnable() { + public void run() { + if (mLayoutEditor.isGraphicalEditorActive()) { + recomputeLayout(); + } else { + mNeedsRecompute = true; + } + } + }; + + private final Runnable mUiUpdateFromResourcesRunnable = new Runnable() { + public void run() { + updateUIFromResources(); + mThemeCombo.getParent().layout(); + } + }; + + public GraphicalLayoutEditor(LayoutEditor layoutEditor) { + mLayoutEditor = layoutEditor; + setEditDomain(new DefaultEditDomain(this)); + setPartName("Layout"); + + IconFactory factory = IconFactory.getInstance(); + mWarningImage = factory.getIcon("warning"); //$NON-NLS-1$ + mMatchImage = factory.getIcon("match"); //$NON-NLS-1$ + mErrorImage = factory.getIcon("error"); //$NON-NLS-1$ + + AdtPlugin.getDefault().addTargetListener(mTargetListener); + } + + // ------------------------------------ + // Methods overridden from base classes + //------------------------------------ + + @Override + public void createPartControl(Composite parent) { + mParent = parent; + GridLayout gl; + GridData gd; + + mClipboard = new Clipboard(parent.getDisplay()); + + parent.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + + // create the top part for the configuration control + int cols = 10; + + Composite topParent = new Composite(parent, SWT.NONE); + topParent.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + topParent.setLayout(gl = new GridLayout(cols, false)); + + new Label(topParent, SWT.NONE).setText("MCC"); + mCountryIcon = createControlComposite(topParent, true /* grab_horizontal */); + mCountry = new Text(mCountryIcon.getParent(), SWT.BORDER); + mCountry.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mCountry.addVerifyListener(new MobileCodeVerifier()); + mCountry.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + onCountryCodeChange(); + } + }); + mCountry.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onCountryCodeChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("MNC"); + mNetworkIcon = createControlComposite(topParent, true /* grab_horizontal */); + mNetwork = new Text(mNetworkIcon.getParent(), SWT.BORDER); + mNetwork.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mNetwork.addVerifyListener(new MobileCodeVerifier()); + mNetwork.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + onNetworkCodeChange(); + } + }); + mNetwork.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onNetworkCodeChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Lang"); + mLanguageIcon = createControlComposite(topParent, true /* grab_horizontal */); + mLanguage = new Combo(mLanguageIcon.getParent(), SWT.DROP_DOWN); + mLanguage.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mLanguage.addVerifyListener(new LanguageRegionVerifier()); + mLanguage.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onLanguageChange(); + } + public void widgetSelected(SelectionEvent e) { + onLanguageChange(); + } + }); + mLanguage.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onLanguageChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Region"); + mRegionIcon = createControlComposite(topParent, true /* grab_horizontal */); + mRegion = new Combo(mRegionIcon.getParent(), SWT.DROP_DOWN); + mRegion.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mRegion.addVerifyListener(new LanguageRegionVerifier()); + mRegion.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onRegionChange(); + } + public void widgetSelected(SelectionEvent e) { + onRegionChange(); + } + }); + mRegion.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onRegionChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Orient"); + mOrientationIcon = createControlComposite(topParent, true /* grab_horizontal */); + mOrientation = new Combo(mOrientationIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY); + ScreenOrientation[] soValues = ScreenOrientation.values(); + mOrientation.add("(Default)"); + for (ScreenOrientation value : soValues) { + mOrientation.add(value.getDisplayValue()); + } + mOrientation.select(0); + mOrientation.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mOrientation.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onOrientationChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Density"); + mDensityIcon = createControlComposite(topParent, true /* grab_horizontal */); + mDensity = new Text(mDensityIcon.getParent(), SWT.BORDER); + mDensity.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mDensity.addVerifyListener(new DensityVerifier()); + mDensity.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + onDensityChange(); + } + }); + mDensity.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onDensityChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Touch"); + mTouchIcon = createControlComposite(topParent, true /* grab_horizontal */); + mTouch = new Combo(mTouchIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY); + TouchScreenType[] tstValues = TouchScreenType.values(); + mTouch.add("(Default)"); + for (TouchScreenType value : tstValues) { + mTouch.add(value.getDisplayValue()); + } + mTouch.select(0); + mTouch.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mTouch.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onTouchChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Keybrd"); + mKeyboardIcon = createControlComposite(topParent, true /* grab_horizontal */); + mKeyboard = new Combo(mKeyboardIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY); + KeyboardState[] ksValues = KeyboardState.values(); + mKeyboard.add("(Default)"); + for (KeyboardState value : ksValues) { + mKeyboard.add(value.getDisplayValue()); + } + mKeyboard.select(0); + mKeyboard.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mKeyboard.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onKeyboardChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Input"); + mTextInputIcon = createControlComposite(topParent, true /* grab_horizontal */); + mTextInput = new Combo(mTextInputIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY); + TextInputMethod[] timValues = TextInputMethod.values(); + mTextInput.add("(Default)"); + for (TextInputMethod value : timValues) { + mTextInput.add(value.getDisplayValue()); + } + mTextInput.select(0); + mTextInput.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mTextInput.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onTextInputChange(); + } + }); + + new Label(topParent, SWT.NONE).setText("Nav"); + mNavigationIcon = createControlComposite(topParent, true /* grab_horizontal */); + mNavigation = new Combo(mNavigationIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY); + NavigationMethod[] nValues = NavigationMethod.values(); + mNavigation.add("(Default)"); + for (NavigationMethod value : nValues) { + mNavigation.add(value.getDisplayValue()); + } + mNavigation.select(0); + mNavigation.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + mNavigation.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onNavigationChange(); + } + }); + + Composite labelParent = new Composite(topParent, SWT.NONE); + labelParent.setLayout(gl = new GridLayout(8, false)); + gl.marginWidth = gl.marginHeight = 0; + labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = cols; + + new Label(labelParent, SWT.NONE).setText("Editing config:"); + mCurrentLayoutLabel = new Label(labelParent, SWT.NONE); + mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.widthHint = 50; + + new Label(labelParent, SWT.NONE).setText("Size"); + mSizeIcon = createControlComposite(labelParent, false); + Composite sizeParent = new Composite(mSizeIcon.getParent(), SWT.NONE); + sizeParent.setLayout(gl = new GridLayout(3, false)); + gl.marginWidth = gl.marginHeight = 0; + gl.horizontalSpacing = 0; + + mSize1 = new Text(sizeParent, SWT.BORDER); + mSize1.setLayoutData(gd = new GridData()); + gd.widthHint = 30; + new Label(sizeParent, SWT.NONE).setText("x"); + mSize2 = new Text(sizeParent, SWT.BORDER); + mSize2.setLayoutData(gd = new GridData()); + gd.widthHint = 30; + + DimensionVerifier verifier = new DimensionVerifier(); + mSize1.addVerifyListener(verifier); + mSize2.addVerifyListener(verifier); + + SelectionListener sl = new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onSizeChange(); + } + public void widgetSelected(SelectionEvent e) { + onSizeChange(); + } + }; + + mSize1.addSelectionListener(sl); + mSize2.addSelectionListener(sl); + + ModifyListener sizeModifyListener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + onSizeChange(); + } + }; + + mSize1.addModifyListener(sizeModifyListener); + mSize2.addModifyListener(sizeModifyListener); + + // first separator + Label separator = new Label(labelParent, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mThemeCombo = new Combo(labelParent, SWT.READ_ONLY | SWT.DROP_DOWN); + mThemeCombo.setEnabled(false); + updateUIFromResources(); + mThemeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onThemeChange(); + } + }); + + // second separator + separator = new Label(labelParent, SWT.SEPARATOR | SWT.VERTICAL); + separator.setLayoutData(gd = new GridData( + GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); + gd.heightHint = 0; + + mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT); + mCreateButton.setText("Create..."); + mCreateButton.setEnabled(false); + mCreateButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + LayoutCreatorDialog dialog = new LayoutCreatorDialog(mCreateButton.getShell(), + mEditedFile.getName(), mCurrentConfig); + if (dialog.open() == Dialog.OK) { + final FolderConfiguration config = new FolderConfiguration(); + dialog.getConfiguration(config); + + createAlternateLayout(config); + } + } + }); + + // create a new composite that will contain the standard editor controls. + Composite editorParent = new Composite(parent, SWT.NONE); + editorParent.setLayoutData(new GridData(GridData.FILL_BOTH)); + editorParent.setLayout(new FillLayout()); + super.createPartControl(editorParent); + } + + @Override + public void dispose() { + if (mTargetListener != null) { + AdtPlugin.getDefault().removeTargetListener(mTargetListener); + mTargetListener = null; + } + + LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this); + + if (mClipboard != null) { + mClipboard.dispose(); + mClipboard = null; + } + + super.dispose(); + } + + /* (non-Javadoc) + * Creates the palette root. + */ + @Override + protected PaletteRoot getPaletteRoot() { + mPaletteRoot = PaletteFactory.createPaletteRoot(mPaletteRoot, + mLayoutEditor.getTargetData()); + return mPaletteRoot; + } + + public Clipboard getClipboard() { + return mClipboard; + } + + /** + * Save operation in the Graphical Layout Editor. + * <p/> + * In our workflow, the model is owned by the Structured XML Editor. + * The graphical layout editor just displays it -- thus we don't really + * save anything here. + * <p/> + * This must NOT call the parent editor part. At the contrary, the parent editor + * part will call this *after* having done the actual save operation. + * <p/> + * The only action this editor must do is mark the undo command stack as + * being no longer dirty. + */ + @Override + public void doSave(IProgressMonitor monitor) { + getCommandStack().markSaveLocation(); + firePropertyChange(PROP_DIRTY); + } + + @Override + protected void configurePaletteViewer() { + super.configurePaletteViewer(); + + // Create a drag source listener on an edit part that is a viewer. + // What this does is use DND with a TemplateTransfer type which is actually + // the PaletteTemplateEntry held in the PaletteRoot. + TemplateTransferDragSourceListener dragSource = + new TemplateTransferDragSourceListener(getPaletteViewer()); + + // Create a drag source on the palette viewer. + // See the drag target associated with the GraphicalViewer in configureGraphicalViewer. + getPaletteViewer().addDragSourceListener(dragSource); + } + + /* (non-javadoc) + * Configure the graphical viewer before it receives its contents. + */ + @Override + protected void configureGraphicalViewer() { + super.configureGraphicalViewer(); + + GraphicalViewer viewer = getGraphicalViewer(); + viewer.setEditPartFactory(new UiElementsEditPartFactory(mParent.getDisplay())); + viewer.setRootEditPart(new ScalableFreeformRootEditPart()); + + // Disable the following -- we don't drag *from* the GraphicalViewer yet: + // viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer)); + + viewer.addDropTargetListener(new DropListener(viewer)); + } + + class DropListener extends TemplateTransferDropTargetListener { + public DropListener(EditPartViewer viewer) { + super(viewer); + } + + // TODO explain + @Override + protected CreationFactory getFactory(final Object template) { + return new CreationFactory() { + public Object getNewObject() { + // We don't know the newly created EditPart since "creating" new + // elements is done by ElementCreateCommand.execute() directly by + // manipulating the XML elements.. + return null; + } + + public Object getObjectType() { + return template; + } + + }; + } + } + + /* (non-javadoc) + * Set the contents of the GraphicalViewer after it has been created. + */ + @Override + protected void initializeGraphicalViewer() { + GraphicalViewer viewer = getGraphicalViewer(); + viewer.setContents(getModel()); + + IEditorInput input = getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + mEditedFile = fileInput.getFile(); + + updateUIFromResources(); + + LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), this); + } else { + // really this shouldn't happen! Log it in case it happens + mEditedFile = null; + AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s", + input.toString()); + } + } + + /* (non-javadoc) + * Sets the graphicalViewer for this EditorPart. + * @param viewer the graphical viewer + */ + @Override + protected void setGraphicalViewer(GraphicalViewer viewer) { + super.setGraphicalViewer(viewer); + + // TODO: viewer.setKeyHandler() + viewer.setContextMenu(createContextMenu(viewer)); + } + + /** + * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node + * created by {@link ElementCreateCommand#execute()}. + * + * @param uiNodeModel The {@link UiElementNode} to select. + */ + public void selectModel(UiElementNode uiNodeModel) { + GraphicalViewer viewer = getGraphicalViewer(); + + // Give focus to the graphical viewer (in case the outline has it) + viewer.getControl().forceFocus(); + + Object editPart = viewer.getEditPartRegistry().get(uiNodeModel); + + if (editPart instanceof EditPart) { + viewer.select((EditPart)editPart); + } + } + + + //-------------- + // Local methods + //-------------- + + public LayoutEditor getLayoutEditor() { + return mLayoutEditor; + } + + private MenuManager createContextMenu(GraphicalViewer viewer) { + MenuManager menuManager = new MenuManager(); + menuManager.setRemoveAllWhenShown(true); + menuManager.addMenuListener(new ActionMenuListener(viewer)); + + return menuManager; + } + + private class ActionMenuListener implements IMenuListener { + private final GraphicalViewer mViewer; + + public ActionMenuListener(GraphicalViewer viewer) { + mViewer = viewer; + } + + /** + * The menu is about to be shown. The menu manager has already been + * requested to remove any existing menu item. This method gets the + * tree selection and if it is of the appropriate type it re-creates + * the necessary actions. + */ + public void menuAboutToShow(IMenuManager manager) { + ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); + + // filter selected items and only keep those we can handle + for (Object obj : mViewer.getSelectedEditParts()) { + if (obj instanceof UiElementEditPart) { + UiElementEditPart part = (UiElementEditPart) obj; + UiElementNode uiNode = part.getUiNode(); + if (uiNode != null) { + selected.add(uiNode); + } + } + } + + if (selected.size() > 0) { + doCreateMenuAction(manager, mViewer, selected); + } + } + } + + private void doCreateMenuAction(IMenuManager manager, + final GraphicalViewer viewer, + final ArrayList<UiElementNode> selected) { + if (selected != null) { + boolean hasXml = false; + for (UiElementNode uiNode : selected) { + if (uiNode.getXmlNode() != null) { + hasXml = true; + break; + } + } + + if (hasXml) { + manager.add(new CopyCutAction(mLayoutEditor, getClipboard(), + null, selected, true /* cut */)); + manager.add(new CopyCutAction(mLayoutEditor, getClipboard(), + null, selected, false /* cut */)); + + // Can't paste with more than one element selected (the selection is the target) + if (selected.size() <= 1) { + // Paste is not valid if it would add a second element on a terminal element + // which parent is a document -- an XML document can only have one child. This + // means paste is valid if the current UI node can have children or if the + // parent is not a document. + UiElementNode ui_root = selected.get(0).getUiRoot(); + if (ui_root.getDescriptor().hasChildren() || + !(ui_root.getUiParent() instanceof UiDocumentNode)) { + manager.add(new PasteAction(mLayoutEditor, getClipboard(), + selected.get(0))); + } + } + manager.add(new Separator()); + } + } + + // Append "add" and "remove" actions. They do the same thing as the add/remove + // buttons on the side. + IconFactory factory = IconFactory.getInstance(); + + final UiEditorActions uiActions = mLayoutEditor.getUiEditorActions(); + + // "Add" makes sense only if there's 0 or 1 item selected since the + // one selected item becomes the target. + if (selected == null || selected.size() <= 1) { + manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-2$ + @Override + public void run() { + UiElementNode node = selected != null && selected.size() > 0 ? selected.get(0) + : null; + uiActions.doAdd(node, viewer.getControl().getShell()); + } + }); + } + + if (selected != null) { + manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-2$ + @Override + public void run() { + uiActions.doRemove(selected, viewer.getControl().getShell()); + } + }); + + manager.add(new Separator()); + + manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-2$ + @Override + public void run() { + uiActions.doUp(selected); + } + }); + manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-2$ + @Override + public void run() { + uiActions.doDown(selected); + } + }); + } + + } + + /** + * Sets the UI for the edition of a new file. + * @param configuration the configuration of the new file. + */ + public void editNewFile(FolderConfiguration configuration) { + // update the configuration UI + setConfiguration(configuration); + + // enable the create button if the current and edited config are not equals + mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false); + } + + public Rectangle getBounds() { + ScreenOrientation orientation = null; + if (mOrientation.getSelectionIndex() == 0) { + orientation = ScreenOrientation.PORTRAIT; + } else { + orientation = ScreenOrientation.getByIndex( + mOrientation.getSelectionIndex() - 1); + } + + int s1, s2; + + // get the size from the UI controls. If it fails, revert to default values. + try { + s1 = Integer.parseInt(mSize1.getText().trim()); + } catch (NumberFormatException e) { + s1 = 480; + } + + try { + s2 = Integer.parseInt(mSize2.getText().trim()); + } catch (NumberFormatException e) { + s2 = 320; + } + + // make sure s1 is bigger than s2 + if (s1 < s2) { + int tmp = s1; + s1 = s2; + s2 = tmp; + } + + switch (orientation) { + default: + case PORTRAIT: + return new Rectangle(0, 0, s2, s1); + case LANDSCAPE: + return new Rectangle(0, 0, s1, s2); + case SQUARE: + return new Rectangle(0, 0, s1, s1); + } + } + + /** + * Renders an Android View described by a {@link ViewElementDescriptor}. + * <p/>This uses the <code>wrap_content</code> mode for both <code>layout_width</code> and + * <code>layout_height</code>, and use the class name for the <code>text</code> attribute. + * @param descriptor the descriptor for the class to render. + * @return an ImageData containing the rendering or <code>null</code> if rendering failed. + */ + public ImageData renderWidget(ViewElementDescriptor descriptor) { + if (mEditedFile == null) { + return null; + } + + IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject()); + if (target == null) { + return null; + } + + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + if (data == null) { + return null; + } + + LayoutBridge bridge = data.getLayoutBridge(); + + if (bridge.bridge != null) { // bridge can never be null. + ResourceManager resManager = ResourceManager.getInstance(); + + ProjectCallback projectCallback = null; + Map<String, Map<String, IResourceValue>> configuredProjectResources = null; + if (mEditedFile != null) { + ProjectResources projectRes = resManager.getProjectResources( + mEditedFile.getProject()); + projectCallback = new ProjectCallback(bridge.classLoader, + projectRes, mEditedFile.getProject()); + + // get the configured resources for the project + // get the resources of the file's project. + if (mConfiguredProjectRes == null && projectRes != null) { + // make sure they are loaded + projectRes.loadAll(); + + // get the project resource values based on the current config + mConfiguredProjectRes = projectRes.getConfiguredResources(mCurrentConfig); + } + + configuredProjectResources = mConfiguredProjectRes; + } else { + // we absolutely need a Map of configured project resources. + configuredProjectResources = new HashMap<String, Map<String, IResourceValue>>(); + } + + // get the framework resources + Map<String, Map<String, IResourceValue>> frameworkResources = + getConfiguredFrameworkResources(); + + if (configuredProjectResources != null && frameworkResources != null) { + // get the selected theme + int themeIndex = mThemeCombo.getSelectionIndex(); + if (themeIndex != -1) { + String theme = mThemeCombo.getItem(themeIndex); + + // change the string if it's a custom theme to make sure we can + // differentiate them + if (themeIndex >= mPlatformThemeCount) { + theme = "*" + theme; //$NON-NLS-1$ + } + + // Render a single object as described by the ViewElementDescriptor. + WidgetPullParser parser = new WidgetPullParser(descriptor); + ILayoutResult result = bridge.bridge.computeLayout(parser, + null /* projectKey */, + 300 /* width */, 300 /* height */, theme, + configuredProjectResources, frameworkResources, projectCallback, + null /* logger */); + + // update the UiElementNode with the layout info. + if (result.getSuccess() == ILayoutResult.SUCCESS) { + BufferedImage largeImage = result.getImage(); + + // we need to resize it to the actual widget size, and convert it into + // an SWT image object. + int width = result.getRootView().getRight(); + int height = result.getRootView().getBottom(); + Raster raster = largeImage.getData(new java.awt.Rectangle(width, height)); + int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData(); + + ImageData imageData = new ImageData(width, height, 32, + new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); + + imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); + + return imageData; + } + } + } + } + return null; + } + + /** + * Reloads this editor, by getting the new model from the {@link LayoutEditor}. + */ + void reloadEditor() { + GraphicalViewer viewer = getGraphicalViewer(); + viewer.setContents(getModel()); + + IEditorInput input = mLayoutEditor.getEditorInput(); + setInput(input); + + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + mEditedFile = fileInput.getFile(); + } else { + // really this shouldn't happen! Log it in case it happens + mEditedFile = null; + AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s", + input.toString()); + } + } + + /** + * Callback for XML model changed. Only update/recompute the layout if the editor is visible + */ + void onXmlModelChanged() { + if (mLayoutEditor.isGraphicalEditorActive()) { + doXmlReload(true /* force */); + recomputeLayout(); + } else { + mNeedsXmlReload = true; + } + } + + /** + * Actually performs the XML reload + * @see #onXmlModelChanged() + */ + private void doXmlReload(boolean force) { + if (force || mNeedsXmlReload) { + GraphicalViewer viewer = getGraphicalViewer(); + + // try to preserve the selection before changing the content + SelectionManager selMan = viewer.getSelectionManager(); + ISelection selection = selMan.getSelection(); + + try { + viewer.setContents(getModel()); + } finally { + selMan.setSelection(selection); + } + + mNeedsXmlReload = false; + } + } + + /** + * Update the UI controls state with a given {@link FolderConfiguration}. + * <p/>If a qualifier is not present in the {@link FolderConfiguration} object, the UI control + * is not modified. However if the value in the control is not the default value, a warning + * icon is showed. + */ + void setConfiguration(FolderConfiguration config) { + mDisableUpdates = true; // we do not want to trigger onXXXChange when setting new values in the widgets. + + mEditedConfig = config; + mConfiguredFrameworkRes = mConfiguredProjectRes = null; + + mCountryIcon.setImage(mMatchImage); + CountryCodeQualifier countryQualifier = config.getCountryCodeQualifier(); + if (countryQualifier != null) { + mCountry.setText(String.format("%1$d", countryQualifier.getCode())); + mCurrentConfig.setCountryCodeQualifier(countryQualifier); + } else if (mCountry.getText().length() > 0) { + mCountryIcon.setImage(mWarningImage); + } + + mNetworkIcon.setImage(mMatchImage); + NetworkCodeQualifier networkQualifier = config.getNetworkCodeQualifier(); + if (networkQualifier != null) { + mNetwork.setText(String.format("%1$d", networkQualifier.getCode())); + mCurrentConfig.setNetworkCodeQualifier(networkQualifier); + } else if (mNetwork.getText().length() > 0) { + mNetworkIcon.setImage(mWarningImage); + } + + mLanguageIcon.setImage(mMatchImage); + LanguageQualifier languageQualifier = config.getLanguageQualifier(); + if (languageQualifier != null) { + mLanguage.setText(languageQualifier.getValue()); + mCurrentConfig.setLanguageQualifier(languageQualifier); + } else if (mLanguage.getText().length() > 0) { + mLanguageIcon.setImage(mWarningImage); + } + + mRegionIcon.setImage(mMatchImage); + RegionQualifier regionQualifier = config.getRegionQualifier(); + if (regionQualifier != null) { + mRegion.setText(regionQualifier.getValue()); + mCurrentConfig.setRegionQualifier(regionQualifier); + } else if (mRegion.getText().length() > 0) { + mRegionIcon.setImage(mWarningImage); + } + + mOrientationIcon.setImage(mMatchImage); + ScreenOrientationQualifier orientationQualifier = config.getScreenOrientationQualifier(); + if (orientationQualifier != null) { + mOrientation.select( + ScreenOrientation.getIndex(orientationQualifier.getValue()) + 1); + mCurrentConfig.setScreenOrientationQualifier(orientationQualifier); + } else if (mOrientation.getSelectionIndex() != 0) { + mOrientationIcon.setImage(mWarningImage); + } + + mDensityIcon.setImage(mMatchImage); + PixelDensityQualifier densityQualifier = config.getPixelDensityQualifier(); + if (densityQualifier != null) { + mDensity.setText(String.format("%1$d", densityQualifier.getValue())); + mCurrentConfig.setPixelDensityQualifier(densityQualifier); + } else if (mDensity.getText().length() > 0) { + mDensityIcon.setImage(mWarningImage); + } + + mTouchIcon.setImage(mMatchImage); + TouchScreenQualifier touchQualifier = config.getTouchTypeQualifier(); + if (touchQualifier != null) { + mTouch.select(TouchScreenType.getIndex(touchQualifier.getValue()) + 1); + mCurrentConfig.setTouchTypeQualifier(touchQualifier); + } else if (mTouch.getSelectionIndex() != 0) { + mTouchIcon.setImage(mWarningImage); + } + + mKeyboardIcon.setImage(mMatchImage); + KeyboardStateQualifier keyboardQualifier = config.getKeyboardStateQualifier(); + if (keyboardQualifier != null) { + mKeyboard.select(KeyboardState.getIndex(keyboardQualifier.getValue()) + 1); + mCurrentConfig.setKeyboardStateQualifier(keyboardQualifier); + } else if (mKeyboard.getSelectionIndex() != 0) { + mKeyboardIcon.setImage(mWarningImage); + } + + mTextInputIcon.setImage(mMatchImage); + TextInputMethodQualifier inputQualifier = config.getTextInputMethodQualifier(); + if (inputQualifier != null) { + mTextInput.select(TextInputMethod.getIndex(inputQualifier.getValue()) + 1); + mCurrentConfig.setTextInputMethodQualifier(inputQualifier); + } else if (mTextInput.getSelectionIndex() != 0) { + mTextInputIcon.setImage(mWarningImage); + } + + mNavigationIcon.setImage(mMatchImage); + NavigationMethodQualifier navigationQualifiter = config.getNavigationMethodQualifier(); + if (navigationQualifiter != null) { + mNavigation.select( + NavigationMethod.getIndex(navigationQualifiter.getValue()) + 1); + mCurrentConfig.setNavigationMethodQualifier(navigationQualifiter); + } else if (mNavigation.getSelectionIndex() != 0) { + mNavigationIcon.setImage(mWarningImage); + } + + mSizeIcon.setImage(mMatchImage); + ScreenDimensionQualifier sizeQualifier = config.getScreenDimensionQualifier(); + if (sizeQualifier != null) { + mSize1.setText(String.format("%1$d", sizeQualifier.getValue1())); + mSize2.setText(String.format("%1$d", sizeQualifier.getValue2())); + mCurrentConfig.setScreenDimensionQualifier(sizeQualifier); + } else if (mSize1.getText().length() > 0 && mSize2.getText().length() > 0) { + mSizeIcon.setImage(mWarningImage); + } + + // update the string showing the folder name + String current = config.toDisplayString(); + mCurrentLayoutLabel.setText(current != null ? current : "(Default)"); + + mDisableUpdates = false; + } + + /** + * Displays an error icon in front of all the non-null qualifiers. + */ + void displayConfigError() { + mCountryIcon.setImage(mMatchImage); + CountryCodeQualifier countryQualifier = mCurrentConfig.getCountryCodeQualifier(); + if (countryQualifier != null) { + mCountryIcon.setImage(mErrorImage); + } + + mNetworkIcon.setImage(mMatchImage); + NetworkCodeQualifier networkQualifier = mCurrentConfig.getNetworkCodeQualifier(); + if (networkQualifier != null) { + mNetworkIcon.setImage(mErrorImage); + } + + mLanguageIcon.setImage(mMatchImage); + LanguageQualifier languageQualifier = mCurrentConfig.getLanguageQualifier(); + if (languageQualifier != null) { + mLanguageIcon.setImage(mErrorImage); + } + + mRegionIcon.setImage(mMatchImage); + RegionQualifier regionQualifier = mCurrentConfig.getRegionQualifier(); + if (regionQualifier != null) { + mRegionIcon.setImage(mErrorImage); + } + + mOrientationIcon.setImage(mMatchImage); + ScreenOrientationQualifier orientationQualifier = + mCurrentConfig.getScreenOrientationQualifier(); + if (orientationQualifier != null) { + mOrientationIcon.setImage(mErrorImage); + } + + mDensityIcon.setImage(mMatchImage); + PixelDensityQualifier densityQualifier = mCurrentConfig.getPixelDensityQualifier(); + if (densityQualifier != null) { + mDensityIcon.setImage(mErrorImage); + } + + mTouchIcon.setImage(mMatchImage); + TouchScreenQualifier touchQualifier = mCurrentConfig.getTouchTypeQualifier(); + if (touchQualifier != null) { + mTouchIcon.setImage(mErrorImage); + } + + mKeyboardIcon.setImage(mMatchImage); + KeyboardStateQualifier keyboardQualifier = mCurrentConfig.getKeyboardStateQualifier(); + if (keyboardQualifier != null) { + mKeyboardIcon.setImage(mErrorImage); + } + + mTextInputIcon.setImage(mMatchImage); + TextInputMethodQualifier inputQualifier = mCurrentConfig.getTextInputMethodQualifier(); + if (inputQualifier != null) { + mTextInputIcon.setImage(mErrorImage); + } + + mNavigationIcon.setImage(mMatchImage); + NavigationMethodQualifier navigationQualifiter = + mCurrentConfig.getNavigationMethodQualifier(); + if (navigationQualifiter != null) { + mNavigationIcon.setImage(mErrorImage); + } + + mSizeIcon.setImage(mMatchImage); + ScreenDimensionQualifier sizeQualifier = mCurrentConfig.getScreenDimensionQualifier(); + if (sizeQualifier != null) { + mSizeIcon.setImage(mErrorImage); + } + + // update the string showing the folder name + String current = mCurrentConfig.toDisplayString(); + mCurrentLayoutLabel.setText(current != null ? current : "(Default)"); + } + + UiDocumentNode getModel() { + return mLayoutEditor.getUiRootNode(); + } + + void reloadPalette() { + PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData()); + } + + private void onCountryCodeChange() { + // because mCountry triggers onCountryCodeChange at each modification, calling setText() + // will trigger notifications, and we don't want that. + if (mDisableUpdates == true) { + return; + } + + // update the current config + String value = mCountry.getText(); + + // empty string, means no qualifier. + if (value.length() == 0) { + mCurrentConfig.setCountryCodeQualifier(null); + } else { + try { + CountryCodeQualifier qualifier = CountryCodeQualifier.getQualifier( + CountryCodeQualifier.getFolderSegment(Integer.parseInt(value))); + if (qualifier != null) { + mCurrentConfig.setCountryCodeQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + // We do nothing in this case. + mCountryIcon.setImage(mErrorImage); + return; + } + } catch (NumberFormatException e) { + // Looks like the code is not a number. This should not happen since the text + // field has a VerifyListener that prevents it. + mCurrentConfig.setCountryCodeQualifier(null); + mCountryIcon.setImage(mErrorImage); + } + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onNetworkCodeChange() { + // because mNetwork triggers onNetworkCodeChange at each modification, calling setText() + // will trigger notifications, and we don't want that. + if (mDisableUpdates == true) { + return; + } + + // update the current config + String value = mNetwork.getText(); + + // empty string, means no qualifier. + if (value.length() == 0) { + mCurrentConfig.setNetworkCodeQualifier(null); + } else { + try { + NetworkCodeQualifier qualifier = NetworkCodeQualifier.getQualifier( + NetworkCodeQualifier.getFolderSegment(Integer.parseInt(value))); + if (qualifier != null) { + mCurrentConfig.setNetworkCodeQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + // We do nothing in this case. + mNetworkIcon.setImage(mErrorImage); + return; + } + } catch (NumberFormatException e) { + // Looks like the code is not a number. This should not happen since the text + // field has a VerifyListener that prevents it. + mCurrentConfig.setNetworkCodeQualifier(null); + mNetworkIcon.setImage(mErrorImage); + } + } + + // look for a file to open/create + onConfigurationChange(); + } + + /** + * Call back for language combo selection + */ + private void onLanguageChange() { + // because mLanguage triggers onLanguageChange at each modification, the filling + // of the combo with data will trigger notifications, and we don't want that. + if (mDisableUpdates == true) { + return; + } + + // update the current config + String value = mLanguage.getText(); + + updateRegionUi(null /* projectResources */, null /* frameworkResources */); + + // empty string, means no qualifier. + if (value.length() == 0) { + mCurrentConfig.setLanguageQualifier(null); + } else { + LanguageQualifier qualifier = null; + String segment = LanguageQualifier.getFolderSegment(value); + if (segment != null) { + qualifier = LanguageQualifier.getQualifier(segment); + } + + if (qualifier != null) { + mCurrentConfig.setLanguageQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + mCurrentConfig.setLanguageQualifier(null); + mLanguageIcon.setImage(mErrorImage); + } + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onRegionChange() { + // because mRegion triggers onRegionChange at each modification, the filling + // of the combo with data will trigger notifications, and we don't want that. + if (mDisableUpdates == true) { + return; + } + + // update the current config + String value = mRegion.getText(); + + // empty string, means no qualifier. + if (value.length() == 0) { + mCurrentConfig.setRegionQualifier(null); + } else { + RegionQualifier qualifier = null; + String segment = RegionQualifier.getFolderSegment(value); + if (segment != null) { + qualifier = RegionQualifier.getQualifier(segment); + } + + if (qualifier != null) { + mCurrentConfig.setRegionQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + mCurrentConfig.setRegionQualifier(null); + mRegionIcon.setImage(mErrorImage); + } + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onOrientationChange() { + // update the current config + int index = mOrientation.getSelectionIndex(); + if (index != 0) { + mCurrentConfig.setScreenOrientationQualifier(new ScreenOrientationQualifier( + ScreenOrientation.getByIndex(index-1))); + } else { + mCurrentConfig.setScreenOrientationQualifier(null); + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onDensityChange() { + // because mDensity triggers onDensityChange at each modification, calling setText() + // will trigger notifications, and we don't want that. + if (mDisableUpdates == true) { + return; + } + + // update the current config + String value = mDensity.getText(); + + // empty string, means no qualifier. + if (value.length() == 0) { + mCurrentConfig.setPixelDensityQualifier(null); + } else { + try { + PixelDensityQualifier qualifier = PixelDensityQualifier.getQualifier( + PixelDensityQualifier.getFolderSegment(Integer.parseInt(value))); + if (qualifier != null) { + mCurrentConfig.setPixelDensityQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + // We do nothing in this case. + return; + } + } catch (NumberFormatException e) { + // Looks like the code is not a number. This should not happen since the text + // field has a VerifyListener that prevents it. + // We do nothing in this case. + mDensityIcon.setImage(mErrorImage); + return; + } + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onTouchChange() { + // update the current config + int index = mTouch.getSelectionIndex(); + if (index != 0) { + mCurrentConfig.setTouchTypeQualifier(new TouchScreenQualifier( + TouchScreenType.getByIndex(index-1))); + } else { + mCurrentConfig.setTouchTypeQualifier(null); + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onKeyboardChange() { + // update the current config + int index = mKeyboard.getSelectionIndex(); + if (index != 0) { + mCurrentConfig.setKeyboardStateQualifier(new KeyboardStateQualifier( + KeyboardState.getByIndex(index-1))); + } else { + mCurrentConfig.setKeyboardStateQualifier(null); + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onTextInputChange() { + // update the current config + int index = mTextInput.getSelectionIndex(); + if (index != 0) { + mCurrentConfig.setTextInputMethodQualifier(new TextInputMethodQualifier( + TextInputMethod.getByIndex(index-1))); + } else { + mCurrentConfig.setTextInputMethodQualifier(null); + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onNavigationChange() { + // update the current config + int index = mNavigation.getSelectionIndex(); + if (index != 0) { + mCurrentConfig.setNavigationMethodQualifier(new NavigationMethodQualifier( + NavigationMethod.getByIndex(index-1))); + } else { + mCurrentConfig.setNavigationMethodQualifier(null); + } + + // look for a file to open/create + onConfigurationChange(); + } + + private void onSizeChange() { + // because mSize1 and mSize2 trigger onSizeChange at each modification, calling setText() + // will trigger notifications, and we don't want that. + if (mDisableUpdates == true) { + return; + } + + // update the current config + String size1 = mSize1.getText(); + String size2 = mSize2.getText(); + + // if only one of the strings is empty, do nothing + if ((size1.length() == 0) ^ (size2.length() == 0)) { + mSizeIcon.setImage(mErrorImage); + return; + } else if (size1.length() == 0 && size2.length() == 0) { + // both sizes are empty: remove the qualifier. + mCurrentConfig.setScreenDimensionQualifier(null); + } else { + ScreenDimensionQualifier qualifier = ScreenDimensionQualifier.getQualifier(size1, + size2); + + if (qualifier != null) { + mCurrentConfig.setScreenDimensionQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong. + // we do nothing in this case. + return; + } + } + + // look for a file to open/create + onConfigurationChange(); + } + + + /** + * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it. + * <p/>If there is no match, notify the user. + */ + private void onConfigurationChange() { + mConfiguredFrameworkRes = mConfiguredProjectRes = null; + + if (mEditedFile == null || mEditedConfig == null) { + return; + } + + // get the resources of the file's project. + ProjectResources resources = ResourceManager.getInstance().getProjectResources( + mEditedFile.getProject()); + + // from the resources, look for a matching file + ResourceFile match = null; + if (resources != null) { + match = resources.getMatchingFile(mEditedFile.getName(), + ResourceFolderType.LAYOUT, + mCurrentConfig); + } + + if (match != null) { + if (match.getFile().equals(mEditedFile) == false) { + try { + IDE.openEditor( + getSite().getWorkbenchWindow().getActivePage(), + match.getFile().getIFile()); + + // we're done! + return; + } catch (PartInitException e) { + // FIXME: do something! + } + } + + // at this point, we have not opened a new file. + + // update the configuration icons with the new edited config. + setConfiguration(mEditedConfig); + + // enable the create button if the current and edited config are not equals + mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false); + + // Even though the layout doesn't change, the config changed, and referenced + // resources need to be updated. + recomputeLayout(); + } else { + // update the configuration icons with the new edited config. + displayConfigError(); + + // enable the Create button + mCreateButton.setEnabled(true); + + // display the error. + String message = String.format( + "No resources match the configuration\n \n\t%1$s\n \nChange the configuration or create:\n \n\tres/%2$s/%3$s\n \nYou can also click the 'Create' button above.", + mCurrentConfig.toDisplayString(), + mCurrentConfig.getFolderName(ResourceFolderType.LAYOUT), + mEditedFile.getName()); + showErrorInEditor(message); + } + } + + private void onThemeChange() { + int themeIndex = mThemeCombo.getSelectionIndex(); + if (themeIndex != -1) { + String theme = mThemeCombo.getItem(themeIndex); + + if (theme.equals(THEME_SEPARATOR)) { + mThemeCombo.select(0); + } + + recomputeLayout(); + } + } + + /** + * Creates a composite with no margin/spacing, and puts a {@link Label} in it with the matching + * icon. + * @param parent the parent to receive the composite + * @return the created {@link Label} object. + */ + private Label createControlComposite(Composite parent, boolean grab) { + GridLayout gl; + + Composite composite = new Composite(parent, SWT.NONE); + composite.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + gl.horizontalSpacing = 0; + if (grab) { + composite.setLayoutData( + new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); + } + + // create the label + Label icon = new Label(composite, SWT.NONE); + icon.setImage(mMatchImage); + + return icon; + } + + /** + * Recomputes the layout with the help of layoutlib. + */ + @SuppressWarnings("deprecation") + void recomputeLayout() { + doXmlReload(false /* force */); + try { + // check that the resource exists. If the file is opened but the project is closed + // or deleted for some reason (changed from outside of eclipse), then this will + // return false; + if (mEditedFile.exists() == false) { + String message = String.format("Resource '%1$s' does not exist.", + mEditedFile.getFullPath().toString()); + + showErrorInEditor(message); + + return; + } + + IProject iProject = mEditedFile.getProject(); + + if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) { + String message = String.format("%1$s is out of sync. Please refresh.", + mEditedFile.getName()); + + showErrorInEditor(message); + + // also print it in the error console. + AdtPlugin.printErrorToConsole(iProject.getName(), message); + return; + } + + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); + if (target == null) { + showErrorInEditor("The project target is not set."); + return; + } + + AndroidTargetData data = currentSdk.getTargetData(target); + if (data == null) { + // It can happen that the workspace refreshes while the SDK is loading its + // data, which could trigger a redraw of the opened layout if some resources + // changed while Eclipse is closed. + // In this case data could be null, but this is not an error. + // We can just silently return, as all the opened editors are automatically + // refreshed once the SDK finishes loading. + if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADING) { + showErrorInEditor(String.format( + "The project target (%s) was not properly loaded.", + target.getName())); + } + return; + } + + // check there is actually a model (maybe the file is empty). + UiDocumentNode model = getModel(); + + if (model.getUiChildren().size() == 0) { + showErrorInEditor("No Xml content. Go to the Outline view and add nodes."); + return; + } + + LayoutBridge bridge = data.getLayoutBridge(); + + if (bridge.bridge != null) { // bridge can never be null. + ResourceManager resManager = ResourceManager.getInstance(); + + ProjectResources projectRes = resManager.getProjectResources(iProject); + if (projectRes == null) { + return; + } + + // get the resources of the file's project. + if (mConfiguredProjectRes == null) { + // make sure they are loaded + projectRes.loadAll(); + + // get the project resource values based on the current config + mConfiguredProjectRes = projectRes.getConfiguredResources(mCurrentConfig); + } + + // get the framework resources + Map<String, Map<String, IResourceValue>> frameworkResources = + getConfiguredFrameworkResources(); + + if (mConfiguredProjectRes != null && frameworkResources != null) { + if (mProjectCallback == null) { + mProjectCallback = new ProjectCallback( + bridge.classLoader, projectRes, iProject); + } + + if (mLogger == null) { + mLogger = new ILayoutLog() { + public void error(String message) { + AdtPlugin.printErrorToConsole(mEditedFile.getName(), message); + } + + public void error(Throwable error) { + String message = error.getMessage(); + if (message == null) { + message = error.getClass().getName(); + } + + PrintStream ps = new PrintStream(AdtPlugin.getErrorStream()); + error.printStackTrace(ps); + } + + public void warning(String message) { + AdtPlugin.printToConsole(mEditedFile.getName(), message); + } + }; + } + + // get the selected theme + int themeIndex = mThemeCombo.getSelectionIndex(); + if (themeIndex != -1) { + String theme = mThemeCombo.getItem(themeIndex); + + // Compute the layout + UiElementPullParser parser = new UiElementPullParser(getModel()); + Rectangle rect = getBounds(); + ILayoutResult result = null; + if (bridge.apiLevel >= 3) { + // call the new api with proper theme differentiator and + // density/dpi support. + boolean isProjectTheme = themeIndex >= mPlatformThemeCount; + + // FIXME pass the density/dpi from somewhere (resource config or skin). + result = bridge.bridge.computeLayout(parser, + iProject /* projectKey */, + rect.width, rect.height, 160, 160.f, 160.f, + theme, isProjectTheme, + mConfiguredProjectRes, frameworkResources, mProjectCallback, + mLogger); + } else if (bridge.apiLevel == 2) { + // api with boolean for separation of project/framework theme + boolean isProjectTheme = themeIndex >= mPlatformThemeCount; + + result = bridge.bridge.computeLayout(parser, + iProject /* projectKey */, + rect.width, rect.height, theme, isProjectTheme, + mConfiguredProjectRes, frameworkResources, mProjectCallback, + mLogger); + } else { + // oldest api with no density/dpi, and project theme boolean mixed + // into the theme name. + + // change the string if it's a custom theme to make sure we can + // differentiate them + if (themeIndex >= mPlatformThemeCount) { + theme = "*" + theme; //$NON-NLS-1$ + } + + result = bridge.bridge.computeLayout(parser, + iProject /* projectKey */, + rect.width, rect.height, theme, + mConfiguredProjectRes, frameworkResources, mProjectCallback, + mLogger); + } + + // update the UiElementNode with the layout info. + if (result.getSuccess() == ILayoutResult.SUCCESS) { + model.setEditData(result.getImage()); + + updateNodeWithBounds(result.getRootView()); + } else { + String message = result.getErrorMessage(); + + // Reset the edit data for all the nodes. + resetNodeBounds(model); + + if (message != null) { + // set the error in the top element. + model.setEditData(message); + } + } + + model.refreshUi(); + } + } + } else { + // SDK is loaded but not the layout library! + String message = null; + // check whether the bridge managed to load, or not + if (bridge.status == LoadStatus.LOADING) { + message = String.format( + "Eclipse is loading framework information and the Layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.", + mEditedFile.getName()); + } else { + message = String.format("Eclipse failed to load the framework information and the Layout library!"); + } + showErrorInEditor(message); + } + } else { + String message = String.format( + "Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", + mEditedFile.getName()); + + showErrorInEditor(message); + } + } finally { + // no matter the result, we are done doing the recompute based on the latest + // resource/code change. + mNeedsRecompute = false; + } + } + + private void showErrorInEditor(String message) { + // get the model to display the error directly in the editor + UiDocumentNode model = getModel(); + + // Reset the edit data for all the nodes. + resetNodeBounds(model); + + if (message != null) { + // set the error in the top element. + model.setEditData(message); + } + + model.refreshUi(); + } + + private void resetNodeBounds(UiElementNode node) { + node.setEditData(null); + + List<UiElementNode> children = node.getUiChildren(); + for (UiElementNode child : children) { + resetNodeBounds(child); + } + } + + private void updateNodeWithBounds(ILayoutViewInfo r) { + if (r != null) { + // update the node itself, as the viewKey is the XML node in this implementation. + Object viewKey = r.getViewKey(); + if (viewKey instanceof UiElementNode) { + Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(), + r.getRight()-r.getLeft(), r.getBottom() - r.getTop()); + + ((UiElementNode)viewKey).setEditData(bounds); + } + + // and then its children. + ILayoutViewInfo[] children = r.getChildren(); + if (children != null) { + for (ILayoutViewInfo child : children) { + updateNodeWithBounds(child); + } + } + } + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean) + * + * Called when the file changes triggered a redraw of the layout + */ + public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) { + boolean recompute = rChange; + + if (resChange) { + recompute = true; + + // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache. + + // force a reparse in case a value XML file changed. + mConfiguredProjectRes = null; + + // clear the cache in the bridge in case a bitmap/9-patch changed. + IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject()); + if (target != null) { + + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + if (data != null) { + LayoutBridge bridge = data.getLayoutBridge(); + + if (bridge.bridge != null) { + bridge.bridge.clearCaches(mEditedFile.getProject()); + } + } + } + + mParent.getDisplay().asyncExec(mUiUpdateFromResourcesRunnable); + } + + if (codeChange) { + // only recompute if the custom view loader was used to load some code. + if (mProjectCallback != null && mProjectCallback.isUsed()) { + mProjectCallback = null; + recompute = true; + } + } + + if (recompute) { + mParent.getDisplay().asyncExec(mConditionalRecomputeRunnable); + } + } + + /** + * Responds to a page change that made the Graphical editor page the activated page. + */ + void activated() { + if (mNeedsRecompute || mNeedsXmlReload) { + recomputeLayout(); + } + } + + /** + * Responds to a page change that made the Graphical editor page the deactivated page + */ + void deactivated() { + // nothing to be done here for now. + } + + /** + * Updates the UI from values in the resources, such as languages, regions, themes, etc... + * This must be called from the UI thread. + */ + private void updateUIFromResources() { + + ResourceManager manager = ResourceManager.getInstance(); + + ProjectResources frameworkProject = getFrameworkResources(); + + mDisableUpdates = true; + + // Reset stuff + int selection = mThemeCombo.getSelectionIndex(); + mThemeCombo.removeAll(); + mPlatformThemeCount = 0; + mLanguage.removeAll(); + + Set<String> languages = new HashSet<String>(); + ArrayList<String> themes = new ArrayList<String>(); + + // get the themes, and languages from the Framework. + if (frameworkProject != null) { + // get the configured resources for the framework + Map<String, Map<String, IResourceValue>> frameworResources = + getConfiguredFrameworkResources(); + + if (frameworResources != null) { + // get the styles. + Map<String, IResourceValue> styles = frameworResources.get( + ResourceType.STYLE.getName()); + + + // collect the themes out of all the styles. + for (IResourceValue value : styles.values()) { + String name = value.getName(); + if (name.startsWith("Theme.") || name.equals("Theme")) { + themes.add(value.getName()); + mPlatformThemeCount++; + } + } + + // sort them and add them to the combo + Collections.sort(themes); + + for (String theme : themes) { + mThemeCombo.add(theme); + } + + mPlatformThemeCount = themes.size(); + themes.clear(); + } + // now get the languages from the framework. + Set<String> frameworkLanguages = frameworkProject.getLanguages(); + if (frameworkLanguages != null) { + languages.addAll(frameworkLanguages); + } + } + + // now get the themes and languages from the project. + ProjectResources project = null; + if (mEditedFile != null) { + project = manager.getProjectResources(mEditedFile.getProject()); + + // in cases where the opened file is not linked to a project, this could be null. + if (project != null) { + // get the configured resources for the project + if (mConfiguredProjectRes == null) { + // make sure they are loaded + project.loadAll(); + + // get the project resource values based on the current config + mConfiguredProjectRes = project.getConfiguredResources(mCurrentConfig); + } + + if (mConfiguredProjectRes != null) { + // get the styles. + Map<String, IResourceValue> styleMap = mConfiguredProjectRes.get( + ResourceType.STYLE.getName()); + + if (styleMap != null) { + // collect the themes out of all the styles, ie styles that extend, + // directly or indirectly a platform theme. + for (IResourceValue value : styleMap.values()) { + if (isTheme(value, styleMap)) { + themes.add(value.getName()); + } + } + + // sort them and add them the to the combo. + if (mPlatformThemeCount > 0 && themes.size() > 0) { + mThemeCombo.add(THEME_SEPARATOR); + } + + Collections.sort(themes); + + for (String theme : themes) { + mThemeCombo.add(theme); + } + } + } + + // now get the languages from the project. + Set<String> projectLanguages = project.getLanguages(); + if (projectLanguages != null) { + languages.addAll(projectLanguages); + } + } + } + + // add the languages to the Combo + for (String language : languages) { + mLanguage.add(language); + } + + mDisableUpdates = false; + + // and update the Region UI based on the current language + updateRegionUi(project, frameworkProject); + + // handle default selection of themes + if (mThemeCombo.getItemCount() > 0) { + mThemeCombo.setEnabled(true); + if (selection == -1) { + selection = 0; + } + + if (mThemeCombo.getItemCount() <= selection) { + mThemeCombo.select(0); + } else { + mThemeCombo.select(selection); + } + } else { + mThemeCombo.setEnabled(false); + } + } + + /** + * Returns whether the given <var>style</var> is a theme. + * This is done by making sure the parent is a theme. + * @param value the style to check + * @param styleMap the map of styles for the current project. Key is the style name. + * @return True if the given <var>style</var> is a theme. + */ + private boolean isTheme(IResourceValue value, Map<String, IResourceValue> styleMap) { + if (value instanceof IStyleResourceValue) { + IStyleResourceValue style = (IStyleResourceValue)value; + + boolean frameworkStyle = false; + String parentStyle = style.getParentStyle(); + if (parentStyle == null) { + // if there is no specified parent style we look an implied one. + // For instance 'Theme.light' is implied child style of 'Theme', + // and 'Theme.light.fullscreen' is implied child style of 'Theme.light' + String name = style.getName(); + int index = name.lastIndexOf('.'); + if (index != -1) { + parentStyle = name.substring(0, index); + } + } else { + // remove the useless @ if it's there + if (parentStyle.startsWith("@")) { + parentStyle = parentStyle.substring(1); + } + + // check for framework identifier. + if (parentStyle.startsWith("android:")) { + frameworkStyle = true; + parentStyle = parentStyle.substring("android:".length()); + } + + // at this point we could have the format style/<name>. we want only the name + if (parentStyle.startsWith("style/")) { + parentStyle = parentStyle.substring("style/".length()); + } + } + + if (frameworkStyle) { + // if the parent is a framework style, it has to be 'Theme' or 'Theme.*' + return parentStyle.equals("Theme") || parentStyle.startsWith("Theme."); + } else { + // if it's a project style, we check this is a theme. + value = styleMap.get(parentStyle); + if (value != null) { + return isTheme(value, styleMap); + } + } + } + + return false; + } + + /** + * Update the Region UI widget based on the current language selection + * @param projectResources the project resources or {@code null}. + * @param frameworkResources the framework resource or {@code null} + */ + private void updateRegionUi(ProjectResources projectResources, + ProjectResources frameworkResources) { + if (projectResources == null && mEditedFile != null) { + projectResources = ResourceManager.getInstance().getProjectResources( + mEditedFile.getProject()); + } + + if (frameworkResources == null) { + frameworkResources = getFrameworkResources(); + } + + String currentLanguage = mLanguage.getText(); + + Set<String> set = null; + + if (projectResources != null) { + set = projectResources.getRegions(currentLanguage); + } + + if (frameworkResources != null) { + if (set != null) { + Set<String> set2 = frameworkResources.getRegions(currentLanguage); + set.addAll(set2); + } else { + set = frameworkResources.getRegions(currentLanguage); + } + } + + if (set != null) { + mDisableUpdates = true; + + mRegion.removeAll(); + for (String region : set) { + mRegion.add(region); + } + + mDisableUpdates = false; + } + } + + private Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() { + if (mConfiguredFrameworkRes == null) { + ProjectResources frameworkRes = getFrameworkResources(); + + if (frameworkRes == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework"); + } + + // get the framework resource values based on the current config + mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(mCurrentConfig); + } + + return mConfiguredFrameworkRes; + } + + /** + * Returns the selection synchronizer object. + * The synchronizer can be used to sync the selection of 2 or more EditPartViewers. + * <p/> + * This is changed from protected to public so that the outline can use it. + * + * @return the synchronizer + */ + @Override + public SelectionSynchronizer getSelectionSynchronizer() { + return super.getSelectionSynchronizer(); + } + + /** + * Returns the edit domain. + * <p/> + * This is changed from protected to public so that the outline can use it. + * + * @return the edit domain + */ + @Override + public DefaultEditDomain getEditDomain() { + return super.getEditDomain(); + } + + /** + * Creates a new layout file from the specificed {@link FolderConfiguration}. + */ + private void createAlternateLayout(final FolderConfiguration config) { + new Job("Create Alternate Resource") { + @Override + protected IStatus run(IProgressMonitor monitor) { + // get the folder name + String folderName = config.getFolderName(ResourceFolderType.LAYOUT); + try { + + // look to see if it exists. + // get the res folder + IFolder res = (IFolder)mEditedFile.getParent().getParent(); + String path = res.getLocation().toOSString(); + + File newLayoutFolder = new File(path + File.separator + folderName); + if (newLayoutFolder.isFile()) { + // this should not happen since aapt would have complained + // before, but if one disable the automatic build, this could + // happen. + String message = String.format("File 'res/%1$s' is in the way!", + folderName); + + AdtPlugin.displayError("Layout Creation", message); + + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); + } else if (newLayoutFolder.exists() == false) { + // create it. + newLayoutFolder.mkdir(); + } + + // now create the file + File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() + + File.separator + mEditedFile.getName()); + + newLayoutFile.createNewFile(); + + InputStream input = mEditedFile.getContents(); + + FileOutputStream fos = new FileOutputStream(newLayoutFile); + + byte[] data = new byte[512]; + int count; + while ((count = input.read(data)) != -1) { + fos.write(data, 0, count); + } + + input.close(); + fos.close(); + + // refreshes the res folder to show up the new + // layout folder (if needed) and the file. + // We use a progress monitor to catch the end of the refresh + // to trigger the edit of the new file. + res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() { + public void done() { + mCurrentConfig.set(config); + mParent.getDisplay().asyncExec(new Runnable() { + public void run() { + onConfigurationChange(); + } + }); + } + + public void beginTask(String name, int totalWork) { + // pass + } + + public void internalWorked(double work) { + // pass + } + + public boolean isCanceled() { + // pass + return false; + } + + public void setCanceled(boolean value) { + // pass + } + + public void setTaskName(String name) { + // pass + } + + public void subTask(String name) { + // pass + } + + public void worked(int work) { + // pass + } + }); + } catch (IOException e2) { + String message = String.format( + "Failed to create File 'res/%1$s/%2$s' : %3$s", + folderName, mEditedFile.getName(), e2.getMessage()); + + AdtPlugin.displayError("Layout Creation", message); + + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + message, e2); + } catch (CoreException e2) { + String message = String.format( + "Failed to create File 'res/%1$s/%2$s' : %3$s", + folderName, mEditedFile.getName(), e2.getMessage()); + + AdtPlugin.displayError("Layout Creation", message); + + return e2.getStatus(); + } + + return Status.OK_STATUS; + + } + }.schedule(); + } + + /** + * Returns a {@link ProjectResources} for the framework resources. + * @return the framework resources or null if not found. + */ + private ProjectResources getFrameworkResources() { + if (mEditedFile != null) { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); + + if (target != null) { + AndroidTargetData data = currentSdk.getTargetData(target); + + if (data != null) { + return data.getFrameworkResources(); + } + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java new file mode 100644 index 0000000..d4ec5e1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +/** + * A bunch of constants that map to either: + * <ul> + * <li>Android Layouts XML element names (Linear, Relative, Absolute, etc.) + * <li>Attributes for layout XML elements. + * <li>Values for attributes. + * </ul> + */ +public class LayoutConstants { + + public static final String RELATIVE_LAYOUT = "RelativeLayout"; //$NON-NLS-1$ + public static final String LINEAR_LAYOUT = "LinearLayout"; //$NON-NLS-1$ + public static final String ABSOLUTE_LAYOUT = "AbsoluteLayout"; //$NON-NLS-1$ + + public static final String ATTR_TEXT = "text"; //$NON-NLS-1$ + public static final String ATTR_ID = "id"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_HEIGHT = "layout_height"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_WIDTH = "layout_width"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_ALIGN_PARENT_TOP = "layout_alignParentTop"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_ALIGN_PARENT_BOTTOM = "layout_alignParentBottom"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_ALIGN_PARENT_LEFT = "layout_alignParentLeft";//$NON-NLS-1$ + public static final String ATTR_LAYOUT_ALIGN_PARENT_RIGHT = "layout_alignParentRight"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_ALIGN_BASELINE = "layout_alignBaseline"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_CENTER_VERTICAL = "layout_centerVertical"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_CENTER_HORIZONTAL = "layout_centerHorizontal"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_TO_RIGHT_OF = "layout_toRightOf"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_TO_LEFT_OF = "layout_toLeftOf"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_BELOW = "layout_below"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_ABOVE = "layout_above"; //$NON-NLS-1$ + + public static final String ATTR_LAYOUT_Y = "layout_y"; //$NON-NLS-1$ + public static final String ATTR_LAYOUT_X = "layout_x"; //$NON-NLS-1$ + + public static final String VALUE_WRAP_CONTENT = "wrap_content"; //$NON-NLS-1$ + public static final String VALUE_FILL_PARENT = "fill_parent"; //$NON-NLS-1$ + public static final String VALUE_TRUE = "true"; //$NON-NLS-1$ + public static final String VALUE_N_DIP = "%ddip"; //$NON-NLS-1$ + + private LayoutConstants() { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java new file mode 100644 index 0000000..9f39495 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidContentAssist; + +/** + * Content Assist Processor for /res/layout XML files + */ +class LayoutContentAssist extends AndroidContentAssist { + + /** + * Constructor for LayoutContentAssist + */ + public LayoutContentAssist() { + super(AndroidTargetData.DESCRIPTOR_LAYOUT); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java new file mode 100644 index 0000000..c4a8f5c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.ConfigurationState; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.TrayDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +/** + * Dialog to choose a non existing {@link FolderConfiguration}. + */ +class LayoutCreatorDialog extends TrayDialog { + + private ConfigurationSelector mSelector; + private Composite mStatusComposite; + private Label mStatusLabel; + private Label mStatusImage; + + private final FolderConfiguration mConfig = new FolderConfiguration(); + private final String mFileName; + + /** + * Creates a dialog, and init the UI from a {@link FolderConfiguration}. + * @param parentShell the parent {@link Shell}. + * @param config The starting configuration. + */ + LayoutCreatorDialog(Shell parentShell, String fileName, FolderConfiguration config) { + super(parentShell); + + mFileName = fileName; + // FIXME: add some data to know what configurations already exist. + mConfig.set(config); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayoutData(new GridData()); + top.setLayout(new GridLayout(1, false)); + + new Label(top, SWT.NONE).setText( + String.format("Configuration for the alternate version of %1$s", mFileName)); + + mSelector = new ConfigurationSelector(top); + mSelector.setConfiguration(mConfig); + + // parent's layout is a GridLayout as specified in the javadoc. + GridData gd = new GridData(); + gd.widthHint = ConfigurationSelector.WIDTH_HINT; + gd.heightHint = ConfigurationSelector.HEIGHT_HINT; + mSelector.setLayoutData(gd); + + // add a listener to check on the validity of the FolderConfiguration as + // they are built. + mSelector.setOnChangeListener(new Runnable() { + public void run() { + ConfigurationState state = mSelector.getState(); + + switch (state) { + case OK: + mSelector.getConfiguration(mConfig); + + resetStatus(); + mStatusImage.setImage(null); + getButton(IDialogConstants.OK_ID).setEnabled(true); + break; + case INVALID_CONFIG: + ResourceQualifier invalidQualifier = mSelector.getInvalidQualifier(); + mStatusLabel.setText(String.format( + "Invalid Configuration: %1$s has no filter set.", + invalidQualifier.getName())); + mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$ + getButton(IDialogConstants.OK_ID).setEnabled(false); + break; + case REGION_WITHOUT_LANGUAGE: + mStatusLabel.setText( + "The Region qualifier requires the Language qualifier."); + mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$ + getButton(IDialogConstants.OK_ID).setEnabled(false); + break; + } + + // need to relayout, because of the change in size in mErrorImage. + mStatusComposite.layout(); + } + }); + + mStatusComposite = new Composite(top, SWT.NONE); + mStatusComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout gl = new GridLayout(2, false); + mStatusComposite.setLayout(gl); + gl.marginHeight = gl.marginWidth = 0; + + mStatusImage = new Label(mStatusComposite, SWT.NONE); + mStatusLabel = new Label(mStatusComposite, SWT.NONE); + mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + resetStatus(); + + return top; + } + + public void getConfiguration(FolderConfiguration config) { + config.set(mConfig); + } + + /** + * resets the status label to show the file that will be created. + */ + private void resetStatus() { + mStatusLabel.setText(String.format("New File: res/%1$s/%2$s", + mConfig.getFolderName(ResourceFolderType.LAYOUT), mFileName)); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java new file mode 100644 index 0000000..dabe797 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.EclipseUiHelper; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolder; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.ui.tree.UiActions; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.gef.ui.parts.TreeViewer; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IPartListener; +import org.eclipse.ui.IShowEditorInput; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.ui.views.contentoutline.IContentOutlinePage; +import org.eclipse.ui.views.properties.IPropertySheetPage; +import org.w3c.dom.Document; + +/** + * Multi-page form editor for /res/layout XML files. + */ +public class LayoutEditor extends AndroidEditor implements IShowEditorInput, IPartListener { + + public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$ + + /** Root node of the UI element hierarchy */ + private UiDocumentNode mUiRootNode; + + private GraphicalLayoutEditor mGraphicalEditor; + private int mGraphicalEditorIndex; + /** Implementation of the {@link IContentOutlinePage} for this editor */ + private UiContentOutlinePage mOutline; + /** Custom implementation of {@link IPropertySheetPage} for this editor */ + private UiPropertySheetPage mPropertyPage; + + private UiEditorActions mUiEditorActions; + + /** + * Creates the form editor for resources XML files. + */ + public LayoutEditor() { + super(); + } + + /** + * @return The root node of the UI element hierarchy + */ + @Override + public UiDocumentNode getUiRootNode() { + return mUiRootNode; + } + + // ---- Base Class Overrides ---- + + @Override + public void dispose() { + getSite().getPage().removePartListener(this); + + super.dispose(); + } + + /** + * Save the XML. + * <p/> + * The actual save operation is done in the super class by committing + * all data to the XML model and then having the Structured XML Editor + * save the XML. + * <p/> + * Here we just need to tell the graphical editor that the model has + * been saved. + */ + @Override + public void doSave(IProgressMonitor monitor) { + super.doSave(monitor); + if (mGraphicalEditor != null) { + mGraphicalEditor.doSave(monitor); + } + } + + /** + * Returns whether the "save as" operation is supported by this editor. + * <p/> + * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + /** + * Create the various form pages. + */ + @Override + protected void createFormPages() { + try { + // The graphical layout editor is now enabled by default. + // In case there's an issue we provide a way to disable it using an + // env variable. + if (System.getenv("ANDROID_DISABLE_LAYOUT") == null) { + if (mGraphicalEditor == null) { + mGraphicalEditor = new GraphicalLayoutEditor(this); + mGraphicalEditorIndex = addPage(mGraphicalEditor, getEditorInput()); + setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle()); + } else { + mGraphicalEditor.reloadEditor(); + } + + // update the config based on the opened file. + IEditorInput input = getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder( + fileInput.getFile()); + if (resFolder != null) { + mGraphicalEditor.editNewFile(resFolder.getConfiguration()); + } + } + + // put in place the listener to handle layout recompute only when needed. + getSite().getPage().addPartListener(this); + } + } catch (PartInitException e) { + AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ + } + } + + /* (non-java doc) + * Change the tab/title name to include the name of the layout. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + handleNewInput(input); + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput) + */ + @Override + protected void setInputWithNotify(IEditorInput input) { + super.setInputWithNotify(input); + handleNewInput(input); + } + + /** + * Called to replace the current {@link IEditorInput} with another one. + * <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're + * opening a different configuration of the same layout. + */ + public void showEditorInput(IEditorInput editorInput) { + // save the current editor input. + doSave(new NullProgressMonitor()); + + // get the current page + int currentPage = getActivePage(); + + // remove the pages, except for the graphical editor, which will be dynamically adapted + // to the new model. + // page after the graphical editor: + int count = getPageCount(); + for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) { + removePage(i); + } + // pages before the graphical editor + for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) { + removePage(i); + } + + // set the current input. + setInputWithNotify(editorInput); + + // re-create or reload the pages with the default page shown as the previous active page. + createAndroidPages(); + selectDefaultPage(Integer.toString(currentPage)); + + // update the outline + if (mOutline != null && mGraphicalEditor != null) { + mOutline.reloadModel(); + } + } + + /** + * Processes the new XML Model, which XML root node is given. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + @Override + protected void xmlModelChanged(Document xml_doc) { + // init the ui root on demand + initUiRootNode(false /*force*/); + + mUiRootNode.loadFromXmlNode(xml_doc); + + // update the model first, since it is used by the viewers. + super.xmlModelChanged(xml_doc); + + if (mGraphicalEditor != null) { + mGraphicalEditor.onXmlModelChanged(); + } + + if (mOutline != null) { + mOutline.reloadModel(); + } + } + + /* (non-java doc) + * Returns the IContentOutlinePage when asked for it. + */ + @SuppressWarnings("unchecked") + @Override + public Object getAdapter(Class adapter) { + // for the outline, force it to come from the Graphical Editor. + // This fixes the case where a layout file is opened in XML view first and the outline + // gets stuck in the XML outline. + if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) { + if (mOutline == null) { + mOutline = new UiContentOutlinePage(mGraphicalEditor, new TreeViewer()); + } + + return mOutline; + } + + if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) { + if (mPropertyPage == null) { + mPropertyPage = new UiPropertySheetPage(); + } + + return mPropertyPage; + } + + // return default + return super.getAdapter(adapter); + } + + @Override + protected void pageChange(int newPageIndex) { + super.pageChange(newPageIndex); + + if (mGraphicalEditor != null) { + if (newPageIndex == mGraphicalEditorIndex) { + mGraphicalEditor.activated(); + } else { + mGraphicalEditor.deactivated(); + } + } + } + + // ----- IPartListener Methods ---- + + public void partActivated(IWorkbenchPart part) { + if (part == this) { + if (mGraphicalEditor != null) { + if (getActivePage() == mGraphicalEditorIndex) { + mGraphicalEditor.activated(); + } else { + mGraphicalEditor.deactivated(); + } + } + } + } + + public void partBroughtToTop(IWorkbenchPart part) { + partActivated(part); + } + + public void partClosed(IWorkbenchPart part) { + // pass + } + + public void partDeactivated(IWorkbenchPart part) { + if (part == this) { + if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) { + mGraphicalEditor.deactivated(); + } + } + } + + public void partOpened(IWorkbenchPart part) { + EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */); + EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */); + } + + public class UiEditorActions extends UiActions { + + @Override + protected UiDocumentNode getRootNode() { + return mUiRootNode; + } + + // Select the new item + @Override + protected void selectUiNode(UiElementNode uiNodeToSelect) { + mGraphicalEditor.selectModel(uiNodeToSelect); + } + + @Override + public void commitPendingXmlChanges() { + // Pass. There is nothing to commit before the XML is changed here. + } + } + + public UiEditorActions getUiEditorActions() { + if (mUiEditorActions == null) { + mUiEditorActions = new UiEditorActions(); + } + return mUiEditorActions; + } + + // ---- Local Methods ---- + + /** + * Returns true if the Graphics editor page is visible. This <b>must</b> be + * called from the UI thread. + */ + boolean isGraphicalEditorActive() { + IWorkbenchPartSite workbenchSite = getSite(); + IWorkbenchPage workbenchPage = workbenchSite.getPage(); + + // check if the editor is visible in the workbench page + if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) { + // and then if the page of the editor is visible (not to be confused with + // the workbench page) + return mGraphicalEditorIndex == getActivePage(); + } + + return false; + } + + @Override + protected void initUiRootNode(boolean force) { + // The root UI node is always created, even if there's no corresponding XML node. + if (mUiRootNode == null || force) { + // get the target data from the opened file (and its project) + AndroidTargetData data = getTargetData(); + + Document doc = null; + if (mUiRootNode != null) { + doc = mUiRootNode.getXmlDocument(); + } + + DocumentDescriptor desc; + if (data == null) { + desc = new DocumentDescriptor("temp", null /*children*/); + } else { + desc = data.getLayoutDescriptors().getDescriptor(); + } + + // get the descriptors from the data. + mUiRootNode = (UiDocumentNode) desc.createUiNode(); + mUiRootNode.setEditor(this); + + onDescriptorsChanged(doc); + } + } + + private void onDescriptorsChanged(Document document) { + if (document != null) { + mUiRootNode.loadFromXmlNode(document); + } else { + mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument()); + } + + if (mOutline != null) { + mOutline.reloadModel(); + } + + if (mGraphicalEditor != null) { + mGraphicalEditor.reloadEditor(); + mGraphicalEditor.reloadPalette(); + mGraphicalEditor.recomputeLayout(); + } + } + + /** + * Handles a new input, and update the part name. + * @param input the new input. + */ + private void handleNewInput(IEditorInput input) { + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + IFile file = fileInput.getFile(); + setPartName(String.format("%1$s", + file.getName())); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java new file mode 100644 index 0000000..cf20288 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolder; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceDelta; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Monitor for file changes triggering a layout redraw. + */ +public final class LayoutReloadMonitor implements IFileListener, IResourceEventListener { + + // singleton, enforced by private constructor. + private final static LayoutReloadMonitor sThis = new LayoutReloadMonitor(); + + /** + * Map of listeners by IProject. + */ + private final Map<IProject, List<ILayoutReloadListener>> mListenerMap = + new HashMap<IProject, List<ILayoutReloadListener>>(); + + private final static int CHANGE_CODE = 0; + private final static int CHANGE_RESOURCES = 1; + private final static int CHANGE_R = 2; + private final static int CHANGE_COUNT = 3; + /** + * List of projects having received a file change. the boolean[] contains 3 values: + * <ul><li>CHANGE_CODE: code change flag.</li> + * <li>CHANGE_RESOURCES: resource change flag.</li> + * <li>CHANGE_R: R clas change flag</li></ul> + */ + private final Map<IProject, boolean[]> mChangedProjects = new HashMap<IProject, boolean[]>(); + + /** + * Classes which implement this interface provide a method to respond to resource changes + * triggering a layout redraw + */ + public interface ILayoutReloadListener { + /** + * Sent when the layout needs to be redrawn + * @param codeChange The trigger happened due to a code change. + * @param rChange The trigger happened due to a change in the R class. + * @param resChange The trigger happened due to a resource change. + */ + void reloadLayout(boolean codeChange, boolean rChange, boolean resChange); + } + + /** + * Returns the single instance of {@link LayoutReloadMonitor}. + */ + public static LayoutReloadMonitor getMonitor() { + return sThis; + } + + private LayoutReloadMonitor() { + ResourceMonitor monitor = ResourceMonitor.getMonitor(); + monitor.addFileListener(this, IResourceDelta.ADDED | IResourceDelta.CHANGED); + monitor.addResourceEventListener(this); + } + + /** + * Adds a listener for a given {@link IProject}. + * @param project + * @param listener + */ + public void addListener(IProject project, ILayoutReloadListener listener) { + synchronized (mListenerMap) { + List<ILayoutReloadListener> list = mListenerMap.get(project); + if (list == null) { + list = new ArrayList<ILayoutReloadListener>(); + mListenerMap.put(project, list); + } + + list.add(listener); + } + } + + /** + * Removes a listener for a given {@link IProject}. + * @param project + * @param listener + */ + public void removeListener(IProject project, ILayoutReloadListener listener) { + synchronized (mListenerMap) { + List<ILayoutReloadListener> list = mListenerMap.get(project); + if (list != null) { + list.remove(listener); + } + } + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener#fileChanged(org.eclipse.core.resources.IFile, org.eclipse.core.resources.IMarkerDelta[], int) + * + * Callback for ResourceMonitor.IFileListener. Called when a file changed. + * This records the changes for each project, but does not notify listeners. + * @see #resourceChangeEventEnd + */ + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { + // get the file project + IProject project = file.getProject(); + + // if this project has already been marked as modified, we do nothing. + boolean[] changeFlags = mChangedProjects.get(project); + if (changeFlags != null && changeFlags[CHANGE_CODE] && changeFlags[CHANGE_RESOURCES] && + changeFlags[CHANGE_R]) { + return; + } + + // now check that the file is *NOT* a layout file (those automatically trigger a layout + // reload and we don't want to do it twice. + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); + if (resFolder != null) { + if (resFolder.getType() != ResourceFolderType.LAYOUT) { + // this is a resource change! + if (changeFlags == null) { + changeFlags = new boolean[CHANGE_COUNT]; + mChangedProjects.put(project, changeFlags); + } + + changeFlags[CHANGE_RESOURCES] = true; + } + } else if (AndroidConstants.EXT_CLASS.equals(file.getFileExtension())) { + if (file.getName().matches("R[\\$\\.](.*)")) { + // this is a R change! + if (changeFlags == null) { + changeFlags = new boolean[CHANGE_COUNT]; + mChangedProjects.put(project, changeFlags); + } + + changeFlags[CHANGE_R] = true; + } else { + // this is a code change! + if (changeFlags == null) { + changeFlags = new boolean[CHANGE_COUNT]; + mChangedProjects.put(project, changeFlags); + } + + changeFlags[CHANGE_CODE] = true; + } + } + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener#resourceChangeEventStart() + * + * Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a resource + * change event. This is called once, while fileChanged can be called several times. + * + */ + public void resourceChangeEventStart() { + // nothing to be done here, it all happens in the resourceChangeEventEnd + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener#resourceChangeEventEnd() + * + * Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource + * change event. This is where we notify the listeners. + */ + public void resourceChangeEventEnd() { + // for each IProject that was changed, we notify all the listeners. + synchronized (mListenerMap) { + for (Entry<IProject, boolean[]> project : mChangedProjects.entrySet()) { + List<ILayoutReloadListener> listeners = mListenerMap.get(project.getKey()); + + boolean[] flags = project.getValue(); + + if (listeners != null) { + for (ILayoutReloadListener listener : listeners) { + listener.reloadLayout(flags[CHANGE_CODE], flags[CHANGE_R], + flags[CHANGE_RESOURCES]); + } + } + } + } + + // empty the list. + mChangedProjects.clear(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java new file mode 100644 index 0000000..1aa1f4c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + + +import com.android.ide.eclipse.editors.AndroidSourceViewerConfig; + +/** + * Source Viewer Configuration that calls in LayoutContentAssist. + */ +public class LayoutSourceViewerConfig extends AndroidSourceViewerConfig { + + public LayoutSourceViewerConfig() { + super(new LayoutContentAssist()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java new file mode 100644 index 0000000..bb075c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.editors.resources.manager.ResourceFolder; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; + +import org.eclipse.core.resources.IFile; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorMatchingStrategy; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; + +/** + * Matching strategy for the Layout Editor. This is used to open all configurations of a layout + * in the same editor. + */ +public class MatchingStrategy implements IEditorMatchingStrategy { + + public boolean matches(IEditorReference editorRef, IEditorInput input) { + // first check that the file being opened is a layout file. + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + + // get the IFile object and check it's in one of the layout folders. + IFile iFile = fileInput.getFile(); + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(iFile); + + // if it's a layout, we know check the name of the fileInput against the name of the + // file being currently edited by the editor since those are independent of the config. + if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT) { + try { + IEditorInput editorInput = editorRef.getEditorInput(); + if (editorInput instanceof FileEditorInput) { + FileEditorInput editorFileInput = (FileEditorInput)editorInput; + IFile editorIFile = editorFileInput.getFile(); + + return editorIFile.getProject().equals(iFile.getProject()) + && editorIFile.getName().equals(iFile.getName()); + } + } catch (PartInitException e) { + // we do nothing, we'll just return false. + } + } + } + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java new file mode 100644 index 0000000..94df28f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; + +import org.eclipse.gef.palette.PaletteDrawer; +import org.eclipse.gef.palette.PaletteGroup; +import org.eclipse.gef.palette.PaletteRoot; +import org.eclipse.gef.palette.PaletteTemplateEntry; + +import java.util.List; + +/** + * Factory that creates the palette for the {@link GraphicalLayoutEditor}. + */ +public class PaletteFactory { + + /** Static factory, nothing to instantiate here. */ + private PaletteFactory() { + } + + public static PaletteRoot createPaletteRoot(PaletteRoot currentPalette, + AndroidTargetData targetData) { + + if (currentPalette == null) { + currentPalette = new PaletteRoot(); + } + + for (int n = currentPalette.getChildren().size() - 1; n >= 0; n--) { + currentPalette.getChildren().remove(n); + } + + if (targetData != null) { + addTools(currentPalette); + addViews(currentPalette, "Layouts", + targetData.getLayoutDescriptors().getLayoutDescriptors()); + addViews(currentPalette, "Views", + targetData.getLayoutDescriptors().getViewDescriptors()); + } + + return currentPalette; + } + + private static void addTools(PaletteRoot paletteRoot) { + PaletteGroup group = new PaletteGroup("Tools"); + + // Default tools: selection. + // Do not use the MarqueeToolEntry since we don't support multiple selection. + /* -- Do not put the selection tool. It's the unique tool so it looks useless. + Leave this piece of code here in case we want it back later. + PanningSelectionToolEntry entry = new PanningSelectionToolEntry(); + group.add(entry); + paletteRoot.setDefaultEntry(entry); + */ + + paletteRoot.add(group); + } + + private static void addViews(PaletteRoot paletteRoot, String groupName, + List<ElementDescriptor> descriptors) { + PaletteDrawer group = new PaletteDrawer(groupName); + + for (ElementDescriptor desc : descriptors) { + PaletteTemplateEntry entry = new PaletteTemplateEntry( + desc.getUiName(), // label + desc.getTooltip(), // short description + desc, // template + desc.getImageDescriptor(), // small icon + desc.getImageDescriptor() // large icon + ); + + group.add(entry); + } + + paletteRoot.add(group); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java new file mode 100644 index 0000000..81fd2ed --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.editors.resources.manager.ProjectClassLoader; +import com.android.ide.eclipse.editors.resources.manager.ProjectResources; +import com.android.layoutlib.api.IProjectCallback; + +import org.eclipse.core.resources.IProject; + +import java.lang.reflect.Constructor; +import java.util.HashMap; + +/** + * Loader for Android Project class in order to use them in the layout editor. + */ +public final class ProjectCallback implements IProjectCallback { + + private final HashMap<String, Class<?>> mLoadedClasses = new HashMap<String, Class<?>>(); + private final IProject mProject; + private final ClassLoader mParentClassLoader; + private final ProjectResources mProjectRes; + private boolean mUsed = false; + private String mNamespace; + + ProjectCallback(ClassLoader classLoader, ProjectResources projectRes, IProject project) { + mParentClassLoader = classLoader; + mProjectRes = projectRes; + mProject = project; + } + + + /** + * {@inheritDoc} + * + * This implementation goes through the output directory of the Eclipse project and loads the + * <code>.class</code> file directly. + */ + @SuppressWarnings("unchecked") + public Object loadView(String className, Class[] constructorSignature, + Object[] constructorParameters) + throws ClassNotFoundException, Exception { + + // look for a cached version + Class<?> clazz = mLoadedClasses.get(className); + if (clazz != null) { + return instantiateClass(clazz, constructorSignature, constructorParameters); + } + + // load the class. + ProjectClassLoader loader = new ProjectClassLoader(mParentClassLoader, mProject); + try { + clazz = loader.loadClass(className); + + if (clazz != null) { + mUsed = true; + mLoadedClasses.put(className, clazz); + return instantiateClass(clazz, constructorSignature, constructorParameters); + } + } catch (Error e) { + // Log this error with the class name we're trying to load and abort. + AdtPlugin.log(e, "ProjectCallback.loadView failed to find class %1$s", className); //$NON-NLS-1$ + } + + return null; + } + + /** + * {@inheritDoc} + * + * Returns the namespace for the project. The namespace contains a standard part + the + * application package. + */ + public String getNamespace() { + if (mNamespace == null) { + AndroidManifestHelper manifest = new AndroidManifestHelper(mProject); + String javaPackage = manifest.getPackageName(); + + mNamespace = String.format(AndroidConstants.NS_CUSTOM_RESOURCES, javaPackage); + } + + return mNamespace; + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.IProjectCallback#resolveResourceValue(int) + */ + public String[] resolveResourceValue(int id) { + if (mProjectRes != null) { + return mProjectRes.resolveResourceValue(id); + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.IProjectCallback#resolveResourceValue(int[]) + */ + public String resolveResourceValue(int[] id) { + if (mProjectRes != null) { + return mProjectRes.resolveResourceValue(id); + } + + return null; + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.IProjectCallback#getResourceValue(java.lang.String, java.lang.String) + */ + public Integer getResourceValue(String type, String name) { + if (mProjectRes != null) { + return mProjectRes.getResourceValue(type, name); + } + + return null; + } + + /** + * Returns whether the loader has received requests to load custom views. + * <p/>This allows to efficiently only recreate when needed upon code change in the project. + */ + boolean isUsed() { + return mUsed; + } + + /** + * Instantiate a class object, using a specific constructor and parameters. + * @param clazz the class to instantiate + * @param constructorSignature the signature of the constructor to use + * @param constructorParameters the parameters to use in the constructor. + * @return A new class object, created using a specific constructor and parameters. + * @throws Exception + */ + @SuppressWarnings("unchecked") + private Object instantiateClass(Class<?> clazz, Class[] constructorSignature, + Object[] constructorParameters) throws Exception { + Constructor<?> constructor = clazz.getConstructor(constructorSignature); + constructor.setAccessible(true); + return constructor.newInstance(constructorParameters); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java new file mode 100644 index 0000000..3e0f5d8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java @@ -0,0 +1,615 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.common.EclipseUiHelper; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.layout.parts.UiDocumentTreeEditPart; +import com.android.ide.eclipse.editors.layout.parts.UiElementTreeEditPart; +import com.android.ide.eclipse.editors.layout.parts.UiElementTreeEditPartFactory; +import com.android.ide.eclipse.editors.layout.parts.UiLayoutTreeEditPart; +import com.android.ide.eclipse.editors.layout.parts.UiViewTreeEditPart; +import com.android.ide.eclipse.editors.ui.tree.CopyCutAction; +import com.android.ide.eclipse.editors.ui.tree.PasteAction; +import com.android.ide.eclipse.editors.ui.tree.UiActions; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.gef.EditPartViewer; +import org.eclipse.gef.ui.parts.ContentOutlinePage; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.IActionBars; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Implementation of the {@link ContentOutlinePage} to display {@link UiElementNode}. + */ +class UiContentOutlinePage extends ContentOutlinePage { + + private GraphicalLayoutEditor mEditor; + + private Action mAddAction; + private Action mDeleteAction; + private Action mUpAction; + private Action mDownAction; + + private UiOutlineActions mUiActions = new UiOutlineActions(); + + public UiContentOutlinePage(GraphicalLayoutEditor editor, final EditPartViewer viewer) { + super(viewer); + mEditor = editor; + IconFactory factory = IconFactory.getInstance(); + + mAddAction = new Action("Add...") { + @Override + public void run() { + List<UiElementNode> nodes = getModelSelections(); + UiElementNode node = nodes != null && nodes.size() > 0 ? nodes.get(0) : null; + + mUiActions.doAdd(node, viewer.getControl().getShell()); + } + }; + mAddAction.setToolTipText("Adds a new element."); + mAddAction.setImageDescriptor(factory.getImageDescriptor("add")); //$NON-NLS-1$ + + mDeleteAction = new Action("Remove...") { + @Override + public void run() { + List<UiElementNode> nodes = getModelSelections(); + + mUiActions.doRemove(nodes, viewer.getControl().getShell()); + } + }; + mDeleteAction.setToolTipText("Removes an existing selected element."); + mDeleteAction.setImageDescriptor(factory.getImageDescriptor("delete")); //$NON-NLS-1$ + + mUpAction = new Action("Up") { + @Override + public void run() { + List<UiElementNode> nodes = getModelSelections(); + + mUiActions.doUp(nodes); + } + }; + mUpAction.setToolTipText("Moves the selected element up"); + mUpAction.setImageDescriptor(factory.getImageDescriptor("up")); //$NON-NLS-1$ + + mDownAction = new Action("Down") { + @Override + public void run() { + List<UiElementNode> nodes = getModelSelections(); + + mUiActions.doDown(nodes); + } + }; + mDownAction.setToolTipText("Moves the selected element down"); + mDownAction.setImageDescriptor(factory.getImageDescriptor("down")); //$NON-NLS-1$ + + // all actions disabled by default. + mAddAction.setEnabled(false); + mDeleteAction.setEnabled(false); + mUpAction.setEnabled(false); + mDownAction.setEnabled(false); + + addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + + // the selection is never empty. The least it'll contain is the + // UiDocumentTreeEditPart object. + if (selection instanceof StructuredSelection) { + StructuredSelection structSel = (StructuredSelection)selection; + + if (structSel.size() == 1 && + structSel.getFirstElement() instanceof UiDocumentTreeEditPart) { + mDeleteAction.setEnabled(false); + mUpAction.setEnabled(false); + mDownAction.setEnabled(false); + } else { + mDeleteAction.setEnabled(true); + mUpAction.setEnabled(true); + mDownAction.setEnabled(true); + } + + // the "add" button is always enabled, in order to be able to set the + // initial root node + mAddAction.setEnabled(true); + } + } + }); + } + + + /* (non-Javadoc) + * @see org.eclipse.ui.part.IPage#createControl(org.eclipse.swt.widgets.Composite) + */ + @Override + public void createControl(Composite parent) { + // create outline viewer page + getViewer().createControl(parent); + + // configure outline viewer + getViewer().setEditPartFactory(new UiElementTreeEditPartFactory()); + + setupOutline(); + setupContextMenu(); + setupTooltip(); + setupDoubleClick(); + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.part.Page#setActionBars(org.eclipse.ui.IActionBars) + * + * Called automatically after createControl + */ + @Override + public void setActionBars(IActionBars actionBars) { + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.add(mAddAction); + toolBarManager.add(mDeleteAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mUpAction); + toolBarManager.add(mDownAction); + + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(mAddAction); + menuManager.add(mDeleteAction); + menuManager.add(new Separator()); + menuManager.add(mUpAction); + menuManager.add(mDownAction); + } + + /* (non-Javadoc) + * @see org.eclipse.ui.part.IPage#dispose() + */ + @Override + public void dispose() { + breakConnectionWithEditor(); + + // dispose + super.dispose(); + } + + /* (non-Javadoc) + * @see org.eclipse.ui.part.IPage#getControl() + */ + @Override + public Control getControl() { + return getViewer().getControl(); + } + + void setNewEditor(GraphicalLayoutEditor editor) { + mEditor = editor; + setupOutline(); + } + + void breakConnectionWithEditor() { + // unhook outline viewer + mEditor.getSelectionSynchronizer().removeViewer(getViewer()); + } + + private void setupOutline() { + getViewer().setEditDomain(mEditor.getEditDomain()); + + // hook outline viewer + mEditor.getSelectionSynchronizer().addViewer(getViewer()); + + // initialize outline viewer with model + getViewer().setContents(mEditor.getModel()); + } + + private void setupContextMenu() { + MenuManager menuManager = new MenuManager(); + menuManager.setRemoveAllWhenShown(true); + menuManager.addMenuListener(new IMenuListener() { + /** + * The menu is about to be shown. The menu manager has already been + * requested to remove any existing menu item. This method gets the + * tree selection and if it is of the appropriate type it re-creates + * the necessary actions. + */ + public void menuAboutToShow(IMenuManager manager) { + List<UiElementNode> selected = getModelSelections(); + + if (selected != null) { + doCreateMenuAction(manager, selected); + return; + } + doCreateMenuAction(manager, null /* ui_node */); + } + }); + Control control = getControl(); + Menu contextMenu = menuManager.createContextMenu(control); + control.setMenu(contextMenu); + } + + /** + * Adds the menu actions to the context menu when the given UI node is selected in + * the tree view. + * + * @param manager The context menu manager + * @param selected The UI node selected in the tree. Can be null, in which case the root + * is to be modified. + */ + private void doCreateMenuAction(IMenuManager manager, List<UiElementNode> selected) { + + if (selected != null) { + boolean hasXml = false; + for (UiElementNode uiNode : selected) { + if (uiNode.getXmlNode() != null) { + hasXml = true; + break; + } + } + + if (hasXml) { + manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), + null, selected, true /* cut */)); + manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), + null, selected, false /* cut */)); + + // Can't paste with more than one element selected (the selection is the target) + if (selected.size() <= 1) { + // Paste is not valid if it would add a second element on a terminal element + // which parent is a document -- an XML document can only have one child. This + // means paste is valid if the current UI node can have children or if the parent + // is not a document. + UiElementNode ui_root = selected.get(0).getUiRoot(); + if (ui_root.getDescriptor().hasChildren() || + !(ui_root.getUiParent() instanceof UiDocumentNode)) { + manager.add(new PasteAction(mEditor.getLayoutEditor(), + mEditor.getClipboard(), + selected.get(0))); + } + } + manager.add(new Separator()); + } + } + + // Append "add" and "remove" actions. They do the same thing as the add/remove + // buttons on the side. + // + // "Add" makes sense only if there's 0 or 1 item selected since the + // one selected item becomes the target. + if (selected == null || selected.size() <= 1) { + manager.add(mAddAction); + } + + if (selected != null) { + manager.add(mDeleteAction); + manager.add(new Separator()); + + manager.add(mUpAction); + manager.add(mDownAction); + } + + if (selected != null && selected.size() == 1) { + manager.add(new Separator()); + + Action propertiesAction = new Action("Properties") { + @Override + public void run() { + EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, + true /* activate */); + } + }; + propertiesAction.setToolTipText("Displays properties of the selected element."); + manager.add(propertiesAction); + } + } + + /** + * Updates the outline view with the model of the {@link GraphicalLayoutEditor}. + * <p/> + * This attemps to preserve the selection, if any. + */ + public void reloadModel() { + // Attemps to preserve the UiNode selection, if any + List<UiElementNode> uiNodes = null; + try { + // get current selection using the model rather than the edit part as + // reloading the content may change the actual edit part. + uiNodes = getModelSelections(); + + // perform the update + getViewer().setContents(mEditor.getModel()); + + } finally { + // restore selection + if (uiNodes != null) { + setModelSelection(uiNodes.get(0)); + } + } + } + + /** + * Returns the currently selected element, if any, in the viewer. + * This returns the viewer's elements (i.e. an {@link UiElementTreeEditPart}) + * and not the underlying model node. + * <p/> + * When there is no actual selection, this might still return the root node, + * which is of type {@link UiDocumentTreeEditPart}. + */ + @SuppressWarnings("unchecked") + private List<UiElementTreeEditPart> getViewerSelections() { + ISelection selection = getSelection(); + if (selection instanceof StructuredSelection) { + StructuredSelection structuredSelection = (StructuredSelection)selection; + + if (structuredSelection.size() > 0) { + ArrayList<UiElementTreeEditPart> selected = new ArrayList<UiElementTreeEditPart>(); + + for (Iterator it = structuredSelection.iterator(); it.hasNext(); ) { + Object selectedObj = it.next(); + + if (selectedObj instanceof UiElementTreeEditPart) { + selected.add((UiElementTreeEditPart) selectedObj); + } + } + + return selected.size() > 0 ? selected : null; + } + } + + return null; + } + + /** + * Returns the currently selected model element, which is either an + * {@link UiViewTreeEditPart} or an {@link UiLayoutTreeEditPart}. + * <p/> + * Returns null if there is no selection or if the implicit root is "selected" + * (which actually represents the lack of a real element selection.) + */ + private List<UiElementNode> getModelSelections() { + + List<UiElementTreeEditPart> parts = getViewerSelections(); + + if (parts != null) { + ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); + + for (UiElementTreeEditPart part : parts) { + if (part instanceof UiViewTreeEditPart || part instanceof UiLayoutTreeEditPart) { + selected.add((UiElementNode) part.getModel()); + } + } + + return selected.size() > 0 ? selected : null; + } + + return null; + } + + /** + * Selects the corresponding edit part in the tree viewer. + */ + private void setViewerSelection(UiElementTreeEditPart selectedPart) { + if (selectedPart != null && !(selectedPart instanceof UiDocumentTreeEditPart)) { + LinkedList<UiElementTreeEditPart> segments = new LinkedList<UiElementTreeEditPart>(); + for (UiElementTreeEditPart part = selectedPart; + !(part instanceof UiDocumentTreeEditPart); + part = (UiElementTreeEditPart) part.getParent()) { + segments.add(0, part); + } + setSelection(new TreeSelection(new TreePath(segments.toArray()))); + } + } + + /** + * Selects the corresponding model element in the tree viewer. + */ + private void setModelSelection(UiElementNode uiNodeToSelect) { + if (uiNodeToSelect != null) { + + // find an edit part that has the requested model element + UiElementTreeEditPart part = findPartForModel( + (UiElementTreeEditPart) getViewer().getContents(), + uiNodeToSelect); + + // if we found a part, select it and reveal it + if (part != null) { + setViewerSelection(part); + getViewer().reveal(part); + } + } + } + + /** + * Utility method that tries to find an edit part that matches a given model UI node. + * + * @param rootPart The root of the viewer edit parts + * @param uiNode The UI node model to find + * @return The part that matches the model or null if it's not in the sub tree. + */ + private UiElementTreeEditPart findPartForModel(UiElementTreeEditPart rootPart, + UiElementNode uiNode) { + if (rootPart.getModel() == uiNode) { + return rootPart; + } + + for (Object part : rootPart.getChildren()) { + if (part instanceof UiElementTreeEditPart) { + UiElementTreeEditPart found = findPartForModel( + (UiElementTreeEditPart) part, uiNode); + if (found != null) { + return found; + } + } + } + + return null; + } + + /** + * Sets up a custom tooltip when hovering over tree items. + * <p/> + * The tooltip will display the element's javadoc, if any, or the item's getText otherwise. + */ + private void setupTooltip() { + final Tree tree = (Tree) getControl(); + + /* + * Reference: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup + */ + + final Listener listener = new Listener() { + Shell tip = null; + Label label = null; + + public void handleEvent(Event event) { + switch(event.type) { + case SWT.Dispose: + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + case SWT.MouseMove: + if (tip != null) { + tip.dispose(); + tip = null; + label = null; + } + break; + case SWT.MouseHover: + if (tip != null) { + tip.dispose(); + tip = null; + label = null; + } + + String tooltip = null; + + TreeItem item = tree.getItem(new Point(event.x, event.y)); + if (item != null) { + Object data = item.getData(); + if (data instanceof UiElementTreeEditPart) { + Object model = ((UiElementTreeEditPart) data).getModel(); + if (model instanceof UiElementNode) { + tooltip = ((UiElementNode) model).getDescriptor().getTooltip(); + } + } + + if (tooltip == null) { + tooltip = item.getText(); + } else { + tooltip = item.getText() + ":\r" + tooltip; + } + } + + + if (tooltip != null) { + Shell shell = tree.getShell(); + Display display = tree.getDisplay(); + + tip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); + tip.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + FillLayout layout = new FillLayout(); + layout.marginWidth = 2; + tip.setLayout(layout); + label = new Label(tip, SWT.NONE); + label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + label.setData("_TABLEITEM", item); + label.setText(tooltip); + label.addListener(SWT.MouseExit, this); + label.addListener(SWT.MouseDown, this); + Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT); + Rectangle rect = item.getBounds(0); + Point pt = tree.toDisplay(rect.x, rect.y); + tip.setBounds(pt.x, pt.y, size.x, size.y); + tip.setVisible(true); + } + } + } + }; + + tree.addListener(SWT.Dispose, listener); + tree.addListener(SWT.KeyDown, listener); + tree.addListener(SWT.MouseMove, listener); + tree.addListener(SWT.MouseHover, listener); + } + + /** + * Sets up double-click action on the tree. + * <p/> + * By default, double-click (a.k.a. "default selection") on a valid list item will + * show the property view. + */ + private void setupDoubleClick() { + final Tree tree = (Tree) getControl(); + + tree.addListener(SWT.DefaultSelection, new Listener() { + public void handleEvent(Event event) { + EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, + true /* activate */); + } + }); + } + + // --------------- + + private class UiOutlineActions extends UiActions { + + @Override + protected UiDocumentNode getRootNode() { + return mEditor.getModel(); // this is LayoutEditor.getUiRootNode() + } + + // Select the new item + @Override + protected void selectUiNode(UiElementNode uiNodeToSelect) { + setModelSelection(uiNodeToSelect); + } + + @Override + public void commitPendingXmlChanges() { + // Pass. There is nothing to commit before the XML is changed here. + } + + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java new file mode 100644 index 0000000..b0e6fdb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.layoutlib.api.IXmlPullParser; + +import org.w3c.dom.Node; +import org.xmlpull.v1.XmlPullParserException; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link IXmlPullParser} implementation on top of {@link UiElementNode}. + * <p/>It's designed to work on layout files, and will most likely not work on other resource + * files. + */ +public final class UiElementPullParser extends BasePullParser { + + private final ArrayList<UiElementNode> mNodeStack = new ArrayList<UiElementNode>(); + private UiElementNode mRoot; + + public UiElementPullParser(UiElementNode top) { + super(); + mRoot = top; + push(mRoot); + } + + private UiElementNode getCurrentNode() { + if (mNodeStack.size() > 0) { + return mNodeStack.get(mNodeStack.size()-1); + } + + return null; + } + + private Node getAttribute(int i) { + if (mParsingState != START_TAG) { + throw new IndexOutOfBoundsException(); + } + + // get the current uiNode + UiElementNode uiNode = getCurrentNode(); + + // get its xml node + Node xmlNode = uiNode.getXmlNode(); + + if (xmlNode != null) { + return xmlNode.getAttributes().item(i); + } + + return null; + } + + private void push(UiElementNode node) { + mNodeStack.add(node); + } + + private UiElementNode pop() { + return mNodeStack.remove(mNodeStack.size()-1); + } + + // ------------- IXmlPullParser -------- + + /** + * {@inheritDoc} + * + * This implementation returns the underlying DOM node. + */ + public Object getViewKey() { + return getCurrentNode(); + } + + // ------------- XmlPullParser -------- + + public String getPositionDescription() { + return "XML DOM element depth:" + mNodeStack.size(); + } + + public int getAttributeCount() { + UiElementNode node = getCurrentNode(); + if (node != null) { + return node.getUiAttributes().size(); + } + + return 0; + } + + public String getAttributeName(int i) { + Node attribute = getAttribute(i); + if (attribute != null) { + return attribute.getLocalName(); + } + + return null; + } + + public String getAttributeNamespace(int i) { + Node attribute = getAttribute(i); + if (attribute != null) { + return attribute.getNamespaceURI(); + } + return ""; //$NON-NLS-1$ + } + + public String getAttributePrefix(int i) { + Node attribute = getAttribute(i); + if (attribute != null) { + return attribute.getPrefix(); + } + return null; + } + + public String getAttributeValue(int i) { + Node attribute = getAttribute(i); + if (attribute != null) { + return attribute.getNodeValue(); + } + + return null; + } + + public String getAttributeValue(String namespace, String localName) { + // get the current uiNode + UiElementNode uiNode = getCurrentNode(); + + // get its xml node + Node xmlNode = uiNode.getXmlNode(); + + if (xmlNode != null) { + Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName); + if (attribute != null) { + return attribute.getNodeValue(); + } + } + + return null; + } + + public int getDepth() { + return mNodeStack.size(); + } + + public String getName() { + if (mParsingState == START_TAG || mParsingState == END_TAG) { + return getCurrentNode().getDescriptor().getXmlLocalName(); + } + + return null; + } + + public String getNamespace() { + if (mParsingState == START_TAG || mParsingState == END_TAG) { + return getCurrentNode().getDescriptor().getNamespace(); + } + + return null; + } + + public String getPrefix() { + if (mParsingState == START_TAG || mParsingState == END_TAG) { + // FIXME will NEVER work + if (getCurrentNode().getDescriptor().getXmlLocalName().startsWith("android:")) { //$NON-NLS-1$ + return "android"; //$NON-NLS-1$ + } + } + + return null; + } + + public boolean isEmptyElementTag() throws XmlPullParserException { + if (mParsingState == START_TAG) { + return getCurrentNode().getUiChildren().size() == 0; + } + + throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG", + this, null); + } + + @Override + public void onNextFromStartDocument() { + onNextFromStartTag(); + } + + @Override + public void onNextFromStartTag() { + // get the current node, and look for text or children (children first) + UiElementNode node = getCurrentNode(); + List<UiElementNode> children = node.getUiChildren(); + if (children.size() > 0) { + // move to the new child, and don't change the state. + push(children.get(0)); + + // in case the current state is CURRENT_DOC, we set the proper state. + mParsingState = START_TAG; + } else { + if (mParsingState == START_DOCUMENT) { + // this handles the case where there's no node. + mParsingState = END_DOCUMENT; + } else { + mParsingState = END_TAG; + } + } + } + + @Override + public void onNextFromEndTag() { + // look for a sibling. if no sibling, go back to the parent + UiElementNode node = getCurrentNode(); + node = node.getUiNextSibling(); + if (node != null) { + // to go to the sibling, we need to remove the current node, + pop(); + // and add its sibling. + push(node); + mParsingState = START_TAG; + } else { + // move back to the parent + pop(); + + // we have only one element left (mRoot), then we're done with the document. + if (mNodeStack.size() == 1) { + mParsingState = END_DOCUMENT; + } else { + mParsingState = END_TAG; + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java new file mode 100644 index 0000000..8093c90 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.views.properties.PropertySheetEntry; +import org.eclipse.ui.views.properties.PropertySheetPage; + +/** + * A customized property sheet page for the graphical layout editor. + * <p/> + * Currently it just provides a custom tooltip to display attributes javadocs. + */ +public class UiPropertySheetPage extends PropertySheetPage { + + + public UiPropertySheetPage() { + super(); + } + + @Override + public void createControl(Composite parent) { + super.createControl(parent); + + setupTooltip(); + } + + /** + * Sets up a custom tooltip when hovering over tree items. + * <p/> + * The tooltip will display the element's javadoc, if any, or the item's getText otherwise. + */ + private void setupTooltip() { + final Tree tree = (Tree) getControl(); + + /* + * Reference: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup + */ + + final Listener listener = new Listener() { + Shell tip = null; + Label label = null; + + public void handleEvent(Event event) { + switch(event.type) { + case SWT.Dispose: + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + case SWT.MouseMove: + if (tip != null) { + tip.dispose(); + tip = null; + label = null; + } + break; + case SWT.MouseHover: + if (tip != null) { + tip.dispose(); + tip = null; + label = null; + } + + String tooltip = null; + + TreeItem item = tree.getItem(new Point(event.x, event.y)); + if (item != null) { + Object data = item.getData(); + if (data instanceof PropertySheetEntry) { + tooltip = ((PropertySheetEntry) data).getDescription(); + } + + if (tooltip == null) { + tooltip = item.getText(); + } else { + tooltip = item.getText() + ":\r" + tooltip; + } + } + + if (tooltip != null) { + Shell shell = tree.getShell(); + Display display = tree.getDisplay(); + + tip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); + tip.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + FillLayout layout = new FillLayout(); + layout.marginWidth = 2; + tip.setLayout(layout); + label = new Label(tip, SWT.NONE); + label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + label.setData("_TABLEITEM", item); + label.setText(tooltip); + label.addListener(SWT.MouseExit, this); + label.addListener(SWT.MouseDown, this); + Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT); + Rectangle rect = item.getBounds(0); + Point pt = tree.toDisplay(rect.x, rect.y); + tip.setBounds(pt.x, pt.y, size.x, size.y); + tip.setVisible(true); + } + } + } + }; + + tree.addListener(SWT.Dispose, listener); + tree.addListener(SWT.KeyDown, listener); + tree.addListener(SWT.MouseMove, listener); + tree.addListener(SWT.MouseHover, listener); + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java new file mode 100644 index 0000000..e62ab69 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor; +import com.android.layoutlib.api.IXmlPullParser; +import com.android.sdklib.SdkConstants; + +import org.xmlpull.v1.XmlPullParserException; + +/** + * {@link IXmlPullParser} implementation to render android widget bitmap. + * <p/>The parser emulates a layout that contains just one widget, described by the + * {@link ViewElementDescriptor} passed in the constructor. + */ +public class WidgetPullParser extends BasePullParser { + + private final ViewElementDescriptor mDescriptor; + private String[][] mAttributes = new String[][] { + { "text", null }, + { "layout_width", "wrap_content" }, + { "layout_height", "wrap_content" }, + }; + + public WidgetPullParser(ViewElementDescriptor descriptor) { + mDescriptor = descriptor; + + String[] segments = mDescriptor.getCanonicalClassName().split(AndroidConstants.RE_DOT); + mAttributes[0][1] = segments[segments.length-1]; + } + + public Object getViewKey() { + // we need a viewKey or the ILayoutResult will not contain any ILayoutViewInfo + return mDescriptor; + } + + public int getAttributeCount() { + return mAttributes.length; // text attribute + } + + public String getAttributeName(int index) { + if (index < mAttributes.length) { + return mAttributes[index][0]; + } + + return null; + } + + public String getAttributeNamespace(int index) { + return SdkConstants.NS_RESOURCES; + } + + public String getAttributePrefix(int index) { + // pass + return null; + } + + public String getAttributeValue(int index) { + if (index < mAttributes.length) { + return mAttributes[index][1]; + } + + return null; + } + + public String getAttributeValue(String ns, String name) { + if (SdkConstants.NS_RESOURCES.equals(ns)) { + for (String[] attribute : mAttributes) { + if (name.equals(attribute[0])) { + return attribute[1]; + } + } + } + + return null; + } + + public int getDepth() { + // pass + return 0; + } + + public String getName() { + return mDescriptor.getXmlLocalName(); + } + + public String getNamespace() { + // pass + return null; + } + + public String getPositionDescription() { + // pass + return null; + } + + public String getPrefix() { + // pass + return null; + } + + public boolean isEmptyElementTag() throws XmlPullParserException { + if (mParsingState == START_TAG) { + return true; + } + + throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG", + this, null); + } + + @Override + public void onNextFromStartDocument() { + // just go to start_tag + mParsingState = START_TAG; + } + + @Override + public void onNextFromStartTag() { + // since we have no children, just go to end_tag + mParsingState = END_TAG; + } + + @Override + public void onNextFromEndTag() { + // just one tag. we are done. + mParsingState = END_DOCUMENT; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java new file mode 100644 index 0000000..d5ee2ca --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.descriptors; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.resources.ViewClassInfo; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import java.util.HashMap; +import java.util.List; + +/** + * Service responsible for creating/managing {@link ElementDescriptor} objects for custom + * View classes per project. + * <p/> + * The service provides an on-demand monitoring of custom classes to check for changes. Monitoring + * starts once a request for an {@link ElementDescriptor} object has been done for a specific + * class.<br> + * The monitoring will notify a listen of any changes in the class triggering a change in its + * associated {@link ElementDescriptor} object. + * <p/> + * If the custom class does not exist, no monitoring is put in place to avoid having to listen + * to all class changes in the projects. + * + */ +public final class CustomViewDescriptorService { + + private static CustomViewDescriptorService sThis = new CustomViewDescriptorService(); + + /** + * Map where keys are the project, and values are another map containing all the known + * custom View class for this project. The custom View class are stored in a map + * where the keys are the fully qualified class name, and the values are their associated + * {@link ElementDescriptor}. + */ + private HashMap<IProject, HashMap<String, ElementDescriptor>> mCustomDescriptorMap = + new HashMap<IProject, HashMap<String, ElementDescriptor>>(); + + /** + * TODO will be used to update the ElementDescriptor of the custom view when it + * is modified (either the class itself or its attributes.xml) + */ + @SuppressWarnings("unused") + private ICustomViewDescriptorListener mListener; + + /** + * Classes which implements this interface provide a method that deal with modifications + * in custom View class triggering a change in its associated {@link ViewClassInfo} object. + */ + public interface ICustomViewDescriptorListener { + /** + * Sent when a custom View class has changed and its {@link ElementDescriptor} was modified. + * @param project the project containing the class. + * @param className the fully qualified class name. + * @param descriptor the updated ElementDescriptor. + */ + public void updatedClassInfo(IProject project, String className, ElementDescriptor descriptor); + } + + /** + * Returns the singleton instance of {@link CustomViewDescriptorService}. + */ + public static CustomViewDescriptorService getInstance() { + return sThis; + } + + /** + * Sets the listener receiving custom View class modification notifications. + * @param listener the listener to receive the notifications. + * + * TODO will be used to update the ElementDescriptor of the custom view when it + * is modified (either the class itself or its attributes.xml) + */ + public void setListener(ICustomViewDescriptorListener listener) { + mListener = listener; + } + + /** + * Returns the {@link ElementDescriptor} for a particular project/class. + * <p/> + * If it is the first time the <code>ElementDescriptor</code> is requested, the method + * will check that the specified class is in fact a custom View class. Once this is + * established, a monitoring for that particular class is initiated. Any change will + * trigger a notification to the {@link ICustomViewDescriptorListener}. + * @param project the project containing the class. + * @param fqClassName the fully qualified name of the class. + * @return a <code>ElementDescriptor</code> or <code>null</code> if the class was not + * a custom View class. + */ + public ElementDescriptor getDescriptor(IProject project, String fqClassName) { + // look in the map first + synchronized (mCustomDescriptorMap) { + HashMap<String, ElementDescriptor> map = mCustomDescriptorMap.get(project); + + if (map != null) { + ElementDescriptor descriptor = map.get(fqClassName); + if (descriptor != null) { + return descriptor; + } + } + + // if we step here, it looks like we haven't created it yet. + // First lets check this is in fact a valid type in the project + + try { + // We expect the project to be both opened and of java type (since it's an android + // project), so we can create a IJavaProject object from our IProject. + IJavaProject javaProject = JavaCore.create(project); + + // replace $ by . in the class name + String javaClassName = fqClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$ + + // look for the IType object for this class + IType type = javaProject.findType(javaClassName); + if (type != null && type.exists()) { + // the type exists. Let's get the parent class and its ViewClassInfo. + + // get the type hierarchy + ITypeHierarchy hierarchy = type.newSupertypeHierarchy( + new NullProgressMonitor()); + + ElementDescriptor parentDescriptor = getDescriptor( + hierarchy.getSuperclass(type), project, hierarchy); + + if (parentDescriptor != null) { + // we have a valid parent, lets create a new ElementDescriptor. + + ViewElementDescriptor descriptor = new ViewElementDescriptor(fqClassName, + fqClassName, // ui_name + fqClassName, // canonical class name + null, // tooltip + null, // sdk_url + getAttributeDescriptor(type, parentDescriptor), + null, // layout attributes + null, // children + false /* mandatory */); + + synchronized (mCustomDescriptorMap) { + map = mCustomDescriptorMap.get(project); + if (map == null) { + map = new HashMap<String, ElementDescriptor>(); + mCustomDescriptorMap.put(project, map); + } + + map.put(fqClassName, descriptor); + } + + //TODO setup listener on this resource change. + + return descriptor; + } + } + } catch (JavaModelException e) { + // there was an error accessing any of the IType, we'll just return null; + } + } + + + return null; + } + + /** + * Computes (if needed) and returns the {@link ElementDescriptor} for the specified type. + * + * @param type + * @param project + * @param typeHierarchy + * @return A ViewElementDescriptor or null if type or typeHierarchy is null. + */ + private ViewElementDescriptor getDescriptor(IType type, IProject project, + ITypeHierarchy typeHierarchy) { + // check if the type is a built-in View class. + List<ElementDescriptor> builtInList = null; + + Sdk currentSdk = Sdk.getCurrent(); + IAndroidTarget target = currentSdk == null ? null : currentSdk.getTarget(project); + if (target != null) { + AndroidTargetData data = currentSdk.getTargetData(target); + builtInList = data.getLayoutDescriptors().getViewDescriptors(); + } + + // give up if there's no type + if (type == null) { + return null; + } + + String canonicalName = type.getFullyQualifiedName(); + + if (builtInList != null) { + for (ElementDescriptor desc : builtInList) { + if (desc instanceof ViewElementDescriptor) { + ViewElementDescriptor viewDescriptor = (ViewElementDescriptor)desc; + if (canonicalName.equals(viewDescriptor.getCanonicalClassName())) { + return viewDescriptor; + } + } + } + } + + // it's not a built-in class? Lets look if the superclass is built-in + // give up if there's no type + if (typeHierarchy == null) { + return null; + } + + IType parentType = typeHierarchy.getSuperclass(type); + if (parentType != null) { + ViewElementDescriptor parentDescriptor = getDescriptor(parentType, project, + typeHierarchy); + + if (parentDescriptor != null) { + // parent class is a valid View class with a descriptor, so we create one + // for this class. + ViewElementDescriptor descriptor = new ViewElementDescriptor(canonicalName, + canonicalName, // ui_name + canonicalName, // canonical name + null, // tooltip + null, // sdk_url + getAttributeDescriptor(type, parentDescriptor), + null, // layout attributes + null, // children + false /* mandatory */); + + // add it to the map + synchronized (mCustomDescriptorMap) { + HashMap<String, ElementDescriptor> map = mCustomDescriptorMap.get(project); + + if (map == null) { + map = new HashMap<String, ElementDescriptor>(); + mCustomDescriptorMap.put(project, map); + } + + map.put(canonicalName, descriptor); + + } + + //TODO setup listener on this resource change. + + return descriptor; + } + } + + // class is neither a built-in view class, nor extend one. return null. + return null; + } + + /** + * Returns the array of {@link AttributeDescriptor} for the specified {@link IType}. + * <p/> + * The array should contain the descriptor for this type and all its supertypes. + * @param type the type for which the {@link AttributeDescriptor} are returned. + * @param parentDescriptor the {@link ElementDescriptor} of the direct superclass. + */ + private AttributeDescriptor[] getAttributeDescriptor(IType type, + ElementDescriptor parentDescriptor) { + // TODO add the class attribute descriptors to the parent descriptors. + return parentDescriptor.getAttributes(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java new file mode 100644 index 0000000..7caa50f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.descriptors; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.ViewClassInfo; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; +import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; +import com.android.sdklib.SdkConstants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +/** + * Complete description of the layout structure. + */ +public final class LayoutDescriptors implements IDescriptorProvider { + + // Public attributes names, attributes descriptors and elements descriptors + public static final String ID_ATTR = "id"; //$NON-NLS-1$ + + /** The document descriptor. Contains all layouts and views linked together. */ + private DocumentDescriptor mDescriptor = + new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$ + + /** The list of all known ViewLayout descriptors. */ + private ArrayList<ElementDescriptor> mLayoutDescriptors = new ArrayList<ElementDescriptor>(); + + /** Read-Only list of View Descriptors. */ + private List<ElementDescriptor> mROLayoutDescriptors; + + /** The list of all known View (not ViewLayout) descriptors. */ + private ArrayList<ElementDescriptor> mViewDescriptors = new ArrayList<ElementDescriptor>(); + + /** Read-Only list of View Descriptors. */ + private List<ElementDescriptor> mROViewDescriptors; + + /** @return the document descriptor. Contains all layouts and views linked together. */ + public DocumentDescriptor getDescriptor() { + return mDescriptor; + } + + /** @return The read-only list of all known ViewLayout descriptors. */ + public List<ElementDescriptor> getLayoutDescriptors() { + return mROLayoutDescriptors; + } + + /** @return The read-only list of all known View (not ViewLayout) descriptors. */ + public List<ElementDescriptor> getViewDescriptors() { + return mROViewDescriptors; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mDescriptor.getChildren(); + } + + /** + * Updates the document descriptor. + * <p/> + * It first computes the new children of the descriptor and then update them + * all at once. + * <p/> + * TODO: differentiate groups from views in the tree UI? => rely on icons + * <p/> + * + * @param views The list of views in the framework. + * @param layouts The list of layouts in the framework. + */ + public synchronized void updateDescriptors(ViewClassInfo[] views, ViewClassInfo[] layouts) { + ArrayList<ElementDescriptor> newViews = new ArrayList<ElementDescriptor>(); + if (views != null) { + for (ViewClassInfo info : views) { + ElementDescriptor desc = convertView(info); + newViews.add(desc); + } + } + + ArrayList<ElementDescriptor> newLayouts = new ArrayList<ElementDescriptor>(); + if (layouts != null) { + for (ViewClassInfo info : layouts) { + ElementDescriptor desc = convertView(info); + newLayouts.add(desc); + } + } + + ArrayList<ElementDescriptor> newDescriptors = new ArrayList<ElementDescriptor>(); + newDescriptors.addAll(newLayouts); + newDescriptors.addAll(newViews); + ElementDescriptor[] newArray = newDescriptors.toArray( + new ElementDescriptor[newDescriptors.size()]); + + // Link all layouts to everything else here.. recursively + for (ElementDescriptor layoutDesc : newLayouts) { + layoutDesc.setChildren(newArray); + } + + mViewDescriptors = newViews; + mLayoutDescriptors = newLayouts; + mDescriptor.setChildren(newArray); + + mROLayoutDescriptors = Collections.unmodifiableList(mLayoutDescriptors); + mROViewDescriptors = Collections.unmodifiableList(mViewDescriptors); + } + + /** + * Creates an element descriptor from a given {@link ViewClassInfo}. + */ + private ElementDescriptor convertView(ViewClassInfo info) { + String xml_name = info.getShortClassName(); + String tooltip = info.getJavaDoc(); + + // Process all View attributes + ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>(); + DescriptorsUtils.appendAttributes(attributes, + null, // elementName + SdkConstants.NS_RESOURCES, + info.getAttributes(), + null, // requiredAttributes + null /* overrides */); + + for (ViewClassInfo link = info.getSuperClass(); + link != null; + link = link.getSuperClass()) { + AttributeInfo[] attrList = link.getAttributes(); + if (attrList.length > 0) { + attributes.add(new SeparatorAttributeDescriptor( + String.format("Attributes from %1$s", link.getShortClassName()))); + DescriptorsUtils.appendAttributes(attributes, + null, // elementName + SdkConstants.NS_RESOURCES, + attrList, + null, // requiredAttributes + null /* overrides */); + } + } + + // Process all LayoutParams attributes + ArrayList<AttributeDescriptor> layoutAttributes = new ArrayList<AttributeDescriptor>(); + LayoutParamsInfo layoutParams = info.getLayoutData(); + + for(; layoutParams != null; layoutParams = layoutParams.getSuperClass()) { + boolean need_separator = true; + for (AttributeInfo attr_info : layoutParams.getAttributes()) { + if (DescriptorsUtils.containsAttribute(layoutAttributes, + SdkConstants.NS_RESOURCES, attr_info)) { + continue; + } + if (need_separator) { + String title; + if (layoutParams.getShortClassName().equals( + AndroidConstants.CLASS_LAYOUTPARAMS)) { + title = String.format("Layout Attributes from %1$s", + layoutParams.getViewLayoutClass().getShortClassName()); + } else { + title = String.format("Layout Attributes from %1$s (%2$s)", + layoutParams.getViewLayoutClass().getShortClassName(), + layoutParams.getShortClassName()); + } + layoutAttributes.add(new SeparatorAttributeDescriptor(title)); + need_separator = false; + } + DescriptorsUtils.appendAttribute(layoutAttributes, + null, // elementName + SdkConstants.NS_RESOURCES, + attr_info, + false, // required + null /* overrides */); + } + } + + return new ViewElementDescriptor(xml_name, + xml_name, // ui_name + info.getCanonicalClassName(), + tooltip, + null, // sdk_url + attributes.toArray(new AttributeDescriptor[attributes.size()]), + layoutAttributes.toArray(new AttributeDescriptor[layoutAttributes.size()]), + null, // children + false /* mandatory */); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java new file mode 100644 index 0000000..d718ebd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.descriptors; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * {@link ViewElementDescriptor} describes the properties expected for a given XML element node + * representing a class in an XML Layout file. + * + * @see ElementDescriptor + */ +public final class ViewElementDescriptor extends ElementDescriptor { + + private String mCanonicalClassName; + + /** The list of layout attributes. Can be empty but not null. */ + private AttributeDescriptor[] mLayoutAttributes; + + + /** + * Constructs a new {@link ViewElementDescriptor} based on its XML name, UI name, + * the canonical name of the class it represents, its tooltip, its SDK url, its attributes list, + * its children list and its mandatory flag. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is + * representing. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param layoutAttributes The list of layout attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). A mandatory + * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory + * UI node MUST have an XML node attached and it will cease to exist when the XML node + * ceases to exist. + */ + public ViewElementDescriptor(String xml_name, String ui_name, + String canonicalClassName, + String tooltip, String sdk_url, + AttributeDescriptor[] attributes, AttributeDescriptor[] layoutAttributes, + ElementDescriptor[] children, boolean mandatory) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory); + mCanonicalClassName = canonicalClassName; + mLayoutAttributes = layoutAttributes != null ? layoutAttributes : new AttributeDescriptor[0]; + } + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name, the canonical + * name of the class it represents, and its children list. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is + * representing. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). A mandatory + * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory + * UI node MUST have an XML node attached and it will cease to exist when the XML node + * ceases to exist. + */ + public ViewElementDescriptor(String xml_name, String canonicalClassName, + ElementDescriptor[] children, + boolean mandatory) { + super(xml_name, children, mandatory); + mCanonicalClassName = canonicalClassName; + } + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name and children list. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is + * representing. + * @param children The list of allowed children. Can be null or empty. + */ + public ViewElementDescriptor(String xml_name, String canonicalClassName, + ElementDescriptor[] children) { + super(xml_name, children); + mCanonicalClassName = canonicalClassName; + } + + /** + * Constructs a new {@link ElementDescriptor} based on its XML name and on the canonical + * name of the class it represents. + * The UI name is build by capitalizing the XML name. + * The UI nodes will be non-mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is + * representing. + */ + public ViewElementDescriptor(String xml_name, String canonicalClassName) { + super(xml_name); + mCanonicalClassName = canonicalClassName; + } + + /** + * Returns the canonical name of the class represented by this element descriptor. + */ + public String getCanonicalClassName() { + return mCanonicalClassName; + } + + /** Returns the list of layout attributes. Can be empty but not null. */ + public AttributeDescriptor[] getLayoutAttributes() { + return mLayoutAttributes; + } + + /** + * @return A new {@link UiViewElementNode} linked to this descriptor. + */ + @Override + public UiElementNode createUiNode() { + return new UiViewElementNode(this); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java new file mode 100644 index 0000000..6e79d64 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java @@ -0,0 +1,761 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.layout.LayoutConstants; +import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions; +import com.android.ide.eclipse.editors.layout.parts.UiLayoutEditPart.HighlightInfo; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.draw2d.geometry.Rectangle; + +import java.util.HashMap; +import java.util.Map.Entry; + +/** + * Utility methods used when dealing with dropping EditPart on the GLE. + * <p/> + * This class uses some temporary static storage to avoid excessive allocations during + * drop operations. It is expected to only be invoked from the main UI thread with no + * concurrent access. + */ +class DropFeedback { + + private static final int TOP = 0; + private static final int LEFT = 1; + private static final int BOTTOM = 2; + private static final int RIGHT = 3; + private static final int MAX_DIR = RIGHT; + + private static final int sOppositeDirection[] = { BOTTOM, RIGHT, TOP, LEFT }; + + private static final UiElementEditPart sTempClosests[] = new UiElementEditPart[4]; + private static final int sTempMinDists[] = new int[4]; + + + /** + * Target information computed from a drop on a RelativeLayout. + * We need only one instance of this and it is sRelativeInfo. + */ + private static class RelativeInfo { + /** The two target parts 0 and 1. They can be null, meaning a border is used. + * The direction from part 0 to 1 is always to-the-right or to-the-bottom. */ + final UiElementEditPart targetParts[] = new UiElementEditPart[2]; + /** Direction from the anchor part to the drop point. */ + int direction; + /** The index of the "anchor" part, i.e. the closest one selected by the drop. + * This can be either 0 or 1. The corresponding part can be null. */ + int anchorIndex; + } + + /** The single RelativeInfo used to compute results from a drop on a RelativeLayout */ + private static final RelativeInfo sRelativeInfo = new RelativeInfo(); + /** A temporary array of 2 {@link UiElementEditPart} to avoid allocations. */ + private static final UiElementEditPart sTempTwoParts[] = new UiElementEditPart[2]; + + + private DropFeedback() { + } + + + //----- Package methods called by users of this helper class ----- + + + /** + * This method is used by {@link ElementCreateCommand#execute()} when a new item + * needs to be "dropped" in the current XML document. It creates the new item using + * the given descriptor as a child of the given parent part. + * + * @param parentPart The parent part. + * @param descriptor The descriptor for the new XML element. + * @param where The drop location (in parent coordinates) + * @param actions The helper that actually modifies the XML model. + */ + static void addElementToXml(UiElementEditPart parentPart, + ElementDescriptor descriptor, Point where, + UiEditorActions actions) { + + String layoutXmlName = getXmlLocalName(parentPart); + RelativeInfo info = null; + UiElementEditPart sibling = null; + + if (LayoutConstants.LINEAR_LAYOUT.equals(layoutXmlName)) { + sibling = findLinearTarget(parentPart, where)[1]; + + } else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutXmlName)) { + info = findRelativeTarget(parentPart, where, sRelativeInfo); + if (info != null) { + sibling = info.targetParts[info.anchorIndex]; + sibling = getNextUiSibling(sibling); + } + } + + if (actions != null) { + UiElementNode uiSibling = sibling != null ? sibling.getUiNode() : null; + UiElementNode uiParent = parentPart.getUiNode(); + UiElementNode uiNode = actions.addElement(uiParent, uiSibling, descriptor, + false /*updateLayout*/); + + if (LayoutConstants.ABSOLUTE_LAYOUT.equals(layoutXmlName)) { + adjustAbsoluteAttributes(uiNode, where); + } else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutXmlName)) { + adustRelativeAttributes(uiNode, info); + } + } + } + + /** + * This method is used by {@link UiLayoutEditPart#showDropTarget(Point)} to compute + * highlight information when a drop target is moved over a valid drop area. + * <p/> + * Since there are no "out" parameters in Java, all the information is returned + * via the {@link HighlightInfo} structure passed as parameter. + * + * @param parentPart The parent part, always a layout. + * @param highlightInfo A structure where result is stored to perform highlight. + * @param where The target drop point, in parent's coordinates + * @return The {@link HighlightInfo} structured passed as a parameter, for convenience. + */ + static HighlightInfo computeDropFeedback(UiLayoutEditPart parentPart, + HighlightInfo highlightInfo, + Point where) { + String layoutType = getXmlLocalName(parentPart); + + if (LayoutConstants.ABSOLUTE_LAYOUT.equals(layoutType)) { + highlightInfo.anchorPoint = where; + + } else if (LayoutConstants.LINEAR_LAYOUT.equals(layoutType)) { + boolean isVertical = isVertical(parentPart); + + highlightInfo.childParts = findLinearTarget(parentPart, where); + computeLinearLine(parentPart, isVertical, highlightInfo); + + } else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutType)) { + + RelativeInfo info = findRelativeTarget(parentPart, where, sRelativeInfo); + if (info != null) { + highlightInfo.childParts = sRelativeInfo.targetParts; + computeRelativeLine(parentPart, info, highlightInfo); + } + } + + return highlightInfo; + } + + + //----- Misc utilities ----- + + /** + * Returns the next UI sibling of this part, i.e. the element which is just after in + * the UI/XML order in the same parent. Returns null if there's no such part. + * <p/> + * Note: by "UI sibling" here we mean the sibling in the UiNode hierarchy. By design the + * UiNode model has the <em>exact</em> same order as the XML model. This has nothing to do + * with the "user interface" order that you see on the rendered Android layouts (e.g. for + * LinearLayout they are the same but for AbsoluteLayout or RelativeLayout the UI/XML model + * order can be vastly different from the user interface order.) + */ + private static UiElementEditPart getNextUiSibling(UiElementEditPart part) { + if (part != null) { + UiElementNode uiNode = part.getUiNode(); + if (uiNode != null) { + uiNode = uiNode.getUiNextSibling(); + } + if (uiNode != null) { + for (Object childPart : part.getParent().getChildren()) { + if (childPart instanceof UiElementEditPart && + ((UiElementEditPart) childPart).getUiNode() == uiNode) { + return (UiElementEditPart) childPart; + } + } + } + } + return null; + } + + /** + * Returns the XML local name of the ui node associated with this edit part or null. + */ + private static String getXmlLocalName(UiElementEditPart editPart) { + UiElementNode uiNode = editPart.getUiNode(); + if (uiNode != null) { + ElementDescriptor desc = uiNode.getDescriptor(); + if (desc != null) { + return desc.getXmlLocalName(); + } + } + return null; + } + + /** + * Adjusts the attributes of a new node dropped in an AbsoluteLayout. + * + * @param uiNode The new node being dropped. + * @param where The drop location (in parent coordinates) + */ + private static void adjustAbsoluteAttributes(final UiElementNode uiNode, final Point where) { + if (where == null) { + return; + } + uiNode.getEditor().editXmlModel(new Runnable() { + public void run() { + uiNode.setAttributeValue(LayoutConstants.ATTR_LAYOUT_X, + String.format(LayoutConstants.VALUE_N_DIP, where.x), + false /* override */); + uiNode.setAttributeValue(LayoutConstants.ATTR_LAYOUT_Y, + String.format(LayoutConstants.VALUE_N_DIP, where.y), + false /* override */); + + uiNode.commitDirtyAttributesToXml(); + } + }); + } + + /** + * Adjusts the attributes of a new node dropped in a RelativeLayout: + * <ul> + * <li> anchor part: the one the user selected (or the closest) and to which the new one + * will "attach". The anchor part can be null, either because the layout is currently + * empty or the user is attaching to an existing empty border. + * <li> direction: the direction from the anchor part to the drop point. That's also the + * direction from the anchor part to the new part. + * <li> the new node; it is created either after the anchor for right or top directions + * or before the anchor for left or bottom directions. This means the new part can + * reference the id of the anchor part. + * </ul> + * + * Several cases: + * <ul> + * <li> set: layout_above/below/toLeftOf/toRightOf to point to the anchor. + * <li> copy: layout_centerHorizontal for top/bottom directions + * <li> copy: layout_centerVertical for left/right directions. + * <li> copy: layout_above/below/toLeftOf/toRightOf for the orthogonal direction + * (i.e. top/bottom or left/right.) + * </ul> + * + * @param uiNode The new node being dropped. + * @param info The context computed by {@link #findRelativeTarget(UiElementEditPart, Point, RelativeInfo)}. + */ + private static void adustRelativeAttributes(final UiElementNode uiNode, RelativeInfo info) { + if (uiNode == null || info == null) { + return; + } + + final UiElementEditPart anchorPart = info.targetParts[info.anchorIndex]; // can be null + final int direction = info.direction; + + uiNode.getEditor().editXmlModel(new Runnable() { + public void run() { + HashMap<String, String> map = new HashMap<String, String>(); + + UiElementNode anchorUiNode = anchorPart != null ? anchorPart.getUiNode() : null; + String anchorId = anchorUiNode != null + ? anchorUiNode.getAttributeValue("id") //$NON-NLS-1$ + : null; + + if (anchorId == null) { + anchorId = DescriptorsUtils.getFreeWidgetId(anchorUiNode); + anchorUiNode.setAttributeValue("id", anchorId, true /*override*/); //$NON-NLS-1$ + } + + if (anchorId != null) { + switch(direction) { + case TOP: + map.put(LayoutConstants.ATTR_LAYOUT_ABOVE, anchorId); + break; + case BOTTOM: + map.put(LayoutConstants.ATTR_LAYOUT_BELOW, anchorId); + break; + case LEFT: + map.put(LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF, anchorId); + break; + case RIGHT: + map.put(LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF, anchorId); + break; + } + + switch(direction) { + case TOP: + case BOTTOM: + map.put(LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL, + anchorUiNode.getAttributeValue( + LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL)); + + map.put(LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF, + anchorUiNode.getAttributeValue( + LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF)); + map.put(LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF, + anchorUiNode.getAttributeValue( + LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF)); + break; + case LEFT: + case RIGHT: + map.put(LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL, + anchorUiNode.getAttributeValue( + LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL)); + map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE, + anchorUiNode.getAttributeValue( + LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE)); + + map.put(LayoutConstants.ATTR_LAYOUT_ABOVE, + anchorUiNode.getAttributeValue(LayoutConstants.ATTR_LAYOUT_ABOVE)); + map.put(LayoutConstants.ATTR_LAYOUT_BELOW, + anchorUiNode.getAttributeValue(LayoutConstants.ATTR_LAYOUT_BELOW)); + break; + } + } else { + // We don't have an anchor node. Assume we're targeting a border and align + // to the parent. + switch(direction) { + case TOP: + map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP, + LayoutConstants.VALUE_TRUE); + break; + case BOTTOM: + map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, + LayoutConstants.VALUE_TRUE); + break; + case LEFT: + map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT, + LayoutConstants.VALUE_TRUE); + break; + case RIGHT: + map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT, + LayoutConstants.VALUE_TRUE); + break; + } + } + + for (Entry<String, String> entry : map.entrySet()) { + uiNode.setAttributeValue(entry.getKey(), entry.getValue(), true /* override */); + } + uiNode.commitDirtyAttributesToXml(); + } + }); + } + + + //----- LinearLayout -------- + + /** + * For a given parent edit part that MUST represent a LinearLayout, finds the + * element before which the location points. + * <p/> + * This computes the edit part that corresponds to what will be the "next sibling" of the new + * element. + * <p/> + * It returns null if it can't be determined, in which case the element will be added at the + * end of the parent child list. + * + * @return The edit parts that correspond to what will be the "prev" and "next sibling" of the + * new element. The previous sibling can be null if adding before the first element. + * The next sibling can be null if adding after the last element. + */ + private static UiElementEditPart[] findLinearTarget(UiElementEditPart parent, Point point) { + // default orientation is horizontal + boolean isVertical = isVertical(parent); + + int target = isVertical ? point.y : point.x; + + UiElementEditPart prev = null; + UiElementEditPart next = null; + + for (Object child : parent.getChildren()) { + if (child instanceof UiElementEditPart) { + UiElementEditPart childPart = (UiElementEditPart) child; + Point p = childPart.getBounds().getCenter(); + int middle = isVertical ? p.y : p.x; + if (target < middle) { + next = childPart; + break; + } + prev = childPart; + } + } + + sTempTwoParts[0] = prev; + sTempTwoParts[1] = next; + return sTempTwoParts; + } + + /** + * Computes the highlight line between two parts. + * <p/> + * The two parts are listed in HighlightInfo.childParts[2]. Any of the parts + * can be null. + * The result is stored in HighlightInfo. + * <p/> + * Caller must clear the HighlightInfo as appropriate before this call. + * + * @param parentPart The parent part, always a layout. + * @param isVertical True for vertical parts, thus computing an horizontal line. + * @param highlightInfo The in-out highlight info. + */ + private static void computeLinearLine(UiLayoutEditPart parentPart, + boolean isVertical, HighlightInfo highlightInfo) { + Rectangle r = parentPart.getBounds(); + + if (isVertical) { + Point p = null; + UiElementEditPart part = highlightInfo.childParts[0]; + if (part != null) { + p = part.getBounds().getBottom(); + } else { + part = highlightInfo.childParts[1]; + if (part != null) { + p = part.getBounds().getTop(); + } + } + if (p != null) { + // horizontal line with middle anchor point + highlightInfo.tempPoints[0].setLocation(0, p.y); + highlightInfo.tempPoints[1].setLocation(r.width, p.y); + highlightInfo.linePoints = highlightInfo.tempPoints; + highlightInfo.anchorPoint = p.setLocation(r.width / 2, p.y); + } + } else { + Point p = null; + UiElementEditPart part = highlightInfo.childParts[0]; + if (part != null) { + p = part.getBounds().getRight(); + } else { + part = highlightInfo.childParts[1]; + if (part != null) { + p = part.getBounds().getLeft(); + } + } + if (p != null) { + // vertical line with middle anchor point + highlightInfo.tempPoints[0].setLocation(p.x, 0); + highlightInfo.tempPoints[1].setLocation(p.x, r.height); + highlightInfo.linePoints = highlightInfo.tempPoints; + highlightInfo.anchorPoint = p.setLocation(p.x, r.height / 2); + } + } + } + + /** + * Returns true if the linear layout is marked as vertical. + * + * @param parent The a layout part that must be a LinearLayout + * @return True if the linear layout has a vertical orientation attribute. + */ + private static boolean isVertical(UiElementEditPart parent) { + String orientation = parent.getStringAttr("orientation"); //$NON-NLS-1$ + boolean isVertical = "vertical".equals(orientation) || //$NON-NLS-1$ + "1".equals(orientation); //$NON-NLS-1$ + return isVertical; + } + + + //----- RelativeLayout -------- + + /** + * Finds the "target" relative layout item for the drop operation & feedback. + * <p/> + * If the drop point is exactly on a current item, simply returns the side the drop will occur + * compared to the center of that element. For the actual XML, we'll need to insert *after* + * that element to make sure that referenced are defined in the right order. + * In that case the result contains two elements, the second one always being on the right or + * bottom side of the first one. When insert in XML, we want to insert right before that + * second element or at the end of the child list if the second element is null. + * <p/> + * If the drop point is not exactly on a current element, find the closest in each + * direction and align with the two closest of these. + * + * @return null if we fail to find anything (such as there are currently no items to compare + * with); otherwise fills the {@link RelativeInfo} and return it. + */ + private static RelativeInfo findRelativeTarget(UiElementEditPart parent, + Point point, + RelativeInfo outInfo) { + + for (int i = 0; i < 4; i++) { + sTempMinDists[i] = Integer.MAX_VALUE; + sTempClosests[i] = null; + } + + + for (Object child : parent.getChildren()) { + if (child instanceof UiElementEditPart) { + UiElementEditPart childPart = (UiElementEditPart) child; + Rectangle r = childPart.getBounds(); + if (r.contains(point)) { + + float rx = ((float)(point.x - r.x) / (float)r.width ) - 0.5f; + float ry = ((float)(point.y - r.y) / (float)r.height) - 0.5f; + + /* TOP + * \ / + * \ / + * L X R + * / \ + * / \ + * BOT + */ + + int index = 0; + if (Math.abs(rx) >= Math.abs(ry)) { + if (rx < 0) { + outInfo.direction = LEFT; + index = 1; + } else { + outInfo.direction = RIGHT; + } + } else { + if (ry < 0) { + outInfo.direction = TOP; + index = 1; + } else { + outInfo.direction = BOTTOM; + } + } + + outInfo.anchorIndex = index; + outInfo.targetParts[index] = childPart; + outInfo.targetParts[1 - index] = findClosestPart(childPart, + outInfo.direction); + + return outInfo; + } + + computeClosest(point, childPart, sTempClosests, sTempMinDists, TOP); + computeClosest(point, childPart, sTempClosests, sTempMinDists, LEFT); + computeClosest(point, childPart, sTempClosests, sTempMinDists, BOTTOM); + computeClosest(point, childPart, sTempClosests, sTempMinDists, RIGHT); + } + } + + UiElementEditPart closest = null; + int minDist = Integer.MAX_VALUE; + int minDir = -1; + + for (int i = 0; i <= MAX_DIR; i++) { + if (sTempClosests[i] != null && sTempMinDists[i] < minDist) { + closest = sTempClosests[i]; + minDist = sTempMinDists[i]; + minDir = i; + } + } + + if (closest != null) { + int index = 0; + switch(minDir) { + case TOP: + case LEFT: + index = 0; + break; + case BOTTOM: + case RIGHT: + index = 1; + break; + } + outInfo.anchorIndex = index; + outInfo.targetParts[index] = closest; + outInfo.targetParts[1 - index] = findClosestPart(closest, sOppositeDirection[minDir]); + outInfo.direction = sOppositeDirection[minDir]; + return outInfo; + } + + return null; + } + + /** + * Computes the highlight line for a drop on a RelativeLayout. + * <p/> + * The line is always placed on the side of the anchor part indicated by the + * direction. The direction always point from the anchor part to the drop point. + * <p/> + * If there's no anchor part, use the other one with a reversed direction. + * <p/> + * On output, this updates the {@link HighlightInfo}. + */ + private static void computeRelativeLine(UiLayoutEditPart parentPart, + RelativeInfo relInfo, + HighlightInfo highlightInfo) { + + UiElementEditPart[] parts = relInfo.targetParts; + int dir = relInfo.direction; + int index = relInfo.anchorIndex; + UiElementEditPart part = parts[index]; + + if (part == null) { + dir = sOppositeDirection[dir]; + part = parts[1 - index]; + } + if (part == null) { + // give up if both parts are null + return; + } + + Rectangle r = part.getBounds(); + Point p = null; + switch(dir) { + case TOP: + p = r.getTop(); + break; + case BOTTOM: + p = r.getBottom(); + break; + case LEFT: + p = r.getLeft(); + break; + case RIGHT: + p = r.getRight(); + break; + } + + highlightInfo.anchorPoint = p; + + r = parentPart.getBounds(); + switch(dir) { + case TOP: + case BOTTOM: + // horizontal line with middle anchor point + highlightInfo.tempPoints[0].setLocation(0, p.y); + highlightInfo.tempPoints[1].setLocation(r.width, p.y); + highlightInfo.linePoints = highlightInfo.tempPoints; + highlightInfo.anchorPoint = p; + break; + case LEFT: + case RIGHT: + // vertical line with middle anchor point + highlightInfo.tempPoints[0].setLocation(p.x, 0); + highlightInfo.tempPoints[1].setLocation(p.x, r.height); + highlightInfo.linePoints = highlightInfo.tempPoints; + highlightInfo.anchorPoint = p; + break; + } + } + + /** + * Given a certain reference point (drop point), computes the distance to the given + * part in the given direction. For example if direction is top, only accepts parts which + * bottom is above the reference point, computes their distance and then updates the + * current minimal distances and current closest parts arrays accordingly. + */ + private static void computeClosest(Point refPoint, + UiElementEditPart compareToPart, + UiElementEditPart[] currClosests, + int[] currMinDists, + int direction) { + Rectangle r = compareToPart.getBounds(); + + Point p = null; + boolean usable = false; + + switch(direction) { + case TOP: + p = r.getBottom(); + usable = p.y <= refPoint.y; + break; + case BOTTOM: + p = r.getTop(); + usable = p.y >= refPoint.y; + break; + case LEFT: + p = r.getRight(); + usable = p.x <= refPoint.x; + break; + case RIGHT: + p = r.getLeft(); + usable = p.x >= refPoint.x; + break; + } + + if (usable) { + int d = p.getDistance2(refPoint); + if (d < currMinDists[direction]) { + currMinDists[direction] = d; + currClosests[direction] = compareToPart; + } + } + } + + /** + * Given a reference parts, finds the closest part in the parent in the given direction. + * For example if direction is top, finds the closest sibling part which is above the + * reference part and non-overlapping (they can touch.) + */ + private static UiElementEditPart findClosestPart(UiElementEditPart referencePart, + int direction) { + if (referencePart == null || referencePart.getParent() == null) { + return null; + } + + Rectangle r = referencePart.getBounds(); + Point ref = null; + switch(direction) { + case TOP: + ref = r.getTop(); + break; + case BOTTOM: + ref = r.getBottom(); + break; + case LEFT: + ref = r.getLeft(); + break; + case RIGHT: + ref = r.getRight(); + break; + } + + int minDist = Integer.MAX_VALUE; + UiElementEditPart closestPart = null; + + for (Object childPart : referencePart.getParent().getChildren()) { + if (childPart != referencePart && childPart instanceof UiElementEditPart) { + r = ((UiElementEditPart) childPart).getBounds(); + Point p = null; + boolean usable = false; + + switch(direction) { + case TOP: + p = r.getBottom(); + usable = p.y <= ref.y; + break; + case BOTTOM: + p = r.getTop(); + usable = p.y >= ref.y; + break; + case LEFT: + p = r.getRight(); + usable = p.x <= ref.x; + break; + case RIGHT: + p = r.getLeft(); + usable = p.x >= ref.x; + break; + } + + if (usable) { + int d = p.getDistance2(ref); + if (d < minDist) { + minDist = d; + closestPart = (UiElementEditPart) childPart; + } + } + } + } + + return closestPart; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java new file mode 100644 index 0000000..d36d9f7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.layout.LayoutEditor; +import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.gef.commands.Command; + +/** + * A command that knows how to instantiate a new element based on a given {@link ElementDescriptor}, + * the parent {@link UiElementEditPart} and an optional target location. + */ +public class ElementCreateCommand extends Command { + + /** Descriptor of the new element to create */ + private final ElementDescriptor mDescriptor; + /** The edit part that hosts the new edit part */ + private final UiElementEditPart mParentPart; + /** The drop location in parent coordinates */ + private final Point mTargetPoint; + + /** + * Creates a new {@link ElementCreateCommand}. + * + * @param descriptor Descriptor of the new element to create + * @param targetPart The edit part that hosts the new edit part + * @param targetPoint The drop location in parent coordinates + */ + public ElementCreateCommand(ElementDescriptor descriptor, + UiElementEditPart targetPart, Point targetPoint) { + mDescriptor = descriptor; + mParentPart = targetPart; + mTargetPoint = targetPoint; + } + + // --- Methods inherited from Command --- + + @Override + public boolean canExecute() { + return mDescriptor != null && + mParentPart != null && + mParentPart.getUiNode() != null && + mParentPart.getUiNode().getEditor() instanceof LayoutEditor; + } + + @Override + public void execute() { + super.execute(); + UiElementNode uiParent = mParentPart.getUiNode(); + if (uiParent != null) { + final AndroidEditor editor = uiParent.getEditor(); + if (editor instanceof LayoutEditor) { + ((LayoutEditor) editor).wrapUndoRecording( + String.format("Create %1$s", mDescriptor.getXmlLocalName()), + new Runnable() { + public void run() { + UiEditorActions actions = ((LayoutEditor) editor).getUiEditorActions(); + if (actions != null) { + DropFeedback.addElementToXml(mParentPart, mDescriptor, mTargetPoint, + actions); + } + } + }); + } + } + } + + @Override + public void redo() { + throw new UnsupportedOperationException("redo not supported by this command"); //$NON-NLS-1$ + } + + @Override + public void undo() { + throw new UnsupportedOperationException("undo not supported by this command"); //$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java new file mode 100644 index 0000000..f863037 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import org.eclipse.draw2d.ColorConstants; +import org.eclipse.draw2d.Figure; +import org.eclipse.draw2d.Graphics; +import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.swt.SWT; + + +/** + * The figure used to draw basic elements. + * <p/> + * The figure is totally empty and transparent except for the selection border. + */ +class ElementFigure extends Figure { + + private boolean mIsSelected; + private Rectangle mInnerBounds; + + public ElementFigure() { + setOpaque(false); + } + + public void setSelected(boolean isSelected) { + if (isSelected != mIsSelected) { + mIsSelected = isSelected; + repaint(); + } + } + + @Override + public void setBounds(Rectangle rect) { + super.setBounds(rect); + + mInnerBounds = getBounds().getCopy(); + if (mInnerBounds.width > 0) { + mInnerBounds.width--; + } + if (mInnerBounds.height > 0) { + mInnerBounds.height--; + } + } + + public Rectangle getInnerBounds() { + return mInnerBounds; + } + + @Override + protected void paintBorder(Graphics graphics) { + super.paintBorder(graphics); + + if (mIsSelected) { + graphics.setLineWidth(1); + graphics.setLineStyle(SWT.LINE_SOLID); + graphics.setForegroundColor(ColorConstants.red); + graphics.drawRectangle(getInnerBounds()); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java new file mode 100644 index 0000000..55ed39b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.layout.parts.UiLayoutEditPart.HighlightInfo; + +import org.eclipse.draw2d.ColorConstants; +import org.eclipse.draw2d.Figure; +import org.eclipse.draw2d.Graphics; +import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.swt.SWT; + +/** + * The figure used to draw the feedback on a layout. + * <p/> + * By default the figure is transparent and empty. + * The base {@link ElementFigure} knows how to draw the selection border. + * This figure knows how to draw the drop feedback. + */ +class LayoutFigure extends ElementFigure { + + private HighlightInfo mHighlightInfo; + + public LayoutFigure() { + super(); + } + + public void setHighlighInfo(HighlightInfo highlightInfo) { + mHighlightInfo = highlightInfo; + repaint(); + } + + /** + * Paints the "border" for this figure. + * <p/> + * The parent {@link Figure#paint(Graphics)} calls {@link #paintFigure(Graphics)} then + * {@link #paintClientArea(Graphics)} then {@link #paintBorder(Graphics)}. Here we thus + * draw the actual highlight border but also the highlight anchor lines and points so that + * we can make sure they are all drawn on top of the border. + * <p/> + * Note: This method doesn't really need to restore its graphic state. The parent + * Figure will do it for us. + * <p/> + * + * @param graphics The Graphics object used for painting + */ + @Override + protected void paintBorder(Graphics graphics) { + super.paintBorder(graphics); + + if (mHighlightInfo == null) { + return; + } + + // Draw the border. We want other highlighting to be drawn on top of the border. + if (mHighlightInfo.drawDropBorder) { + graphics.setLineWidth(3); + graphics.setLineStyle(SWT.LINE_SOLID); + graphics.setForegroundColor(ColorConstants.green); + graphics.drawRectangle(getInnerBounds().getCopy().shrink(1, 1)); + } + + Rectangle bounds = getBounds(); + int bx = bounds.x; + int by = bounds.y; + int w = bounds.width; + int h = bounds.height; + + // Draw frames of target child parts, if any + if (mHighlightInfo.childParts != null) { + graphics.setLineWidth(2); + graphics.setLineStyle(SWT.LINE_DOT); + graphics.setForegroundColor(ColorConstants.lightBlue); + for (UiElementEditPart part : mHighlightInfo.childParts) { + if (part != null) { + graphics.drawRectangle(part.getBounds().getCopy().translate(bx, by)); + } + } + } + + // Draw the target line, if any + if (mHighlightInfo.linePoints != null) { + int x1 = mHighlightInfo.linePoints[0].x; + int y1 = mHighlightInfo.linePoints[0].y; + int x2 = mHighlightInfo.linePoints[1].x; + int y2 = mHighlightInfo.linePoints[1].y; + + // if the line is right to the edge, draw it one pixel more inside so that the + // full 2-pixel width be visible. + if (x1 <= 0) x1++; + if (x2 <= 0) x2++; + if (y1 <= 0) y1++; + if (y2 <= 0) y2++; + + if (x1 >= w - 1) x1--; + if (x2 >= w - 1) x2--; + if (y1 >= h - 1) y1--; + if (y2 >= h - 1) y2--; + + x1 += bx; + x2 += bx; + y1 += by; + y2 += by; + + graphics.setLineWidth(2); + graphics.setLineStyle(SWT.LINE_DASH); + graphics.setLineCap(SWT.CAP_ROUND); + graphics.setForegroundColor(ColorConstants.orange); + graphics.drawLine(x1, y1, x2, y2); + } + + // Draw the anchor point, if any + if (mHighlightInfo.anchorPoint != null) { + int x = mHighlightInfo.anchorPoint.x; + int y = mHighlightInfo.anchorPoint.y; + + // If the point is right on the edge, draw it one pixel inside so that it + // matches the highlight line. It makes it slightly more visible that way. + if (x <= 0) x++; + if (y <= 0) y++; + if (x >= w - 1) x--; + if (y >= h - 1) y--; + x += bx; + y += by; + + graphics.setLineWidth(2); + graphics.setLineStyle(SWT.LINE_SOLID); + graphics.setLineCap(SWT.CAP_ROUND); + graphics.setForegroundColor(ColorConstants.orange); + graphics.drawLine(x-5, y-5, x+5, y+5); + graphics.drawLine(x-5, y+5, x+5, y-5); + // 7 * cos(45) == 5 so we use 8 for the circle radius (it looks slightly better than 7) + graphics.setLineWidth(1); + graphics.drawOval(x-8, y-8, 16, 16); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java new file mode 100644 index 0000000..2f7636d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.draw2d.AbstractBackground; +import org.eclipse.draw2d.ColorConstants; +import org.eclipse.draw2d.FreeformLayer; +import org.eclipse.draw2d.FreeformLayout; +import org.eclipse.draw2d.Graphics; +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.Label; +import org.eclipse.draw2d.geometry.Insets; +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.gef.EditPart; +import org.eclipse.gef.EditPolicy; +import org.eclipse.gef.Request; +import org.eclipse.gef.RequestConstants; +import org.eclipse.gef.editpolicies.RootComponentEditPolicy; +import org.eclipse.gef.requests.DropRequest; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.widgets.Display; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +/** + * Graphical edit part for the root document. + * <p/> + * It acts as a simple container. + */ +public class UiDocumentEditPart extends UiElementEditPart { + + private Display mDisplay; + private FreeformLayer mLayer; + private ImageBackground mImage; + private Label mChild = null; + + final static class ImageBackground extends AbstractBackground { + + private BufferedImage mBufferedImage; + private Image mImage; + + ImageBackground() { + } + + ImageBackground(BufferedImage image, Display display) { + setImage(image, display); + } + + @Override + public void paintBackground(IFigure figure, Graphics graphics, Insets insets) { + if (mImage != null) { + Rectangle rect = getPaintRectangle(figure, insets); + graphics.drawImage(mImage, rect.x, rect.y); + } + } + + void setImage(BufferedImage image, Display display) { + if (image != null) { + int[] data = ((DataBufferInt)image.getData().getDataBuffer()).getData(); + + ImageData imageData = new ImageData(image.getWidth(), image.getHeight(), 32, + new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); + + imageData.setPixels(0, 0, data.length, data, 0); + + mImage = new Image(display, imageData); + } else { + mImage = null; + } + } + + BufferedImage getBufferedImage() { + return mBufferedImage; + } + } + + public UiDocumentEditPart(UiDocumentNode uiDocumentNode, Display display) { + super(uiDocumentNode); + mDisplay = display; + } + + @Override + protected IFigure createFigure() { + mLayer = new FreeformLayer(); + mLayer.setLayoutManager(new FreeformLayout()); + + mLayer.setOpaque(true); + mLayer.setBackgroundColor(ColorConstants.lightGray); + + return mLayer; + } + + @Override + protected void refreshVisuals() { + UiElementNode model = (UiElementNode)getModel(); + + Object editData = model.getEditData(); + if (editData instanceof BufferedImage) { + BufferedImage image = (BufferedImage)editData; + + if (mImage == null || image != mImage.getBufferedImage()) { + mImage = new ImageBackground(image, mDisplay); + } + + mLayer.setBorder(mImage); + + if (mChild != null && mChild.getParent() == mLayer) { + mLayer.remove(mChild); + } + } else if (editData instanceof String) { + mLayer.setBorder(null); + if (mChild == null) { + mChild = new Label(); + } + mChild.setText((String)editData); + + if (mChild != null && mChild.getParent() != mLayer) { + mLayer.add(mChild); + } + Rectangle bounds = mChild.getTextBounds(); + bounds.x = bounds.y = 0; + mLayer.setConstraint(mChild, bounds); + } + + // refresh the children as well + refreshChildrenVisuals(); + } + + @Override + protected void hideSelection() { + // no selection at this level. + } + + @Override + protected void showSelection() { + // no selection at this level. + } + + @Override + protected void createEditPolicies() { + super.createEditPolicies(); + + // This policy indicates this a root component that cannot be removed + installEditPolicy(EditPolicy.COMPONENT_ROLE, new RootComponentEditPolicy()); + + installLayoutEditPolicy(this); + } + + /** + * Returns the EditPart that should be used as the target for the specified Request. + * For instance this is called during drag'n'drop with a CreateRequest. + * <p/> + * For the root document, we want the first child edit part to the be the target + * since an XML document can have only one root element. + * + * {@inheritDoc} + */ + @Override + public EditPart getTargetEditPart(Request request) { + if (request != null && request.getType() == RequestConstants.REQ_CREATE) { + // We refuse the drop if it's not in the bounds of the document. + if (request instanceof DropRequest) { + Point where = ((DropRequest) request).getLocation().getCopy(); + UiElementNode uiNode = getUiNode(); + if (uiNode instanceof UiDocumentNode) { + // Take the bounds of the background image as the valid drop zone + Object editData = uiNode.getEditData(); + if (editData instanceof BufferedImage) { + BufferedImage image = (BufferedImage)editData; + int w = image.getWidth(); + int h = image.getHeight(); + if (where.x > w || where.y > h) { + return null; + } + } + + } + } + + // For the root document, we want the first child edit part to the be the target + // since an XML document can have only one root element. + if (getChildren().size() > 0) { + Object o = getChildren().get(0); + if (o instanceof EditPart) { + return ((EditPart) o).getTargetEditPart(request); + } + } + } + return super.getTargetEditPart(request); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java new file mode 100644 index 0000000..af22afb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; + +import java.util.List; + +/** + * Implementation of {@link UiElementTreeEditPart} for the document root. + */ +public class UiDocumentTreeEditPart extends UiElementTreeEditPart { + + public UiDocumentTreeEditPart(UiDocumentNode model) { + super(model); + } + + @SuppressWarnings("unchecked") + @Override + protected List getModelChildren() { + return getUiNode().getUiChildren(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java new file mode 100644 index 0000000..a2e05c7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.gef.DragTracker; +import org.eclipse.gef.EditPart; +import org.eclipse.gef.EditPolicy; +import org.eclipse.gef.GraphicalEditPart; +import org.eclipse.gef.Request; +import org.eclipse.gef.RequestConstants; +import org.eclipse.gef.commands.Command; +import org.eclipse.gef.editparts.AbstractGraphicalEditPart; +import org.eclipse.gef.editpolicies.LayoutEditPolicy; +import org.eclipse.gef.editpolicies.SelectionEditPolicy; +import org.eclipse.gef.requests.CreateRequest; +import org.eclipse.gef.requests.DropRequest; +import org.eclipse.gef.tools.SelectEditPartTracker; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.List; + +/** + * An {@link EditPart} for a {@link UiElementNode}. + */ +public abstract class UiElementEditPart extends AbstractGraphicalEditPart + implements IUiUpdateListener { + + public UiElementEditPart(UiElementNode uiElementNode) { + setModel(uiElementNode); + } + + //------------------------- + // Derived classes must define these + + abstract protected void hideSelection(); + abstract protected void showSelection(); + + //------------------------- + // Base class overrides + + @Override + public DragTracker getDragTracker(Request request) { + return new SelectEditPartTracker(this); + } + + @Override + protected void createEditPolicies() { + /* + * This is no longer needed, as a selection edit policy is set by the parent layout. + * Leave this code commented out right now, I'll want to play with this later. + * + installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, + new NonResizableSelectionEditPolicy(this)); + */ + } + + /* (non-javadoc) + * Returns a List containing the children model objects. + * Must not return null, instead use the super which returns an empty list. + */ + @SuppressWarnings("unchecked") + @Override + protected List getModelChildren() { + return getUiNode().getUiChildren(); + } + + @Override + public void activate() { + super.activate(); + getUiNode().addUpdateListener(this); + } + + @Override + public void deactivate() { + super.deactivate(); + getUiNode().removeUpdateListener(this); + } + + @Override + protected void refreshVisuals() { + if (getFigure().getParent() != null) { + ((GraphicalEditPart) getParent()).setLayoutConstraint(this, getFigure(), getBounds()); + } + + // update the visuals of the children as well + refreshChildrenVisuals(); + } + + protected void refreshChildrenVisuals() { + if (children != null) { + for (Object child : children) { + if (child instanceof UiElementEditPart) { + UiElementEditPart childPart = (UiElementEditPart)child; + childPart.refreshVisuals(); + } + } + } + } + + //------------------------- + // IUiUpdateListener implementation + + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + // TODO: optimize by refreshing only when needed + switch(state) { + case ATTR_UPDATED: + refreshVisuals(); + break; + case CHILDREN_CHANGED: + refreshChildren(); + + // new children list, need to update the layout + refreshVisuals(); + break; + case CREATED: + refreshVisuals(); + break; + case DELETED: + // pass + break; + } + } + + //------------------------- + // Local methods + + /** @return The object model casted to an {@link UiElementNode} */ + public final UiElementNode getUiNode() { + return (UiElementNode) getModel(); + } + + protected final ElementDescriptor getDescriptor() { + return getUiNode().getDescriptor(); + } + + protected final UiElementEditPart getEditPartParent() { + EditPart parent = getParent(); + if (parent instanceof UiElementEditPart) { + return (UiElementEditPart)parent; + } + return null; + } + + /** + * Returns a given XML attribute. + * @param attrName The local name of the attribute. + * @return the attribute as a {@link String}, if it exists, or <code>null</code> + */ + protected final String getStringAttr(String attrName) { + UiElementNode uiNode = getUiNode(); + if (uiNode.getXmlNode() != null) { + Node xmlNode = uiNode.getXmlNode(); + if (xmlNode != null) { + NamedNodeMap nodeAttributes = xmlNode.getAttributes(); + if (nodeAttributes != null) { + Node attr = nodeAttributes.getNamedItemNS( + SdkConstants.NS_RESOURCES, attrName); + if (attr != null) { + return attr.getNodeValue(); + } + } + } + } + return null; + } + + protected final Rectangle getBounds() { + UiElementNode model = (UiElementNode)getModel(); + + Object editData = model.getEditData(); + + if (editData != null) { + // assert with fully qualified class name to prevent import changes to another + // Rectangle class. + assert (editData instanceof org.eclipse.draw2d.geometry.Rectangle); + + return (Rectangle)editData; + } + + // return a dummy rect + return new Rectangle(0, 0, 0, 0); + } + + /** + * Returns the EditPart that should be used as the target for the specified Request. + * <p/> + * For instance this is called during drag'n'drop with a CreateRequest. + * <p/> + * Reject being a target for elements which descriptor does not allow children. + * + * {@inheritDoc} + */ + @Override + public EditPart getTargetEditPart(Request request) { + if (request != null && request.getType() == RequestConstants.REQ_CREATE) { + // Reject being a target for elements which descriptor does not allow children. + if (!getUiNode().getDescriptor().hasChildren()) { + return null; + } + } + return super.getTargetEditPart(request); + } + + /** + * Used by derived classes {@link UiDocumentEditPart} and {@link UiLayoutEditPart} + * to accept drag'n'drop of new items from the palette. + * + * @param layoutEditPart The layout edit part where this policy is installed. It can + * be either a {@link UiDocumentEditPart} or a {@link UiLayoutEditPart}. + */ + protected void installLayoutEditPolicy(final UiElementEditPart layoutEditPart) { + // This policy indicates how elements can be constrained by the layout. + // TODO Right now we use the XY layout policy since our constraints are + // handled by the android rendering engine rather than GEF. Tweak as + // appropriate. + installEditPolicy(EditPolicy.LAYOUT_ROLE, new LayoutEditPolicy() { + + /** + * We don't allow layout children to be resized yet. + * <p/> + * Typical choices would be: + * <ul> + * <li> ResizableEditPolicy, to allow for selection, move and resize. + * <li> NonResizableEditPolicy, to allow for selection, move but not resize. + * <li> SelectionEditPolicy to allow for only selection. + * </ul> + * <p/> + * TODO: make this depend on the part layout. For an AbsoluteLayout we should + * probably use a NonResizableEditPolicy and SelectionEditPolicy for the rest. + * Whether to use ResizableEditPolicy or NonResizableEditPolicy should depend + * on the child in an AbsoluteLayout. + */ + @Override + protected EditPolicy createChildEditPolicy(EditPart child) { + if (child instanceof UiElementEditPart) { + return new NonResizableSelectionEditPolicy((UiElementEditPart) child); + } + return null; + } + + @Override + protected Command getCreateCommand(CreateRequest request) { + // We store the ElementDescriptor in the request.factory.type + Object newType = request.getNewObjectType(); + if (newType instanceof ElementDescriptor) { + Point where = request.getLocation().getCopy(); + Point origin = getLayoutContainer().getClientArea().getLocation(); + where.translate(origin.getNegated()); + + // The host is the EditPart where this policy is installed, + // e.g. this UiElementEditPart. + EditPart host = getHost(); + if (host instanceof UiElementEditPart) { + + return new ElementCreateCommand((ElementDescriptor) newType, + (UiElementEditPart) host, + where); + } + } + + return null; + } + + @Override + protected Command getMoveChildrenCommand(Request request) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void showLayoutTargetFeedback(Request request) { + super.showLayoutTargetFeedback(request); + + // for debugging + // System.out.println("target: " + request.toString() + " -- " + layoutEditPart.getUiNode().getBreadcrumbTrailDescription(false)); + + if (layoutEditPart instanceof UiLayoutEditPart && + request instanceof DropRequest) { + Point where = ((DropRequest) request).getLocation().getCopy(); + Point origin = getLayoutContainer().getClientArea().getLocation(); + where.translate(origin.getNegated()); + + ((UiLayoutEditPart) layoutEditPart).showDropTarget(where); + } + } + + @Override + protected void eraseLayoutTargetFeedback(Request request) { + super.eraseLayoutTargetFeedback(request); + if (layoutEditPart instanceof UiLayoutEditPart) { + ((UiLayoutEditPart) layoutEditPart).hideDropTarget(); + } + } + + @Override + protected IFigure createSizeOnDropFeedback(CreateRequest createRequest) { + // TODO understand if this is useful for us or remove + return super.createSizeOnDropFeedback(createRequest); + } + + }); + } + + protected static class NonResizableSelectionEditPolicy extends SelectionEditPolicy { + + private final UiElementEditPart mEditPart; + + public NonResizableSelectionEditPolicy(UiElementEditPart editPart) { + mEditPart = editPart; + } + + @Override + protected void hideSelection() { + mEditPart.hideSelection(); + } + + @Override + protected void showSelection() { + mEditPart.showSelection(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java new file mode 100644 index 0000000..fd788dd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.gef.editparts.AbstractTreeEditPart; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.views.contentoutline.IContentOutlinePage; + +/** + * Base {@link AbstractTreeEditPart} to represent {@link UiElementNode} objects in the + * {@link IContentOutlinePage} linked to the layout editor. + */ +public class UiElementTreeEditPart extends AbstractTreeEditPart { + + public UiElementTreeEditPart(UiElementNode uiElementNode) { + setModel(uiElementNode); + } + + @Override + protected void createEditPolicies() { + // TODO Auto-generated method stub + super.createEditPolicies(); + } + + @Override + protected Image getImage() { + return getUiNode().getDescriptor().getIcon(); + } + + @Override + protected String getText() { + return getUiNode().getShortDescription(); + } + + @Override + public void activate() { + if (!isActive()) { + super.activate(); + // TODO + } + } + + @Override + public void deactivate() { + if (isActive()) { + super.deactivate(); + // TODO + } + } + + /** + * Returns the casted model object represented by this {@link AbstractTreeEditPart}. + */ + protected UiElementNode getUiNode() { + return (UiElementNode)getModel(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java new file mode 100644 index 0000000..de6c404 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.gef.EditPart; +import org.eclipse.gef.EditPartFactory; +import org.eclipse.gef.editparts.AbstractTreeEditPart; +import org.eclipse.ui.views.contentoutline.IContentOutlinePage; + +/** + * {@link EditPartFactory} to create {@link AbstractTreeEditPart} for {@link UiElementNode} objects. + * These objects are used in the {@link IContentOutlinePage} linked to the layout editor. + */ +public class UiElementTreeEditPartFactory implements EditPartFactory { + + public EditPart createEditPart(EditPart context, Object model) { + if (model instanceof UiDocumentNode) { + return new UiDocumentTreeEditPart((UiDocumentNode) model); + } else if (model instanceof UiElementNode) { + UiElementNode node = (UiElementNode) model; + if (node.getDescriptor().hasChildren()) { + return new UiLayoutTreeEditPart(node); + } else { + return new UiViewTreeEditPart(node); + } + } + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java new file mode 100644 index 0000000..18dcd9c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.gef.EditPart; +import org.eclipse.gef.EditPartFactory; +import org.eclipse.swt.widgets.Display; + +/** + * A factory that returns the appropriate {@link EditPart} for a given model object. + * <p/> + * The only model objects we use are {@link UiElementNode} objects and they are + * edited using {@link UiElementEditPart}. + */ +public class UiElementsEditPartFactory implements EditPartFactory { + + private Display mDisplay; + + public UiElementsEditPartFactory(Display display) { + mDisplay = display; + } + + public EditPart createEditPart(EditPart context, Object model) { + if (model instanceof UiDocumentNode) { + return new UiDocumentEditPart((UiDocumentNode) model, mDisplay); + } else if (model instanceof UiElementNode) { + UiElementNode node = (UiElementNode) model; + + if (node.getDescriptor().hasChildren()) { + return new UiLayoutEditPart(node); + } + + return new UiViewEditPart(node); + } + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java new file mode 100644 index 0000000..43a70a5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.XYLayout; +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.gef.EditPolicy; +import org.eclipse.gef.commands.Command; +import org.eclipse.gef.editpolicies.ContainerEditPolicy; +import org.eclipse.gef.requests.CreateRequest; + +/** + * Graphical edit part for an {@link UiElementNode} that represents a ViewLayout. + * <p/> + * It acts as a simple container. + */ +public final class UiLayoutEditPart extends UiElementEditPart { + + static class HighlightInfo { + public boolean drawDropBorder; + public UiElementEditPart[] childParts; + public Point anchorPoint; + public Point linePoints[]; + + public final Point tempPoints[] = new Point[] { new Point(), new Point() }; + + public void clear() { + drawDropBorder = false; + childParts = null; + anchorPoint = null; + linePoints = null; + } + } + + private final HighlightInfo mHighlightInfo = new HighlightInfo(); + + public UiLayoutEditPart(UiElementNode uiElementNode) { + super(uiElementNode); + } + + @Override + protected void createEditPolicies() { + super.createEditPolicies(); + + installEditPolicy(EditPolicy.CONTAINER_ROLE, new ContainerEditPolicy() { + @Override + protected Command getCreateCommand(CreateRequest request) { + return null; + } + }); + + installLayoutEditPolicy(this); + } + + @Override + protected IFigure createFigure() { + IFigure f = new LayoutFigure(); + f.setLayoutManager(new XYLayout()); + return f; + } + + @Override + protected void showSelection() { + IFigure f = getFigure(); + if (f instanceof ElementFigure) { + ((ElementFigure) f).setSelected(true); + } + } + + @Override + protected void hideSelection() { + IFigure f = getFigure(); + if (f instanceof ElementFigure) { + ((ElementFigure) f).setSelected(false); + } + } + + public void showDropTarget(Point where) { + if (where != null) { + mHighlightInfo.clear(); + mHighlightInfo.drawDropBorder = true; + DropFeedback.computeDropFeedback(this, mHighlightInfo, where); + + IFigure f = getFigure(); + if (f instanceof LayoutFigure) { + ((LayoutFigure) f).setHighlighInfo(mHighlightInfo); + } + } + } + + public void hideDropTarget() { + mHighlightInfo.clear(); + IFigure f = getFigure(); + if (f instanceof LayoutFigure) { + ((LayoutFigure) f).setHighlighInfo(mHighlightInfo); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java new file mode 100644 index 0000000..4359e23 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import java.util.List; + +/** + * Implementation of {@link UiElementTreeEditPart} for layout objects. + */ +public class UiLayoutTreeEditPart extends UiElementTreeEditPart { + + public UiLayoutTreeEditPart(UiElementNode node) { + super(node); + } + + @SuppressWarnings("unchecked") + @Override + protected List getModelChildren() { + return getUiNode().getUiChildren(); + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java new file mode 100644 index 0000000..05329f3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.XYLayout; + +/** + * Graphical edit part for an {@link UiElementNode} that represents a View. + */ +public class UiViewEditPart extends UiElementEditPart { + + public UiViewEditPart(UiElementNode uiElementNode) { + super(uiElementNode); + } + + @Override + protected IFigure createFigure() { + IFigure f = new ElementFigure(); + f.setLayoutManager(new XYLayout()); + return f; + } + + @Override + protected void showSelection() { + IFigure f = getFigure(); + if (f instanceof ElementFigure) { + ((ElementFigure) f).setSelected(true); + } + } + + @Override + protected void hideSelection() { + IFigure f = getFigure(); + if (f instanceof ElementFigure) { + ((ElementFigure) f).setSelected(false); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java new file mode 100644 index 0000000..62b5e8a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.parts; + +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * Implementation of {@link UiElementTreeEditPart} for view objects. + */ +public class UiViewTreeEditPart extends UiElementTreeEditPart { + + public UiViewTreeEditPart(UiElementNode node) { + super(node); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java new file mode 100644 index 0000000..1bf5d5a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout.uimodel; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IProject; + +import java.util.List; + +/** + * Specialized version of {@link UiElementNode} for the {@link ViewElementDescriptor}s. + */ +public class UiViewElementNode extends UiElementNode { + + private AttributeDescriptor[] mCachedAttributeDescriptors; + + public UiViewElementNode(ViewElementDescriptor elementDescriptor) { + super(elementDescriptor); + } + + /** + * Returns an AttributeDescriptor array that depends on the current UiParent. + * <p/> + * The array merges both "direct" attributes with the descriptor layout attributes. + * The array instance is cached and cleared if the UiParent is changed. + */ + @Override + public AttributeDescriptor[] getAttributeDescriptors() { + if (mCachedAttributeDescriptors != null) { + return mCachedAttributeDescriptors; + } + + UiElementNode ui_parent = getUiParent(); + AttributeDescriptor[] direct_attrs = super.getAttributeDescriptors(); + mCachedAttributeDescriptors = direct_attrs; + + AttributeDescriptor[] layout_attrs = null; + boolean need_xmlns = false; + + if (ui_parent instanceof UiDocumentNode) { + // Limitation: right now the layout behaves as if everything was + // owned by a FrameLayout. + // TODO replace by something user-configurable. + + List<ElementDescriptor> layoutDescriptors = null; + IProject project = getEditor().getProject(); + if (project != null) { + Sdk currentSdk = Sdk.getCurrent(); + IAndroidTarget target = currentSdk.getTarget(project); + if (target != null) { + AndroidTargetData data = currentSdk.getTargetData(target); + layoutDescriptors = data.getLayoutDescriptors().getLayoutDescriptors(); + } + } + + if (layoutDescriptors != null) { + for (ElementDescriptor desc : layoutDescriptors) { + if (desc instanceof ViewElementDescriptor && + desc.getXmlName().equals(AndroidConstants.CLASS_FRAMELAYOUT)) { + layout_attrs = ((ViewElementDescriptor) desc).getLayoutAttributes(); + need_xmlns = true; + break; + } + } + } + } else if (ui_parent instanceof UiViewElementNode){ + layout_attrs = + ((ViewElementDescriptor) ui_parent.getDescriptor()).getLayoutAttributes(); + } + + if (layout_attrs == null || layout_attrs.length == 0) { + return mCachedAttributeDescriptors; + } + + mCachedAttributeDescriptors = + new AttributeDescriptor[direct_attrs.length + + layout_attrs.length + + (need_xmlns ? 1 : 0)]; + System.arraycopy(direct_attrs, 0, + mCachedAttributeDescriptors, 0, + direct_attrs.length); + System.arraycopy(layout_attrs, 0, + mCachedAttributeDescriptors, direct_attrs.length, + layout_attrs.length); + if (need_xmlns) { + AttributeDescriptor desc = new XmlnsAttributeDescriptor( + "android", //$NON-NLS-1$ + SdkConstants.NS_RESOURCES); + mCachedAttributeDescriptors[direct_attrs.length + layout_attrs.length] = desc; + } + + return mCachedAttributeDescriptors; + } + + /** + * Sets the parent of this UI node. + * <p/> + * Also removes the cached AttributeDescriptor array that depends on the current UiParent. + */ + @Override + protected void setUiParent(UiElementNode parent) { + super.setUiParent(parent); + mCachedAttributeDescriptors = null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java new file mode 100644 index 0000000..b40e458 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidContentAssist; + +/** + * Content Assist Processor for AndroidManifest.xml + */ +final class ManifestContentAssist extends AndroidContentAssist { + + /** + * Constructor for ManifestContentAssist + */ + public ManifestContentAssist() { + super(AndroidTargetData.DESCRIPTOR_MANIFEST); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java new file mode 100644 index 0000000..d0f8d7b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidXPathFactory; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.manifest.pages.ApplicationPage; +import com.android.ide.eclipse.editors.manifest.pages.InstrumentationPage; +import com.android.ide.eclipse.editors.manifest.pages.OverviewPage; +import com.android.ide.eclipse.editors.manifest.pages.PermissionPage; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import java.util.List; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +/** + * Multi-page form editor for AndroidManifest.xml. + */ +public final class ManifestEditor extends AndroidEditor { + private final static String EMPTY = ""; //$NON-NLS-1$ + + + /** Root node of the UI element hierarchy */ + private UiElementNode mUiManifestNode; + /** The Application Page tab */ + private ApplicationPage mAppPage; + /** The Overview Manifest Page tab */ + private OverviewPage mOverviewPage; + /** The Permission Page tab */ + private PermissionPage mPermissionPage; + /** The Instrumentation Page tab */ + private InstrumentationPage mInstrumentationPage; + + private IFileListener mMarkerMonitor; + + + /** + * Creates the form editor for AndroidManifest.xml. + */ + public ManifestEditor() { + super(); + } + + @Override + public void dispose() { + super.dispose(); + + ResourceMonitor.getMonitor().removeFileListener(mMarkerMonitor); + } + + /** + * Return the root node of the UI element hierarchy, which here + * is the "manifest" node. + */ + @Override + public UiElementNode getUiRootNode() { + return mUiManifestNode; + } + + /** + * Returns the Manifest descriptors for the file being edited. + */ + public AndroidManifestDescriptors getManifestDescriptors() { + AndroidTargetData data = getTargetData(); + if (data != null) { + return data.getManifestDescriptors(); + } + + return null; + } + + // ---- Base Class Overrides ---- + + /** + * Returns whether the "save as" operation is supported by this editor. + * <p/> + * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + /** + * Creates the various form pages. + */ + @Override + protected void createFormPages() { + try { + addPage(mOverviewPage = new OverviewPage(this)); + addPage(mAppPage = new ApplicationPage(this)); + addPage(mPermissionPage = new PermissionPage(this)); + addPage(mInstrumentationPage = new InstrumentationPage(this)); + } catch (PartInitException e) { + AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ + } + } + + /* (non-java doc) + * Change the tab/title name to include the project name. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + IFile inputFile = getInputFile(); + if (inputFile != null) { + startMonitoringMarkers(); + setPartName(String.format("%1$s Manifest", inputFile.getProject().getName())); + } + } + + /** + * Processes the new XML Model, which XML root node is given. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + @Override + protected void xmlModelChanged(Document xml_doc) { + // create the ui root node on demand. + initUiRootNode(false /*force*/); + + loadFromXml(xml_doc); + + super.xmlModelChanged(xml_doc); + } + + private void loadFromXml(Document xmlDoc) { + mUiManifestNode.setXmlDocument(xmlDoc); + if (xmlDoc != null) { + ElementDescriptor manifest_desc = mUiManifestNode.getDescriptor(); + try { + XPath xpath = AndroidXPathFactory.newXPath(); + Node node = (Node) xpath.evaluate("/" + manifest_desc.getXmlName(), //$NON-NLS-1$ + xmlDoc, + XPathConstants.NODE); + assert node != null && node.getNodeName().equals(manifest_desc.getXmlName()); + + // Refresh the manifest UI node and all its descendants + mUiManifestNode.loadFromXmlNode(node); + } catch (XPathExpressionException e) { + AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$ + manifest_desc.getXmlName()); + } + } + } + + private void onDescriptorsChanged(UiElementNode oldManifestNode) { + mUiManifestNode.reloadFromXmlNode(oldManifestNode.getXmlNode()); + + if (mOverviewPage != null) { + mOverviewPage.refreshUiApplicationNode(); + } + + if (mAppPage != null) { + mAppPage.refreshUiApplicationNode(); + } + + if (mPermissionPage != null) { + mPermissionPage.refreshUiNode(); + } + + if (mInstrumentationPage != null) { + mInstrumentationPage.refreshUiNode(); + } + } + + /** + * Reads and processes the current markers and adds a listener for marker changes. + */ + private void startMonitoringMarkers() { + final IFile inputFile = getInputFile(); + if (inputFile != null) { + updateFromExistingMarkers(inputFile); + + mMarkerMonitor = new IFileListener() { + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { + if (file.equals(inputFile)) { + processMarkerChanges(markerDeltas); + } + } + }; + + ResourceMonitor.getMonitor().addFileListener(mMarkerMonitor, IResourceDelta.CHANGED); + } + } + + /** + * Processes the markers of the specified {@link IFile} and updates the error status of + * {@link UiElementNode}s and {@link UiAttributeNode}s. + * @param inputFile the file being edited. + */ + private void updateFromExistingMarkers(IFile inputFile) { + try { + // get the markers for the file + IMarker[] markers = inputFile.findMarkers(AndroidConstants.MARKER_ANDROID, true, + IResource.DEPTH_ZERO); + + AndroidManifestDescriptors desc = getManifestDescriptors(); + if (desc != null) { + ElementDescriptor appElement = desc.getApplicationElement(); + + if (appElement != null) { + UiElementNode app_ui_node = mUiManifestNode.findUiChildNode( + appElement.getXmlName()); + List<UiElementNode> children = app_ui_node.getUiChildren(); + + for (IMarker marker : markers) { + processMarker(marker, children, IResourceDelta.ADDED); + } + } + } + + } catch (CoreException e) { + // findMarkers can throw an exception, in which case, we'll do nothing. + } + } + + /** + * Processes a {@link IMarker} change. + * @param markerDeltas the list of {@link IMarkerDelta} + */ + private void processMarkerChanges(IMarkerDelta[] markerDeltas) { + AndroidManifestDescriptors descriptors = getManifestDescriptors(); + if (descriptors != null && descriptors.getApplicationElement() != null) { + UiElementNode app_ui_node = mUiManifestNode.findUiChildNode( + descriptors.getApplicationElement().getXmlName()); + List<UiElementNode> children = app_ui_node.getUiChildren(); + + for (IMarkerDelta markerDelta : markerDeltas) { + processMarker(markerDelta.getMarker(), children, markerDelta.getKind()); + } + } + } + + /** + * Processes a new/old/updated marker. + * @param marker The marker being added/removed/changed + * @param nodeList the list of activity/service/provider/receiver nodes. + * @param kind the change kind. Can be {@link IResourceDelta#ADDED}, + * {@link IResourceDelta#REMOVED}, or {@link IResourceDelta#CHANGED} + */ + private void processMarker(IMarker marker, List<UiElementNode> nodeList, int kind) { + // get the data from the marker + String nodeType = marker.getAttribute(AndroidConstants.MARKER_ATTR_TYPE, EMPTY); + if (nodeType == EMPTY) { + return; + } + + String className = marker.getAttribute(AndroidConstants.MARKER_ATTR_CLASS, EMPTY); + if (className == EMPTY) { + return; + } + + for (UiElementNode ui_node : nodeList) { + if (ui_node.getDescriptor().getXmlName().equals(nodeType)) { + for (UiAttributeNode attr : ui_node.getUiAttributes()) { + if (attr.getDescriptor().getXmlLocalName().equals( + AndroidManifestDescriptors.ANDROID_NAME_ATTR)) { + if (attr.getCurrentValue().equals(className)) { + if (kind == IResourceDelta.REMOVED) { + attr.setHasError(false); + } else { + attr.setHasError(true); + } + return; + } + } + } + } + } + } + + /** + * Creates the initial UI Root Node, including the known mandatory elements. + * @param force if true, a new UiManifestNode is recreated even if it already exists. + */ + @Override + protected void initUiRootNode(boolean force) { + // The manifest UI node is always created, even if there's no corresponding XML node. + if (mUiManifestNode != null && force == false) { + return; + } + + + AndroidManifestDescriptors manifestDescriptor = getManifestDescriptors(); + + if (manifestDescriptor != null) { + // save the old manifest node if it exists + UiElementNode oldManifestNode = mUiManifestNode; + + ElementDescriptor manifestElement = manifestDescriptor.getManifestElement(); + mUiManifestNode = manifestElement.createUiNode(); + mUiManifestNode.setEditor(this); + + // Similarly, always create the /manifest/application and /manifest/uses-sdk nodes + ElementDescriptor appElement = manifestDescriptor.getApplicationElement(); + boolean present = false; + for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) { + if (ui_node.getDescriptor() == appElement) { + present = true; + break; + } + } + if (!present) { + mUiManifestNode.appendNewUiChild(appElement); + } + + appElement = manifestDescriptor.getUsesSdkElement(); + present = false; + for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) { + if (ui_node.getDescriptor() == appElement) { + present = true; + break; + } + } + if (!present) { + mUiManifestNode.appendNewUiChild(appElement); + } + + if (oldManifestNode != null) { + onDescriptorsChanged(oldManifestNode); + } + } else { + // create a dummy descriptor/uinode until we have real descriptors + ElementDescriptor desc = new ElementDescriptor("manifest", //$NON-NLS-1$ + "temporary descriptors due to missing decriptors", //$NON-NLS-1$ + null /*tooltip*/, null /*sdk_url*/, null /*attributes*/, + null /*children*/, false /*mandatory*/); + mUiManifestNode = desc.createUiNode(); + mUiManifestNode.setEditor(this); + } + } + + /** + * Returns the {@link IFile} being edited, or <code>null</code> if it couldn't be computed. + */ + private IFile getInputFile() { + IEditorInput input = getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + return fileInput.getFile(); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java new file mode 100644 index 0000000..911faa1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest; + +import org.eclipse.jface.action.IAction; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.ide.IDEActionFactory; +import org.eclipse.ui.part.MultiPageEditorActionBarContributor; +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.ui.texteditor.ITextEditorActionConstants; + +/** + * Manages the installation/deinstallation of global actions for multi-page + * editors. Responsible for the redirection of global actions to the active + * editor. Multi-page contributor replaces the contributors for the individual + * editors in the multi-page editor. + * + * TODO: Doesn't look like we need this. Remove it if not needed. + * @deprecated + */ +final class ManifestEditorContributor extends MultiPageEditorActionBarContributor { + private IEditorPart mActiveEditorPart; + + /** + * Creates a multi-page contributor. + * + * Marked as Private so it can't be instanciated. This is a cheap way to make sure + * it's not being used. As noted in constructor, should be removed if not used. + * @deprecated + */ + private ManifestEditorContributor() { + super(); + } + + /** + * Returns the action registed with the given text editor. + * + * @return IAction or null if editor is null. + */ + protected IAction getAction(ITextEditor editor, String actionID) { + return (editor == null ? null : editor.getAction(actionID)); + } + + /* + * (non-JavaDoc) Method declared in + * AbstractMultiPageEditorActionBarContributor. + */ + + @Override + public void setActivePage(IEditorPart part) { + if (mActiveEditorPart == part) + return; + + mActiveEditorPart = part; + + IActionBars actionBars = getActionBars(); + if (actionBars != null) { + + ITextEditor editor = + (part instanceof ITextEditor) ? (ITextEditor)part : null; + + actionBars.setGlobalActionHandler(ActionFactory.DELETE.getId(), + getAction(editor, ITextEditorActionConstants.DELETE)); + actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), + getAction(editor, ITextEditorActionConstants.UNDO)); + actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), + getAction(editor, ITextEditorActionConstants.REDO)); + actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(), + getAction(editor, ITextEditorActionConstants.CUT)); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), + getAction(editor, ITextEditorActionConstants.COPY)); + actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), + getAction(editor, ITextEditorActionConstants.PASTE)); + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), + getAction(editor, ITextEditorActionConstants.SELECT_ALL)); + actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), + getAction(editor, ITextEditorActionConstants.FIND)); + actionBars.setGlobalActionHandler( + IDEActionFactory.BOOKMARK.getId(), getAction(editor, + IDEActionFactory.BOOKMARK.getId())); + actionBars.updateActionBars(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java new file mode 100644 index 0000000..e33e1ef --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest; + + +import com.android.ide.eclipse.editors.AndroidSourceViewerConfig; + +/** + * Source Viewer Configuration that calls in ManifestContentAssist. + */ +public final class ManifestSourceViewerConfig extends AndroidSourceViewerConfig { + + public ManifestSourceViewerConfig() { + super(new ManifestContentAssist()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java new file mode 100644 index 0000000..77c08b5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.descriptors.ListAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ReferenceAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.runtime.IStatus; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.Map.Entry; + + +/** + * Complete description of the AndroidManifest.xml structure. + * <p/> + * The root element are static instances which always exists. + * However their sub-elements and attributes are created only when the SDK changes or is + * loaded the first time. + */ +public final class AndroidManifestDescriptors implements IDescriptorProvider { + + private static final String MANIFEST_NODE_NAME = "manifest"; //$NON-NLS-1$ + private static final String ANDROID_MANIFEST_STYLEABLE = "AndroidManifest"; //$NON-NLS-1$ + + // Public attributes names, attributes descriptors and elements descriptors + + public static final String ANDROID_LABEL_ATTR = "label"; //$NON-NLS-1$ + public static final String ANDROID_NAME_ATTR = "name"; //$NON-NLS-1$ + public static final String PACKAGE_ATTR = "package"; //$NON-NLS-1$ + + /** The {@link ElementDescriptor} for the root Manifest element. */ + private final ElementDescriptor MANIFEST_ELEMENT; + /** The {@link ElementDescriptor} for the root Application element. */ + private final ElementDescriptor APPLICATION_ELEMENT; + + /** The {@link ElementDescriptor} for the root Instrumentation element. */ + private final ElementDescriptor INTRUMENTATION_ELEMENT; + /** The {@link ElementDescriptor} for the root Permission element. */ + private final ElementDescriptor PERMISSION_ELEMENT; + /** The {@link ElementDescriptor} for the root UsesPermission element. */ + private final ElementDescriptor USES_PERMISSION_ELEMENT; + /** The {@link ElementDescriptor} for the root UsesSdk element. */ + private final ElementDescriptor USES_SDK_ELEMENT; + + /** The {@link ElementDescriptor} for the root PermissionGroup element. */ + private final ElementDescriptor PERMISSION_GROUP_ELEMENT; + /** The {@link ElementDescriptor} for the root PermissionTree element. */ + private final ElementDescriptor PERMISSION_TREE_ELEMENT; + + /** Private package attribute for the manifest element. Needs to be handled manually. */ + private final TextAttributeDescriptor PACKAGE_ATTR_DESC; + + public AndroidManifestDescriptors() { + APPLICATION_ELEMENT = createElement("application", null, true); //$NON-NLS-1$ + no child & mandatory + INTRUMENTATION_ELEMENT = createElement("instrumentation"); //$NON-NLS-1$ + + PERMISSION_ELEMENT = createElement("permission"); //$NON-NLS-1$ + USES_PERMISSION_ELEMENT = createElement("uses-permission"); //$NON-NLS-1$ + USES_SDK_ELEMENT = createElement("uses-sdk", null, true); //$NON-NLS-1$ + no child & mandatory + + PERMISSION_GROUP_ELEMENT = createElement("permission-group"); //$NON-NLS-1$ + PERMISSION_TREE_ELEMENT = createElement("permission-tree"); //$NON-NLS-1$ + + MANIFEST_ELEMENT = createElement( + MANIFEST_NODE_NAME, // xml name + new ElementDescriptor[] { + APPLICATION_ELEMENT, + INTRUMENTATION_ELEMENT, + PERMISSION_ELEMENT, + USES_PERMISSION_ELEMENT, + PERMISSION_GROUP_ELEMENT, + PERMISSION_TREE_ELEMENT, + USES_SDK_ELEMENT, + }, + true /* mandatory */); + + // The "package" attribute is treated differently as it doesn't have the standard + // Android XML namespace. + PACKAGE_ATTR_DESC = new PackageAttributeDescriptor(PACKAGE_ATTR, + "Package", + null /* nsUri */, + "This attribute gives a unique name for the package, using a Java-style naming convention to avoid name collisions.\nFor example, applications published by Google could have names of the form com.google.app.appname"); + } + + public ElementDescriptor[] getRootElementDescriptors() { + return new ElementDescriptor[] { MANIFEST_ELEMENT }; + } + + public ElementDescriptor getDescriptor() { + return getManifestElement(); + } + + public ElementDescriptor getApplicationElement() { + return APPLICATION_ELEMENT; + } + + public ElementDescriptor getManifestElement() { + return MANIFEST_ELEMENT; + } + + public ElementDescriptor getUsesSdkElement() { + return USES_SDK_ELEMENT; + } + + public ElementDescriptor getInstrumentationElement() { + return INTRUMENTATION_ELEMENT; + } + + public ElementDescriptor getPermissionElement() { + return PERMISSION_ELEMENT; + } + + public ElementDescriptor getUsesPermissionElement() { + return USES_PERMISSION_ELEMENT; + } + + public ElementDescriptor getPermissionGroupElement() { + return PERMISSION_GROUP_ELEMENT; + } + + public ElementDescriptor getPermissionTreeElement() { + return PERMISSION_TREE_ELEMENT; + } + + /** + * Updates the document descriptor. + * <p/> + * It first computes the new children of the descriptor and then updates them + * all at once. + * + * @param manifestMap The map style => attributes from the attrs_manifest.xml file + */ + public synchronized void updateDescriptors( + Map<String, DeclareStyleableInfo> manifestMap) { + + XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor( + "android", //$NON-NLS-1$ + SdkConstants.NS_RESOURCES); + + // -- setup the required attributes overrides -- + + Set<String> required = new HashSet<String>(); + required.add("provider/authorities"); //$NON-NLS-1$ + + // -- setup the various attribute format overrides -- + + // The key for each override is "element1,element2,.../attr-xml-local-name" or + // "*/attr-xml-local-name" to match the attribute in any element. + + Map<String, Object> overrides = new HashMap<String, Object>(); + + overrides.put("*/icon", new DescriptorsUtils.ITextAttributeCreator() { //$NON-NLS-1$ + public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri, + String tooltip) { + return new ReferenceAttributeDescriptor( + ResourceType.DRAWABLE, + xmlName, uiName, nsUri, + tooltip); + } + }); + + overrides.put("*/theme", ThemeAttributeDescriptor.class); //$NON-NLS-1$ + overrides.put("*/permission", ListAttributeDescriptor.class); //$NON-NLS-1$ + overrides.put("*/targetPackage", PackageAttributeDescriptor.class); //$NON-NLS-1$ + + overrides.put("uses-library/name", ListAttributeDescriptor.class); //$NON-NLS-1$ + + overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR, //$NON-NLS-1$ + ListAttributeDescriptor.class); + overrides.put("application/" + ANDROID_NAME_ATTR, ApplicationAttributeDescriptor.class); //$NON-NLS-1$ + + overrideClassName(overrides, "activity", AndroidConstants.CLASS_ACTIVITY); //$NON-NLS-1$ + overrideClassName(overrides, "receiver", AndroidConstants.CLASS_BROADCASTRECEIVER); //$NON-NLS-1$ + overrideClassName(overrides, "service", AndroidConstants.CLASS_SERVICE); //$NON-NLS-1$ + overrideClassName(overrides, "provider", AndroidConstants.CLASS_CONTENTPROVIDER); //$NON-NLS-1$ + + // -- list element nodes already created -- + // These elements are referenced by already opened editors, so we want to update them + // but not re-create them when reloading an SDK on the fly. + + HashMap<String, ElementDescriptor> elementDescs = + new HashMap<String, ElementDescriptor>(); + elementDescs.put(MANIFEST_ELEMENT.getXmlLocalName(), MANIFEST_ELEMENT); + elementDescs.put(APPLICATION_ELEMENT.getXmlLocalName(), APPLICATION_ELEMENT); + elementDescs.put(INTRUMENTATION_ELEMENT.getXmlLocalName(), INTRUMENTATION_ELEMENT); + elementDescs.put(PERMISSION_ELEMENT.getXmlLocalName(), PERMISSION_ELEMENT); + elementDescs.put(USES_PERMISSION_ELEMENT.getXmlLocalName(), USES_PERMISSION_ELEMENT); + elementDescs.put(USES_SDK_ELEMENT.getXmlLocalName(), USES_SDK_ELEMENT); + elementDescs.put(PERMISSION_GROUP_ELEMENT.getXmlLocalName(), PERMISSION_GROUP_ELEMENT); + elementDescs.put(PERMISSION_TREE_ELEMENT.getXmlLocalName(), PERMISSION_TREE_ELEMENT); + + // -- + + inflateElement(manifestMap, + overrides, + required, + elementDescs, + MANIFEST_ELEMENT, + "AndroidManifest"); //$NON-NLS-1$ + insertAttribute(MANIFEST_ELEMENT, PACKAGE_ATTR_DESC); + + sanityCheck(manifestMap, MANIFEST_ELEMENT); + } + + /** + * Sets up an attribute override for ANDROID_NAME_ATTR using a ClassAttributeDescriptor + * with the specified class name. + */ + private static void overrideClassName(Map<String, Object> overrides, + String elementName, final String className) { + overrides.put(elementName + "/" + ANDROID_NAME_ATTR, + new DescriptorsUtils.ITextAttributeCreator() { + public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri, + String tooltip) { + if (AndroidConstants.CLASS_ACTIVITY.equals(className)) { + return new ClassAttributeDescriptor( + className, + PostActivityCreationAction.getAction(), + xmlName, uiName + "*", //$NON-NLS-1$ + nsUri, + tooltip, + true /*mandatory */); + } else if (AndroidConstants.CLASS_BROADCASTRECEIVER.equals(className)) { + return new ClassAttributeDescriptor( + className, + PostReceiverCreationAction.getAction(), + xmlName, uiName + "*", //$NON-NLS-1$ + nsUri, + tooltip, + true /*mandatory */); + + } else { + return new ClassAttributeDescriptor( + className, + xmlName, uiName + "*", //$NON-NLS-1$ + nsUri, + tooltip, + true /*mandatory */); + } + } + }); + } + + /** + * Returns a new ElementDescriptor constructed from the information given here + * and the javadoc & attributes extracted from the style map if any. + * <p/> + * Creates an element with no attribute overrides. + */ + private ElementDescriptor createElement( + String xmlName, + ElementDescriptor[] childrenElements, + boolean mandatory) { + // Creates an element with no attribute overrides. + String styleName = guessStyleName(xmlName); + String sdkUrl = DescriptorsUtils.MANIFEST_SDK_URL + styleName; + String uiName = getUiName(xmlName); + + ElementDescriptor element = new ManifestElementDescriptor(xmlName, uiName, null, sdkUrl, + null, childrenElements, mandatory); + + return element; + } + + /** + * Returns a new ElementDescriptor constructed from its XML local name. + * <p/> + * This version creates an element not mandatory. + */ + private ElementDescriptor createElement(String xmlName) { + // Creates an element with no child and not mandatory + return createElement(xmlName, null, false); + } + + /** + * Inserts an attribute in this element attribute list if it is not present there yet + * (based on the attribute XML name.) + * The attribute is inserted at the beginning of the attribute list. + */ + private void insertAttribute(ElementDescriptor element, AttributeDescriptor newAttr) { + AttributeDescriptor[] attributes = element.getAttributes(); + for (AttributeDescriptor attr : attributes) { + if (attr.getXmlLocalName().equals(newAttr.getXmlLocalName())) { + return; + } + } + + AttributeDescriptor[] newArray = new AttributeDescriptor[attributes.length + 1]; + newArray[0] = newAttr; + System.arraycopy(attributes, 0, newArray, 1, attributes.length); + element.setAttributes(newArray); + } + + /** + * "Inflates" the properties of an {@link ElementDescriptor} from the styleable declaration. + * <p/> + * This first creates all the attributes for the given ElementDescriptor. + * It then finds all children of the descriptor, inflate them recursively and set them + * as child to this ElementDescriptor. + * + * @param styleMap The input styleable map for manifest elements & attributes. + * @param overrides A list of attribute overrides (to customize the type of the attribute + * descriptors). + * @param requiredAttributes Set of attributes to be marked as required. + * @param existingElementDescs A map of already created element descriptors, keyed by + * XML local name. This is used to use the static elements created initially by this + * class, which are referenced directly by editors (so that reloading an SDK won't + * break these references). + * @param elemDesc The current {@link ElementDescriptor} to inflate. + * @param styleName The name of the {@link ElementDescriptor} to inflate. Its XML local name + * will be guessed automatically from the style name. + */ + private void inflateElement( + Map<String, DeclareStyleableInfo> styleMap, + Map<String, Object> overrides, + Set<String> requiredAttributes, + HashMap<String, ElementDescriptor> existingElementDescs, + ElementDescriptor elemDesc, + String styleName) { + assert elemDesc != null; + assert styleName != null; + + // define attributes + DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null; + if (style != null) { + ArrayList<AttributeDescriptor> attrDescs = new ArrayList<AttributeDescriptor>(); + DescriptorsUtils.appendAttributes(attrDescs, + elemDesc.getXmlLocalName(), + SdkConstants.NS_RESOURCES, + style.getAttributes(), + requiredAttributes, + overrides); + elemDesc.setTooltip(style.getJavaDoc()); + elemDesc.setAttributes(attrDescs.toArray(new AttributeDescriptor[attrDescs.size()])); + } + + // find all elements that have this one as parent + ArrayList<ElementDescriptor> children = new ArrayList<ElementDescriptor>(); + for (Entry<String, DeclareStyleableInfo> entry : styleMap.entrySet()) { + DeclareStyleableInfo childStyle = entry.getValue(); + boolean isParent = false; + String[] parents = childStyle.getParents(); + if (parents != null) { + for (String parent: parents) { + if (styleName.equals(parent)) { + isParent = true; + break; + } + } + } + if (isParent) { + String childStyleName = entry.getKey(); + String childXmlName = guessXmlName(childStyleName); + + // create or re-use element + ElementDescriptor child = existingElementDescs.get(childXmlName); + if (child == null) { + child = createElement(childXmlName); + existingElementDescs.put(childXmlName, child); + } + children.add(child); + + inflateElement(styleMap, + overrides, + requiredAttributes, + existingElementDescs, + child, + childStyleName); + } + } + elemDesc.setChildren(children.toArray(new ElementDescriptor[children.size()])); + } + + /** + * Get an UI name from the element XML name. + * <p/> + * Capitalizes the first letter and replace non-alphabet by a space followed by a capital. + */ + private String getUiName(String xmlName) { + StringBuilder sb = new StringBuilder(); + + boolean capitalize = true; + for (char c : xmlName.toCharArray()) { + if (capitalize && c >= 'a' && c <= 'z') { + sb.append((char)(c + 'A' - 'a')); + capitalize = false; + } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) { + sb.append(' '); + capitalize = true; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * Guesses the style name for a given XML element name. + * <p/> + * The rules are: + * - capitalize the first letter: + * - if there's a dash, skip it and capitalize the next one + * - prefix AndroidManifest + * The exception is "manifest" which just becomes AndroidManifest. + * <p/> + * Examples: + * - manifest => AndroidManifest + * - application => AndroidManifestApplication + * - uses-permission => AndroidManifestUsesPermission + */ + private String guessStyleName(String xmlName) { + StringBuilder sb = new StringBuilder(); + + if (!xmlName.equals(MANIFEST_NODE_NAME)) { + boolean capitalize = true; + for (char c : xmlName.toCharArray()) { + if (capitalize && c >= 'a' && c <= 'z') { + sb.append((char)(c + 'A' - 'a')); + capitalize = false; + } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) { + // not a letter -- skip the character and capitalize the next one + capitalize = true; + } else { + sb.append(c); + } + } + } + + sb.insert(0, ANDROID_MANIFEST_STYLEABLE); + return sb.toString(); + } + + /** + * This method performs a sanity check to make sure all the styles declared in the + * manifestMap are actually defined in the actual element descriptors and reachable from + * the manifestElement root node. + */ + private void sanityCheck(Map<String, DeclareStyleableInfo> manifestMap, + ElementDescriptor manifestElement) { + TreeSet<String> elementsDeclared = new TreeSet<String>(); + findAllElementNames(manifestElement, elementsDeclared); + + TreeSet<String> stylesDeclared = new TreeSet<String>(); + for (String styleName : manifestMap.keySet()) { + if (styleName.startsWith(ANDROID_MANIFEST_STYLEABLE)) { + stylesDeclared.add(styleName); + } + } + + for (Iterator<String> it = elementsDeclared.iterator(); it.hasNext();) { + String xmlName = it.next(); + String styleName = guessStyleName(xmlName); + if (stylesDeclared.remove(styleName)) { + it.remove(); + } + } + + StringBuilder sb = new StringBuilder(); + if (!stylesDeclared.isEmpty()) { + sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by the SDK but unknown to ADT: "); + for (String name : stylesDeclared) { + name = guessXmlName(name); + + if (name != stylesDeclared.last()) { + sb.append(", "); //$NON-NLS-1$ + } + } + + AdtPlugin.log(IStatus.WARNING, "%s", sb.toString()); + AdtPlugin.printToConsole((String)null, sb); + sb.setLength(0); + } + + if (!elementsDeclared.isEmpty()) { + sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by ADT but not by the SDK: "); + for (String name : elementsDeclared) { + sb.append(name); + if (name != elementsDeclared.last()) { + sb.append(", "); //$NON-NLS-1$ + } + } + + AdtPlugin.log(IStatus.WARNING, "%s", sb.toString()); + AdtPlugin.printToConsole((String)null, sb); + } + } + + /** + * Performs an approximate translation of the style name into a potential + * xml name. This is more or less the reverse from guessStyleName(). + * + * @return The XML local name for a given style name. + */ + private String guessXmlName(String name) { + StringBuilder sb = new StringBuilder(); + if (ANDROID_MANIFEST_STYLEABLE.equals(name)) { + sb.append(MANIFEST_NODE_NAME); + } else { + name = name.replace(ANDROID_MANIFEST_STYLEABLE, ""); //$NON-NLS-1$ + boolean first_char = true; + for (char c : name.toCharArray()) { + if (c >= 'A' && c <= 'Z') { + if (!first_char) { + sb.append('-'); + } + c = (char) (c - 'A' + 'a'); + } + sb.append(c); + first_char = false; + } + } + return sb.toString(); + } + + /** + * Helper method used by {@link #sanityCheck(Map, ElementDescriptor)} to find all the + * {@link ElementDescriptor} names defined by the tree of descriptors. + * <p/> + * Note: this assumes no circular reference in the tree of {@link ElementDescriptor}s. + */ + private void findAllElementNames(ElementDescriptor element, TreeSet<String> declared) { + declared.add(element.getXmlName()); + for (ElementDescriptor desc : element.getChildren()) { + findAllElementNames(desc, declared); + } + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java new file mode 100644 index 0000000..eab7f09 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * Describes an 'Application' class XML attribute. It is displayed by a + * {@link UiClassAttributeNode}, that restricts creation and selection to classes + * inheriting from android.app.Application. + */ +public class ApplicationAttributeDescriptor extends TextAttributeDescriptor { + + public ApplicationAttributeDescriptor(String xmlLocalName, String uiName, + String nsUri, String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * @return A new {@link UiClassAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiClassAttributeNode("android.app.Application", //$NON-NLS-1$ + null /* postCreationAction */, false /* mandatory */, this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java new file mode 100644 index 0000000..629b37c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode; +import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +/** + * Describes an XML attribute representing a class name. + * It is displayed by a {@link UiClassAttributeNode}. + */ +public class ClassAttributeDescriptor extends TextAttributeDescriptor { + + /** Superclass of the class value. */ + private String mSuperClassName; + + private IPostTypeCreationAction mPostCreationAction; + + /** indicates if the class parameter is mandatory */ + boolean mMandatory; + + /** + * Creates a new {@link ClassAttributeDescriptor} + * @param superClassName the fully qualified name of the superclass of the class represented + * by the attribute. + * @param xmlLocalName The XML name of the attribute (case sensitive, with android: prefix). + * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param tooltip A non-empty tooltip string or null. + * @param mandatory indicates if the class attribute is mandatory. + */ + public ClassAttributeDescriptor(String superClassName, + String xmlLocalName, String uiName, String nsUri, + String tooltip, boolean mandatory) { + super(xmlLocalName, uiName, nsUri, tooltip); + mSuperClassName = superClassName; + } + + /** + * Creates a new {@link ClassAttributeDescriptor} + * @param superClassName the fully qualified name of the superclass of the class represented + * by the attribute. + * @param postCreationAction the {@link IPostTypeCreationAction} to be executed on the + * newly created class. + * @param xmlLocalName The XML local name of the attribute (case sensitive). + * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null. + * @param nsUri The URI of the attribute. Can be null if attribute has no namespace. + * See {@link SdkConstants#NS_RESOURCES} for a common value. + * @param tooltip A non-empty tooltip string or null. + * @param mandatory indicates if the class attribute is mandatory. + */ + public ClassAttributeDescriptor(String superClassName, + IPostTypeCreationAction postCreationAction, + String xmlLocalName, String uiName, String nsUri, + String tooltip, boolean mandatory) { + super(xmlLocalName, uiName, nsUri, tooltip); + mSuperClassName = superClassName; + mPostCreationAction = postCreationAction; + } + + /** + * @return A new {@link UiClassAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiClassAttributeNode(mSuperClassName, mPostCreationAction, + mMandatory, this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java new file mode 100644 index 0000000..6e589f7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * Describes a 'Instrumentation' class XML attribute. It is displayed by a + * {@link UiClassAttributeNode}, that restricts creation and selection to classes inheriting from + * android.app.Instrumentation. + */ +public class InstrumentationAttributeDescriptor extends TextAttributeDescriptor { + + public InstrumentationAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * @return A new {@link UiClassAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiClassAttributeNode(AndroidConstants.CLASS_INSTRUMENTATION, + null /* postCreationAction */, true /* mandatory */, this, uiParent); + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java new file mode 100644 index 0000000..d89292b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.model.UiManifestElementNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * {@link ManifestElementDescriptor} describes an XML element node, with its + * element name, its possible attributes, its possible child elements but also + * its display name and tooltip. + * + * This {@link ElementDescriptor} is specialized to create {@link UiManifestElementNode} UI nodes. + */ +public class ManifestElementDescriptor extends ElementDescriptor { + + /** + * Constructs a new {@link ManifestElementDescriptor}. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). + */ + public ManifestElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url, + AttributeDescriptor[] attributes, + ElementDescriptor[] children, + boolean mandatory) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory); + } + + /** + * Constructs a new {@link ManifestElementDescriptor}. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + */ + public ManifestElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url, + AttributeDescriptor[] attributes, + ElementDescriptor[] children) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, false); + } + + /** + * This is a shortcut for + * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, children). + * This constructor is mostly used for unit tests. + * + * @param xml_name The XML element node name. Case sensitive. + */ + public ManifestElementDescriptor(String xml_name, ElementDescriptor[] children) { + super(xml_name, children); + } + + /** + * This is a shortcut for + * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, null). + * This constructor is mostly used for unit tests. + * + * @param xml_name The XML element node name. Case sensitive. + */ + public ManifestElementDescriptor(String xml_name) { + super(xml_name, null); + } + + /** + * @return A new {@link UiElementNode} linked to this descriptor. + */ + @Override + public UiElementNode createUiNode() { + return new UiManifestElementNode(this); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java new file mode 100644 index 0000000..34c5d0d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.model.UiPackageAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * Describes a package XML attribute. It is displayed by a {@link UiPackageAttributeNode}. + */ +public class PackageAttributeDescriptor extends TextAttributeDescriptor { + + public PackageAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * @return A new {@link UiPackageAttributeNode} linked to this descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiPackageAttributeNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java new file mode 100644 index 0000000..3442c24 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +/** + * Action to be executed after an Activity class is created. + */ +class PostActivityCreationAction implements IPostTypeCreationAction { + + private final static PostActivityCreationAction sAction = new PostActivityCreationAction(); + + private PostActivityCreationAction() { + // private constructor to enforce singleton. + } + + + /** + * Returns the action. + */ + public static IPostTypeCreationAction getAction() { + return sAction; + } + + /** + * Processes a newly created Activity. + * + */ + public void processNewType(IType newType) { + try { + String methodContent = + " /** Called when the activity is first created. */\n" + + " @Override\n" + + " public void onCreate(Bundle savedInstanceState) {\n" + + " super.onCreate(savedInstanceState);\n" + + "\n" + + " // TODO Auto-generated method stub\n" + + " }"; + newType.createMethod(methodContent, null /* sibling*/, false /* force */, + new NullProgressMonitor()); + + // we need to add the import for Bundle, so we need the compilation unit. + // Since the type could be enclosed in other types, we loop till we find it. + ICompilationUnit compilationUnit = null; + IJavaElement element = newType; + do { + IJavaElement parentElement = element.getParent(); + if (parentElement != null) { + if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) { + compilationUnit = (ICompilationUnit)parentElement; + } + + element = parentElement; + } else { + break; + } + } while (compilationUnit == null); + + if (compilationUnit != null) { + compilationUnit.createImport(AndroidConstants.CLASS_BUNDLE, + null /* sibling */, new NullProgressMonitor()); + } + } catch (JavaModelException e) { + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java new file mode 100644 index 0000000..5a8137d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +/** + * Action to be executed after an BroadcastReceiver class is created. + */ +class PostReceiverCreationAction implements IPostTypeCreationAction { + + private final static PostReceiverCreationAction sAction = new PostReceiverCreationAction(); + + private PostReceiverCreationAction() { + // private constructor to enforce singleton. + } + + /** + * Returns the action. + */ + public static IPostTypeCreationAction getAction() { + return sAction; + } + + /** + * Processes a newly created Activity. + * + */ + public void processNewType(IType newType) { + try { + String methodContent = + " @Override\n" + + " public void onReceive(Context context, Intent intent) {\n" + + " // TODO Auto-generated method stub\n" + + " }"; + newType.createMethod(methodContent, null /* sibling*/, false /* force */, + new NullProgressMonitor()); + + // we need to add the import for Bundle, so we need the compilation unit. + // Since the type could be enclosed in other types, we loop till we find it. + ICompilationUnit compilationUnit = null; + IJavaElement element = newType; + do { + IJavaElement parentElement = element.getParent(); + if (parentElement != null) { + if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) { + compilationUnit = (ICompilationUnit)parentElement; + } + + element = parentElement; + } else { + break; + } + } while (compilationUnit == null); + + if (compilationUnit != null) { + compilationUnit.createImport(AndroidConstants.CLASS_CONTEXT, + null /* sibling */, new NullProgressMonitor()); + compilationUnit.createImport(AndroidConstants.CLASS_INTENT, + null /* sibling */, new NullProgressMonitor()); + } + } catch (JavaModelException e) { + // looks like the class already existed (this happens when the user check to create + // inherited abstract methods). + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java new file mode 100644 index 0000000..4219007 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.descriptors; + +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode; + +/** + * Describes a Theme/Style XML attribute displayed by a {@link UiResourceAttributeNode} + */ +public final class ThemeAttributeDescriptor extends TextAttributeDescriptor { + + public ThemeAttributeDescriptor(String xmlLocalName, String uiName, String nsUri, + String tooltip) { + super(xmlLocalName, uiName, nsUri, tooltip); + } + + /** + * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiResourceAttributeNode(ResourceType.STYLE, this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java new file mode 100644 index 0000000..f8aac1d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java @@ -0,0 +1,624 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.model; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.project.BaseProjectHelper; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.ui.SectionHelper; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiTextAttributeNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.ui.IJavaElementSearchConstants; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction; +import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension; +import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor; +import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent; +import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension; +import org.eclipse.jdt.ui.wizards.NewClassWizardPage; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.SelectionDialog; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.events.HyperlinkAdapter; +import org.eclipse.ui.forms.events.HyperlinkEvent; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.TableWrapData; +import org.w3c.dom.Element; + +import java.util.ArrayList; + +/** + * Represents an XML attribute for a class, that can be modified using a simple text field or + * a dialog to choose an existing class. Also, there's a link to create a new class. + * <p/> + * See {@link UiTextAttributeNode} for more information. + */ +public class UiClassAttributeNode extends UiTextAttributeNode { + + private String mReferenceClass; + private IPostTypeCreationAction mPostCreationAction; + private boolean mMandatory; + + private class HierarchyTypeSelection extends TypeSelectionExtension { + + private IJavaProject mJavaProject; + + public HierarchyTypeSelection(IProject project, String referenceClass) { + mJavaProject = JavaCore.create(project); + mReferenceClass = referenceClass; + } + + @Override + public ITypeInfoFilterExtension getFilterExtension() { + return new ITypeInfoFilterExtension() { + public boolean select(ITypeInfoRequestor typeInfoRequestor) { + String packageName = typeInfoRequestor.getPackageName(); + String typeName = typeInfoRequestor.getTypeName(); + String enclosingType = typeInfoRequestor.getEnclosingName(); + + // build the full class name. + StringBuilder sb = new StringBuilder(packageName); + sb.append('.'); + if (enclosingType.length() > 0) { + sb.append(enclosingType); + sb.append('.'); + } + sb.append(typeName); + + String className = sb.toString(); + + try { + IType type = mJavaProject.findType(className); + if (type != null) { + // get the type hierarchy + ITypeHierarchy hierarchy = type.newSupertypeHierarchy( + new NullProgressMonitor()); + + // if the super class is not the reference class, it may inherit from + // it so we get its supertype. At some point it will be null and we + // will return false; + IType superType = type; + while ((superType = hierarchy.getSuperclass(superType)) != null) { + if (mReferenceClass.equals(superType.getFullyQualifiedName())) { + return true; + } + } + } + } catch (JavaModelException e) { + } + + return false; + } + }; + } + } + + /** + * Classes which implement this interface provide a method processing newly created classes. + */ + public static interface IPostTypeCreationAction { + /** + * Sent to process a newly created class. + * @param newType the IType representing the newly created class. + */ + public void processNewType(IType newType); + } + + /** + * Creates a {@link UiClassAttributeNode} object that will display ui to select or create + * classes. + * @param referenceClass The allowed supertype of the classes that are to be selected + * or created. Can be null. + * @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation + * modification of the class. + * @param mandatory indicates if the class value is mandatory + * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node. + */ + public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction, + boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + + mReferenceClass = referenceClass; + mPostCreationAction = postCreationAction; + mMandatory = mandatory; + } + + /* (non-java doc) + * Creates a label widget and an associated text field. + * <p/> + * As most other parts of the android manifest editor, this assumes the + * parent uses a table layout with 2 columns. + */ + @Override + public void createUiControl(final Composite parent, IManagedForm managedForm) { + setManagedForm(managedForm); + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + StringBuilder label = new StringBuilder(); + label.append("<form><p><a href='unused'>"); + label.append(desc.getUiName()); + label.append("</a></p></form>"); + FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */, + label.toString(), true /* setupLayoutData */); + formText.addHyperlinkListener(new HyperlinkAdapter() { + @Override + public void linkActivated(HyperlinkEvent e) { + super.linkActivated(e); + handleLabelClick(); + } + }); + formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); + SectionHelper.addControlTooltip(formText, desc.getTooltip()); + + Composite composite = toolkit.createComposite(parent); + composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE)); + GridLayout gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + composite.setLayout(gl); + // Fixes missing text borders under GTK... also requires adding a 1-pixel margin + // for the text field below + toolkit.paintBordersFor(composite); + + final Text text = toolkit.createText(composite, getCurrentValue()); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK + text.setLayoutData(gd); + Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH); + + setTextWidget(text); + + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + handleBrowseClick(); + } + }); + } + + /* (non-java doc) + * + * Add a modify listener that will check the validity of the class + */ + @Override + protected void onAddValidators(final Text text) { + ModifyListener listener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + try { + String textValue = text.getText().trim(); + if (textValue.length() == 0) { + if (mMandatory) { + setErrorMessage("Value is mandatory", text); + } else { + setErrorMessage(null, text); + } + return; + } + // first we need the current java package. + String javaPackage = getManifestPackage(); + + // build the fully qualified name of the class + String className = AndroidManifestHelper.combinePackageAndClassName(javaPackage, + textValue); + + // only test the vilibility for activities. + boolean testVisibility = AndroidConstants.CLASS_ACTIVITY.equals( + mReferenceClass); + + // test the class + setErrorMessage(BaseProjectHelper.testClassForManifest( + BaseProjectHelper.getJavaProject(getProject()), className, + mReferenceClass, testVisibility), text); + } catch (CoreException ce) { + setErrorMessage(ce.getMessage(), text); + } + } + }; + + text.addModifyListener(listener); + + // Make sure the validator removes its message(s) when the widget is disposed + text.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + // we don't want to use setErrorMessage, because we don't want to reset + // the error flag in the UiAttributeNode + getManagedForm().getMessageManager().removeMessage(text, text); + } + }); + + // Finally call the validator once to make sure the initial value is processed + listener.modifyText(null); + } + + private void handleBrowseClick() { + Text text = getTextWidget(); + + // we need to get the project of the manifest. + IProject project = getProject(); + if (project != null) { + + // Create a search scope including only the source folder of the current + // project. + IJavaSearchScope scope = SearchEngine.createJavaSearchScope( + getPackageFragmentRoots(project), false); + + try { + SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(), + PlatformUI.getWorkbench().getProgressService(), + scope, + IJavaElementSearchConstants.CONSIDER_CLASSES, // style + false, // no multiple selection + "**", //$NON-NLS-1$ //filter + new HierarchyTypeSelection(project, mReferenceClass)); + dlg.setMessage(String.format("Select class name for element %1$s:", + getUiParent().getBreadcrumbTrailDescription(false /* include_root */))); + if (dlg instanceof ITypeSelectionComponent) { + ((ITypeSelectionComponent)dlg).triggerSearch(); + } + + if (dlg.open() == Window.OK) { + Object[] results = dlg.getResult(); + if (results.length == 1) { + handleNewType((IType)results[0]); + } + } + } catch (JavaModelException e1) { + } + } + } + + private void handleLabelClick() { + // get the current value + String className = getTextWidget().getText().trim(); + + // get the package name from the manifest. + String packageName = getManifestPackage(); + + if (className.length() == 0) { + createNewClass(packageName, null /* className */); + } else { + // build back the fully qualified class name. + String fullClassName = className; + if (className.startsWith(".")) { //$NON-NLS-1$ + fullClassName = packageName + className; + } else { + String[] segments = className.split(AndroidConstants.RE_DOT); + if (segments.length == 1) { + fullClassName = packageName + "." + className; //$NON-NLS-1$ + } + } + + // in case the type is enclosed, we need to replace the $ with . + fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$ + + // now we try to find the file that contains this class and we open it in the editor. + IProject project = getProject(); + IJavaProject javaProject = JavaCore.create(project); + + try { + IType result = javaProject.findType(fullClassName); + if (result != null) { + JavaUI.openInEditor(result); + } else { + // split the last segment from the fullClassname + int index = fullClassName.lastIndexOf('.'); + if (index != -1) { + createNewClass(fullClassName.substring(0, index), + fullClassName.substring(index+1)); + } else { + createNewClass(packageName, className); + } + } + } catch (JavaModelException e) { + } catch (PartInitException e) { + } + } + } + + private IProject getProject() { + UiElementNode uiNode = getUiParent(); + AndroidEditor editor = uiNode.getEditor(); + IEditorInput input = editor.getEditorInput(); + if (input instanceof IFileEditorInput) { + // from the file editor we can get the IFile object, and from it, the IProject. + IFile file = ((IFileEditorInput)input).getFile(); + return file.getProject(); + } + + return null; + } + + + /** + * Returns the current value of the /manifest/package attribute. + * @return the package or an empty string if not found + */ + private String getManifestPackage() { + // get the root uiNode to get the 'package' attribute value. + UiElementNode rootNode = getUiParent().getUiRoot(); + + Element xmlElement = (Element) rootNode.getXmlNode(); + + if (xmlElement != null) { + return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR); + } + return ""; //$NON-NLS-1$ + } + + + /** + * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of + * the specified project. + * @param project the project + * @return an array of IPackageFragmentRoot. + */ + private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project) { + ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>(); + try { + IJavaProject javaProject = JavaCore.create(project); + IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); + for (int i = 0; i < roots.length; i++) { + IClasspathEntry entry = roots[i].getRawClasspathEntry(); + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + result.add(roots[i]); + } + } + } catch (JavaModelException e) { + } + + return result.toArray(new IPackageFragmentRoot[result.size()]); + } + + private void handleNewType(IType type) { + Text text = getTextWidget(); + + // get the fully qualified name with $ to properly detect the enclosing types. + String name = type.getFullyQualifiedName('$'); + + String packageValue = getManifestPackage(); + + // check if the class doesn't start with the package. + if (packageValue.length() > 0 && name.startsWith(packageValue)) { + // if it does, we remove the package and the first dot. + name = name.substring(packageValue.length() + 1); + + // look for how many segments we have left. + // if one, just write it that way. + // if more than one, write it with a leading dot. + String[] packages = name.split(AndroidConstants.RE_DOT); + if (packages.length == 1) { + text.setText(name); + } else { + text.setText("." + name); //$NON-NLS-1$ + } + } else { + text.setText(name); + } + } + + private void createNewClass(String packageName, String className) { + // create the wizard page for the class creation, and configure it + NewClassWizardPage page = new NewClassWizardPage(); + + // set the parent class + page.setSuperClass(mReferenceClass, true /* canBeModified */); + + // get the source folders as java elements. + IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject()); + + IPackageFragmentRoot currentRoot = null; + IPackageFragment currentFragment = null; + int packageMatchCount = -1; + + for (IPackageFragmentRoot root : roots) { + // Get the java element for the package. + // This method is said to always return a IPackageFragment even if the + // underlying folder doesn't exist... + IPackageFragment fragment = root.getPackageFragment(packageName); + if (fragment != null && fragment.exists()) { + // we have a perfect match! we use it. + currentRoot = root; + currentFragment = fragment; + packageMatchCount = -1; + break; + } else { + // we don't have a match. we look for the fragment with the best match + // (ie the closest parent package we can find) + try { + IJavaElement[] children; + children = root.getChildren(); + for (IJavaElement child : children) { + if (child instanceof IPackageFragment) { + fragment = (IPackageFragment)child; + if (packageName.startsWith(fragment.getElementName())) { + // its a match. get the number of segments + String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$ + if (segments.length > packageMatchCount) { + packageMatchCount = segments.length; + currentFragment = fragment; + currentRoot = root; + } + } + } + } + } catch (JavaModelException e) { + // Couldn't get the children: we just ignore this package root. + } + } + } + + ArrayList<IPackageFragment> createdFragments = null; + + if (currentRoot != null) { + // if we have a perfect match, we set it and we're done. + if (packageMatchCount == -1) { + page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/); + page.setPackageFragment(currentFragment, true /* canBeModified */); + } else { + // we have a partial match. + // create the package. We have to start with the first segment so that we + // know what to delete in case of a cancel. + try { + createdFragments = new ArrayList<IPackageFragment>(); + + int totalCount = packageName.split("\\.").length; //$NON-NLS-1$ + int count = 0; + int index = -1; + // skip the matching packages + while (count < packageMatchCount) { + index = packageName.indexOf('.', index+1); + count++; + } + + // create the rest of the segments, except for the last one as indexOf will + // return -1; + while (count < totalCount - 1) { + index = packageName.indexOf('.', index+1); + count++; + createdFragments.add(currentRoot.createPackageFragment( + packageName.substring(0, index), + true /* force*/, new NullProgressMonitor())); + } + + // create the last package + createdFragments.add(currentRoot.createPackageFragment( + packageName, true /* force*/, new NullProgressMonitor())); + + // set the root and fragment in the Wizard page + page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/); + page.setPackageFragment(createdFragments.get(createdFragments.size()-1), + true /* canBeModified */); + } catch (JavaModelException e) { + // if we can't create the packages, there's a problem. we revert to the default + // package + for (IPackageFragmentRoot root : roots) { + // Get the java element for the package. + // This method is said to always return a IPackageFragment even if the + // underlying folder doesn't exist... + IPackageFragment fragment = root.getPackageFragment(packageName); + if (fragment != null && fragment.exists()) { + page.setPackageFragmentRoot(root, true /* canBeModified*/); + page.setPackageFragment(fragment, true /* canBeModified */); + break; + } + } + } + } + } else if (roots.length > 0) { + // if we haven't found a valid fragment, we set the root to the first source folder. + page.setPackageFragmentRoot(roots[0], true /* canBeModified*/); + } + + // if we have a starting class name we use it + if (className != null) { + page.setTypeName(className, true /* canBeModified*/); + } + + // create the action that will open it the wizard. + OpenNewClassWizardAction action = new OpenNewClassWizardAction(); + action.setConfiguredWizardPage(page); + action.run(); + IJavaElement element = action.getCreatedElement(); + + if (element != null) { + if (element.getElementType() == IJavaElement.TYPE) { + + IType type = (IType)element; + + if (mPostCreationAction != null) { + mPostCreationAction.processNewType(type); + } + + handleNewType(type); + } + } else { + // lets delete the packages we created just for this. + // we need to start with the leaf and go up + if (createdFragments != null) { + try { + for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) { + createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor()); + } + } catch (JavaModelException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Sets the error messages. If message is <code>null</code>, the message is removed. + * @param message the message to set, or <code>null</code> to remove the current message + * @param textWidget the {@link Text} widget associated to the message. + */ + private final void setErrorMessage(String message, Text textWidget) { + if (message != null) { + setHasError(true); + getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */, + IMessageProvider.ERROR, textWidget); + } else { + setHasError(false); + getManagedForm().getMessageManager().removeMessage(textWidget, textWidget); + } + } + + @Override + public String[] getPossibleValues() { + // TODO: compute a list of existing classes for content assist completion + return null; + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java new file mode 100644 index 0000000..fb8f211 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.model; + +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.manifest.descriptors.ManifestElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +import org.w3c.dom.Element; + +/** + * Represents an XML node that can be modified by the user interface in the XML editor. + * <p/> + * Each tree viewer used in the application page's parts needs to keep a model representing + * each underlying node in the tree. This interface represents the base type for such a node. + * <p/> + * Each node acts as an intermediary model between the actual XML model (the real data support) + * and the tree viewers or the corresponding page parts. + * <p/> + * Element nodes don't contain data per se. Their data is contained in their attributes + * as well as their children's attributes, see {@link UiAttributeNode}. + * <p/> + * The structure of a given {@link UiElementNode} is declared by a corresponding + * {@link ElementDescriptor}. + */ +public final class UiManifestElementNode extends UiElementNode { + + /** + * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}. + * + * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null. + */ + public UiManifestElementNode(ManifestElementDescriptor elementDescriptor) { + super(elementDescriptor); + } + + /** + * Computes a short string describing the UI node suitable for tree views. + * Uses the element's attribute "android:name" if present, or the "android:label" one + * followed by the element's name. + * + * @return A short string describing the UI node suitable for tree views. + */ + @Override + public String getShortDescription() { + if (getXmlNode() != null && + getXmlNode() instanceof Element && + getXmlNode().hasAttributes()) { + + AndroidManifestDescriptors manifestDescriptors = + getAndroidTarget().getManifestDescriptors(); + + // Application and Manifest nodes have a special treatment: they are unique nodes + // so we don't bother trying to differentiate their strings and we fall back to + // just using the UI name below. + ElementDescriptor desc = getDescriptor(); + if (desc != manifestDescriptors.getManifestElement() && + desc != manifestDescriptors.getApplicationElement()) { + Element elem = (Element) getXmlNode(); + String attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES, + AndroidManifestDescriptors.ANDROID_NAME_ATTR); + if (attr == null || attr.length() == 0) { + attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES, + AndroidManifestDescriptors.ANDROID_LABEL_ATTR); + } + if (attr != null && attr.length() > 0) { + return String.format("%1$s (%2$s)", attr, getDescriptor().getUiName()); + } + } + } + + return String.format("%1$s", getDescriptor().getUiName()); + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java new file mode 100644 index 0000000..02fb44f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.model; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.ui.SectionHelper; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiTextAttributeNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.actions.OpenNewPackageWizardAction; +import org.eclipse.jdt.ui.actions.ShowInPackageViewAction; +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.dialogs.SelectionDialog; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.events.HyperlinkAdapter; +import org.eclipse.ui.forms.events.HyperlinkEvent; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.TableWrapData; + +import java.util.ArrayList; + +/** + * Represents an XML attribute for a package, that can be modified using a simple text field or + * a dialog to choose an existing package. Also, there's a link to create a new package. + * <p/> + * See {@link UiTextAttributeNode} for more information. + */ +public class UiPackageAttributeNode extends UiTextAttributeNode { + + /** + * Creates a {@link UiPackageAttributeNode} object that will display ui to select or create + * a package. + * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node. + */ + public UiPackageAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (non-java doc) + * Creates a label widget and an associated text field. + * <p/> + * As most other parts of the android manifest editor, this assumes the + * parent uses a table layout with 2 columns. + */ + @Override + public void createUiControl(final Composite parent, final IManagedForm managedForm) { + setManagedForm(managedForm); + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + StringBuilder label = new StringBuilder(); + label.append("<form><p><a href='unused'>"); //$NON-NLS-1$ + label.append(desc.getUiName()); + label.append("</a></p></form>"); //$NON-NLS-1$ + FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */, + label.toString(), true /* setupLayoutData */); + formText.addHyperlinkListener(new HyperlinkAdapter() { + @Override + public void linkActivated(HyperlinkEvent e) { + super.linkActivated(e); + doLabelClick(); + } + }); + formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); + SectionHelper.addControlTooltip(formText, desc.getTooltip()); + + Composite composite = toolkit.createComposite(parent); + composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE)); + GridLayout gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + composite.setLayout(gl); + // Fixes missing text borders under GTK... also requires adding a 1-pixel margin + // for the text field below + toolkit.paintBordersFor(composite); + + final Text text = toolkit.createText(composite, getCurrentValue()); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK + text.setLayoutData(gd); + + setTextWidget(text); + + Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH); + + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doBrowseClick(); + } + }); + + } + + /* (non-java doc) + * Adds a validator to the text field that calls managedForm.getMessageManager(). + */ + @Override + protected void onAddValidators(final Text text) { + ModifyListener listener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + String package_name = text.getText(); + if (package_name.indexOf('.') < 1) { + getManagedForm().getMessageManager().addMessage(text, + "Package name should contain at least two identifiers.", + null /* data */, IMessageProvider.ERROR, text); + } else { + getManagedForm().getMessageManager().removeMessage(text, text); + } + } + }; + + text.addModifyListener(listener); + + // Make sure the validator removes its message(s) when the widget is disposed + text.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + getManagedForm().getMessageManager().removeMessage(text, text); + } + }); + + // Finally call the validator once to make sure the initial value is processed + listener.modifyText(null); + } + + /** + * Handles response to the Browse button by creating a Package dialog. + * */ + private void doBrowseClick() { + Text text = getTextWidget(); + + // we need to get the project of the manifest. + IProject project = getProject(); + if (project != null) { + + try { + SelectionDialog dlg = JavaUI.createPackageDialog(text.getShell(), + JavaCore.create(project), 0); + dlg.setTitle("Select Android Package"); + dlg.setMessage("Select the package for the Android project."); + SelectionDialog.setDefaultImage(AdtPlugin.getAndroidLogo()); + + if (dlg.open() == Window.OK) { + Object[] results = dlg.getResult(); + if (results.length == 1) { + setPackageTextField((IPackageFragment)results[0]); + } + } + } catch (JavaModelException e1) { + } + } + } + + /** + * Handles response to the Label hyper link being activated. + */ + private void doLabelClick() { + // get the current package name + String package_name = getTextWidget().getText().trim(); + + if (package_name.length() == 0) { + createNewPackage(); + } else { + // Try to select the package in the Package Explorer for the current + // project and the current editor's site. + + IProject project = getProject(); + if (project == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get project for UiPackageAttribute"); //$NON-NLS-1$ + return; + } + + IWorkbenchPartSite site = getUiParent().getEditor().getSite(); + if (site == null) { + AdtPlugin.log(IStatus.ERROR, "Failed to get editor site for UiPackageAttribute"); //$NON-NLS-1$ + return; + } + + for (IPackageFragmentRoot root : getPackageFragmentRoots(project)) { + IPackageFragment fragment = root.getPackageFragment(package_name); + if (fragment != null && fragment.exists()) { + ShowInPackageViewAction action = new ShowInPackageViewAction(site); + action.run(fragment); + // This action's run() doesn't provide the status (although internally it could) + // so we just assume it worked. + return; + } + } + } + } + + /** + * Utility method that returns the project for the current file being edited. + * + * @return The IProject for the current file being edited or null. + */ + private IProject getProject() { + UiElementNode uiNode = getUiParent(); + AndroidEditor editor = uiNode.getEditor(); + IEditorInput input = editor.getEditorInput(); + if (input instanceof IFileEditorInput) { + // from the file editor we can get the IFile object, and from it, the IProject. + IFile file = ((IFileEditorInput)input).getFile(); + return file.getProject(); + } + + return null; + } + + /** + * Utility method that computes and returns the list of {@link IPackageFragmentRoot} + * corresponding to the source folder of the specified project. + * + * @param project the project + * @return an array of IPackageFragmentRoot. Can be empty but not null. + */ + private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project) { + ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>(); + try { + IJavaProject javaProject = JavaCore.create(project); + IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); + for (int i = 0; i < roots.length; i++) { + IClasspathEntry entry = roots[i].getRawClasspathEntry(); + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + result.add(roots[i]); + } + } + } catch (JavaModelException e) { + } + + return result.toArray(new IPackageFragmentRoot[result.size()]); + } + + /** + * Utility method that sets the package's text field to the package fragment's name. + * */ + private void setPackageTextField(IPackageFragment type) { + Text text = getTextWidget(); + + String name = type.getElementName(); + + text.setText(name); + } + + + /** + * Displays and handles a "Create Package Wizard". + * + * This is invoked by doLabelClick() when clicking on the hyperlink label with an + * empty package text field. + */ + private void createNewPackage() { + OpenNewPackageWizardAction action = new OpenNewPackageWizardAction(); + + IProject project = getProject(); + action.setSelection(new StructuredSelection(project)); + action.run(); + + IJavaElement element = action.getCreatedElement(); + if (element != null && + element.exists() && + element.getElementType() == IJavaElement.PACKAGE_FRAGMENT) { + setPackageTextField((IPackageFragment) element); + } + } + + @Override + public String[] getPossibleValues() { + // TODO: compute a list of existing packages for content assist completion + return null; + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java new file mode 100644 index 0000000..01b0f8f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.ui.UiElementPart; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Application's attributes section part for Application page. + * <p/> + * This part is displayed at the top of the application page and displays all the possible + * attributes of an application node in the AndroidManifest (icon, class name, label, etc.) + */ +final class ApplicationAttributesPart extends UiElementPart { + + /** Listen to changes to the UI node for <application> and updates the UI */ + private AppNodeUpdateListener mAppNodeUpdateListener; + /** ManagedForm needed to create the UI controls */ + private IManagedForm mManagedForm; + + public ApplicationAttributesPart(Composite body, FormToolkit toolkit, ManifestEditor editor, + UiElementNode applicationUiNode) { + super(body, toolkit, editor, applicationUiNode, + "Application Attributes", // section title + "Defines the attributes specific to the application.", // section description + Section.TWISTIE | Section.EXPANDED); + } + + /** + * Changes and refreshes the Application UI node handle by the this part. + */ + @Override + public void setUiElementNode(UiElementNode uiElementNode) { + super.setUiElementNode(uiElementNode); + + createUiAttributes(mManagedForm); + } + + /* (non-java doc) + * Create the controls to edit the attributes for the given ElementDescriptor. + * <p/> + * This MUST not be called by the constructor. Instead it must be called from + * <code>initialize</code> (i.e. right after the form part is added to the managed form.) + * <p/> + * Derived classes can override this if necessary. + * + * @param managedForm The owner managed form + */ + @Override + protected void createFormControls(final IManagedForm managedForm) { + mManagedForm = managedForm; + setTable(createTableLayout(managedForm.getToolkit(), 4 /* numColumns */)); + + mAppNodeUpdateListener = new AppNodeUpdateListener(); + getUiElementNode().addUpdateListener(mAppNodeUpdateListener); + + createUiAttributes(mManagedForm); + } + + @Override + public void dispose() { + super.dispose(); + if (getUiElementNode() != null && mAppNodeUpdateListener != null) { + getUiElementNode().removeUpdateListener(mAppNodeUpdateListener); + mAppNodeUpdateListener = null; + } + } + + @Override + protected void createUiAttributes(IManagedForm managedForm) { + Composite table = getTable(); + if (table == null || managedForm == null) { + return; + } + + // Remove any old UI controls + for (Control c : table.getChildren()) { + c.dispose(); + } + + UiElementNode uiElementNode = getUiElementNode(); + AttributeDescriptor[] attr_desc_list = uiElementNode.getAttributeDescriptors(); + + // Display the attributes in 2 columns: + // attr 0 | attr 4 + // attr 1 | attr 5 + // attr 2 | attr 6 + // attr 3 | attr 7 + // that is we have to fill the grid in order 0, 4, 1, 5, 2, 6, 3, 7 + // thus index = i/2 + (i is odd * n/2) + int n = attr_desc_list.length; + int n2 = (int) Math.ceil(n / 2.0); + for (int i = 0; i < n; i++) { + AttributeDescriptor attr_desc = attr_desc_list[i / 2 + (i & 1) * n2]; + if (attr_desc instanceof XmlnsAttributeDescriptor) { + // Do not show hidden attributes + continue; + } + + UiAttributeNode ui_attr = uiElementNode.findUiAttribute(attr_desc); + if (ui_attr != null) { + ui_attr.createUiControl(table, managedForm); + } else { + // The XML has an extra attribute which wasn't declared in + // AndroidManifestDescriptors. This is not a problem, we just ignore it. + AdtPlugin.log(IStatus.WARNING, + "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$ + attr_desc.getXmlLocalName(), + uiElementNode.getDescriptor().getXmlName()); + } + } + + if (n == 0) { + createLabel(table, managedForm.getToolkit(), + "No attributes to display, waiting for SDK to finish loading...", + null /* tooltip */ ); + } + + // Initialize the enabled/disabled state + if (mAppNodeUpdateListener != null) { + mAppNodeUpdateListener.uiElementNodeUpdated(uiElementNode, null /* state, not used */); + } + + // Tell the section that the layout has changed. + layoutChanged(); + } + + /** + * This listener synchronizes the UI with the actual presence of the application XML node. + */ + private class AppNodeUpdateListener implements IUiUpdateListener { + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + // The UiElementNode for the application XML node always exists, even + // if there is no corresponding XML node in the XML file. + // + // We enable the UI here if the XML node is not null. + Composite table = getTable(); + boolean exists = (ui_node.getXmlNode() != null); + if (table != null && table.getEnabled() != exists) { + table.setEnabled(exists); + for (Control c : table.getChildren()) { + c.setEnabled(exists); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java new file mode 100644 index 0000000..77527f0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.ScrolledForm; + + +/** + * Page for "Application" settings, part of the AndroidManifest form editor. + * <p/> + * Useful reference: + * <a href="http://www.eclipse.org/articles/Article-Forms/article.html"> + * http://www.eclipse.org/articles/Article-Forms/article.html</a> + */ +public final class ApplicationPage extends FormPage { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "application_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + /** The Application Toogle part */ + private ApplicationToggle mTooglePart; + /** The Application Attributes part */ + private ApplicationAttributesPart mAttrPart; + /** The tree view block */ + private UiTreeBlock mTreeBlock; + + public ApplicationPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Application"); // tab's label, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Application"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode appUiNode = getUiApplicationNode(); + + Composite body = form.getBody(); + FormToolkit toolkit = managedForm.getToolkit(); + + // We usually prefer to have a ColumnLayout here. However + // MasterDetailsBlock.createContent() below will reset the body's layout to a grid layout. + mTooglePart = new ApplicationToggle(body, toolkit, mEditor, appUiNode); + mTooglePart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(mTooglePart); + mAttrPart = new ApplicationAttributesPart(body, toolkit, mEditor, appUiNode); + mAttrPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + managedForm.addPart(mAttrPart); + + mTreeBlock = new UiTreeBlock(mEditor, appUiNode, + false /* autoCreateRoot */, + null /* element filters */, + "Application Nodes", + "List of all elements in the application"); + mTreeBlock.createContent(managedForm); + } + + /** + * Retrieves the application UI node. Since this is a mandatory node, it *always* + * exists, even if there is no matching XML node. + */ + private UiElementNode getUiApplicationNode() { + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + if (manifestDescriptor != null) { + ElementDescriptor desc = manifestDescriptor.getApplicationElement(); + return mEditor.getUiRootNode().findUiChildNode(desc.getXmlName()); + } else { + // return the ui root node, as a dummy application root node. + return mEditor.getUiRootNode(); + } + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiApplicationNode() { + UiElementNode appUiNode = getUiApplicationNode(); + if (mTooglePart != null) { + mTooglePart.setUiElementNode(appUiNode); + } + if (mAttrPart != null) { + mAttrPart.setUiElementNode(appUiNode); + } + if (mTreeBlock != null) { + mTreeBlock.changeRootAndDescriptors(appUiNode, + null /* element filters */, + true /* refresh */); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java new file mode 100644 index 0000000..139575d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.ui.UiElementPart; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener.UiUpdateState; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; +import org.eclipse.ui.forms.widgets.TableWrapData; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.Text; + +/** + * Appllication Toogle section part for application page. + */ +final class ApplicationToggle extends UiElementPart { + + /** Checkbox indicating whether an application node is present */ + private Button mCheckbox; + /** Listen to changes to the UI node for <application> and updates the checkbox */ + private AppNodeUpdateListener mAppNodeUpdateListener; + /** Internal flag to know where we're programmatically modifying the checkbox and we want to + * avoid triggering the checkbox's callback. */ + public boolean mInternalModification; + private FormText mTooltipFormText; + + public ApplicationToggle(Composite body, FormToolkit toolkit, ManifestEditor editor, + UiElementNode applicationUiNode) { + super(body, toolkit, editor, applicationUiNode, + "Application Toggle", + null, /* description */ + Section.TWISTIE | Section.EXPANDED); + } + + @Override + public void dispose() { + super.dispose(); + if (getUiElementNode() != null && mAppNodeUpdateListener != null) { + getUiElementNode().removeUpdateListener(mAppNodeUpdateListener); + mAppNodeUpdateListener = null; + } + } + + /** + * Changes and refreshes the Application UI node handle by the this part. + */ + @Override + public void setUiElementNode(UiElementNode uiElementNode) { + super.setUiElementNode(uiElementNode); + + updateTooltip(); + + // Set the state of the checkbox + mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(), + UiUpdateState.CHILDREN_CHANGED); + } + + /** + * Create the controls to edit the attributes for the given ElementDescriptor. + * <p/> + * This MUST not be called by the constructor. Instead it must be called from + * <code>initialize</code> (i.e. right after the form part is added to the managed form.) + * + * @param managedForm The owner managed form + */ + @Override + protected void createFormControls(IManagedForm managedForm) { + FormToolkit toolkit = managedForm.getToolkit(); + Composite table = createTableLayout(toolkit, 1 /* numColumns */); + + mTooltipFormText = createFormText(table, toolkit, true, "<form></form>", + false /* setupLayoutData */); + updateTooltip(); + + mCheckbox = toolkit.createButton(table, + "Define an <application> tag in the AndroidManifest.xml", + SWT.CHECK); + mCheckbox.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP)); + mCheckbox.setSelection(false); + mCheckbox.addSelectionListener(new CheckboxSelectionListener()); + + mAppNodeUpdateListener = new AppNodeUpdateListener(); + getUiElementNode().addUpdateListener(mAppNodeUpdateListener); + + // Initialize the state of the checkbox + mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(), + UiUpdateState.CHILDREN_CHANGED); + + // Tell the section that the layout has changed. + layoutChanged(); + } + + /** + * Updates the application tooltip in the form text. + * If there is no tooltip, the form text is hidden. + */ + private void updateTooltip() { + boolean isVisible = false; + + String tooltip = getUiElementNode().getDescriptor().getTooltip(); + if (tooltip != null) { + tooltip = DescriptorsUtils.formatFormText(tooltip, + getUiElementNode().getDescriptor(), + Sdk.getCurrent().getDocumentationBaseUrl()); + + mTooltipFormText.setText(tooltip, true /* parseTags */, true /* expandURLs */); + mTooltipFormText.setImage(DescriptorsUtils.IMAGE_KEY, AdtPlugin.getAndroidLogo()); + mTooltipFormText.addHyperlinkListener(getEditor().createHyperlinkListener()); + isVisible = true; + } + + mTooltipFormText.setVisible(isVisible); + } + + /** + * This listener synchronizes the XML application node when the checkbox + * is changed by the user. + */ + private class CheckboxSelectionListener extends SelectionAdapter { + private Node mUndoXmlNode; + private Node mUndoXmlParent; + private Node mUndoXmlNextNode; + private Node mUndoXmlNextElement; + private Document mUndoXmlDocument; + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + if (!mInternalModification && getUiElementNode() != null) { + getUiElementNode().getEditor().wrapUndoRecording( + mCheckbox.getSelection() + ? "Create or restore Application node" + : "Remove Application node", + new Runnable() { + public void run() { + getUiElementNode().getEditor().editXmlModel(new Runnable() { + public void run() { + if (mCheckbox.getSelection()) { + // The user wants an <application> node. + // Either restore a previous one + // or create a full new one. + boolean create = true; + if (mUndoXmlNode != null) { + create = !restoreApplicationNode(); + } + if (create) { + getUiElementNode().createXmlNode(); + } + } else { + // Users no longer wants the <application> node. + removeApplicationNode(); + } + } + }); + } + }); + } + } + + /** + * Restore a previously "saved" application node. + * + * @return True if the node could be restored, false otherwise. + */ + private boolean restoreApplicationNode() { + if (mUndoXmlDocument == null || mUndoXmlNode == null) { + return false; + } + + // Validate node references... + mUndoXmlParent = validateNode(mUndoXmlDocument, mUndoXmlParent); + mUndoXmlNextNode = validateNode(mUndoXmlDocument, mUndoXmlNextNode); + mUndoXmlNextElement = validateNode(mUndoXmlDocument, mUndoXmlNextElement); + + if (mUndoXmlParent == null){ + // If the parent node doesn't exist, try to find a new manifest node. + // If it doesn't exist, create it. + mUndoXmlParent = getUiElementNode().getUiParent().prepareCommit(); + mUndoXmlNextNode = null; + mUndoXmlNextElement = null; + } + + boolean success = false; + if (mUndoXmlParent != null) { + // If the parent is still around, reuse the same node. + + // Ideally we want to insert the node before what used to be its next sibling. + // If that's not possible, we try to insert it before its next sibling element. + // If that's not possible either, it will be inserted at the end of the parent's. + Node next = mUndoXmlNextNode; + if (next == null) { + next = mUndoXmlNextElement; + } + mUndoXmlParent.insertBefore(mUndoXmlNode, next); + if (next == null) { + Text sep = mUndoXmlDocument.createTextNode("\n"); //$NON-NLS-1$ + mUndoXmlParent.insertBefore(sep, null); // insert separator before end tag + } + success = true; + } + + // Remove internal references to avoid using them twice + mUndoXmlParent = null; + mUndoXmlNextNode = null; + mUndoXmlNextElement = null; + mUndoXmlNode = null; + mUndoXmlDocument = null; + return success; + } + + /** + * Validates that the given xml_node is still either the root node or one of its + * direct descendants. + * + * @param root_node The root of the node hierarchy to examine. + * @param xml_node The XML node to find. + * @return Returns xml_node if it is, otherwise returns null. + */ + private Node validateNode(Node root_node, Node xml_node) { + if (root_node == xml_node) { + return xml_node; + } else { + for (Node node = root_node.getFirstChild(); node != null; + node = node.getNextSibling()) { + if (root_node == xml_node || validateNode(node, xml_node) != null) { + return xml_node; + } + } + } + return null; + } + + /** + * Removes the <application> node from the hierarchy. + * Before doing that, we try to remember where it was so that we can put it back + * in the same place. + */ + private void removeApplicationNode() { + // Make sure the node actually exists... + Node xml_node = getUiElementNode().getXmlNode(); + if (xml_node == null) { + return; + } + + // Save its parent, next sibling and next element + mUndoXmlDocument = xml_node.getOwnerDocument(); + mUndoXmlParent = xml_node.getParentNode(); + mUndoXmlNextNode = xml_node.getNextSibling(); + mUndoXmlNextElement = mUndoXmlNextNode; + while (mUndoXmlNextElement != null && + mUndoXmlNextElement.getNodeType() != Node.ELEMENT_NODE) { + mUndoXmlNextElement = mUndoXmlNextElement.getNextSibling(); + } + + // Actually remove the node from the hierarchy and keep it here. + // The returned node looses its parents/siblings pointers. + mUndoXmlNode = getUiElementNode().deleteXmlNode(); + } + } + + /** + * This listener synchronizes the UI (i.e. the checkbox) with the + * actual presence of the application XML node. + */ + private class AppNodeUpdateListener implements IUiUpdateListener { + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + // The UiElementNode for the application XML node always exists, even + // if there is no corresponding XML node in the XML file. + // + // To update the checkbox to reflect the actual state, we just need + // to check if the XML node is null. + try { + mInternalModification = true; + boolean exists = ui_node.getXmlNode() != null; + if (mCheckbox.getSelection() != exists) { + mCheckbox.setSelection(exists); + } + } finally { + mInternalModification = false; + } + + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java new file mode 100644 index 0000000..86d0dd1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Page for instrumentation settings, part of the AndroidManifest form editor. + */ +public final class InstrumentationPage extends FormPage { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "instrumentation_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + + private UiTreeBlock mTreeBlock; + + public InstrumentationPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Instrumentation"); // tab's label, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Instrumentation"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + ElementDescriptor[] descriptorFilters = null; + if (manifestDescriptor != null) { + descriptorFilters = new ElementDescriptor[] { + manifestDescriptor.getInstrumentationElement(), + }; + } + + mTreeBlock = new UiTreeBlock(mEditor, manifest, + true /* autoCreateRoot */, + descriptorFilters, + "Instrumentation", + "List of instrumentations defined in the manifest"); + mTreeBlock.createContent(managedForm); + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiNode() { + if (mTreeBlock != null) { + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + mTreeBlock.changeRootAndDescriptors(manifest, + new ElementDescriptor[] { + manifestDescriptor.getInstrumentationElement() + }, + true /* refresh */); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java new file mode 100644 index 0000000..66af84c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.common.project.ExportHelper; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.forms.events.HyperlinkAdapter; +import org.eclipse.ui.forms.events.HyperlinkEvent; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; +import org.eclipse.ui.part.FileEditorInput; + +/** + * Export section part for overview page. + */ +final class OverviewExportPart extends ManifestSectionPart { + + private final OverviewPage mOverviewPage; + + public OverviewExportPart(OverviewPage overviewPage, Composite body, FormToolkit toolkit, ManifestEditor editor) { + super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); + mOverviewPage = overviewPage; + Section section = getSection(); + section.setText("Exporting"); + section.setDescription("To export the application for distribution, you have the following options:"); + + Composite table = createTableLayout(toolkit, 2 /* numColumns */); + + StringBuffer buf = new StringBuffer(); + buf.append("<form><li><a href=\"wizard\">"); //$NON-NLS-1$ + buf.append("Use the Export Wizard"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(" to export and sign an APK"); + buf.append("</li>"); //$NON-NLS-1$ + buf.append("<li><a href=\"manual\">"); //$NON-NLS-1$ + buf.append("Export an unsigned APK"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(" and sign it manually"); + buf.append("</li></form>"); //$NON-NLS-1$ + + FormText text = createFormText(table, toolkit, true, buf.toString(), + false /* setupLayoutData */); + text.addHyperlinkListener(new HyperlinkAdapter() { + @Override + public void linkActivated(HyperlinkEvent e) { + // get the project from the editor + IEditorInput input = mOverviewPage.mEditor.getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + IFile file = fileInput.getFile(); + IProject project = file.getProject(); + + if ("manual".equals(e.data)) { //$NON-NLS-1$ + // now we can export an unsigned apk for the project. + ExportHelper.exportProject(project); + } else { + // call the export wizard + ExportHelper.startExportWizard(project); + } + } + } + }); + + layoutChanged(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java new file mode 100644 index 0000000..026b760 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.ui.UiElementPart; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; +import org.w3c.dom.Node; + +/** + * Generic info section part for overview page + */ +final class OverviewInfoPart extends UiElementPart { + + private IManagedForm mManagedForm; + + public OverviewInfoPart(Composite body, FormToolkit toolkit, ManifestEditor editor) { + super(body, toolkit, editor, + null, // uiElementNode + "General Information", // section title + "Defines general information about the AndroidManifest.xml", // section description + Section.TWISTIE | Section.EXPANDED); + } + + /** + * Retrieves the UiElementNode that this part will edit. The node must exist + * and can't be null, by design, because it's a mandatory node. + */ + private static UiElementNode getManifestUiNode(ManifestEditor editor) { + AndroidManifestDescriptors manifestDescriptors = editor.getManifestDescriptors(); + if (manifestDescriptors != null) { + ElementDescriptor desc = manifestDescriptors.getManifestElement(); + if (editor.getUiRootNode().getDescriptor() == desc) { + return editor.getUiRootNode(); + } else { + return editor.getUiRootNode().findUiChildNode(desc.getXmlName()); + } + } + + // No manifest descriptor: we have a dummy UiRootNode, so we return that. + // The editor will be reloaded once we have the proper descriptors anyway. + return editor.getUiRootNode(); + } + + /** + * Retrieves the uses-sdk UI node. Since this is a mandatory node, it *always* + * exists, even if there is no matching XML node. + */ + private UiElementNode getUsesSdkUiNode(ManifestEditor editor) { + AndroidManifestDescriptors manifestDescriptors = editor.getManifestDescriptors(); + if (manifestDescriptors != null) { + ElementDescriptor desc = manifestDescriptors.getUsesSdkElement(); + return editor.getUiRootNode().findUiChildNode(desc.getXmlName()); + } + + // No manifest descriptor: we have a dummy UiRootNode, so we return that. + // The editor will be reloaded once we have the proper descriptors anyway. + return editor.getUiRootNode(); + } + + /** + * Overridden in order to capture the current managed form. + * + * {@inheritDoc} + */ + @Override + protected void createFormControls(final IManagedForm managedForm) { + mManagedForm = managedForm; + super.createFormControls(managedForm); + } + + /** + * Removes any existing Attribute UI widgets and recreate them if the SDK has changed. + * <p/> + * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the + * SDK has changed. + */ + public void onSdkChanged() { + createUiAttributes(mManagedForm); + } + + /** + * Overridden to add the description and the ui attributes of both the + * manifest and uses-sdk UI nodes. + * <p/> + * {@inheritDoc} + */ + @Override + protected void fillTable(Composite table, IManagedForm managedForm) { + int n = 0; + + UiElementNode uiNode = getManifestUiNode(getEditor()); + n += insertUiAttributes(uiNode, table, managedForm); + + uiNode = getUsesSdkUiNode(getEditor()); + n += insertUiAttributes(uiNode, table, managedForm); + + if (n == 0) { + createLabel(table, managedForm.getToolkit(), + "No attributes to display, waiting for SDK to finish loading...", + null /* tooltip */ ); + } + + layoutChanged(); + } + + /** + * Overridden to tests whether either the manifest or uses-sdk nodes parts are dirty. + * <p/> + * {@inheritDoc} + * + * @return <code>true</code> if the part is dirty, <code>false</code> + * otherwise. + */ + @Override + public boolean isDirty() { + boolean dirty = super.isDirty(); + + if (!dirty) { + UiElementNode uiNode = getManifestUiNode(getEditor()); + if (uiNode != null) { + for (UiAttributeNode ui_attr : uiNode.getUiAttributes()) { + if (ui_attr.isDirty()) { + markDirty(); + dirty = true; + break; + } + } + } + } + + if (!dirty) { + UiElementNode uiNode = getUsesSdkUiNode(getEditor()); + if (uiNode != null) { + for (UiAttributeNode ui_attr : uiNode.getUiAttributes()) { + if (ui_attr.isDirty()) { + markDirty(); + dirty = true; + break; + } + } + } + } + + return dirty; + } + + /** + * Overridden to save both the manifest or uses-sdk nodes. + * <p/> + * {@inheritDoc} + */ + @Override + public void commit(boolean onSave) { + final UiElementNode manifestUiNode = getManifestUiNode(getEditor()); + final UiElementNode usesSdkUiNode = getUsesSdkUiNode(getEditor()); + + getEditor().editXmlModel(new Runnable() { + public void run() { + if (manifestUiNode != null && manifestUiNode.isDirty()) { + for (UiAttributeNode ui_attr : manifestUiNode.getUiAttributes()) { + ui_attr.commit(); + } + } + + if (usesSdkUiNode != null && usesSdkUiNode.isDirty()) { + for (UiAttributeNode ui_attr : usesSdkUiNode.getUiAttributes()) { + ui_attr.commit(); + } + + if (!usesSdkUiNode.isDirty()) { + // Remove the <uses-sdk> XML element if it is empty. + // Rather than rely on the internal UI state, actually check that the + // XML element has no attributes and no child element so that we don't + // trash some user-generated content. + Node element = usesSdkUiNode.prepareCommit(); + if (element != null && + !element.hasAttributes() && + !element.hasChildNodes()) { + // Important note: we MUST NOT use usesSdkUiNode.deleteXmlNode() + // here, as it would clear the UiAttribute list and thus break the + // link between the controls and the ui attribute field. + // Instead what we want is simply to remove the XML node and let the + // UiElementNode node know. + Node parent = element.getParentNode(); + if (parent != null) { + parent.removeChild(element); + usesSdkUiNode.loadFromXmlNode(null /*xml_node*/); + } + } + } + } + } + }); + + // We need to call super's commit after we synchronized the nodes to make sure we + // reset the dirty flag after all the side effects from committing have occurred. + super.commit(onSave); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java new file mode 100644 index 0000000..d637a8f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Links section part for overview page. + */ +final class OverviewLinksPart extends ManifestSectionPart { + + private final ManifestEditor mEditor; + private FormText mFormText; + + public OverviewLinksPart(Composite body, FormToolkit toolkit, ManifestEditor editor) { + super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */); + mEditor = editor; + Section section = getSection(); + section.setText("Links"); + section.setDescription("The content of the Android Manifest is made up of three sections. You can also edit the XML directly."); + + Composite table = createTableLayout(toolkit, 2 /* numColumns */); + + StringBuffer buf = new StringBuffer(); + buf.append(String.format("<form><li style=\"image\" value=\"app_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$ + ApplicationPage.PAGE_ID)); + buf.append("Application"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Activities, intent filters, providers, services and receivers."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append(String.format("<li style=\"image\" value=\"perm_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$ + PermissionPage.PAGE_ID)); + buf.append("Permission"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Permissions defined and permissions used."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append(String.format("<li style=\"image\" value=\"inst_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$ + InstrumentationPage.PAGE_ID)); + buf.append("Instrumentation"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Instrumentation defined."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append(String.format("<li style=\"image\" value=\"android_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$ + ManifestEditor.TEXT_EDITOR_ID)); + buf.append("XML Source"); + buf.append("</a>"); //$NON-NLS-1$ + buf.append(": Directly edit the AndroidManifest.xml file."); + buf.append("</li>"); //$NON-NLS-1$ + + buf.append("<li style=\"image\" value=\"android_img\">"); // $NON-NLS-1$ + buf.append("<a href=\"http://code.google.com/android/devel/bblocks-manifest.html\">Documentation</a>: Documentation from the Android SDK for AndroidManifest.xml."); // $NON-NLS-1$ + buf.append("</li>"); //$NON-NLS-1$ + buf.append("</form>"); //$NON-NLS-1$ + + mFormText = createFormText(table, toolkit, true, buf.toString(), + false /* setupLayoutData */); + + AndroidManifestDescriptors manifestDescriptor = editor.getManifestDescriptors(); + + Image androidLogo = AdtPlugin.getAndroidLogo(); + mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$ + + if (manifestDescriptor != null) { + mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$ + mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$ + mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$ + } else { + mFormText.setImage("app_img", androidLogo); //$NON-NLS-1$ + mFormText.setImage("perm_img", androidLogo); //$NON-NLS-1$ + mFormText.setImage("inst_img", androidLogo); //$NON-NLS-1$ + } + mFormText.addHyperlinkListener(editor.createHyperlinkListener()); + } + + /** + * Update the UI with information from the new descriptors. + * <p/>At this point, this only refreshes the icons. + * <p/> + * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the + * SDK has changed. + */ + public void onSdkChanged() { + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + if (manifestDescriptor != null) { + mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$ + mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$ + mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$ + } + } + + private Image getIcon(ElementDescriptor desc) { + if (desc != null && desc.getIcon() != null) { + return desc.getIcon(); + } + + return AdtPlugin.getAndroidLogo(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java new file mode 100644 index 0000000..62954bd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ColumnLayout; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.ScrolledForm; + + +/** + * Page for overview settings, part of the AndroidManifest form editor. + * <p/> + * Useful reference: + * <a href="http://www.eclipse.org/articles/Article-Forms/article.html"> + * http://www.eclipse.org/articles/Article-Forms/article.html</a> + */ +public final class OverviewPage extends FormPage { + + /** Page id used for switching tabs programmatically */ + final static String PAGE_ID = "overview_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + /** Overview part (attributes for manifest) */ + private OverviewInfoPart mOverviewPart; + /** Overview link part */ + private OverviewLinksPart mOverviewLinkPart; + + public OverviewPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Overview"); // tab's label, user visible, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Overview"); + form.setImage(AdtPlugin.getAndroidLogo()); + + Composite body = form.getBody(); + FormToolkit toolkit = managedForm.getToolkit(); + ColumnLayout cl = new ColumnLayout(); + cl.minNumColumns = cl.maxNumColumns = 1; + body.setLayout(cl); + mOverviewPart = new OverviewInfoPart(body, toolkit, mEditor); + managedForm.addPart(mOverviewPart); + managedForm.addPart(new OverviewExportPart(this, body, toolkit, mEditor)); + mOverviewLinkPart = new OverviewLinksPart(body, toolkit, mEditor); + managedForm.addPart(mOverviewLinkPart); + } + + /** + * Changes and refreshes the Application UI node handle by the sub parts. + */ + public void refreshUiApplicationNode() { + if (mOverviewPart != null) { + mOverviewPart.onSdkChanged(); + } + + if (mOverviewLinkPart != null) { + mOverviewLinkPart.onSdkChanged(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java new file mode 100644 index 0000000..41ba22e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.pages; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Page for permissions settings, part of the AndroidManifest form editor. + * <p/> + * Useful reference: + * <a href="http://www.eclipse.org/articles/Article-Forms/article.html"> + * http://www.eclipse.org/articles/Article-Forms/article.html</a> + */ +public final class PermissionPage extends FormPage { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "permission_page"; //$NON-NLS-1$ + + /** Container editor */ + ManifestEditor mEditor; + + private UiTreeBlock mTreeBlock; + + public PermissionPage(ManifestEditor editor) { + super(editor, PAGE_ID, "Permissions"); // tab label, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Manifest Permissions"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + ElementDescriptor[] descriptorFilters = null; + if (manifestDescriptor != null) { + descriptorFilters = new ElementDescriptor[] { + manifestDescriptor.getPermissionElement(), + manifestDescriptor.getUsesPermissionElement(), + manifestDescriptor.getPermissionGroupElement(), + manifestDescriptor.getPermissionTreeElement() + }; + } + mTreeBlock = new UiTreeBlock(mEditor, manifest, + true /* autoCreateRoot */, + descriptorFilters, + "Permissions", + "List of permissions defined and used by the manifest"); + mTreeBlock.createContent(managedForm); + } + + /** + * Changes and refreshes the Application UI node handled by the sub parts. + */ + public void refreshUiNode() { + if (mTreeBlock != null) { + UiElementNode manifest = mEditor.getUiRootNode(); + AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors(); + + mTreeBlock.changeRootAndDescriptors(manifest, + new ElementDescriptor[] { + manifestDescriptor.getPermissionElement(), + manifestDescriptor.getUsesPermissionElement(), + manifestDescriptor.getPermissionGroupElement(), + manifestDescriptor.getPermissionTreeElement() + }, + true /* refresh */); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java new file mode 100644 index 0000000..bf76d53 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.menu; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidContentAssist; + +/** + * Content Assist Processor for /res/menu XML files + */ +class MenuContentAssist extends AndroidContentAssist { + + /** + * Constructor for LayoutContentAssist + */ + public MenuContentAssist() { + super(AndroidTargetData.DESCRIPTOR_MENU); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java new file mode 100644 index 0000000..cff1746 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.menu; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidXPathFactory; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +/** + * Multi-page form editor for /res/menu XML files. + */ +public class MenuEditor extends AndroidEditor { + + public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".menu.MenuEditor"; //$NON-NLS-1$ + + /** Root node of the UI element hierarchy */ + private UiElementNode mUiRootNode; + + /** + * Creates the form editor for resources XML files. + */ + public MenuEditor() { + super(); + } + + /** + * Returns the root node of the UI element hierarchy, which here is + * the "menu" node. + */ + @Override + public UiElementNode getUiRootNode() { + return mUiRootNode; + } + + // ---- Base Class Overrides ---- + + /** + * Returns whether the "save as" operation is supported by this editor. + * <p/> + * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + /** + * Create the various form pages. + */ + @Override + protected void createFormPages() { + try { + addPage(new MenuTreePage(this)); + } catch (PartInitException e) { + AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ + } + + } + + /* (non-java doc) + * Change the tab/title name to include the project name. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + IFile file = fileInput.getFile(); + setPartName(String.format("%1$s", file.getName())); + } + } + + /** + * Processes the new XML Model, which XML root node is given. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + @Override + protected void xmlModelChanged(Document xml_doc) { + // init the ui root on demand + initUiRootNode(false /*force*/); + + mUiRootNode.setXmlDocument(xml_doc); + if (xml_doc != null) { + ElementDescriptor root_desc = mUiRootNode.getDescriptor(); + try { + XPath xpath = AndroidXPathFactory.newXPath(); + Node node = (Node) xpath.evaluate("/" + root_desc.getXmlName(), //$NON-NLS-1$ + xml_doc, + XPathConstants.NODE); + if (node == null && root_desc.isMandatory()) { + // Create the root element if it doesn't exist yet (for empty new documents) + node = mUiRootNode.createXmlNode(); + } + + // Refresh the manifest UI node and all its descendants + mUiRootNode.loadFromXmlNode(node); + + // TODO ? startMonitoringMarkers(); + } catch (XPathExpressionException e) { + AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$ + root_desc.getXmlName()); + } + } + + super.xmlModelChanged(xml_doc); + } + + /** + * Creates the initial UI Root Node, including the known mandatory elements. + * @param force if true, a new UiRootNode is recreated even if it already exists. + */ + @Override + protected void initUiRootNode(boolean force) { + // The root UI node is always created, even if there's no corresponding XML node. + if (mUiRootNode == null || force) { + Document doc = null; + if (mUiRootNode != null) { + doc = mUiRootNode.getXmlDocument(); + } + + // get the target data from the opened file (and its project) + AndroidTargetData data = getTargetData(); + + ElementDescriptor desc; + if (data == null) { + desc = new ElementDescriptor("temp", null /*children*/); + } else { + desc = data.getMenuDescriptors().getDescriptor(); + } + + mUiRootNode = desc.createUiNode(); + mUiRootNode.setEditor(this); + + onDescriptorsChanged(doc); + } + } + + // ---- Local Methods ---- + + /** + * Reloads the UI manifest node from the XML, and calls the pages to update. + */ + private void onDescriptorsChanged(Document document) { + if (document != null) { + mUiRootNode.loadFromXmlNode(document); + } else { + mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlNode()); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java new file mode 100644 index 0000000..a5e3b09 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.menu; + + +import com.android.ide.eclipse.editors.AndroidSourceViewerConfig; + +/** + * Source Viewer Configuration that calls in MenuContentAssist. + */ +public class MenuSourceViewerConfig extends AndroidSourceViewerConfig { + + public MenuSourceViewerConfig() { + super(new MenuContentAssist()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java new file mode 100644 index 0000000..edbfa5e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.menu; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Page for the menu form editor. + */ +public final class MenuTreePage extends FormPage { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "layout_tree_page"; //$NON-NLS-1$ + + /** Container editor */ + MenuEditor mEditor; + + public MenuTreePage(MenuEditor editor) { + super(editor, PAGE_ID, "Layout"); // tab's label, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Menu"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode rootNode = mEditor.getUiRootNode(); + UiTreeBlock block = new UiTreeBlock(mEditor, rootNode, + true /* autoCreateRoot */, + null /* no element filters */, + "Menu Elements", + "List of all menu elements in this XML file."); + block.createContent(managedForm); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java new file mode 100644 index 0000000..40a8f16 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.menu.descriptors; + +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.sdklib.SdkConstants; + +import java.util.ArrayList; +import java.util.Map; + + +/** + * Complete description of the menu structure. + */ +public final class MenuDescriptors implements IDescriptorProvider { + + public static final String MENU_ROOT_ELEMENT = "menu"; //$NON-NLS-1$ + + /** The root element descriptor. */ + private ElementDescriptor mDescriptor = null; + + /** @return the root descriptor. */ + public ElementDescriptor getDescriptor() { + return mDescriptor; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mDescriptor.getChildren(); + } + + /** + * Updates the document descriptor. + * <p/> + * It first computes the new children of the descriptor and then updates them + * all at once. + * + * @param styleMap The map style => attributes from the attrs.xml file + */ + public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) { + + // There are 3 elements: menu, item and group. + // The root element MUST be a menu. + // A top menu can contain items or group: + // - top groups can contain top items + // - top items can contain sub-menus + // A sub menu can contains sub items or sub groups: + // - sub groups can contain sub items + // - sub items cannot contain anything + + if (mDescriptor == null) { + mDescriptor = createElement(styleMap, + MENU_ROOT_ELEMENT, // xmlName + "Menu", // uiName, + null, // TODO SDK URL + null, // extraAttribute + null, // childrenElements, + true /* mandatory */); + } + + // -- sub menu can have sub_items, sub_groups but not sub_menus + + ElementDescriptor sub_item = createElement(styleMap, + "item", // xmlName //$NON-NLS-1$ + "Item", // uiName, + null, // TODO SDK URL + null, // extraAttribute + null, // childrenElements, + false /* mandatory */); + + ElementDescriptor sub_group = createElement(styleMap, + "group", // xmlName //$NON-NLS-1$ + "Group", // uiName, + null, // TODO SDK URL + null, // extraAttribute + new ElementDescriptor[] { sub_item }, // childrenElements, + false /* mandatory */); + + ElementDescriptor sub_menu = createElement(styleMap, + MENU_ROOT_ELEMENT, // xmlName //$NON-NLS-1$ + "Sub-Menu", // uiName, + null, // TODO SDK URL + null, // extraAttribute + new ElementDescriptor[] { sub_item, sub_group }, // childrenElements, + true /* mandatory */); + + // -- top menu can have all top groups and top items (which can have sub menus) + + ElementDescriptor top_item = createElement(styleMap, + "item", // xmlName //$NON-NLS-1$ + "Item", // uiName, + null, // TODO SDK URL + null, // extraAttribute + new ElementDescriptor[] { sub_menu }, // childrenElements, + false /* mandatory */); + + ElementDescriptor top_group = createElement(styleMap, + "group", // xmlName //$NON-NLS-1$ + "Group", // uiName, + null, // TODO SDK URL + null, // extraAttribute + new ElementDescriptor[] { top_item }, // childrenElements, + false /* mandatory */); + + XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor("android", //$NON-NLS-1$ + SdkConstants.NS_RESOURCES); + + updateElement(mDescriptor, styleMap, "Menu", xmlns); //$NON-NLS-1$ + mDescriptor.setChildren(new ElementDescriptor[] { top_item, top_group }); + } + + /** + * Returns a new ElementDescriptor constructed from the information given here + * and the javadoc & attributes extracted from the style map if any. + */ + private ElementDescriptor createElement( + Map<String, DeclareStyleableInfo> styleMap, + String xmlName, String uiName, String sdkUrl, + AttributeDescriptor extraAttribute, + ElementDescriptor[] childrenElements, boolean mandatory) { + + ElementDescriptor element = new ElementDescriptor(xmlName, uiName, null, sdkUrl, + null, childrenElements, mandatory); + + return updateElement(element, styleMap, + getStyleName(xmlName), + extraAttribute); + } + + /** + * Updates an ElementDescriptor with the javadoc & attributes extracted from the style + * map if any. + */ + private ElementDescriptor updateElement(ElementDescriptor element, + Map<String, DeclareStyleableInfo> styleMap, + String styleName, + AttributeDescriptor extraAttribute) { + ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>(); + + DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null; + if (style != null) { + DescriptorsUtils.appendAttributes(descs, + null, // elementName + SdkConstants.NS_RESOURCES, + style.getAttributes(), + null, // requiredAttributes + null); // overrides + element.setTooltip(style.getJavaDoc()); + } + + if (extraAttribute != null) { + descs.add(extraAttribute); + } + + element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()])); + return element; + } + + /** + * Returns the style name (i.e. the <declare-styleable> name found in attrs.xml) + * for a given XML element name. + * <p/> + * The rule is that all elements have for style name: + * - their xml name capitalized + * - a "Menu" prefix, except for <menu> itself which is just "Menu". + */ + private String getStyleName(String xmlName) { + String styleName = DescriptorsUtils.capitalize(xmlName); + + // This is NOT the UI Name but the expected internal style name + final String MENU_STYLE_BASE_NAME = "Menu"; //$NON-NLS-1$ + + if (!styleName.equals(MENU_STYLE_BASE_NAME)) { + styleName = MENU_STYLE_BASE_NAME + styleName; + } + return styleName; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java new file mode 100644 index 0000000..c9c8e17 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidContentAssist; + +/** + * Content Assist Processor for /res/values and /res/drawable XML files + */ +class ResourcesContentAssist extends AndroidContentAssist { + + /** + * Constructor for ResourcesContentAssist + */ + public ResourcesContentAssist() { + super(AndroidTargetData.DESCRIPTOR_RESOURCES); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java new file mode 100644 index 0000000..46a9112 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidXPathFactory; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +/** + * Multi-page form editor for /res/values and /res/drawable XML files. + */ +public class ResourcesEditor extends AndroidEditor { + + public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".resources.ResourcesEditor"; //$NON-NLS-1$ + + /** Root node of the UI element hierarchy */ + private UiElementNode mUiResourcesNode; + + + /** + * Creates the form editor for resources XML files. + */ + public ResourcesEditor() { + super(); + } + + /** + * Returns the root node of the UI element hierarchy, which + * here is the "resources" node. + */ + @Override + public UiElementNode getUiRootNode() { + return mUiResourcesNode; + } + + // ---- Base Class Overrides ---- + + /** + * Returns whether the "save as" operation is supported by this editor. + * <p/> + * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + /** + * Create the various form pages. + */ + @Override + protected void createFormPages() { + try { + addPage(new ResourcesTreePage(this)); + } catch (PartInitException e) { + AdtPlugin.log(IStatus.ERROR, "Error creating nested page"); //$NON-NLS-1$ + AdtPlugin.getDefault().getLog().log(e.getStatus()); + } + } + + /* (non-java doc) + * Change the tab/title name to include the project name. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + IFile file = fileInput.getFile(); + setPartName(String.format("%1$s", + file.getName())); + } + } + + /** + * Processes the new XML Model, which XML root node is given. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + @Override + protected void xmlModelChanged(Document xml_doc) { + // init the ui root on demand + initUiRootNode(false /*force*/); + + mUiResourcesNode.setXmlDocument(xml_doc); + if (xml_doc != null) { + ElementDescriptor resources_desc = + ResourcesDescriptors.getInstance().getElementDescriptor(); + try { + XPath xpath = AndroidXPathFactory.newXPath(); + Node node = (Node) xpath.evaluate("/" + resources_desc.getXmlName(), //$NON-NLS-1$ + xml_doc, + XPathConstants.NODE); + assert node != null && node.getNodeName().equals(resources_desc.getXmlName()); + + // Refresh the manifest UI node and all its descendants + mUiResourcesNode.loadFromXmlNode(node); + } catch (XPathExpressionException e) { + AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$ + resources_desc.getXmlName()); + } + } + + super.xmlModelChanged(xml_doc); + } + + /** + * Creates the initial UI Root Node, including the known mandatory elements. + * @param force if true, a new UiRootNode is recreated even if it already exists. + */ + @Override + protected void initUiRootNode(boolean force) { + // The manifest UI node is always created, even if there's no corresponding XML node. + if (mUiResourcesNode == null || force) { + ElementDescriptor resources_desc = + ResourcesDescriptors.getInstance().getElementDescriptor(); + mUiResourcesNode = resources_desc.createUiNode(); + mUiResourcesNode.setEditor(this); + + onDescriptorsChanged(); + } + } + + // ---- Local Methods ---- + + private void onDescriptorsChanged() { + // nothing to be done, as the descriptor are static for now. + // FIXME Update when the descriptors are not static + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java new file mode 100644 index 0000000..1804312 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources; + + +import com.android.ide.eclipse.editors.AndroidSourceViewerConfig; + +/** + * Source Viewer Configuration that calls in ResourcesContentAssist. + */ +public class ResourcesSourceViewerConfig extends AndroidSourceViewerConfig { + + public ResourcesSourceViewerConfig() { + super(new ResourcesContentAssist()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java new file mode 100644 index 0000000..5c1b0e1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolder; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.resources.IFile; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; +import org.eclipse.ui.part.FileEditorInput; + +/** + * Page for instrumentation settings, part of the AndroidManifest form editor. + */ +public final class ResourcesTreePage extends FormPage { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "res_tree_page"; //$NON-NLS-1$ + + /** Container editor */ + ResourcesEditor mEditor; + + public ResourcesTreePage(ResourcesEditor editor) { + super(editor, PAGE_ID, "Resources"); // tab's label, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + + String configText = null; + IEditorInput input = mEditor.getEditorInput(); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput)input; + IFile iFile = fileInput.getFile(); + + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(iFile); + if (resFolder != null) { + configText = resFolder.getConfiguration().toDisplayString(); + } + } + + if (configText != null) { + form.setText(String.format("Android Resources (%1$s)", configText)); + } else { + form.setText("Android Resources"); + } + + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode resources = mEditor.getUiRootNode(); + UiTreeBlock block = new UiTreeBlock(mEditor, resources, + true /* autoCreateRoot */, + null /* no element filters */, + "Resources Elements", + "List of all resources elements in this XML file."); + block.createContent(managedForm); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java new file mode 100644 index 0000000..9a61d17 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Mobile Country Code. + */ +public final class CountryCodeQualifier extends ResourceQualifier { + /** Default pixel density value. This means the property is not set. */ + private final static int DEFAULT_CODE = -1; + + private final static Pattern sCountryCodePattern = Pattern.compile("^mcc(\\d{3})$");//$NON-NLS-1$ + + private int mCode = DEFAULT_CODE; + + public static final String NAME = "Mobile Country Code"; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link CountryCodeQualifier} object or <code>null</code> + */ + public static CountryCodeQualifier getQualifier(String segment) { + Matcher m = sCountryCodePattern.matcher(segment); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return null; + } + + CountryCodeQualifier qualifier = new CountryCodeQualifier(); + qualifier.mCode = code; + return qualifier; + } + + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link CountryCodeQualifier} object. + * @param code the value of the qualifier, as returned by {@link #getCode()}. + */ + public static String getFolderSegment(int code) { + if (code != DEFAULT_CODE && code >= 100 && code <=999) { // code is 3 digit.) { + return String.format("mcc%1$d", code); //$NON-NLS-1$ + } + + return ""; //$NON-NLS-1$ + } + + public int getCode() { + return mCode; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Country Code"; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("mcc"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mCode != DEFAULT_CODE; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + CountryCodeQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setCountryCodeQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof CountryCodeQualifier) { + return mCode == ((CountryCodeQualifier)qualifier).mCode; + } + + return false; + } + + @Override + public int hashCode() { + return mCode; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + return getFolderSegment(mCode); + } + + @Override + public String getStringValue() { + if (mCode != DEFAULT_CODE) { + return String.format("MCC %1$d", mCode); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java new file mode 100644 index 0000000..3c3e11f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; + + +/** + * Represents the configuration for Resource Folders. All the properties have a default + * value which means that the property is not set. + */ +public final class FolderConfiguration implements Comparable<FolderConfiguration> { + public final static String QUALIFIER_SEP = "-"; //$NON-NLS-1$ + + private final ResourceQualifier[] mQualifiers = new ResourceQualifier[INDEX_COUNT]; + + private final static int INDEX_COUNTRY_CODE = 0; + private final static int INDEX_NETWORK_CODE = 1; + private final static int INDEX_LANGUAGE = 2; + private final static int INDEX_REGION = 3; + private final static int INDEX_SCREEN_ORIENTATION = 4; + private final static int INDEX_PIXEL_DENSITY = 5; + private final static int INDEX_TOUCH_TYPE = 6; + private final static int INDEX_KEYBOARD_STATE = 7; + private final static int INDEX_TEXT_INPUT_METHOD = 8; + private final static int INDEX_NAVIGATION_METHOD = 9; + private final static int INDEX_SCREEN_DIMENSION = 10; + private final static int INDEX_COUNT = 11; + + /** + * Sets the config from the qualifiers of a given <var>config</var>. + * @param config + */ + public void set(FolderConfiguration config) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + mQualifiers[i] = config.mQualifiers[i]; + } + } + + /** + * Removes the qualifiers from the receiver if they are present (and valid) + * in the given configuration. + */ + public void substract(FolderConfiguration config) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (config.mQualifiers[i] != null && config.mQualifiers[i].isValid()) { + mQualifiers[i] = null; + } + } + } + + /** + * Returns the first invalid qualifier, or <code>null<code> if they are all valid (or if none + * exists). + */ + public ResourceQualifier getInvalidQualifier() { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null && mQualifiers[i].isValid() == false) { + return mQualifiers[i]; + } + } + + // all allocated qualifiers are valid, we return null. + return null; + } + + /** + * Returns whether the Region qualifier is valid. Region qualifier can only be present if a + * Language qualifier is present as well. + * @return true if the Region qualifier is valid. + */ + public boolean checkRegion() { + if (mQualifiers[INDEX_LANGUAGE] == null && mQualifiers[INDEX_REGION] != null) { + return false; + } + + return true; + } + + /** + * Adds a qualifier to the {@link FolderConfiguration} + * @param qualifier the {@link ResourceQualifier} to add. + */ + public void addQualifier(ResourceQualifier qualifier) { + if (qualifier instanceof CountryCodeQualifier) { + mQualifiers[INDEX_COUNTRY_CODE] = qualifier; + } else if (qualifier instanceof NetworkCodeQualifier) { + mQualifiers[INDEX_NETWORK_CODE] = qualifier; + } else if (qualifier instanceof LanguageQualifier) { + mQualifiers[INDEX_LANGUAGE] = qualifier; + } else if (qualifier instanceof RegionQualifier) { + mQualifiers[INDEX_REGION] = qualifier; + } else if (qualifier instanceof ScreenOrientationQualifier) { + mQualifiers[INDEX_SCREEN_ORIENTATION] = qualifier; + } else if (qualifier instanceof PixelDensityQualifier) { + mQualifiers[INDEX_PIXEL_DENSITY] = qualifier; + } else if (qualifier instanceof TouchScreenQualifier) { + mQualifiers[INDEX_TOUCH_TYPE] = qualifier; + } else if (qualifier instanceof KeyboardStateQualifier) { + mQualifiers[INDEX_KEYBOARD_STATE] = qualifier; + } else if (qualifier instanceof TextInputMethodQualifier) { + mQualifiers[INDEX_TEXT_INPUT_METHOD] = qualifier; + } else if (qualifier instanceof NavigationMethodQualifier) { + mQualifiers[INDEX_NAVIGATION_METHOD] = qualifier; + } else if (qualifier instanceof ScreenDimensionQualifier) { + mQualifiers[INDEX_SCREEN_DIMENSION] = qualifier; + } + } + + /** + * Removes a given qualifier from the {@link FolderConfiguration}. + * @param qualifier the {@link ResourceQualifier} to remove. + */ + public void removeQualifier(ResourceQualifier qualifier) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] == qualifier) { + mQualifiers[i] = null; + return; + } + } + } + + public void setCountryCodeQualifier(CountryCodeQualifier qualifier) { + mQualifiers[INDEX_COUNTRY_CODE] = qualifier; + } + + public CountryCodeQualifier getCountryCodeQualifier() { + return (CountryCodeQualifier)mQualifiers[INDEX_COUNTRY_CODE]; + } + + public void setNetworkCodeQualifier(NetworkCodeQualifier qualifier) { + mQualifiers[INDEX_NETWORK_CODE] = qualifier; + } + + public NetworkCodeQualifier getNetworkCodeQualifier() { + return (NetworkCodeQualifier)mQualifiers[INDEX_NETWORK_CODE]; + } + + public void setLanguageQualifier(LanguageQualifier qualifier) { + mQualifiers[INDEX_LANGUAGE] = qualifier; + } + + public LanguageQualifier getLanguageQualifier() { + return (LanguageQualifier)mQualifiers[INDEX_LANGUAGE]; + } + + public void setRegionQualifier(RegionQualifier qualifier) { + mQualifiers[INDEX_REGION] = qualifier; + } + + public RegionQualifier getRegionQualifier() { + return (RegionQualifier)mQualifiers[INDEX_REGION]; + } + + public void setScreenOrientationQualifier(ScreenOrientationQualifier qualifier) { + mQualifiers[INDEX_SCREEN_ORIENTATION] = qualifier; + } + + public ScreenOrientationQualifier getScreenOrientationQualifier() { + return (ScreenOrientationQualifier)mQualifiers[INDEX_SCREEN_ORIENTATION]; + } + + public void setPixelDensityQualifier(PixelDensityQualifier qualifier) { + mQualifiers[INDEX_PIXEL_DENSITY] = qualifier; + } + + public PixelDensityQualifier getPixelDensityQualifier() { + return (PixelDensityQualifier)mQualifiers[INDEX_PIXEL_DENSITY]; + } + + public void setTouchTypeQualifier(TouchScreenQualifier qualifier) { + mQualifiers[INDEX_TOUCH_TYPE] = qualifier; + } + + public TouchScreenQualifier getTouchTypeQualifier() { + return (TouchScreenQualifier)mQualifiers[INDEX_TOUCH_TYPE]; + } + + public void setKeyboardStateQualifier(KeyboardStateQualifier qualifier) { + mQualifiers[INDEX_KEYBOARD_STATE] = qualifier; + } + + public KeyboardStateQualifier getKeyboardStateQualifier() { + return (KeyboardStateQualifier)mQualifiers[INDEX_KEYBOARD_STATE]; + } + + public void setTextInputMethodQualifier(TextInputMethodQualifier qualifier) { + mQualifiers[INDEX_TEXT_INPUT_METHOD] = qualifier; + } + + public TextInputMethodQualifier getTextInputMethodQualifier() { + return (TextInputMethodQualifier)mQualifiers[INDEX_TEXT_INPUT_METHOD]; + } + + public void setNavigationMethodQualifier(NavigationMethodQualifier qualifier) { + mQualifiers[INDEX_NAVIGATION_METHOD] = qualifier; + } + + public NavigationMethodQualifier getNavigationMethodQualifier() { + return (NavigationMethodQualifier)mQualifiers[INDEX_NAVIGATION_METHOD]; + } + + public void setScreenDimensionQualifier(ScreenDimensionQualifier qualifier) { + mQualifiers[INDEX_SCREEN_DIMENSION] = qualifier; + } + + public ScreenDimensionQualifier getScreenDimensionQualifier() { + return (ScreenDimensionQualifier)mQualifiers[INDEX_SCREEN_DIMENSION]; + } + + /** + * Returns whether an object is equals to the receiver. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof FolderConfiguration) { + FolderConfiguration fc = (FolderConfiguration)obj; + for (int i = 0 ; i < INDEX_COUNT ; i++) { + ResourceQualifier qualifier = mQualifiers[i]; + ResourceQualifier fcQualifier = fc.mQualifiers[i]; + if (qualifier != null) { + if (qualifier.equals(fcQualifier) == false) { + return false; + } + } else if (fcQualifier != null) { + return false; + } + } + + return true; + } + + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Returns whether the Configuration has only default values. + */ + public boolean isDefault() { + for (ResourceQualifier irq : mQualifiers) { + if (irq != null) { + return false; + } + } + + return true; + } + + /** + * Returns the name of a folder with the configuration. + */ + public String getFolderName(ResourceFolderType folder) { + StringBuilder result = new StringBuilder(folder.getName()); + + for (ResourceQualifier qualifier : mQualifiers) { + if (qualifier != null) { + result.append(QUALIFIER_SEP); + result.append(qualifier.toString()); + } + } + + return result.toString(); + } + + /** + * Returns a string valid for usage in a folder name, or <code>null</code> if the configuration + * is default. + */ + @Override + public String toString() { + StringBuilder result = null; + + for (ResourceQualifier irq : mQualifiers) { + if (irq != null) { + if (result == null) { + result = new StringBuilder(); + } else { + result.append(QUALIFIER_SEP); + } + result.append(irq.toString()); + } + } + + if (result != null) { + return result.toString(); + } else { + return null; + } + } + + /** + * Returns a string valid for display purpose. + */ + public String toDisplayString() { + if (isDefault()) { + return "default"; + } + + StringBuilder result = null; + int index = 0; + ResourceQualifier qualifier = null; + + // pre- language/region qualifiers + while (index < INDEX_LANGUAGE) { + qualifier = mQualifiers[index++]; + if (qualifier != null) { + if (result == null) { + result = new StringBuilder(); + } else { + result.append(", "); //$NON-NLS-1$ + } + result.append(qualifier.getStringValue()); + + } + } + + // process the language/region qualifier in a custom way, if there are both non null. + if (mQualifiers[INDEX_LANGUAGE] != null && mQualifiers[INDEX_REGION] != null) { + String language = mQualifiers[INDEX_LANGUAGE].getStringValue(); + String region = mQualifiers[INDEX_REGION].getStringValue(); + + if (result == null) { + result = new StringBuilder(); + } else { + result.append(", "); //$NON-NLS-1$ + } + result.append(String.format("%s_%s", language, region)); //$NON-NLS-1$ + + index += 2; + } + + // post language/region qualifiers. + while (index < INDEX_COUNT) { + qualifier = mQualifiers[index++]; + if (qualifier != null) { + if (result == null) { + result = new StringBuilder(); + } else { + result.append(", "); //$NON-NLS-1$ + } + result.append(qualifier.getStringValue()); + + } + } + + return result.toString(); + } + + public int compareTo(FolderConfiguration folderConfig) { + // default are always at the top. + if (isDefault()) { + if (folderConfig.isDefault()) { + return 0; + } + return -1; + } + + // now we compare the qualifiers + for (int i = 0 ; i < INDEX_COUNT; i++) { + ResourceQualifier qualifier1 = mQualifiers[i]; + ResourceQualifier qualifier2 = folderConfig.mQualifiers[i]; + + if (qualifier1 == null) { + if (qualifier2 == null) { + continue; + } else { + return -1; + } + } else { + if (qualifier2 == null) { + return 1; + } else { + int result = qualifier1.compareTo(qualifier2); + + if (result == 0) { + continue; + } + + return result; + } + } + } + + // if we arrive here, all the qualifier matches + return 0; + } + + /** + * Returns whether the configuration match the given reference config. + * <p/>A match means that: + * <ul> + * <li>This config does not use any qualifier not used by the reference config</li> + * <li>The qualifier used by this config have the same values as the qualifiers of + * the reference config.</li> + * </ul> + * @param referenceConfig The reference configuration to test against. + * @return the number of matching qualifiers or -1 if the configurations are not compatible. + */ + public int match(FolderConfiguration referenceConfig) { + int matchCount = 0; + + for (int i = 0 ; i < INDEX_COUNT ; i++) { + ResourceQualifier testQualifier = mQualifiers[i]; + ResourceQualifier referenceQualifier = referenceConfig.mQualifiers[i]; + + // we only care if testQualifier is non null. If it's null, it's a match but + // without increasing the matchCount. + if (testQualifier != null) { + if (referenceQualifier == null) { + return -1; + } else if (testQualifier.equals(referenceQualifier) == false) { + return -1; + } + + // the qualifier match, increment the count + matchCount++; + } + } + return matchCount; + } + + /** + * Returns the index of the first non null {@link ResourceQualifier} starting at index + * <var>startIndex</var> + * @param startIndex + * @return -1 if no qualifier was found. + */ + public int getHighestPriorityQualifier(int startIndex) { + for (int i = startIndex ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null) { + return i; + } + } + + return -1; + } + + /** + * Create default qualifiers. + */ + public void createDefault() { + mQualifiers[INDEX_COUNTRY_CODE] = new CountryCodeQualifier(); + mQualifiers[INDEX_NETWORK_CODE] = new NetworkCodeQualifier(); + mQualifiers[INDEX_LANGUAGE] = new LanguageQualifier(); + mQualifiers[INDEX_REGION] = new RegionQualifier(); + mQualifiers[INDEX_SCREEN_ORIENTATION] = new ScreenOrientationQualifier(); + mQualifiers[INDEX_PIXEL_DENSITY] = new PixelDensityQualifier(); + mQualifiers[INDEX_TOUCH_TYPE] = new TouchScreenQualifier(); + mQualifiers[INDEX_KEYBOARD_STATE] = new KeyboardStateQualifier(); + mQualifiers[INDEX_TEXT_INPUT_METHOD] = new TextInputMethodQualifier(); + mQualifiers[INDEX_NAVIGATION_METHOD] = new NavigationMethodQualifier(); + mQualifiers[INDEX_SCREEN_DIMENSION] = new ScreenDimensionQualifier(); + } + + /** + * Returns an array of all the non null qualifiers. + */ + public ResourceQualifier[] getQualifiers() { + int count = 0; + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null) { + count++; + } + } + + ResourceQualifier[] array = new ResourceQualifier[count]; + int index = 0; + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null) { + array[index++] = mQualifiers[i]; + } + } + + return array; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java new file mode 100644 index 0000000..ad232ed --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + + + +/** + * Resource Qualifier for keyboard state. + */ +public final class KeyboardStateQualifier extends ResourceQualifier { + + public static final String NAME = "Keyboard State"; + + private KeyboardState mValue = null; + + /** + * Screen Orientation enum. + */ + public static enum KeyboardState { + EXPOSED("keysexposed", "Exposed"), //$NON-NLS-1$ + HIDDEN("keyshidden", "Hidden"); //$NON-NLS-1$ + + private String mValue; + private String mDisplayValue; + + private KeyboardState(String value, String displayValue) { + mValue = value; + mDisplayValue = displayValue; + } + + /** + * Returns the enum for matching the provided qualifier value. + * @param value The qualifier value. + * @return the enum for the qualifier value or null if no matching was found. + */ + static KeyboardState getEnum(String value) { + for (KeyboardState orient : values()) { + if (orient.mValue.equals(value)) { + return orient; + } + } + + return null; + } + + public String getValue() { + return mValue; + } + + public String getDisplayValue() { + return mDisplayValue; + } + + public static int getIndex(KeyboardState value) { + int i = 0; + for (KeyboardState input : values()) { + if (value == input) { + return i; + } + + i++; + } + + return -1; + } + + public static KeyboardState getByIndex(int index) { + int i = 0; + for (KeyboardState value : values()) { + if (i == index) { + return value; + } + i++; + } + return null; + } + } + + public KeyboardStateQualifier() { + // pass + } + + public KeyboardStateQualifier(KeyboardState value) { + mValue = value; + } + + public KeyboardState getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Keyboard"; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("keyboard"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + KeyboardState orientation = KeyboardState.getEnum(value); + if (orientation != null) { + KeyboardStateQualifier qualifier = new KeyboardStateQualifier(); + qualifier.mValue = orientation; + config.setKeyboardStateQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof KeyboardStateQualifier) { + return mValue == ((KeyboardStateQualifier)qualifier).mValue; + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + if (mValue != null) { + return mValue.getValue(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue.getDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java new file mode 100644 index 0000000..99c3a43 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Language. + */ +public final class LanguageQualifier extends ResourceQualifier { + private final static Pattern sLanguagePattern = Pattern.compile("^[a-z]{2}$"); //$NON-NLS-1$ + + public static final String NAME = "Language"; + + private String mValue; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link LanguageQualifier} object or <code>null</code> + */ + public static LanguageQualifier getQualifier(String segment) { + if (sLanguagePattern.matcher(segment).matches()) { + LanguageQualifier qualifier = new LanguageQualifier(); + qualifier.mValue = segment; + + return qualifier; + } + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link LanguageQualifier} object. + * @param value the value of the qualifier, as returned by {@link #getValue()}. + */ + public static String getFolderSegment(String value) { + String segment = value.toLowerCase(); + if (sLanguagePattern.matcher(segment).matches()) { + return segment; + } + + return null; + } + + public String getValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("language"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + LanguageQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setLanguageQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof LanguageQualifier) { + if (mValue == null) { + return ((LanguageQualifier)qualifier).mValue == null; + } + return mValue.equals(((LanguageQualifier)qualifier).mValue); + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + if (mValue != null) { + return getFolderSegment(mValue); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java new file mode 100644 index 0000000..1a2cf53 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + + + +/** + * Resource Qualifier for Navigation Method. + */ +public final class NavigationMethodQualifier extends ResourceQualifier { + + public static final String NAME = "Navigation Method"; + + private NavigationMethod mValue; + + /** + * Navigation Method enum. + */ + public static enum NavigationMethod { + DPAD("dpad", "D-pad"), //$NON-NLS-1$ + TRACKBALL("trackball", "Trackball"), //$NON-NLS-1$ + WHEEL("wheel", "Wheel"), //$NON-NLS-1$ + NONAV("nonav", "No Navigation"); //$NON-NLS-1$ + + private String mValue; + private String mDisplay; + + private NavigationMethod(String value, String display) { + mValue = value; + mDisplay = display; + } + + /** + * Returns the enum for matching the provided qualifier value. + * @param value The qualifier value. + * @return the enum for the qualifier value or null if no matching was found. + */ + static NavigationMethod getEnum(String value) { + for (NavigationMethod orient : values()) { + if (orient.mValue.equals(value)) { + return orient; + } + } + + return null; + } + + public String getValue() { + return mValue; + } + + public String getDisplayValue() { + return mDisplay; + } + + public static int getIndex(NavigationMethod value) { + int i = 0; + for (NavigationMethod nav : values()) { + if (nav == value) { + return i; + } + + i++; + } + + return -1; + } + + public static NavigationMethod getByIndex(int index) { + int i = 0; + for (NavigationMethod value : values()) { + if (i == index) { + return value; + } + i++; + } + return null; + } + } + + public NavigationMethodQualifier() { + // pass + } + + public NavigationMethodQualifier(NavigationMethod value) { + mValue = value; + } + + public NavigationMethod getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Navigation"; + } + + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("navpad"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + NavigationMethod method = NavigationMethod.getEnum(value); + if (method != null) { + NavigationMethodQualifier qualifier = new NavigationMethodQualifier(); + qualifier.mValue = method; + config.setNavigationMethodQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof NavigationMethodQualifier) { + return mValue == ((NavigationMethodQualifier)qualifier).mValue; + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + if (mValue != null) { + return mValue.getValue(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue.getDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java new file mode 100644 index 0000000..7e30901 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Mobile Network Code Pixel Density. + */ +public final class NetworkCodeQualifier extends ResourceQualifier { + /** Default pixel density value. This means the property is not set. */ + private final static int DEFAULT_CODE = -1; + + private final static Pattern sNetworkCodePattern = Pattern.compile("^mnc(\\d{1,3})$"); //$NON-NLS-1$ + + private int mCode = DEFAULT_CODE; + + public final static String NAME = "Mobile Network Code"; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link CountryCodeQualifier} object or <code>null</code> + */ + public static NetworkCodeQualifier getQualifier(String segment) { + Matcher m = sNetworkCodePattern.matcher(segment); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return null; + } + + NetworkCodeQualifier qualifier = new NetworkCodeQualifier(); + qualifier.mCode = code; + return qualifier; + } + + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link NetworkCodeQualifier} object. + * @param code the value of the qualifier, as returned by {@link #getCode()}. + */ + public static String getFolderSegment(int code) { + if (code != DEFAULT_CODE && code >= 1 && code <= 999) { // code is 1-3 digit. + return String.format("mnc%1$d", code); //$NON-NLS-1$ + } + + return ""; //$NON-NLS-1$ + } + + public int getCode() { + return mCode; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Network Code"; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("mnc"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mCode != DEFAULT_CODE; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sNetworkCodePattern.matcher(value); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return false; + } + + NetworkCodeQualifier qualifier = new NetworkCodeQualifier(); + qualifier.mCode = code; + config.setNetworkCodeQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof NetworkCodeQualifier) { + return mCode == ((NetworkCodeQualifier)qualifier).mCode; + } + + return false; + } + + @Override + public int hashCode() { + return mCode; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + return getFolderSegment(mCode); + } + + @Override + public String getStringValue() { + if (mCode != DEFAULT_CODE) { + return String.format("MNC %1$d", mCode); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java new file mode 100644 index 0000000..c47bb83 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Pixel Density. + */ +public final class PixelDensityQualifier extends ResourceQualifier { + /** Default pixel density value. This means the property is not set. */ + private final static int DEFAULT_DENSITY = -1; + + private final static Pattern sPixelDensityPattern = Pattern.compile("^(\\d+)dpi$");//$NON-NLS-1$ + + public static final String NAME = "Pixel Density"; + + private int mValue = DEFAULT_DENSITY; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param folderSegment the folder segment from which to create a qualifier. + * @return a new {@link CountryCodeQualifier} object or <code>null</code> + */ + public static PixelDensityQualifier getQualifier(String folderSegment) { + Matcher m = sPixelDensityPattern.matcher(folderSegment); + if (m.matches()) { + String v = m.group(1); + + int density = -1; + try { + density = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return null; + } + + PixelDensityQualifier qualifier = new PixelDensityQualifier(); + qualifier.mValue = density; + + return qualifier; + } + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link NetworkCodeQualifier} object. + * @param value the value of the qualifier, as returned by {@link #getValue()}. + */ + public static String getFolderSegment(int value) { + if (value != DEFAULT_DENSITY) { + return String.format("%1$ddpi", value); //$NON-NLS-1$ + } + + return ""; //$NON-NLS-1$ + } + + public int getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("dpi"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != DEFAULT_DENSITY; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + PixelDensityQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setPixelDensityQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof PixelDensityQualifier) { + return mValue == ((PixelDensityQualifier)qualifier).mValue; + } + + return false; + } + + @Override + public int hashCode() { + return mValue; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + return getFolderSegment(mValue); + } + + @Override + public String getStringValue() { + if (mValue != DEFAULT_DENSITY) { + return String.format("%1$d dpi", mValue); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java new file mode 100644 index 0000000..dc4d5fa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Region. + */ +public final class RegionQualifier extends ResourceQualifier { + private final static Pattern sRegionPattern = Pattern.compile("^r([A-Z]{2})$"); //$NON-NLS-1$ + + public static final String NAME = "Region"; + + private String mValue; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link RegionQualifier} object or <code>null</code> + */ + public static RegionQualifier getQualifier(String segment) { + Matcher m = sRegionPattern.matcher(segment); + if (m.matches()) { + RegionQualifier qualifier = new RegionQualifier(); + qualifier.mValue = m.group(1); + + return qualifier; + } + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link RegionQualifier} object. + * @param value the value of the qualifier, as returned by {@link #getValue()}. + */ + public static String getFolderSegment(String value) { + if (value != null) { + String segment = "r" + value.toUpperCase(); //$NON-NLS-1$ + if (sRegionPattern.matcher(segment).matches()) { + return segment; + } + } + + return ""; //$NON-NLS-1$ + } + + public String getValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("region"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + RegionQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setRegionQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof RegionQualifier) { + if (mValue == null) { + return ((RegionQualifier)qualifier).mValue == null; + } + return mValue.equals(((RegionQualifier)qualifier).mValue); + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + return getFolderSegment(mValue); + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java new file mode 100644 index 0000000..0257afa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import org.eclipse.swt.graphics.Image; + +/** + * Base class for resource qualifiers. + * <p/>The resource qualifier classes are designed as immutable. + */ +public abstract class ResourceQualifier implements Comparable<ResourceQualifier> { + + /** + * Returns the human readable name of the qualifier. + */ + public abstract String getName(); + + /** + * Returns a shorter human readable name for the qualifier. + * @see #getName() + */ + public abstract String getShortName(); + + /** + * Returns the icon for the qualifier. + */ + public abstract Image getIcon(); + + /** + * Returns whether the qualifier has a valid filter value. + */ + public abstract boolean isValid(); + + /** + * Check if the value is valid for this qualifier, and if so sets the value + * into a Folder Configuration. + * @param value The value to check and set. Must not be null. + * @param config The folder configuration to receive the value. Must not be null. + * @return true if the value was valid and was set. + */ + public abstract boolean checkAndSet(String value, FolderConfiguration config); + + /** + * Returns a string formated to be used in a folder name. + * <p/>This is declared as abstract to force children classes to implement it. + */ + @Override + public abstract String toString(); + + /** + * Returns a string formatted for display purpose. + */ + public abstract String getStringValue(); + + /** + * Returns <code>true</code> if both objects are equal. + * <p/>This is declared as abstract to force children classes to implement it. + */ + @Override + public abstract boolean equals(Object object); + + /** + * Returns a hash code value for the object. + * <p/>This is declared as abstract to force children classes to implement it. + */ + @Override + public abstract int hashCode(); + + public final int compareTo(ResourceQualifier o) { + return toString().compareTo(o.toString()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java new file mode 100644 index 0000000..a2cc789 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Dimension. + */ +public final class ScreenDimensionQualifier extends ResourceQualifier { + /** Default screen size value. This means the property is not set */ + final static int DEFAULT_SIZE = -1; + + private final static Pattern sDimensionPattern = Pattern.compile( + "^(\\d+)x(\\d+)$"); //$NON-NLS-1$ + + public static final String NAME = "Screen Dimension"; + + /** Screen size 1 value. This is not size X or Y because the folder name always + * contains the biggest size first. So if the qualifier is 400x200, size 1 will always be + * 400 but that'll be X in landscape and Y in portrait. + * Default value is <code>DEFAULT_SIZE</code> */ + private int mValue1 = DEFAULT_SIZE; + + /** Screen size 2 value. This is not size X or Y because the folder name always + * contains the biggest size first. So if the qualifier is 400x200, size 2 will always be + * 200 but that'll be Y in landscape and X in portrait. + * Default value is <code>DEFAULT_SIZE</code> */ + private int mValue2 = DEFAULT_SIZE; + + public int getValue1() { + return mValue1; + } + + public int getValue2() { + return mValue2; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Dimension"; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("dimension"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue1 != DEFAULT_SIZE && mValue2 != DEFAULT_SIZE; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sDimensionPattern.matcher(value); + if (m.matches()) { + String d1 = m.group(1); + String d2 = m.group(2); + + ScreenDimensionQualifier qualifier = getQualifier(d1, d2); + if (qualifier != null) { + config.setScreenDimensionQualifier(qualifier); + return true; + } + } + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof ScreenDimensionQualifier) { + ScreenDimensionQualifier q = (ScreenDimensionQualifier)qualifier; + return (mValue1 == q.mValue1 && mValue2 == q.mValue2); + } + + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + public static ScreenDimensionQualifier getQualifier(String size1, String size2) { + try { + int s1 = Integer.parseInt(size1); + int s2 = Integer.parseInt(size2); + + ScreenDimensionQualifier qualifier = new ScreenDimensionQualifier(); + + if (s1 > s2) { + qualifier.mValue1 = s1; + qualifier.mValue2 = s2; + } else { + qualifier.mValue1 = s2; + qualifier.mValue2 = s1; + } + + return qualifier; + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + } + + return null; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + return String.format("%1$dx%2$d", mValue1, mValue2); //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue1 != -1 && mValue2 != -1) { + return String.format("%1$dx%2$d", mValue1, mValue2); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java new file mode 100644 index 0000000..e30930f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + +/** + * Resource Qualifier for Screen Orientation. + */ +public final class ScreenOrientationQualifier extends ResourceQualifier { + + public static final String NAME = "Screen Orientation"; + + private ScreenOrientation mValue = null; + + /** + * Screen Orientation enum. + */ + public static enum ScreenOrientation { + PORTRAIT("port", "Portrait"), //$NON-NLS-1$ + LANDSCAPE("land", "Landscape"), //$NON-NLS-1$ + SQUARE("square", "Square"); //$NON-NLS-1$ + + private String mValue; + private String mDisplayValue; + + private ScreenOrientation(String value, String displayValue) { + mValue = value; + mDisplayValue = displayValue; + } + + /** + * Returns the enum for matching the provided qualifier value. + * @param value The qualifier value. + * @return the enum for the qualifier value or null if no matching was found. + */ + static ScreenOrientation getEnum(String value) { + for (ScreenOrientation orient : values()) { + if (orient.mValue.equals(value)) { + return orient; + } + } + + return null; + } + + public String getValue() { + return mValue; + } + + public String getDisplayValue() { + return mDisplayValue; + } + + public static int getIndex(ScreenOrientation orientation) { + int i = 0; + for (ScreenOrientation orient : values()) { + if (orient == orientation) { + return i; + } + + i++; + } + + return -1; + } + + public static ScreenOrientation getByIndex(int index) { + int i = 0; + for (ScreenOrientation orient : values()) { + if (i == index) { + return orient; + } + i++; + } + + return null; + } + } + + public ScreenOrientationQualifier() { + } + + public ScreenOrientationQualifier(ScreenOrientation value) { + mValue = value; + } + + public ScreenOrientation getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Orientation"; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("orientation"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + ScreenOrientation orientation = ScreenOrientation.getEnum(value); + if (orientation != null) { + ScreenOrientationQualifier qualifier = new ScreenOrientationQualifier(orientation); + config.setScreenOrientationQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof ScreenOrientationQualifier) { + return mValue == ((ScreenOrientationQualifier)qualifier).mValue; + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + if (mValue != null) { + return mValue.getValue(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue.getDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java new file mode 100644 index 0000000..de40138 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + + + + +/** + * Resource Qualifier for Text Input Method. + */ +public final class TextInputMethodQualifier extends ResourceQualifier { + + public static final String NAME = "Text Input Method"; + + private TextInputMethod mValue; + + /** + * Screen Orientation enum. + */ + public static enum TextInputMethod { + NOKEY("nokeys", "No Keys"), //$NON-NLS-1$ + QWERTY("qwerty", "Qwerty"), //$NON-NLS-1$ + TWELVEKEYS("12key", "12 Key"); //$NON-NLS-1$ + + private String mValue; + private String mDisplayValue; + + private TextInputMethod(String value, String displayValue) { + mValue = value; + mDisplayValue = displayValue; + } + + /** + * Returns the enum for matching the provided qualifier value. + * @param value The qualifier value. + * @return the enum for the qualifier value or null if no matching was found. + */ + static TextInputMethod getEnum(String value) { + for (TextInputMethod orient : values()) { + if (orient.mValue.equals(value)) { + return orient; + } + } + + return null; + } + + public String getValue() { + return mValue; + } + + public String getDisplayValue() { + return mDisplayValue; + } + + public static int getIndex(TextInputMethod value) { + int i = 0; + for (TextInputMethod input : values()) { + if (value == input) { + return i; + } + + i++; + } + + return -1; + } + + public static TextInputMethod getByIndex(int index) { + int i = 0; + for (TextInputMethod value : values()) { + if (i == index) { + return value; + } + i++; + } + return null; + } + } + + public TextInputMethodQualifier() { + // pass + } + + public TextInputMethodQualifier(TextInputMethod value) { + mValue = value; + } + + public TextInputMethod getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Text Input"; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("text_input"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + TextInputMethod method = TextInputMethod.getEnum(value); + if (method != null) { + TextInputMethodQualifier qualifier = new TextInputMethodQualifier(); + qualifier.mValue = method; + config.setTextInputMethodQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof TextInputMethodQualifier) { + return mValue == ((TextInputMethodQualifier)qualifier).mValue; + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + if (mValue != null) { + return mValue.getValue(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue.getDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java new file mode 100644 index 0000000..2390e2c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import com.android.ide.eclipse.editors.IconFactory; + +import org.eclipse.swt.graphics.Image; + + +/** + * Resource Qualifier for Touch Screen type. + */ +public final class TouchScreenQualifier extends ResourceQualifier { + + public static final String NAME = "Touch Screen"; + + private TouchScreenType mValue; + + /** + * Screen Orientation enum. + */ + public static enum TouchScreenType { + NOTOUCH("notouch", "No Touch"), //$NON-NLS-1$ + STYLUS("stylus", "Stylus"), //$NON-NLS-1$ + FINGER("finger", "Finger"); //$NON-NLS-1$ + + private String mValue; + private String mDisplayValue; + + private TouchScreenType(String value, String displayValue) { + mValue = value; + mDisplayValue = displayValue; + } + + /** + * Returns the enum for matching the provided qualifier value. + * @param value The qualifier value. + * @return the enum for the qualifier value or null if no matching was found. + */ + static TouchScreenType getEnum(String value) { + for (TouchScreenType orient : values()) { + if (orient.mValue.equals(value)) { + return orient; + } + } + + return null; + } + + public String getValue() { + return mValue; + } + + public String getDisplayValue() { + return mDisplayValue; + } + + public static int getIndex(TouchScreenType touch) { + int i = 0; + for (TouchScreenType t : values()) { + if (t == touch) { + return i; + } + + i++; + } + + return -1; + } + + public static TouchScreenType getByIndex(int index) { + int i = 0; + for (TouchScreenType value : values()) { + if (i == index) { + return value; + } + i++; + } + + return null; + } + } + + public TouchScreenQualifier() { + // pass + } + + public TouchScreenQualifier(TouchScreenType touchValue) { + mValue = touchValue; + } + + public TouchScreenType getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public Image getIcon() { + return IconFactory.getInstance().getIcon("touch"); //$NON-NLS-1$ + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + TouchScreenType type = TouchScreenType.getEnum(value); + if (type != null) { + TouchScreenQualifier qualifier = new TouchScreenQualifier(); + qualifier.mValue = type; + config.setTouchTypeQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof TouchScreenQualifier) { + return mValue == ((TouchScreenQualifier)qualifier).mValue; + } + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String toString() { + if (mValue != null) { + return mValue.getValue(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getStringValue() { + if (mValue != null) { + return mValue.getDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java new file mode 100644 index 0000000..92288ba --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.descriptors; + +import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor; +import com.android.ide.eclipse.editors.resources.uimodel.UiColorValueNode; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode; + +/** + * Describes a Color XML element value displayed by an {@link UiColorValueNode}. + */ +public final class ColorValueDescriptor extends TextValueDescriptor { + + public ColorValueDescriptor(String uiName, String tooltip) { + super(uiName, tooltip); + } + + /** + * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor. + */ + @Override + public UiAttributeNode createUiNode(UiElementNode uiParent) { + return new UiColorValueNode(this, uiParent); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java new file mode 100644 index 0000000..bf83d52 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.descriptors; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.resources.uimodel.UiItemElementNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +/** + * {@link ItemElementDescriptor} is a special version of {@link ElementDescriptor} that + * uses a specialized {@link UiItemElementNode} for display. + */ +public class ItemElementDescriptor extends ElementDescriptor { + + /** + * Constructs a new {@link ItemElementDescriptor} based on its XML name, UI name, + * tooltip, SDK url, attributes list, children list and mandatory. + * + * @param xml_name The XML element node name. Case sensitive. + * @param ui_name The XML element name for the user interface, typically capitalized. + * @param tooltip An optional tooltip. Can be null or empty. + * @param sdk_url An optional SKD URL. Can be null or empty. + * @param attributes The list of allowed attributes. Can be null or empty. + * @param children The list of allowed children. Can be null or empty. + * @param mandatory Whether this node must always exist (even for empty models). A mandatory + * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory + * UI node MUST have an XML node attached and it will cease to exist when the XML node + * ceases to exist. + */ + public ItemElementDescriptor(String xml_name, String ui_name, + String tooltip, String sdk_url, AttributeDescriptor[] attributes, + ElementDescriptor[] children, boolean mandatory) { + super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory); + } + + @Override + public UiElementNode createUiNode() { + return new UiItemElementNode(this); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java new file mode 100644 index 0000000..1075897 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.descriptors; + +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.FlagAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.descriptors.ListAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor; + + +/** + * Complete description of the structure for resources XML files (under res/values/) + */ +public class ResourcesDescriptors implements IDescriptorProvider { + + // Public attributes names, attributes descriptors and elements descriptors + + public static final String ROOT_ELEMENT = "resources"; //$NON-NLS-1$ + + public static final String NAME_ATTR = "name"; //$NON-NLS-1$ + public static final String TYPE_ATTR = "type"; //$NON-NLS-1$ + + private static final ResourcesDescriptors sThis = new ResourcesDescriptors(); + + /** The {@link ElementDescriptor} for the root Resources element. */ + public final ElementDescriptor mResourcesElement; + + public static ResourcesDescriptors getInstance() { + return sThis; + } + + /* + * @see com.android.ide.eclipse.editors.descriptors.IDescriptorProvider#getRootElementDescriptors() + */ + public ElementDescriptor[] getRootElementDescriptors() { + return new ElementDescriptor[] { mResourcesElement }; + } + + public ElementDescriptor getDescriptor() { + return mResourcesElement; + } + + public ElementDescriptor getElementDescriptor() { + return mResourcesElement; + } + + private ResourcesDescriptors() { + + // Common attributes used in many placed + + // Elements + + ElementDescriptor color_element = new ElementDescriptor( + "color", //$NON-NLS-1$ + "Color", + "A @color@ value specifies an RGB value with an alpha channel, which can be used in various places such as specifying a solid color for a Drawable or the color to use for text. It always begins with a # character and then is followed by the alpha-red-green-blue information in one of the following formats: #RGB, #ARGB, #RRGGBB or #AARRGGBB.", + "http://code.google.com/android/reference/available-resources.html#colorvals", //$NON-NLS-1$ + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this color."), + new ColorValueDescriptor( + "Value*", + "A mandatory color value.") + }, + null, // no child nodes + false /* not mandatory */); + + ElementDescriptor string_element = new ElementDescriptor( + "string", //$NON-NLS-1$ + "String", + "@Strings@, with optional simple formatting, can be stored and retrieved as resources. You can add formatting to your string by using three standard HTML tags: b, i, and u. If you use an apostrophe or a quote in your string, you must either escape it or enclose the whole string in the other kind of enclosing quotes.", + "http://code.google.com/android/reference/available-resources.html#stringresources", //$NON-NLS-1$ + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this string."), + new TextValueDescriptor( + "Value*", + "A mandatory string value.") + }, + null, // no child nodes + false /* not mandatory */); + + ElementDescriptor item_element = new ItemElementDescriptor( + "item", //$NON-NLS-1$ + "Item", + null, // TODO find javadoc + null, // TODO find link to javadoc + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this resource."), + new ListAttributeDescriptor(TYPE_ATTR, + "Type*", + null /* nsUri */, + "The mandatory type of this resource.", + ResourceType.getNames() + ), + new FlagAttributeDescriptor("format", + "Format", + null /* nsUri */, + "The optional format of this resource.", + new String[] { + "boolean", //$NON-NLS-1$ + "color", //$NON-NLS-1$ + "dimension", //$NON-NLS-1$ + "float", //$NON-NLS-1$ + "fraction", //$NON-NLS-1$ + "integer", //$NON-NLS-1$ + "reference", //$NON-NLS-1$ + "string" //$NON-NLS-1$ + }), + new TextValueDescriptor( + "Value", + "A standard string, hex color value, or reference to any other resource type.") + }, + null, // no child nodes + false /* not mandatory */); + + ElementDescriptor drawable_element = new ElementDescriptor( + "drawable", //$NON-NLS-1$ + "Drawable", + "A @drawable@ defines a rectangle of color. Android accepts color values written in various web-style formats -- a hexadecimal constant in any of the following forms: #RGB, #ARGB, #RRGGBB, #AARRGGBB. Zero in the alpha channel means transparent. The default value is opaque.", + "http://code.google.com/android/reference/available-resources.html#colordrawableresources", //$NON-NLS-1$ + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this drawable."), + new TextValueDescriptor( + "Value*", + "A mandatory color value in the form #RGB, #ARGB, #RRGGBB or #AARRGGBB.") + }, + null, // no child nodes + false /* not mandatory */); + + ElementDescriptor dimen_element = new ElementDescriptor( + "dimen", //$NON-NLS-1$ + "Dimension", + "You can create common dimensions to use for various screen elements by defining @dimension@ values in XML. A dimension resource is a number followed by a unit of measurement. Supported units are px (pixels), in (inches), mm (millimeters), pt (points at 72 DPI), dp (density-independent pixels) and sp (scale-independent pixels)", + "http://code.google.com/android/reference/available-resources.html#dimension", //$NON-NLS-1$ + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this dimension."), + new TextValueDescriptor( + "Value*", + "A mandatory dimension value is a number followed by a unit of measurement. For example: 10px, 2in, 5sp.") + }, + null, // no child nodes + false /* not mandatory */); + + ElementDescriptor style_element = new ElementDescriptor( + "style", //$NON-NLS-1$ + "Style/Theme", + "Both @styles and themes@ are defined in a style block containing one or more string or numerical values (typically color values), or references to other resources (drawables and so on).", + "http://code.google.com/android/reference/available-resources.html#stylesandthemes", //$NON-NLS-1$ + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this theme."), + new TextAttributeDescriptor("parent", // $NON-NLS-1$ + "Parent", + null /* nsUri */, + "An optional parent theme. All values from the specified theme will be inherited into this theme. Any values with identical names that you specify will override inherited values."), + }, + new ElementDescriptor[] { + new ElementDescriptor( + "item", //$NON-NLS-1$ + "Item", + "A value to use in this @theme@. It can be a standard string, a hex color value, or a reference to any other resource type.", + "http://code.google.com/android/reference/available-resources.html#stylesandthemes", //$NON-NLS-1$ + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this item."), + new TextValueDescriptor( + "Value*", + "A mandatory standard string, hex color value, or reference to any other resource type.") + }, + null, // no child nodes + false /* not mandatory */) + }, + false /* not mandatory */); + + ElementDescriptor string_array_element = new ElementDescriptor( + "string-array", //$NON-NLS-1$ + "String Array", + "An array of strings. Strings are added as underlying item elements to the array.", + null, // tooltips + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this string array."), + }, + new ElementDescriptor[] { + new ElementDescriptor( + "item", //$NON-NLS-1$ + "Item", + "A string value to use in this string array.", + null, // tooltip + new AttributeDescriptor[] { + new TextValueDescriptor( + "Value*", + "A mandatory string.") + }, + null, // no child nodes + false /* not mandatory */) + }, + false /* not mandatory */); + + ElementDescriptor integer_array_element = new ElementDescriptor( + "integer-array", //$NON-NLS-1$ + "Integer Array", + "An array of integers. Integers are added as underlying item elements to the array.", + null, // tooltips + new AttributeDescriptor[] { + new TextAttributeDescriptor(NAME_ATTR, + "Name*", + null /* nsUri */, + "The mandatory name used in referring to this integer array."), + }, + new ElementDescriptor[] { + new ElementDescriptor( + "item", //$NON-NLS-1$ + "Item", + "An integer value to use in this integer array.", + null, // tooltip + new AttributeDescriptor[] { + new TextValueDescriptor( + "Value*", + "A mandatory integer.") + }, + null, // no child nodes + false /* not mandatory */) + }, + false /* not mandatory */); + + mResourcesElement = new ElementDescriptor( + ROOT_ELEMENT, + "Resources", + null, + "http://code.google.com/android/reference/available-resources.html", //$NON-NLS-1$ + null, // no attributes + new ElementDescriptor[] { + string_element, + color_element, + dimen_element, + drawable_element, + style_element, + item_element, + string_array_element, + integer_array_element, + }, + true /* mandatory */); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java new file mode 100644 index 0000000..d1d8891 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.explorer; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.resources.manager.ProjectResourceItem; +import com.android.ide.eclipse.editors.resources.manager.ProjectResources; +import com.android.ide.eclipse.editors.resources.manager.ResourceFile; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener; +import com.android.ide.eclipse.editors.wizards.ResourceContentProvider; +import com.android.ide.eclipse.editors.wizards.ResourceLabelProvider; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.ISelectionListener; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.ViewPart; + +import java.util.Iterator; + +/** + * Resource Explorer View. + * <p/> + * This contains a basic Tree view, and uses a TreeViewer to handle the data. + * <p/> + * The view listener to change in selection in the workbench, and update to show the resource + * of the project of the current selected item (either item in the package explorer, or of the + * current editor). + * + * @see ResourceContentProvider + */ +public class ResourceExplorerView extends ViewPart implements ISelectionListener, + IResourceEventListener { + + // Note: keep using the obsolete AndroidConstants.EDITORS_NAMESPACE (which used + // to be the Editors Plugin ID) to keep existing preferences functional. + private final static String PREFS_COLUMN_RES = + AndroidConstants.EDITORS_NAMESPACE + "ResourceExplorer.Col1"; //$NON-NLS-1$ + private final static String PREFS_COLUMN_2 = + AndroidConstants.EDITORS_NAMESPACE + "ResourceExplorer.Col2"; //$NON-NLS-1$ + + private Tree mTree; + private TreeViewer mTreeViewer; + + private IProject mCurrentProject; + + public ResourceExplorerView() { + } + + @Override + public void createPartControl(Composite parent) { + mTree = new Tree(parent, SWT.SINGLE | SWT.VIRTUAL); + mTree.setLayoutData(new GridData(GridData.FILL_BOTH)); + mTree.setHeaderVisible(true); + mTree.setLinesVisible(true); + + final IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + + // create 2 columns. The main one with the resources, and an "info" column. + createTreeColumn(mTree, "Resources", SWT.LEFT, + "abcdefghijklmnopqrstuvwxz", -1, PREFS_COLUMN_RES, store); //$NON-NLS-1$ + createTreeColumn(mTree, "Info", SWT.LEFT, + "0123456789", -1, PREFS_COLUMN_2, store); //$NON-NLS-1$ + + // create the jface wrapper + mTreeViewer = new TreeViewer(mTree); + + mTreeViewer.setContentProvider(new ResourceContentProvider(true /* fullLevels */)); + mTreeViewer.setLabelProvider(new ResourceLabelProvider()); + + // listen to selection change in the workbench. + IWorkbenchPage page = getSite().getPage(); + + page.addSelectionListener(this); + + // init with current selection + selectionChanged(getSite().getPart(), page.getSelection()); + + // add support for double click. + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + public void doubleClick(DoubleClickEvent event) { + ISelection sel = event.getSelection(); + + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + + if (selection.size() == 1) { + Object element = selection.getFirstElement(); + + // if it's a resourceFile, we directly open it. + if (element instanceof ResourceFile) { + try { + IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(), + ((ResourceFile)element).getFile().getIFile()); + } catch (PartInitException e) { + } + } else if (element instanceof ProjectResourceItem) { + // if it's a ResourceItem, we open the first file, but only if + // there's no alternate files. + ProjectResourceItem item = (ProjectResourceItem)element; + + if (item.isEditableDirectly()) { + ResourceFile[] files = item.getSourceFileArray(); + if (files[0] != null) { + try { + IDE.openEditor( + getSite().getWorkbenchWindow().getActivePage(), + files[0].getFile().getIFile()); + } catch (PartInitException e) { + } + } + } + } + } + } + } + }); + + // set up the resource manager to send us resource change notification + AdtPlugin.getDefault().getResourceMonitor().addResourceEventListener(this); + } + + @Override + public void dispose() { + AdtPlugin.getDefault().getResourceMonitor().removeResourceEventListener(this); + + super.dispose(); + } + + @Override + public void setFocus() { + mTree.setFocus(); + } + + /** + * Processes a new selection. + */ + public void selectionChanged(IWorkbenchPart part, ISelection selection) { + // first we test if the part is an editor. + if (part instanceof IEditorPart) { + // if it is, we check if it's a file editor. + IEditorInput input = ((IEditorPart)part).getEditorInput(); + + if (input instanceof IFileEditorInput) { + // from the file editor we can get the IFile object, and from it, the IProject. + IFile file = ((IFileEditorInput)input).getFile(); + + // get the file project + IProject project = file.getProject(); + + handleProjectSelection(project); + } + } else if (selection instanceof IStructuredSelection) { + // if it's not an editor, we look for structured selection. + for (Iterator<?> it = ((IStructuredSelection) selection).iterator(); + it.hasNext();) { + Object element = it.next(); + IProject project = null; + + // if we are in the navigator or package explorer, the selection could contain a + // IResource object. + if (element instanceof IResource) { + project = ((IResource) element).getProject(); + } else if (element instanceof IJavaElement) { + // if we are in the package explorer on a java element, we handle that too. + IJavaElement javaElement = (IJavaElement)element; + IJavaProject javaProject = javaElement.getJavaProject(); + if (javaProject != null) { + project = javaProject.getProject(); + } + } else if (element instanceof IAdaptable) { + // finally we try to get a project object from IAdaptable. + project = (IProject) ((IAdaptable) element) + .getAdapter(IProject.class); + } + + // if we found a project, handle it, and return. + if (project != null) { + if (handleProjectSelection(project)) { + return; + } + } + } + } + } + + /** + * Handles a project selection. + * @param project the new selected project + * @return true if the project could be processed. + */ + private boolean handleProjectSelection(IProject project) { + try { + // if it's an android project, then we get its resources, and feed them + // to the tree viewer. + if (project.hasNature(AndroidConstants.NATURE)) { + if (mCurrentProject != project) { + ProjectResources projRes = ResourceManager.getInstance().getProjectResources( + project); + if (projRes != null) { + mTreeViewer.setInput(projRes); + mCurrentProject = project; + return true; + } + } + } + } catch (CoreException e) { + } + + return false; + } + + /** + * Create a TreeColumn with the specified parameters. If a + * <code>PreferenceStore</code> object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param fixedSize a fixed size. If != -1 the column is non resizable + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public void createTreeColumn(Tree parent, String header, int style, + String sample_text, int fixedSize, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + if (fixedSize != -1) { + col.setWidth(fixedSize); + col.setResizable(false); + } else { + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put the new width value into the store. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + public void controlMoved(ControlEvent e) { + } + + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } + + // set the header + col.setText(header); + } + + /** + * Processes a start in a resource event change. + */ + public void resourceChangeEventStart() { + // pass + } + + /** + * Processes the end of a resource change event. + */ + public void resourceChangeEventEnd() { + try { + mTree.getDisplay().asyncExec(new Runnable() { + public void run() { + if (mTree.isDisposed() == false) { + mTreeViewer.refresh(); + } + } + }); + } catch (SWTException e) { + // display is disposed. nothing to do. + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java new file mode 100644 index 0000000..455c825 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.AndroidManifestHelper; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; + +/** + * A monitor for the compiled resources. This only monitors changes in the resources of type + * {@link ResourceType#ID}. + */ +public final class CompiledResourcesMonitor implements IFileListener, IProjectListener { + + private final static CompiledResourcesMonitor sThis = new CompiledResourcesMonitor(); + + /** + * Sets up the monitoring system. + * @param monitor The main Resource Monitor. + */ + public static void setupMonitor(ResourceMonitor monitor) { + monitor.addFileListener(sThis, IResourceDelta.ADDED | IResourceDelta.CHANGED); + monitor.addProjectListener(sThis); + } + + /** + * private constructor to prevent construction. + */ + private CompiledResourcesMonitor() { + } + + + /* (non-Javadoc) + * Sent when a file changed : if the file is the R class, then it is parsed again to update + * the internal data. + * + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * + * @see IFileListener#fileChanged + */ + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { + if (file.getName().equals(AndroidConstants.FN_COMPILED_RESOURCE_CLASS)) { + loadAndParseRClass(file.getProject()); + } + } + + /** + * Processes project close event. + */ + public void projectClosed(IProject project) { + // the ProjectResources object will be removed by the ResourceManager. + } + + /** + * Processes project delete event. + */ + public void projectDeleted(IProject project) { + // the ProjectResources object will be removed by the ResourceManager. + } + + /** + * Processes project open event. + */ + public void projectOpened(IProject project) { + // when the project is opened, we get an ADDED event for each file, so we don't + // need to do anything here. + } + + /** + * Processes existing project at init time. + */ + public void projectOpenedWithWorkspace(IProject project) { + try { + // check this is an android project + if (project.hasNature(AndroidConstants.NATURE)) { + loadAndParseRClass(project); + } + } catch (CoreException e) { + // pass + } + } + + private void loadAndParseRClass(IProject project) { + try { + // first check there's a ProjectResources to store the content + ProjectResources projectResources = ResourceManager.getInstance().getProjectResources( + project); + + if (projectResources != null) { + // create the classname + String className = getRClassName(project); + + // create a temporary class loader to load it. + ProjectClassLoader loader = new ProjectClassLoader(null /* parentClassLoader */, + project); + + try { + Class<?> clazz = loader.loadClass(className); + + if (clazz != null) { + // create the maps to store the result of the parsing + Map<String, Map<String, Integer>> resourceValueMap = + new HashMap<String, Map<String, Integer>>(); + Map<Integer, String[]> genericValueToNameMap = + new HashMap<Integer, String[]>(); + Map<IntArrayWrapper, String> styleableValueToNameMap = + new HashMap<IntArrayWrapper, String>(); + + // parse the class + if (parseClass(clazz, genericValueToNameMap, styleableValueToNameMap, + resourceValueMap)) { + // now we associate the maps to the project. + projectResources.setCompiledResources(genericValueToNameMap, + styleableValueToNameMap, resourceValueMap); + } + } + } catch (Error e) { + // Log this error with the class name we're trying to load and abort. + AdtPlugin.log(e, "loadAndParseRClass failed to find class %1$s", className); //$NON-NLS-1$ + } + } + } catch (ClassNotFoundException e) { + // pass + } + } + + /** + * Parses a R class, and fills maps. + * @param rClass the class to parse + * @param genericValueToNameMap + * @param styleableValueToNameMap + * @param resourceValueMap + * @return True if we managed to parse the R class. + */ + private boolean parseClass(Class<?> rClass, Map<Integer, String[]> genericValueToNameMap, + Map<IntArrayWrapper, String> styleableValueToNameMap, Map<String, + Map<String, Integer>> resourceValueMap) { + try { + for (Class<?> inner : rClass.getDeclaredClasses()) { + String resType = inner.getSimpleName(); + + Map<String, Integer> fullMap = new HashMap<String, Integer>(); + resourceValueMap.put(resType, fullMap); + + for (Field f : inner.getDeclaredFields()) { + // only process static final fields. + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) { + Class<?> type = f.getType(); + if (type.isArray() && type.getComponentType() == int.class) { + // if the object is an int[] we put it in the styleable map + styleableValueToNameMap.put(new IntArrayWrapper((int[]) f.get(null)), + f.getName()); + } else if (type == int.class) { + Integer value = (Integer) f.get(null); + genericValueToNameMap.put(value, new String[] { f.getName(), resType }); + fullMap.put(f.getName(), value); + } else { + assert false; + } + } + } + } + + return true; + } catch (IllegalArgumentException e) { + } catch (IllegalAccessException e) { + } + return false; + } + + private String getRClassName(IProject project) { + // create the classname + AndroidManifestHelper manifest = new AndroidManifestHelper(project); + String javaPackage = manifest.getPackageName(); + + return javaPackage + ".R"; //$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java new file mode 100644 index 0000000..57c17fc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +/** + * Represents a resource item that can exist in multiple "alternate" versions. + */ +public class ConfigurableResourceItem extends ProjectResourceItem { + + /** + * Constructs a new Resource Item. + * @param name the name of the resource as it appears in the XML and R.java files. + */ + public ConfigurableResourceItem(String name) { + super(name); + } + + /** + * Returns if the resource item has at least one non-default configuration. + */ + public boolean hasAlternates() { + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault() == false) { + return true; + } + } + + return false; + } + + /** + * Returns whether the resource has a default version, with no qualifier. + */ + public boolean hasDefault() { + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault()) { + return true; + } + } + + // We only want to return false if there's no default and more than 0 items. + return (mFiles.size() == 0); + } + + /** + * Returns the number of alternate versions of this resource. + */ + public int getAlternateCount() { + int count = 0; + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault() == false) { + count++; + } + } + + return count; + } + + /* + * (non-Javadoc) + * Returns whether the item can be edited directly (ie it does not have alternate versions). + */ + @Override + public boolean isEditableDirectly() { + return hasAlternates() == false; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java new file mode 100644 index 0000000..a9f80bd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.ResourceType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +/** + * This class gives access to the bi directional relationship between {@link ResourceType} and + * {@link ResourceFolderType}. + */ +public final class FolderTypeRelationship { + + private final static HashMap<ResourceType, ResourceFolderType[]> mTypeToFolderMap = + new HashMap<ResourceType, ResourceFolderType[]>(); + + private final static HashMap<ResourceFolderType, ResourceType[]> mFolderToTypeMap = + new HashMap<ResourceFolderType, ResourceType[]>(); + + // generate the relationships. + static { + HashMap<ResourceType, List<ResourceFolderType>> typeToFolderMap = + new HashMap<ResourceType, List<ResourceFolderType>>(); + + HashMap<ResourceFolderType, List<ResourceType>> folderToTypeMap = + new HashMap<ResourceFolderType, List<ResourceType>>(); + + add(ResourceType.ANIM, ResourceFolderType.ANIM, typeToFolderMap, folderToTypeMap); + add(ResourceType.ARRAY, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.COLOR, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.COLOR, ResourceFolderType.COLOR, typeToFolderMap, folderToTypeMap); + add(ResourceType.DIMEN, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.DRAWABLE, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.DRAWABLE, ResourceFolderType.DRAWABLE, typeToFolderMap, folderToTypeMap); + add(ResourceType.ID, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.LAYOUT, ResourceFolderType.LAYOUT, typeToFolderMap, folderToTypeMap); + add(ResourceType.MENU, ResourceFolderType.MENU, typeToFolderMap, folderToTypeMap); + add(ResourceType.RAW, ResourceFolderType.RAW, typeToFolderMap, folderToTypeMap); + add(ResourceType.STRING, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.STYLE, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap); + add(ResourceType.XML, ResourceFolderType.XML, typeToFolderMap, folderToTypeMap); + + optimize(typeToFolderMap, folderToTypeMap); + } + + /** + * Returns a list of {@link ResourceType}s that can be generated from files inside a folder + * of the specified type. + * @param folderType The folder type. + * @return an array of {@link ResourceType} + */ + public static ResourceType[] getRelatedResourceTypes(ResourceFolderType folderType) { + ResourceType[] array = mFolderToTypeMap.get(folderType); + if (array != null) { + return array; + } + return new ResourceType[0]; + } + + /** + * Returns a list of {@link ResourceFolderType} that can contain files generating resources + * of the specified type. + * @param resType the type of resource. + * @return an array of {@link ResourceFolderType} + */ + public static ResourceFolderType[] getRelatedFolders(ResourceType resType) { + ResourceFolderType[] array = mTypeToFolderMap.get(resType); + if (array != null) { + return array; + } + return new ResourceFolderType[0]; + } + + /** + * Returns true if the {@link ResourceType} and the {@link ResourceFolderType} values match. + * @param resType the resource type. + * @param folderType the folder type. + * @return true if files inside the folder of the specified {@link ResourceFolderType} + * could generate a resource of the specified {@link ResourceType} + */ + public static boolean match(ResourceType resType, ResourceFolderType folderType) { + ResourceFolderType[] array = mTypeToFolderMap.get(resType); + + if (array != null && array.length > 0) { + for (ResourceFolderType fType : array) { + if (fType == folderType) { + return true; + } + } + } + + return false; + } + + /** + * Adds a {@link ResourceType} - {@link ResourceFolderType} relationship. this indicates that + * a file in the folder can generate a resource of the specified type. + * @param type The resourceType + * @param folder The {@link ResourceFolderType} + * @param folderToTypeMap + * @param typeToFolderMap + */ + private static void add(ResourceType type, ResourceFolderType folder, + HashMap<ResourceType, List<ResourceFolderType>> typeToFolderMap, + HashMap<ResourceFolderType, List<ResourceType>> folderToTypeMap) { + // first we add the folder to the list associated with the type. + List<ResourceFolderType> folderList = typeToFolderMap.get(type); + if (folderList == null) { + folderList = new ArrayList<ResourceFolderType>(); + typeToFolderMap.put(type, folderList); + } + if (folderList.indexOf(folder) == -1) { + folderList.add(folder); + } + + // now we add the type to the list associated with the folder. + List<ResourceType> typeList = folderToTypeMap.get(folder); + if (typeList == null) { + typeList = new ArrayList<ResourceType>(); + folderToTypeMap.put(folder, typeList); + } + if (typeList.indexOf(type) == -1) { + typeList.add(type); + } + } + + /** + * Optimize the map to contains array instead of lists (since the api returns arrays) + * @param typeToFolderMap + * @param folderToTypeMap + */ + private static void optimize(HashMap<ResourceType, List<ResourceFolderType>> typeToFolderMap, + HashMap<ResourceFolderType, List<ResourceType>> folderToTypeMap) { + Set<ResourceType> types = typeToFolderMap.keySet(); + for (ResourceType type : types) { + List<ResourceFolderType> list = typeToFolderMap.get(type); + mTypeToFolderMap.put(type, list.toArray(new ResourceFolderType[list.size()])); + } + + Set<ResourceFolderType> folders = folderToTypeMap.keySet(); + for (ResourceFolderType folder : folders) { + List<ResourceType> list = folderToTypeMap.get(folder); + mFolderToTypeMap.put(folder, list.toArray(new ResourceType[list.size()])); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java new file mode 100644 index 0000000..552aec9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.IIdResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; + +/** + * Represents a resource item of type {@link ResourceType#ID} + */ +public class IdResourceItem extends ProjectResourceItem implements IIdResourceItem { + + private final boolean mIsDeclaredInline; + + /** + * Constructs a new ResourceItem. + * @param name the name of the resource as it appears in the XML and R.java files. + * @param isDeclaredInline Whether this id was declared inline. + */ + IdResourceItem(String name, boolean isDeclaredInline) { + super(name); + mIsDeclaredInline = isDeclaredInline; + } + + /* + * (non-Javadoc) + * Returns whether the ID resource has been declared inline inside another resource XML file. + */ + public boolean isDeclaredInline() { + return mIsDeclaredInline; + } + + /* (non-Javadoc) + * Returns whether the item can be edited (ie, the id was not declared inline). + */ + @Override + public boolean isEditableDirectly() { + return !mIsDeclaredInline; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java new file mode 100644 index 0000000..25eb112 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import java.util.Arrays; + + +/** + * Wrapper around a int[] to provide hashCode/equals support. + */ +public final class IntArrayWrapper { + + private int[] mData; + + public IntArrayWrapper(int[] data) { + mData = data; + } + + public void set(int[] data) { + mData = data; + } + + @Override + public int hashCode() { + return Arrays.hashCode(mData); + } + + @Override + public boolean equals(Object obj) { + if (getClass().equals(obj.getClass())) { + return Arrays.equals(mData, ((IntArrayWrapper)obj).mData); + } + + return super.equals(obj); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java new file mode 100644 index 0000000..3812791 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.utils.ResourceValue; +import com.android.layoutlib.utils.ValueResourceParser; +import com.android.layoutlib.utils.ValueResourceParser.IValueResourceRepository; + +import org.eclipse.core.runtime.CoreException; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Represents a resource file able to declare multiple resources, which could be of + * different {@link ResourceType}. + * <p/> + * This is typically an XML file inside res/values. + */ +public final class MultiResourceFile extends ResourceFile implements IValueResourceRepository { + + private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance(); + + private final HashMap<ResourceType, HashMap<String, ResourceValue>> mResourceItems = + new HashMap<ResourceType, HashMap<String, ResourceValue>>(); + + public MultiResourceFile(IAbstractFile file, ResourceFolder folder) { + super(file, folder); + } + + @Override + public ResourceType[] getResourceTypes() { + update(); + + Set<ResourceType> keys = mResourceItems.keySet(); + + return keys.toArray(new ResourceType[keys.size()]); + } + + @Override + public boolean hasResources(ResourceType type) { + update(); + + HashMap<String, ResourceValue> list = mResourceItems.get(type); + return (list != null && list.size() > 0); + } + + @Override + public Collection<ProjectResourceItem> getResources(ResourceType type, + ProjectResources projectResources) { + update(); + + HashMap<String, ResourceValue> list = mResourceItems.get(type); + + ArrayList<ProjectResourceItem> items = new ArrayList<ProjectResourceItem>(); + + if (list != null) { + Collection<ResourceValue> values = list.values(); + for (ResourceValue res : values) { + ProjectResourceItem item = projectResources.findResourceItem(type, res.getName()); + + if (item == null) { + if (type == ResourceType.ID) { + item = new IdResourceItem(res.getName(), false /* isDeclaredInline */); + } else { + item = new ConfigurableResourceItem(res.getName()); + } + items.add(item); + } + + item.add(this); + } + } + + return items; + } + + /** + * Updates the Resource items if necessary. + */ + private void update() { + if (isTouched() == true) { + // reset current content. + mResourceItems.clear(); + + // need to parse the file and find the content. + parseFile(); + + resetTouch(); + } + } + + /** + * Parses the file and creates a list of {@link ResourceType}. + */ + private void parseFile() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + parser.parse(getFile().getContents(), new ValueResourceParser(this, isFramework())); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } catch (CoreException e) { + } + } + + /** + * Adds a resource item to the list + * @param resType The type of the resource + * @param value The value of the resource. + */ + public void addResourceValue(String resType, ResourceValue value) { + ResourceType type = ResourceType.getEnum(resType); + if (type != null) { + HashMap<String, ResourceValue> list = mResourceItems.get(type); + + // if the list does not exist, create it. + if (list == null) { + list = new HashMap<String, ResourceValue>(); + mResourceItems.put(type, list); + } else { + // look for a possible value already existing. + ResourceValue oldValue = list.get(value.getName()); + + if (oldValue != null) { + oldValue.replaceWith(value); + return; + } + } + + // empty list or no match found? add the given resource + list.put(value.getName(), value); + } + } + + @Override + public IResourceValue getValue(ResourceType type, String name) { + update(); + + // get the list for the given type + HashMap<String, ResourceValue> list = mResourceItems.get(type); + + if (list != null) { + return list.get(name); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java new file mode 100644 index 0000000..8b6c3c1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.AndroidConstants; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; + +/** + * ClassLoader able to load class from output of an Eclipse project. + */ +public final class ProjectClassLoader extends ClassLoader { + + private final IJavaProject mJavaProject; + private URLClassLoader mJarClassLoader; + private boolean mInsideJarClassLoader = false; + + public ProjectClassLoader(ClassLoader parentClassLoader, IProject project) { + super(parentClassLoader); + mJavaProject = JavaCore.create(project); + } + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + try { + // get the project output folder. + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IPath outputLocation = mJavaProject.getOutputLocation(); + IResource outRes = root.findMember(outputLocation); + if (outRes == null) { + throw new ClassNotFoundException(name); + } + + File outFolder = new File(outRes.getLocation().toOSString()); + + // get the class name segments + String[] segments = name.split("\\."); //$NON-NLS-1$ + + File classFile = getFile(outFolder, segments, 0); + if (classFile == null) { + if (mInsideJarClassLoader == false) { + // if no file matching the class name was found, look in the 3rd party jars + return loadClassFromJar(name); + } else { + throw new ClassNotFoundException(name); + } + } + + // load the content of the file and create the class. + FileInputStream fis = new FileInputStream(classFile); + byte[] data = new byte[(int)classFile.length()]; + int read = 0; + try { + read = fis.read(data); + } catch (IOException e) { + data = null; + } + fis.close(); + + if (data != null) { + Class<?> clazz = defineClass(null, data, 0, read); + if (clazz != null) { + return clazz; + } + } + } catch (Exception e) { + throw new ClassNotFoundException(e.getMessage()); + } + + throw new ClassNotFoundException(name); + } + + /** + * Returns the File matching the a certain path from a root {@link File}. + * <p/>The methods checks that the file ends in .class even though the last segment + * does not. + * @param parent the root of the file. + * @param segments the segments containing the path of the file + * @param index the offset at which to start looking into segments. + * @throws FileNotFoundException + */ + private File getFile(File parent, String[] segments, int index) + throws FileNotFoundException { + // reached the end with no match? + if (index == segments.length) { + throw new FileNotFoundException(); + } + + String toMatch = segments[index]; + File[] files = parent.listFiles(); + + // we're at the last segments. we look for a matching <file>.class + if (index == segments.length - 1) { + toMatch = toMatch + ".class"; + + if (files != null) { + for (File file : files) { + if (file.isFile() && file.getName().equals(toMatch)) { + return file; + } + } + } + + // no match? abort. + throw new FileNotFoundException(); + } + + String innerClassName = null; + + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + if (toMatch.equals(file.getName())) { + return getFile(file, segments, index+1); + } + } else if (file.getName().startsWith(toMatch)) { + if (innerClassName == null) { + StringBuilder sb = new StringBuilder(segments[index]); + for (int i = index + 1 ; i < segments.length ; i++) { + sb.append('$'); + sb.append(segments[i]); + } + sb.append(".class"); + + innerClassName = sb.toString(); + } + + if (file.getName().equals(innerClassName)) { + return file; + } + } + } + } + + return null; + } + + /** + * Loads a class from the 3rd party jar present in the project + * @throws ClassNotFoundException + */ + private Class<?> loadClassFromJar(String name) throws ClassNotFoundException { + if (mJarClassLoader == null) { + // get the OS path to all the external jars + URL[] jars = getExternalJars(); + + mJarClassLoader = new URLClassLoader(jars, this /* parent */); + } + + try { + // because a class loader always look in its parent loader first, we need to know + // that we are querying the jar classloader. This will let us know to not query + // it again for classes we don't find, or this would create an infinite loop. + mInsideJarClassLoader = true; + return mJarClassLoader.loadClass(name); + } finally { + mInsideJarClassLoader = false; + } + } + + /** + * Returns an array of external jar files used by the project. + * @return an array of OS-specific absolute file paths + */ + private final URL[] getExternalJars() { + // get a java project from it + IJavaProject javaProject = JavaCore.create(mJavaProject.getProject()); + + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + + ArrayList<URL> oslibraryList = new ArrayList<URL>(); + IClasspathEntry[] classpaths = javaProject.readRawClasspath(); + if (classpaths != null) { + for (IClasspathEntry e : classpaths) { + if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY || + e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + // if this is a classpath variable reference, we resolve it. + if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { + e = JavaCore.getResolvedClasspathEntry(e); + } + + // get the IPath + IPath path = e.getPath(); + + // check the name ends with .jar + if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { + boolean local = false; + IResource resource = wsRoot.findMember(path); + if (resource != null && resource.exists() && + resource.getType() == IResource.FILE) { + local = true; + try { + oslibraryList.add( + new File(resource.getLocation().toOSString()).toURL()); + } catch (MalformedURLException mue) { + // pass + } + } + + if (local == false) { + // if the jar path doesn't match a workspace resource, + // then we get an OSString and check if this links to a valid file. + String osFullPath = path.toOSString(); + + File f = new File(osFullPath); + if (f.exists()) { + try { + oslibraryList.add(f.toURL()); + } catch (MalformedURLException mue) { + // pass + } + } + } + } + } + } + } + + return oslibraryList.toArray(new URL[oslibraryList.size()]); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java new file mode 100644 index 0000000..ba770b2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java @@ -0,0 +1,91 @@ +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Base class for Resource Item coming from an Android Project. + */ +public abstract class ProjectResourceItem extends ResourceItem { + + private final static Comparator<ResourceFile> sComparator = new Comparator<ResourceFile>() { + public int compare(ResourceFile file1, ResourceFile file2) { + // get both FolderConfiguration and compare them + FolderConfiguration fc1 = file1.getFolder().getConfiguration(); + FolderConfiguration fc2 = file2.getFolder().getConfiguration(); + + return fc1.compareTo(fc2); + } + }; + + /** + * List of files generating this ResourceItem. + */ + protected final ArrayList<ResourceFile> mFiles = new ArrayList<ResourceFile>(); + + /** + * Constructs a new ResourceItem. + * @param name the name of the resource as it appears in the XML and R.java files. + */ + public ProjectResourceItem(String name) { + super(name); + } + + /** + * Returns whether the resource item is editable directly. + * <p/> + * This is typically the case for resources that don't have alternate versions, or resources + * of type {@link ResourceType#ID} that aren't declared inline. + */ + public abstract boolean isEditableDirectly(); + + /** + * Adds a new version of this resource item, by adding its {@link ResourceFile}. + * @param file the {@link ResourceFile} object. + */ + protected void add(ResourceFile file) { + mFiles.add(file); + } + + /** + * Reset the item by emptying its version list. + */ + protected void reset() { + mFiles.clear(); + } + + /** + * Returns the sorted list of {@link ResourceItem} objects for this resource item. + */ + public ResourceFile[] getSourceFileArray() { + ArrayList<ResourceFile> list = new ArrayList<ResourceFile>(); + list.addAll(mFiles); + + Collections.sort(list, sComparator); + + return list.toArray(new ResourceFile[list.size()]); + } + + /** + * Returns the list of {@link ResourceItem} objects for this resource item. + */ + public List<ResourceFile> getSourceFileList() { + return Collections.unmodifiableList(mFiles); + } + + + /** + * Replaces the content of the receiver with the ResourceItem received as parameter. + * @param item + */ + protected void replaceWith(ProjectResourceItem item) { + mFiles.clear(); + mFiles.addAll(item.mFiles); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java new file mode 100644 index 0000000..40e4e3b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java @@ -0,0 +1,804 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.LanguageQualifier; +import com.android.ide.eclipse.editors.resources.configurations.RegionQualifier; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.utils.ResourceValue; + +import org.eclipse.core.resources.IFolder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents the resources of a project. This is a file view of the resources, with handling + * for the alternate resource types. For a compiled view use CompiledResources. + */ +public class ProjectResources implements IResourceRepository { + private final HashMap<ResourceFolderType, List<ResourceFolder>> mFolderMap = + new HashMap<ResourceFolderType, List<ResourceFolder>>(); + + private final HashMap<ResourceType, List<ProjectResourceItem>> mResourceMap = + new HashMap<ResourceType, List<ProjectResourceItem>>(); + + /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */ + private Map<String, Map<String, Integer>> mResourceValueMap; + /** Map of (id, [name, resType]) for all resources coming from R.java */ + private Map<Integer, String[]> mResIdValueToNameMap; + /** Map of (int[], name) for styleable resources coming from R.java */ + private Map<IntArrayWrapper, String> mStyleableValueToNameMap; + + /** Cached list of {@link IdResourceItem}. This is mix of IdResourceItem created by + * {@link MultiResourceFile} for ids coming from XML files under res/values and + * {@link IdResourceItem} created manually, from the list coming from R.java */ + private final ArrayList<IdResourceItem> mIdResourceList = new ArrayList<IdResourceItem>(); + + private final boolean mIsFrameworkRepository; + + private final IntArrayWrapper mWrapper = new IntArrayWrapper(null); + + public ProjectResources(boolean isFrameworkRepository) { + mIsFrameworkRepository = isFrameworkRepository; + } + + public boolean isSystemRepository() { + return mIsFrameworkRepository; + } + + /** + * Adds a Folder Configuration to the project. + * @param type The resource type. + * @param config The resource configuration. + * @param folder The workspace folder object. + * @return the {@link ResourceFolder} object associated to this folder. + */ + protected ResourceFolder add(ResourceFolderType type, FolderConfiguration config, + IAbstractFolder folder) { + // get the list for the resource type + List<ResourceFolder> list = mFolderMap.get(type); + + if (list == null) { + list = new ArrayList<ResourceFolder>(); + + ResourceFolder cf = new ResourceFolder(type, config, folder, mIsFrameworkRepository); + list.add(cf); + + mFolderMap.put(type, list); + + return cf; + } + + // look for an already existing folder configuration. + for (ResourceFolder cFolder : list) { + if (cFolder.mConfiguration.equals(config)) { + // config already exist. Nothing to be done really, besides making sure + // the IFolder object is up to date. + cFolder.mFolder = folder; + return cFolder; + } + } + + // If we arrive here, this means we didn't find a matching configuration. + // So we add one. + ResourceFolder cf = new ResourceFolder(type, config, folder, mIsFrameworkRepository); + list.add(cf); + + return cf; + } + + /** + * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}. + * @param type The type of the folder + * @param folder the IFolder object. + */ + protected void removeFolder(ResourceFolderType type, IFolder folder) { + // get the list of folders for the resource type. + List<ResourceFolder> list = mFolderMap.get(type); + + if (list != null) { + int count = list.size(); + for (int i = 0 ; i < count ; i++) { + ResourceFolder resFolder = list.get(i); + if (resFolder.getFolder().getIFolder().equals(folder)) { + // we found the matching ResourceFolder. we need to remove it. + list.remove(i); + + // we now need to invalidate this resource type. + // The easiest way is to touch one of the other folders of the same type. + if (list.size() > 0) { + list.get(0).touch(); + } else { + // if the list is now empty, and we have a single ResouceType out of this + // ResourceFolderType, then we are done. + // However, if another ResourceFolderType can generate similar ResourceType + // than this, we need to update those ResourceTypes as well. + // For instance, if the last "drawable-*" folder is deleted, we need to + // refresh the ResourceItem associated with ResourceType.DRAWABLE. + // Those can be found in ResourceFolderType.DRAWABLE but also in + // ResourceFolderType.VALUES. + // If we don't find a single folder to touch, then it's fine, as the top + // level items (the list of generated resource types) is not cached + // (for now) + + // get the lists of ResourceTypes generated by this ResourceFolderType + ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes( + type); + + // for each of those, make sure to find one folder to touch so that the + // list of ResourceItem associated with the type is rebuilt. + for (ResourceType resType : resTypes) { + // get the list of folder that can generate this type + ResourceFolderType[] folderTypes = + FolderTypeRelationship.getRelatedFolders(resType); + + // we only need to touch one folder in any of those (since it's one + // folder per type, not per folder type). + for (ResourceFolderType folderType : folderTypes) { + List<ResourceFolder> resFolders = mFolderMap.get(folderType); + + if (resFolders != null && resFolders.size() > 0) { + resFolders.get(0).touch(); + break; + } + } + } + } + + // we're done updating/touching, we can stop + break; + } + } + } + } + + + /** + * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}. + * @param type The {@link ResourceFolderType} + */ + public List<ResourceFolder> getFolders(ResourceFolderType type) { + return mFolderMap.get(type); + } + + /* (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.IResourceRepository#getAvailableResourceTypes() + */ + public ResourceType[] getAvailableResourceTypes() { + ArrayList<ResourceType> list = new ArrayList<ResourceType>(); + + // For each key, we check if there's a single ResourceType match. + // If not, we look for the actual content to give us the resource type. + + for (ResourceFolderType folderType : mFolderMap.keySet()) { + ResourceType types[] = FolderTypeRelationship.getRelatedResourceTypes(folderType); + if (types.length == 1) { + // before we add it we check if it's not already present, since a ResourceType + // could be created from multiple folders, even for the folders that only create + // one type of resource (drawable for instance, can be created from drawable/ and + // values/) + if (list.indexOf(types[0]) == -1) { + list.add(types[0]); + } + } else { + // there isn't a single resource type out of this folder, so we look for all + // content. + List<ResourceFolder> folders = mFolderMap.get(folderType); + if (folders != null) { + for (ResourceFolder folder : folders) { + Collection<ResourceType> folderContent = folder.getResourceTypes(); + + // then we add them, but only if they aren't already in the list. + for (ResourceType folderResType : folderContent) { + if (list.indexOf(folderResType) == -1) { + list.add(folderResType); + } + } + } + } + } + } + + // in case ResourceType.ID haven't been added yet because there's no id defined + // in XML, we check on the list of compiled id resources. + if (list.indexOf(ResourceType.ID) == -1 && mResourceValueMap != null) { + Map<String, Integer> map = mResourceValueMap.get(ResourceType.ID.getName()); + if (map != null && map.size() > 0) { + list.add(ResourceType.ID); + } + } + + // at this point the list is full of ResourceType defined in the files. + // We need to sort it. + Collections.sort(list); + + return list.toArray(new ResourceType[list.size()]); + } + + /* (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.IResourceRepository#getResources(com.android.ide.eclipse.common.resources.ResourceType) + */ + public ProjectResourceItem[] getResources(ResourceType type) { + checkAndUpdate(type); + + if (type == ResourceType.ID) { + synchronized (mIdResourceList) { + return mIdResourceList.toArray(new ProjectResourceItem[mIdResourceList.size()]); + } + } + + List<ProjectResourceItem> items = mResourceMap.get(type); + + return items.toArray(new ProjectResourceItem[items.size()]); + } + + /* (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.IResourceRepository#hasResources(com.android.ide.eclipse.common.resources.ResourceType) + */ + public boolean hasResources(ResourceType type) { + checkAndUpdate(type); + + if (type == ResourceType.ID) { + synchronized (mIdResourceList) { + return mIdResourceList.size() > 0; + } + } + + List<ProjectResourceItem> items = mResourceMap.get(type); + return (items != null && items.size() > 0); + } + + /** + * Returns the {@link ResourceFolder} associated with a {@link IFolder}. + * @param folder The {@link IFolder} object. + * @return the {@link ResourceFolder} or null if it was not found. + */ + public ResourceFolder getResourceFolder(IFolder folder) { + for (List<ResourceFolder> list : mFolderMap.values()) { + for (ResourceFolder resFolder : list) { + if (resFolder.getFolder().getIFolder().equals(folder)) { + return resFolder; + } + } + } + + return null; + } + + /** + * Returns the {@link ResourceFile} matching the given name, {@link ResourceFolderType} and + * configuration. + * <p/>This only works with files generating one resource named after the file (for instance, + * layouts, bitmap based drawable, xml, anims). + * @return the matching file or <code>null</code> if no match was found. + */ + public ResourceFile getMatchingFile(String name, ResourceFolderType type, + FolderConfiguration config) { + // get the folders for the given type + List<ResourceFolder> folders = mFolderMap.get(type); + + // look for folders containing a file with the given name. + ArrayList<ResourceFolder> matchingFolders = new ArrayList<ResourceFolder>(); + + // remove the folders that do not have a file with the given name, or if their config + // is incompatible. + for (int i = 0 ; i < folders.size(); i++) { + ResourceFolder folder = folders.get(i); + + if (folder.hasFile(name) == true) { + matchingFolders.add(folder); + } + } + + // from those, get the folder with a config matching the given reference configuration. + Resource match = findMatchingConfiguredResource(matchingFolders, config); + + // do we have a matching folder? + if (match instanceof ResourceFolder) { + // get the ResourceFile from the filename + return ((ResourceFolder)match).getFile(name); + } + + return null; + } + + /** + * Returns the resources values matching a given {@link FolderConfiguration}. + * @param referenceConfig the configuration that each value must match. + */ + public Map<String, Map<String, IResourceValue>> getConfiguredResources( + FolderConfiguration referenceConfig) { + + Map<String, Map<String, IResourceValue>> map = + new HashMap<String, Map<String, IResourceValue>>(); + + // special case for Id since there's a mix of compiled id (declared inline) and id declared + // in the XML files. + if (mIdResourceList.size() > 0) { + Map<String, IResourceValue> idMap = new HashMap<String, IResourceValue>(); + String idType = ResourceType.ID.getName(); + for (IdResourceItem id : mIdResourceList) { + // FIXME: cache the ResourceValue! + idMap.put(id.getName(), new ResourceValue(idType, id.getName(), + mIsFrameworkRepository)); + } + + map.put(ResourceType.ID.getName(), idMap); + } + + Set<ResourceType> keys = mResourceMap.keySet(); + for (ResourceType key : keys) { + // we don't process ID resources since we already did it above. + if (key != ResourceType.ID) { + map.put(key.getName(), getConfiguredResource(key, referenceConfig)); + } + } + + return map; + } + + /** + * Loads all the resources. Essentially this forces to load the values from the + * {@link ResourceFile} objects to make sure they are up to date and loaded + * in {@link #mResourceMap}. + */ + public void loadAll() { + // gets all the resource types available. + ResourceType[] types = getAvailableResourceTypes(); + + // loop on them and load them + for (ResourceType type: types) { + checkAndUpdate(type); + } + } + + /** + * Resolves a compiled resource id into the resource name and type + * @param id + * @return an array of 2 strings { name, type } or null if the id could not be resolved + */ + public String[] resolveResourceValue(int id) { + if (mResIdValueToNameMap != null) { + return mResIdValueToNameMap.get(id); + } + + return null; + } + + /** + * Resolves a compiled resource id of type int[] into the resource name. + */ + public String resolveResourceValue(int[] id) { + if (mStyleableValueToNameMap != null) { + mWrapper.set(id); + return mStyleableValueToNameMap.get(mWrapper); + } + + return null; + } + + /** + * Returns the value of a resource by its type and name. + */ + public Integer getResourceValue(String type, String name) { + if (mResourceValueMap != null) { + Map<String, Integer> map = mResourceValueMap.get(type); + if (map != null) { + return map.get(name); + } + } + + return null; + } + + /** + * Returns the list of languages used in the resources. + */ + public Set<String> getLanguages() { + Set<String> set = new HashSet<String>(); + + Collection<List<ResourceFolder>> folderList = mFolderMap.values(); + for (List<ResourceFolder> folderSubList : folderList) { + for (ResourceFolder folder : folderSubList) { + FolderConfiguration config = folder.getConfiguration(); + LanguageQualifier lang = config.getLanguageQualifier(); + if (lang != null) { + set.add(lang.getStringValue()); + } + } + } + + return set; + } + + /** + * Returns the list of regions used in the resources with the given language. + * @param currentLanguage the current language the region must be associated with. + */ + public Set<String> getRegions(String currentLanguage) { + Set<String> set = new HashSet<String>(); + + Collection<List<ResourceFolder>> folderList = mFolderMap.values(); + for (List<ResourceFolder> folderSubList : folderList) { + for (ResourceFolder folder : folderSubList) { + FolderConfiguration config = folder.getConfiguration(); + + // get the language + LanguageQualifier lang = config.getLanguageQualifier(); + if (lang != null && lang.getStringValue().equals(currentLanguage)) { + RegionQualifier region = config.getRegionQualifier(); + if (region != null) { + set.add(region.getStringValue()); + } + } + } + } + + return set; + } + + /** + * Returns a map of (resource name, resource value) for the given {@link ResourceType}. + * <p/>The values returned are taken from the resource files best matching a given + * {@link FolderConfiguration}. + * @param type the type of the resources. + * @param referenceConfig the configuration to best match. + */ + private Map<String, IResourceValue> getConfiguredResource(ResourceType type, + FolderConfiguration referenceConfig) { + // get the resource item for the given type + List<ProjectResourceItem> items = mResourceMap.get(type); + + // create the map + HashMap<String, IResourceValue> map = new HashMap<String, IResourceValue>(); + + for (ProjectResourceItem item : items) { + // get the source files generating this resource + List<ResourceFile> list = item.getSourceFileList(); + + // look for the best match for the given configuration + Resource match = findMatchingConfiguredResource(list, referenceConfig); + + if (match instanceof ResourceFile) { + ResourceFile matchResFile = (ResourceFile)match; + + // get the value of this configured resource. + IResourceValue value = matchResFile.getValue(type, item.getName()); + + if (value != null) { + map.put(item.getName(), value); + } + } + } + + return map; + } + + /** + * Returns the best matching {@link Resource}. + * @param resources the list of {@link Resource} to choose from. + * @param referenceConfig the {@link FolderConfiguration} to match. + */ + private Resource findMatchingConfiguredResource(List<? extends Resource> resources, + FolderConfiguration referenceConfig) { + // look for resources with the maximum number of qualifier match. + int currentMax = -1; + ArrayList<Resource> matchingResources = new ArrayList<Resource>(); + for (int i = 0 ; i < resources.size(); i++) { + Resource res = resources.get(i); + + int count = res.getConfiguration().match(referenceConfig); + if (count > currentMax) { + matchingResources.clear(); + matchingResources.add(res); + currentMax = count; + } else if (count != -1 && count == currentMax) { + matchingResources.add(res); + } + } + + // if we have more than one match, we look for the match with the qualifiers with the + // highest priority. + Resource resMatch = null; + if (matchingResources.size() == 1) { + resMatch = matchingResources.get(0); + } else if (matchingResources.size() > 1) { + // More than one resource with the same number of qualifier match. + // We loop, looking for the resource with the highest priority qualifiers. + ArrayList<Resource> tmpResources = new ArrayList<Resource>(); + int startIndex = 0; + while (matchingResources.size() > 1) { + int highest = -1; + for (int i = 0 ; i < matchingResources.size() ; i++) { + Resource folder = matchingResources.get(i); + + // get highest priority qualifiers. + int m = folder.getConfiguration().getHighestPriorityQualifier(startIndex); + + // add to the list if highest. + if (m != -1) { + if (highest == -1 || m == highest) { + tmpResources.add(folder); + highest = m; + } else if (m < highest) { // highest priority == lowest index. + tmpResources.clear(); + tmpResources.add(folder); + } + } + } + + // at this point, we have a list with 1+ resources that all have the same highest + // priority qualifiers. Go through the list again looking for the next highest + // priority qualifier. + startIndex = highest + 1; + + // this should not happen, but it's better to check. + if (matchingResources.size() == tmpResources.size() && highest == -1) { + // this means all the resources match with the same qualifiers + // (highest == -1 means we reached the end of the qualifier list) + // In this case, we arbitrarily take the first resource. + matchingResources.clear(); + matchingResources.add(tmpResources.get(0)); + } else { + matchingResources.clear(); + matchingResources.addAll(tmpResources); + } + tmpResources.clear(); + } + + // we should have only one match here. + resMatch = matchingResources.get(0); + } + + return resMatch; + } + + /** + * Checks if the list of {@link ResourceItem}s for the specified {@link ResourceType} needs + * to be updated. + * @param type the Resource Type. + */ + private void checkAndUpdate(ResourceType type) { + // get the list of folder that can output this type + ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type); + + for (ResourceFolderType folderType : folderTypes) { + List<ResourceFolder> folders = mFolderMap.get(folderType); + + if (folders != null) { + for (ResourceFolder folder : folders) { + if (folder.isTouched()) { + // if this folder is touched we need to update all the types that can + // be generated from a file in this folder. + // This will include 'type' obviously. + ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes( + folderType); + for (ResourceType resType : resTypes) { + update(resType); + } + return; + } + } + } + } + } + + /** + * Updates the list of {@link ResourceItem} objects associated with a {@link ResourceType}. + * This will reset the touch status of all the folders that can generate this resource type. + * @param type the Resource Type. + */ + private void update(ResourceType type) { + // get the cache list, and lets make a backup + List<ProjectResourceItem> items = mResourceMap.get(type); + List<ProjectResourceItem> backup = new ArrayList<ProjectResourceItem>(); + + if (items == null) { + items = new ArrayList<ProjectResourceItem>(); + mResourceMap.put(type, items); + } else { + // backup the list + backup.addAll(items); + + // we reset the list itself. + items.clear(); + } + + // get the list of folder that can output this type + ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type); + + for (ResourceFolderType folderType : folderTypes) { + List<ResourceFolder> folders = mFolderMap.get(folderType); + + if (folders != null) { + for (ResourceFolder folder : folders) { + items.addAll(folder.getResources(type, this)); + folder.resetTouch(); + } + } + } + + // now items contains the new list. We "merge" it with the backup list. + // Basically, we need to keep the old instances of ResourceItem (where applicable), + // but replace them by the content of the new items. + // This will let the resource explorer keep the expanded state of the nodes whose data + // is a ResourceItem object. + if (backup.size() > 0) { + // this is not going to change as we're only replacing instances. + int count = items.size(); + + for (int i = 0 ; i < count;) { + // get the "new" item + ProjectResourceItem item = items.get(i); + + // look for a similar item in the old list. + ProjectResourceItem foundOldItem = null; + for (ProjectResourceItem oldItem : backup) { + if (oldItem.getName().equals(item.getName())) { + foundOldItem = oldItem; + break; + } + } + + if (foundOldItem != null) { + // erase the data of the old item with the data from the new one. + foundOldItem.replaceWith(item); + + // remove the old and new item from their respective lists + items.remove(i); + backup.remove(foundOldItem); + + // add the old item to the new list + items.add(foundOldItem); + } else { + // this is a new item, we skip to the next object + i++; + } + } + } + + // if this is the ResourceType.ID, we create the actual list, from this list and + // the compiled resource list. + if (type == ResourceType.ID) { + mergeIdResources(); + } else { + // else this is the list that will actually be displayed, so we sort it. + Collections.sort(items); + } + } + + /** + * Looks up an existing {@link ProjectResourceItem} by {@link ResourceType} and name. + * @param type the Resource Type. + * @param name the Resource name. + * @return the existing ResourceItem or null if no match was found. + */ + protected ProjectResourceItem findResourceItem(ResourceType type, String name) { + List<ProjectResourceItem> list = mResourceMap.get(type); + + for (ProjectResourceItem item : list) { + if (name.equals(item.getName())) { + return item; + } + } + + return null; + } + + /** + * Sets compiled resource information. + * @param resIdValueToNameMap a map of compiled resource id to resource name. + * The map is acquired by the {@link ProjectResources} object. + * @param styleableValueMap + * @param resourceValueMap a map of (name, id) for resources of type {@link ResourceType#ID}. + * The list is acquired by the {@link ProjectResources} object. + */ + void setCompiledResources(Map<Integer, String[]> resIdValueToNameMap, + Map<IntArrayWrapper, String> styleableValueMap, + Map<String, Map<String, Integer>> resourceValueMap) { + mResourceValueMap = resourceValueMap; + mResIdValueToNameMap = resIdValueToNameMap; + mStyleableValueToNameMap = styleableValueMap; + mergeIdResources(); + } + + /** + * Merges the list of ID resource coming from R.java and the list of ID resources + * coming from XML declaration into the cached list {@link #mIdResourceList}. + */ + void mergeIdResources() { + // get the list of IDs coming from XML declaration. Those ids are present in + // mCompiledIdResources already, so we'll need to use those instead of creating + // new IdResourceItem + List<ProjectResourceItem> xmlIdResources = mResourceMap.get(ResourceType.ID); + + synchronized (mIdResourceList) { + // copy the currently cached items. + ArrayList<IdResourceItem> oldItems = new ArrayList<IdResourceItem>(); + oldItems.addAll(mIdResourceList); + + // empty the current list + mIdResourceList.clear(); + + // get the list of compile id resources. + Map<String, Integer> idMap = null; + if (mResourceValueMap != null) { + idMap = mResourceValueMap.get(ResourceType.ID.getName()); + } + + if (idMap == null) { + if (xmlIdResources != null) { + for (ProjectResourceItem resourceItem : xmlIdResources) { + // check the actual class just for safety. + if (resourceItem instanceof IdResourceItem) { + mIdResourceList.add((IdResourceItem)resourceItem); + } + } + } + } else { + // loop on the full list of id, and look for a match in the old list, + // in the list coming from XML (in case a new XML item was created.) + + Set<String> idSet = idMap.keySet(); + + idLoop: for (String idResource : idSet) { + // first look in the XML list in case an id went from inline to XML declared. + if (xmlIdResources != null) { + for (ProjectResourceItem resourceItem : xmlIdResources) { + if (resourceItem instanceof IdResourceItem && + resourceItem.getName().equals(idResource)) { + mIdResourceList.add((IdResourceItem)resourceItem); + continue idLoop; + } + } + } + + // if we haven't found it, look in the old items. + int count = oldItems.size(); + for (int i = 0 ; i < count ; i++) { + IdResourceItem resourceItem = oldItems.get(i); + if (resourceItem.getName().equals(idResource)) { + oldItems.remove(i); + mIdResourceList.add(resourceItem); + continue idLoop; + } + } + + // if we haven't found it, it looks like it's a new id that was + // declared inline. + mIdResourceList.add(new IdResourceItem(idResource, + true /* isDeclaredInline */)); + } + } + + // now we sort the list + Collections.sort(mIdResourceList); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java new file mode 100644 index 0000000..dd8d080 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; + +/** + * Base class for file system resource items (Folders, Files). + */ +public abstract class Resource { + private boolean mTouched = true; + + /** + * Returns the {@link FolderConfiguration} for this object. + */ + public abstract FolderConfiguration getConfiguration(); + + /** + * Indicates that the underlying file was changed. + */ + public final void touch() { + mTouched = true; + } + + public final boolean isTouched() { + return mTouched; + } + + public final void resetTouch() { + mTouched = false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java new file mode 100644 index 0000000..f927a9a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile; +import com.android.layoutlib.api.IResourceValue; + +import java.util.Collection; + +/** + * Represents a Resource file (a file under $Project/res/) + */ +public abstract class ResourceFile extends Resource { + + private final IAbstractFile mFile; + private final ResourceFolder mFolder; + + protected ResourceFile(IAbstractFile file, ResourceFolder folder) { + mFile = file; + mFolder = folder; + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.Resource#getConfiguration() + */ + @Override + public FolderConfiguration getConfiguration() { + return mFolder.getConfiguration(); + } + + /** + * Returns the IFile associated with the ResourceFile. + */ + public final IAbstractFile getFile() { + return mFile; + } + + /** + * Returns the parent folder as a {@link ResourceFolder}. + */ + public final ResourceFolder getFolder() { + return mFolder; + } + + /** + * Returns whether the resource is a framework resource. + */ + public final boolean isFramework() { + return mFolder.isFramework(); + } + + /** + * Returns the list of {@link ResourceType} generated by the file. + */ + public abstract ResourceType[] getResourceTypes(); + + /** + * Returns whether the file generated a resource of a specific type. + * @param type The {@link ResourceType} + */ + public abstract boolean hasResources(ResourceType type); + + /** + * Get the list of {@link ProjectResourceItem} of a specific type generated by the file. + * This method must make sure not to create duplicate. + * @param type The type of {@link ProjectResourceItem} to return. + * @param projectResources The global Project Resource object, allowing the implementation to + * query for already existing {@link ProjectResourceItem} + * @return The list of <b>new</b> {@link ProjectResourceItem} + * @see ProjectResources#findResourceItem(ResourceType, String) + */ + public abstract Collection<ProjectResourceItem> getResources(ResourceType type, + ProjectResources projectResources); + + /** + * Returns the value of a resource generated by this file by {@link ResourceType} and name. + * <p/>If no resource match, <code>null</code> is returned. + * @param type the type of the resource. + * @param name the name of the resource. + */ + public abstract IResourceValue getValue(ResourceType type, String name); +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java new file mode 100644 index 0000000..98f5b39 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Resource Folder class. Contains list of {@link ResourceFile}s, + * the {@link FolderConfiguration}, and a link to the workspace {@link IFolder} object. + */ +public final class ResourceFolder extends Resource { + ResourceFolderType mType; + FolderConfiguration mConfiguration; + IAbstractFolder mFolder; + ArrayList<ResourceFile> mFiles = null; + private final boolean mIsFramework; + + /** + * Creates a new {@link ResourceFolder} + * @param type The type of the folder + * @param config The configuration of the folder + * @param folder The associated {@link IAbstractFolder} object. + * @param isFrameworkRepository + */ + public ResourceFolder(ResourceFolderType type, FolderConfiguration config, + IAbstractFolder folder, boolean isFrameworkRepository) { + mType = type; + mConfiguration = config; + mFolder = folder; + mIsFramework = isFrameworkRepository; + } + + /** + * Adds a {@link ResourceFile} to the folder. + * @param file The {@link ResourceFile}. + */ + public void addFile(ResourceFile file) { + if (mFiles == null) { + mFiles = new ArrayList<ResourceFile>(); + } + + mFiles.add(file); + } + + /** + * Attempts to remove the {@link ResourceFile} associated with a specified {@link IFile}. + * @param file the IFile object. + */ + public void removeFile(IFile file) { + if (mFiles != null) { + int count = mFiles.size(); + for (int i = 0 ; i < count ; i++) { + ResourceFile resFile = mFiles.get(i); + if (resFile != null) { + IFile iFile = resFile.getFile().getIFile(); + if (iFile != null && iFile.equals(file)) { + mFiles.remove(i); + touch(); + } + } + } + } + } + + /** + * Returns the {@link IFolder} associated with this object. + */ + public IAbstractFolder getFolder() { + return mFolder; + } + + /** + * Returns the {@link ResourceFolderType} of this object. + */ + public ResourceFolderType getType() { + return mType; + } + + /** + * Returns whether the folder is a framework resource folder. + */ + public boolean isFramework() { + return mIsFramework; + } + + /** + * Returns the list of {@link ResourceType}s generated by the files inside this folder. + */ + public Collection<ResourceType> getResourceTypes() { + ArrayList<ResourceType> list = new ArrayList<ResourceType>(); + + if (mFiles != null) { + for (ResourceFile file : mFiles) { + ResourceType[] types = file.getResourceTypes(); + + // loop through those and add them to the main list, + // if they are not already present + if (types != null) { + for (ResourceType resType : types) { + if (list.indexOf(resType) == -1) { + list.add(resType); + } + } + } + } + } + + return list; + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.Resource#getConfiguration() + */ + @Override + public FolderConfiguration getConfiguration() { + return mConfiguration; + } + + /** + * Returns whether the folder contains a file with the given name. + * @param name the name of the file. + */ + public boolean hasFile(String name) { + return mFolder.hasFile(name); + } + + /** + * Returns the {@link ResourceFile} matching a {@link IAbstractFile} object. + * @param file The {@link IFile} object. + * @return the {@link ResourceFile} or null if no match was found. + */ + public ResourceFile getFile(IAbstractFile file) { + if (mFiles != null) { + for (ResourceFile f : mFiles) { + if (f.getFile().equals(file)) { + return f; + } + } + } + return null; + } + + /** + * Returns the {@link ResourceFile} matching a {@link IFile} object. + * @param file The {@link IFile} object. + * @return the {@link ResourceFile} or null if no match was found. + */ + public ResourceFile getFile(IFile file) { + if (mFiles != null) { + for (ResourceFile f : mFiles) { + IFile iFile = f.getFile().getIFile(); + if (iFile != null && iFile.equals(file)) { + return f; + } + } + } + return null; + } + + + /** + * Returns the {@link ResourceFile} matching a given name. + * @param filename The name of the file to return. + * @return the {@link ResourceFile} or <code>null</code> if no match was found. + */ + public ResourceFile getFile(String filename) { + if (mFiles != null) { + for (ResourceFile f : mFiles) { + if (f.getFile().getName().equals(filename)) { + return f; + } + } + } + return null; + } + + /** + * Returns whether a file in the folder is generating a resource of a specified type. + * @param type The {@link ResourceType} being looked up. + */ + public boolean hasResources(ResourceType type) { + // Check if the folder type is able to generate resource of the type that was asked. + // this is a first check to avoid going through the files. + ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type); + + boolean valid = false; + for (ResourceFolderType rft : folderTypes) { + if (rft == mType) { + valid = true; + break; + } + } + + if (valid) { + if (mFiles != null) { + for (ResourceFile f : mFiles) { + if (f.hasResources(type)) { + return true; + } + } + } + } + return false; + } + + /** + * Get the list of {@link ResourceItem} of a specific type generated by all the files + * in the folder. + * This method must make sure not to create duplicates. + * @param type The type of {@link ResourceItem} to return. + * @param projectResources The global Project Resource object, allowing the implementation to + * query for already existing {@link ResourceItem} + * @return The list of <b>new</b> {@link ResourceItem} + * @see ProjectResources#findResourceItem(ResourceType, String) + */ + public Collection<ProjectResourceItem> getResources(ResourceType type, + ProjectResources projectResources) { + Collection<ProjectResourceItem> list = new ArrayList<ProjectResourceItem>(); + if (mFiles != null) { + for (ResourceFile f : mFiles) { + list.addAll(f.getResources(type, projectResources)); + } + } + return list; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java new file mode 100644 index 0000000..5fc7393 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.sdklib.SdkConstants; + +/** + * Enum representing a type of resource folder. + */ +public enum ResourceFolderType { + ANIM(SdkConstants.FD_ANIM), + COLOR(SdkConstants.FD_COLOR), + DRAWABLE(SdkConstants.FD_DRAWABLE), + LAYOUT(SdkConstants.FD_LAYOUT), + MENU(SdkConstants.FD_MENU), + RAW(SdkConstants.FD_RAW), + VALUES(SdkConstants.FD_VALUES), + XML(SdkConstants.FD_XML); + + private final String mName; + + ResourceFolderType(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + /** + * Returns the enum by name. + * @param name The enum string value. + * @return the enum or null if not found. + */ + public static ResourceFolderType getTypeByName(String name) { + for (ResourceFolderType rType : values()) { + if (rType.mName.equals(name)) { + return rType; + } + } + return null; + } + + /** + * Returns the {@link ResourceFolderType} from the folder name + * @param folderName The name of the folder. This must be a valid folder name in the format + * <code>resType[-resqualifiers[-resqualifiers[...]]</code> + * @return the <code>ResourceFolderType</code> representing the type of the folder, or + * <code>null</code> if no matching type was found. + */ + public static ResourceFolderType getFolderType(String folderName) { + // split the name of the folder in segments. + String[] folderSegments = folderName.split(FolderConfiguration.QUALIFIER_SEP); + + // get the enum for the resource type. + return getTypeByName(folderSegments[0]); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java new file mode 100644 index 0000000..6099008 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFolderListener; +import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener; +import com.android.ide.eclipse.editors.resources.manager.files.FileWrapper; +import com.android.ide.eclipse.editors.resources.manager.files.FolderWrapper; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder; +import com.android.ide.eclipse.editors.resources.manager.files.IFileWrapper; +import com.android.ide.eclipse.editors.resources.manager.files.IFolderWrapper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +public final class ResourceManager implements IProjectListener, IFolderListener, IFileListener { + + private final static ResourceManager sThis = new ResourceManager(); + + /** List of the qualifier object helping for the parsing of folder names */ + private final ResourceQualifier[] mQualifiers; + + /** + * Map associating project resource with project objects. + */ + private final HashMap<IProject, ProjectResources> mMap = + new HashMap<IProject, ProjectResources>(); + + /** + * Sets up the resource manager with the global resource monitor. + * @param monitor The global resource monitor + */ + public static void setup(ResourceMonitor monitor) { + monitor.addProjectListener(sThis); + int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED; + monitor.addFolderListener(sThis, mask); + monitor.addFileListener(sThis, mask); + + CompiledResourcesMonitor.setupMonitor(monitor); + } + + /** + * Returns the singleton instance. + */ + public static ResourceManager getInstance() { + return sThis; + } + + /** + * Returns the resources of a project. + * @param project The project + * @return a ProjectResources object or null if none was found. + */ + public ProjectResources getProjectResources(IProject project) { + return mMap.get(project); + } + + /** + * Processes folder event. + */ + public void folderChanged(IFolder folder, int kind) { + ProjectResources resources; + + final IProject project = folder.getProject(); + + try { + if (project.hasNature(AndroidConstants.NATURE) == false) { + return; + } + } catch (CoreException e) { + // can't get the project nature? return! + return; + } + + switch (kind) { + case IResourceDelta.ADDED: + // checks if the folder is under res. + IPath path = folder.getFullPath(); + + // the path will be project/res/<something> + if (path.segmentCount() == 3) { + if (isInResFolder(path)) { + // get the project and its resource object. + resources = mMap.get(project); + + // if it doesn't exist, we create it. + if (resources == null) { + resources = new ProjectResources(false /* isFrameworkRepository */); + mMap.put(project, resources); + } + + processFolder(new IFolderWrapper(folder), resources); + } + } + break; + case IResourceDelta.CHANGED: + resources = mMap.get(folder.getProject()); + if (resources != null) { + ResourceFolder resFolder = resources.getResourceFolder(folder); + if (resFolder != null) { + resFolder.touch(); + } + } + break; + case IResourceDelta.REMOVED: + resources = mMap.get(folder.getProject()); + if (resources != null) { + // lets get the folder type + ResourceFolderType type = ResourceFolderType.getFolderType(folder.getName()); + + resources.removeFolder(type, folder); + } + break; + } + } + + /* (non-Javadoc) + * Sent when a file changed. Depending on the file being changed, and the type of change (ADDED, + * REMOVED, CHANGED), the file change is processed to update the resource manager data. + * + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + * + * @see IFileListener#fileChanged + */ + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) { + ProjectResources resources; + + final IProject project = file.getProject(); + + try { + if (project.hasNature(AndroidConstants.NATURE) == false) { + return; + } + } catch (CoreException e) { + // can't get the project nature? return! + return; + } + + switch (kind) { + case IResourceDelta.ADDED: + // checks if the file is under res/something. + IPath path = file.getFullPath(); + + if (path.segmentCount() == 4) { + if (isInResFolder(path)) { + // get the project and its resources + resources = mMap.get(project); + + IContainer container = file.getParent(); + if (container instanceof IFolder && resources != null) { + + ResourceFolder folder = resources.getResourceFolder((IFolder)container); + + if (folder != null) { + processFile(new IFileWrapper(file), folder); + } + } + } + } + break; + case IResourceDelta.CHANGED: + // try to find a matching ResourceFile + resources = mMap.get(project); + if (resources != null) { + IContainer container = file.getParent(); + if (container instanceof IFolder) { + ResourceFolder resFolder = resources.getResourceFolder((IFolder)container); + + // we get the delete on the folder before the file, so it is possible + // the associated ResourceFolder doesn't exist anymore. + if (resFolder != null) { + // get the resourceFile, and touch it. + ResourceFile resFile = resFolder.getFile(file); + if (resFile != null) { + resFile.touch(); + } + } + } + } + break; + case IResourceDelta.REMOVED: + // try to find a matching ResourceFile + resources = mMap.get(project); + if (resources != null) { + IContainer container = file.getParent(); + if (container instanceof IFolder) { + ResourceFolder resFolder = resources.getResourceFolder((IFolder)container); + + // we get the delete on the folder before the file, so it is possible + // the associated ResourceFolder doesn't exist anymore. + if (resFolder != null) { + // remove the file + resFolder.removeFile(file); + } + } + } + break; + } + } + + public void projectClosed(IProject project) { + mMap.remove(project); + } + + public void projectDeleted(IProject project) { + mMap.remove(project); + } + + public void projectOpened(IProject project) { + createProject(project); + } + + public void projectOpenedWithWorkspace(IProject project) { + createProject(project); + } + + /** + * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists. + */ + public ResourceFolder getResourceFolder(IFile file) { + IContainer container = file.getParent(); + if (container.getType() == IResource.FOLDER) { + IFolder parent = (IFolder)container; + IProject project = file.getProject(); + + ProjectResources resources = getProjectResources(project); + if (resources != null) { + return resources.getResourceFolder(parent); + } + } + + return null; + } + + /** + * Loads and returns the resources for a given {@link IAndroidTarget} + * @param androidTarget the target from which to load the framework resources + */ + public ProjectResources loadFrameworkResources(IAndroidTarget androidTarget) { + String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES); + + File frameworkRes = new File(osResourcesPath); + if (frameworkRes.isDirectory()) { + ProjectResources resources = new ProjectResources(true /* isFrameworkRepository */); + + try { + File[] files = frameworkRes.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + ResourceFolder resFolder = processFolder(new FolderWrapper(file), + resources); + + if (resFolder != null) { + // now we process the content of the folder + File[] children = file.listFiles(); + + for (File childRes : children) { + if (childRes.isFile()) { + processFile(new FileWrapper(childRes), resFolder); + } + } + } + + } + } + + // now that we have loaded the files, we need to force load the resources from them + resources.loadAll(); + + return resources; + + } catch (IOException e) { + // since we test that folders are folders, and files are files, this shouldn't + // happen. We can ignore it. + } + } + + return null; + } + + /** + * Initial project parsing to gather resource info. + * @param project + */ + private void createProject(IProject project) { + if (project.isOpen()) { + try { + if (project.hasNature(AndroidConstants.NATURE) == false) { + return; + } + } catch (CoreException e1) { + // can't check the nature of the project? ignore it. + return; + } + + IFolder resourceFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + ProjectResources projectResources = mMap.get(project); + if (projectResources == null) { + projectResources = new ProjectResources(false /* isFrameworkRepository */); + mMap.put(project, projectResources); + } + + if (resourceFolder != null && resourceFolder.exists()) { + try { + IResource[] resources = resourceFolder.members(); + + for (IResource res : resources) { + if (res.getType() == IResource.FOLDER) { + IFolder folder = (IFolder)res; + ResourceFolder resFolder = processFolder(new IFolderWrapper(folder), + projectResources); + + if (resFolder != null) { + // now we process the content of the folder + IResource[] files = folder.members(); + + for (IResource fileRes : files) { + if (fileRes.getType() == IResource.FILE) { + IFile file = (IFile)fileRes; + + processFile(new IFileWrapper(file), resFolder); + } + } + } + } + } + } catch (CoreException e) { + // This happens if the project is closed or if the folder doesn't exist. + // Since we already test for that, we can ignore this exception. + } + } + } + } + + /** + * Creates a {@link FolderConfiguration} matching the folder segments. + * @param folderSegments The segments of the folder name. The first segments should contain + * the name of the folder + * @return a FolderConfiguration object, or null if the folder name isn't valid.. + */ + public FolderConfiguration getConfig(String[] folderSegments) { + FolderConfiguration config = new FolderConfiguration(); + + // we are going to loop through the segments, and match them with the first + // available qualifier. If the segment doesn't match we try with the next qualifier. + // Because the order of the qualifier is fixed, we do not reset the first qualifier + // after each sucessful segment. + // If we run out of qualifier before processing all the segments, we fail. + + int qualifierIndex = 0; + int qualifierCount = mQualifiers.length; + + for (int i = 1 ; i < folderSegments.length; i++) { + String seg = folderSegments[i]; + if (seg.length() > 0) { + while (qualifierIndex < qualifierCount && + mQualifiers[qualifierIndex].checkAndSet(seg, config) == false) { + qualifierIndex++; + } + + // if we reached the end of the qualifier we didn't find a matching qualifier. + if (qualifierIndex == qualifierCount) { + return null; + } + + } else { + return null; + } + } + + return config; + } + + /** + * Processes a folder and adds it to the list of the project resources. + * @param folder the folder to process + * @param project the folder's project. + * @return the ConfiguredFolder created from this folder, or null if the process failed. + */ + private ResourceFolder processFolder(IAbstractFolder folder, ProjectResources project) { + // split the name of the folder in segments. + String[] folderSegments = folder.getName().split(FolderConfiguration.QUALIFIER_SEP); + + // get the enum for the resource type. + ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]); + + if (type != null) { + // get the folder configuration. + FolderConfiguration config = getConfig(folderSegments); + + if (config != null) { + ResourceFolder configuredFolder = project.add(type, config, folder); + + return configuredFolder; + } + } + + return null; + } + + /** + * Processes a file and adds it to its parent folder resource. + * @param file + * @param folder + */ + private void processFile(IAbstractFile file, ResourceFolder folder) { + // get the type of the folder + ResourceFolderType type = folder.getType(); + + // look for this file if it's already been created + ResourceFile resFile = folder.getFile(file); + + if (resFile != null) { + // invalidate the file + resFile.touch(); + } else { + // create a ResourceFile for it. + + // check if that's a single or multi resource type folder. For now we define this by + // the number of possible resource type output by files in the folder. This does + // not make the difference between several resource types from a single file or + // the ability to have 2 files in the same folder generating 2 different types of + // resource. The former is handled by MultiResourceFile properly while we don't + // handle the latter. If we were to add this behavior we'd have to change this call. + ResourceType[] types = FolderTypeRelationship.getRelatedResourceTypes(type); + + if (types.length == 1) { + resFile = new SingleResourceFile(file, folder); + } else { + resFile = new MultiResourceFile(file, folder); + } + + // add it to the folder + folder.addFile(resFile); + } + } + + /** + * Returns true if the path is under /project/res/ + * @param path a workspace relative path + * @return true if the path is under /project res/ + */ + private boolean isInResFolder(IPath path) { + return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1)); + } + + /** + * Private constructor to enforce singleton design. + */ + ResourceManager() { + // get the default qualifiers. + FolderConfiguration defaultConfig = new FolderConfiguration(); + defaultConfig.createDefault(); + mQualifiers = defaultConfig.getQualifiers(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java new file mode 100644 index 0000000..59a72fb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; + +import java.util.ArrayList; + +/** + * Resource Monitor for the whole editor plugin. Other, more simple, listeners can register to + * that one. + */ +public class ResourceMonitor implements IResourceChangeListener { + + private final static ResourceMonitor sThis = new ResourceMonitor(); + + /** + * Classes which implement this interface provide a method that deals + * with file change events. + */ + public interface IFileListener { + /** + * Sent when a file changed. + * @param file The file that changed. + * @param markerDeltas The marker deltas for the file. + * @param kind The change kind. This is equivalent to + * {@link IResourceDelta#accept(IResourceDeltaVisitor)} + */ + public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind); + } + + /** + * Classes which implements this interface provide methods dealing with project events. + */ + public interface IProjectListener { + /** + * Sent for each opened android project at the time the listener is put in place. + * @param project the opened project. + */ + public void projectOpenedWithWorkspace(IProject project); + /** + * Sent when a project is opened. + * @param project the project being opened. + */ + public void projectOpened(IProject project); + /** + * Sent when a project is closed. + * @param project the project being closed. + */ + public void projectClosed(IProject project); + /** + * Sent when a project is deleted. + * @param project the project about to be deleted. + */ + public void projectDeleted(IProject project); + } + + /** + * Classes which implement this interface provide a method that deals + * with folder change events + */ + public interface IFolderListener { + /** + * Sent when a folder changed. + * @param folder The file that was changed + * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()} + */ + public void folderChanged(IFolder folder, int kind); + } + + /** + * Interface for a listener to be notified when resource change event starts and ends. + */ + public interface IResourceEventListener { + public void resourceChangeEventStart(); + public void resourceChangeEventEnd(); + } + + /** + * Base listener bundle to associate a listener to an event mask. + */ + private static class ListenerBundle { + /** Mask value to accept all events */ + public final static int MASK_NONE = -1; + + /** + * Event mask. Values accepted are IResourceDelta.### + * @see IResourceDelta#ADDED + * @see IResourceDelta#REMOVED + * @see IResourceDelta#CHANGED + * @see IResourceDelta#ADDED_PHANTOM + * @see IResourceDelta#REMOVED_PHANTOM + * */ + int kindMask; + } + + /** + * Listener bundle for file event. + */ + private static class FileListenerBundle extends ListenerBundle { + + /** The file listener */ + IFileListener listener; + } + + /** + * Listener bundle for folder event. + */ + private static class FolderListenerBundle extends ListenerBundle { + /** The file listener */ + IFolderListener listener; + } + + private final ArrayList<FileListenerBundle> mFileListeners = + new ArrayList<FileListenerBundle>(); + + private final ArrayList<FolderListenerBundle> mFolderListeners = + new ArrayList<FolderListenerBundle>(); + + private final ArrayList<IProjectListener> mProjectListeners = new ArrayList<IProjectListener>(); + + private final ArrayList<IResourceEventListener> mEventListeners = + new ArrayList<IResourceEventListener>(); + + private IWorkspace mWorkspace; + + /** + * Delta visitor for resource changes. + */ + private final class DeltaVisitor implements IResourceDeltaVisitor { + + public boolean visit(IResourceDelta delta) { + IResource r = delta.getResource(); + int type = r.getType(); + if (type == IResource.FILE) { + int kind = delta.getKind(); + // notify the listeners. + for (FileListenerBundle bundle : mFileListeners) { + if (bundle.kindMask == ListenerBundle.MASK_NONE + || (bundle.kindMask & kind) != 0) { + bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind); + } + } + return false; + } else if (type == IResource.FOLDER) { + int kind = delta.getKind(); + // notify the listeners. + for (FolderListenerBundle bundle : mFolderListeners) { + if (bundle.kindMask == ListenerBundle.MASK_NONE + || (bundle.kindMask & kind) != 0) { + bundle.listener.folderChanged((IFolder)r, kind); + } + } + return true; + } else if (type == IResource.PROJECT) { + int flags = delta.getFlags(); + + if (flags == IResourceDelta.OPEN) { + // the project is opening or closing. + IProject project = (IProject)r; + + if (project.isOpen()) { + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + pl.projectOpened(project); + } + } else { + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + pl.projectClosed(project); + } + } + } + } + + return true; + } + } + + public static ResourceMonitor getMonitor() { + return sThis; + } + + + /** + * Starts the resource monitoring. + * @param ws The current workspace. + * @return The monitor object. + */ + public static ResourceMonitor startMonitoring(IWorkspace ws) { + if (sThis != null) { + ws.addResourceChangeListener(sThis, + IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE); + sThis.mWorkspace = ws; + } + return sThis; + } + + /** + * Stops the resource monitoring. + * @param ws The current workspace. + */ + public static void stopMonitoring(IWorkspace ws) { + if (sThis != null) { + ws.removeResourceChangeListener(sThis); + + sThis.mFileListeners.clear(); + sThis.mProjectListeners.clear(); + } + } + + /** + * Adds a file listener. + * @param listener The listener to receive the events. + * @param kindMask The event mask to filter out specific events. + * {@link ListenerBundle#MASK_NONE} will forward all events. + */ + public synchronized void addFileListener(IFileListener listener, int kindMask) { + FileListenerBundle bundle = new FileListenerBundle(); + bundle.listener = listener; + bundle.kindMask = kindMask; + + mFileListeners.add(bundle); + } + + /** + * Removes an existing file listener. + * @param listener the listener to remove. + */ + public synchronized void removeFileListener(IFileListener listener) { + for (int i = 0 ; i < mFileListeners.size() ; i++) { + FileListenerBundle bundle = mFileListeners.get(i); + if (bundle.listener == listener) { + mFileListeners.remove(i); + return; + } + } + } + + /** + * Adds a folder listener. + * @param listener The listener to receive the events. + * @param kindMask The event mask to filter out specific events. + * {@link ListenerBundle#MASK_NONE} will forward all events. + */ + public synchronized void addFolderListener(IFolderListener listener, int kindMask) { + FolderListenerBundle bundle = new FolderListenerBundle(); + bundle.listener = listener; + bundle.kindMask = kindMask; + + mFolderListeners.add(bundle); + } + + /** + * Removes an existing folder listener. + * @param listener the listener to remove. + */ + public synchronized void removeFolderListener(IFolderListener listener) { + for (int i = 0 ; i < mFolderListeners.size() ; i++) { + FolderListenerBundle bundle = mFolderListeners.get(i); + if (bundle.listener == listener) { + mFolderListeners.remove(i); + return; + } + } + } + + /** + * Adds a project listener. + * @param listener The listener to receive the events. + */ + public synchronized void addProjectListener(IProjectListener listener) { + mProjectListeners.add(listener); + + // we need to look at the opened projects and give them to the listener. + + // get the list of opened android projects. + IWorkspaceRoot workspaceRoot = mWorkspace.getRoot(); + IJavaModel javaModel = JavaCore.create(workspaceRoot); + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(javaModel); + + for (IJavaProject androidProject : androidProjects) { + listener.projectOpenedWithWorkspace(androidProject.getProject()); + } + } + + /** + * Removes an existing project listener. + * @param listener the listener to remove. + */ + public synchronized void removeProjectListener(IProjectListener listener) { + mProjectListeners.remove(listener); + } + + /** + * Adds a resource event listener. + * @param listener The listener to receive the events. + */ + public synchronized void addResourceEventListener(IResourceEventListener listener) { + mEventListeners.add(listener); + } + + /** + * Removes an existing Resource Event listener. + * @param listener the listener to remove. + */ + public synchronized void removeResourceEventListener(IResourceEventListener listener) { + mEventListeners.remove(listener); + } + + /** + * Processes the workspace resource change events. + */ + public void resourceChanged(IResourceChangeEvent event) { + // notify the event listeners of a start. + for (IResourceEventListener listener : mEventListeners) { + listener.resourceChangeEventStart(); + } + + if (event.getType() == IResourceChangeEvent.PRE_DELETE) { + // a project is being deleted. Lets get the project object and remove + // its compiled resource list. + IResource r = event.getResource(); + IProject project = r.getProject(); + + // notify the listeners. + for (IProjectListener pl : mProjectListeners) { + pl.projectDeleted(project); + } + } else { + // this a regular resource change. We get the delta and go through it with a visitor. + IResourceDelta delta = event.getDelta(); + + DeltaVisitor visitor = new DeltaVisitor(); + try { + delta.accept(visitor); + } catch (CoreException e) { + } + } + + // we're done, notify the event listeners. + for (IResourceEventListener listener : mEventListeners) { + listener.resourceChangeEventEnd(); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java new file mode 100644 index 0000000..32b1107 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.utils.ResourceValue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.SAXParserFactory; + +/** + * Represents a resource file describing a single resource. + * <p/> + * This is typically an XML file inside res/anim, res/layout, or res/menu or an image file + * under res/drawable. + */ +public class SingleResourceFile extends ResourceFile { + + private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance(); + static { + sParserFactory.setNamespaceAware(true); + } + + private final static Pattern sXmlPattern = Pattern.compile("^(.+)\\.xml", //$NON-NLS-1$ + Pattern.CASE_INSENSITIVE); + + private final static Pattern[] sDrawablePattern = new Pattern[] { + Pattern.compile("^(.+)\\.9\\.png", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$ + Pattern.compile("^(.+)\\.png", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$ + Pattern.compile("^(.+)\\.jpg", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$ + Pattern.compile("^(.+)\\.gif", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$ + }; + + private String mResourceName; + private ResourceType mType; + private IResourceValue mValue; + + public SingleResourceFile(IAbstractFile file, ResourceFolder folder) { + super(file, folder); + + // we need to infer the type of the resource from the folder type. + // This is easy since this is a single Resource file. + ResourceType[] types = FolderTypeRelationship.getRelatedResourceTypes(folder.getType()); + mType = types[0]; + + // compute the resource name + mResourceName = getResourceName(mType); + + mValue = new ResourceValue(mType.getName(), getResourceName(mType), file.getOsLocation(), + isFramework()); + } + + @Override + public ResourceType[] getResourceTypes() { + return FolderTypeRelationship.getRelatedResourceTypes(getFolder().getType()); + } + + @Override + public boolean hasResources(ResourceType type) { + return FolderTypeRelationship.match(type, getFolder().getType()); + } + + @Override + public Collection<ProjectResourceItem> getResources(ResourceType type, + ProjectResources projectResources) { + + // looking for an existing ResourceItem with this name and type + ProjectResourceItem item = projectResources.findResourceItem(type, mResourceName); + + ArrayList<ProjectResourceItem> items = new ArrayList<ProjectResourceItem>(); + + if (item == null) { + item = new ConfigurableResourceItem(mResourceName); + items.add(item); + } + + // add this ResourceFile to the ResourceItem + item.add(this); + + return items; + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceFile#getValue(com.android.ide.eclipse.common.resources.ResourceType, java.lang.String) + * + * This particular implementation does not care about the type or name since a + * SingleResourceFile represents a file generating only one resource. + * The value returned is the full absolute path of the file in OS form. + */ + @Override + public IResourceValue getValue(ResourceType type, String name) { + return mValue; + } + + /** + * Returns the name of the resources. + */ + private String getResourceName(ResourceType type) { + // get the name from the filename. + String name = getFile().getName(); + + if (type == ResourceType.ANIM || type == ResourceType.LAYOUT || type == ResourceType.MENU || + type == ResourceType.COLOR || type == ResourceType.XML) { + Matcher m = sXmlPattern.matcher(name); + if (m.matches()) { + return m.group(1); + } + } else if (type == ResourceType.DRAWABLE) { + for (Pattern p : sDrawablePattern) { + Matcher m = p.matcher(name); + if (m.matches()) { + return m.group(1); + } + } + + // also try the Xml pattern for selector/shape based drawable. + Matcher m = sXmlPattern.matcher(name); + if (m.matches()) { + return m.group(1); + } + } + return name; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java new file mode 100644 index 0000000..d99cb13 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * An implementation of {@link IAbstractFile} on top of a {@link File} object. + * + */ +public class FileWrapper implements IAbstractFile { + + private File mFile; + + /** + * Constructs a {@link FileWrapper} object. If {@link File#isFile()} returns <code>false</code> + * then an {@link IOException} is thrown. + */ + public FileWrapper(File file) throws IOException { + if (file.isFile() == false) { + throw new IOException("FileWrapper must wrap a File object representing an existing file!"); //$NON-NLS-1$ + } + + mFile = file; + } + + public InputStream getContents() { + try { + return new FileInputStream(mFile); + } catch (FileNotFoundException e) { + // we'll return null below. + } + + return null; + } + + public IFile getIFile() { + return null; + } + + public String getOsLocation() { + return mFile.getAbsolutePath(); + } + + public String getName() { + return mFile.getName(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FileWrapper) { + return mFile.equals(((FileWrapper)obj).mFile); + } + + if (obj instanceof File) { + return mFile.equals(obj); + } + + return super.equals(obj); + } + + @Override + public int hashCode() { + return mFile.hashCode(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java new file mode 100644 index 0000000..9ad7460 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFolder; + +import java.io.File; +import java.io.IOException; + +/** + * An implementation of {@link IAbstractFolder} on top of a {@link File} object. + */ +public class FolderWrapper implements IAbstractFolder { + + private File mFolder; + + /** + * Constructs a {@link FileWrapper} object. If {@link File#isDirectory()} returns + * <code>false</code> then an {@link IOException} is thrown. + */ + public FolderWrapper(File folder) throws IOException { + if (folder.isDirectory() == false) { + throw new IOException("FileWrapper must wrap a File object representing an existing folder!"); //$NON-NLS-1$ + } + + mFolder = folder; + } + + public boolean hasFile(String name) { + return false; + } + + public String getName() { + return mFolder.getName(); + } + + public IFolder getIFolder() { + return null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FolderWrapper) { + return mFolder.equals(((FolderWrapper)obj).mFolder); + } + + if (obj instanceof File) { + return mFolder.equals(obj); + } + + return super.equals(obj); + } + + @Override + public int hashCode() { + return mFolder.hashCode(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java new file mode 100644 index 0000000..7e807f9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; + +import java.io.InputStream; + +/** + * A file. + */ +public interface IAbstractFile extends IAbstractResource { + + /** + * Returns an {@link InputStream} object on the file content. + * @throws CoreException + */ + InputStream getContents() throws CoreException; + + /** + * Returns the OS path of the file location. + */ + String getOsLocation(); + + /** + * Returns the {@link IFile} object that the receiver could represent. Can be <code>null</code> + */ + IFile getIFile(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java new file mode 100644 index 0000000..b35283d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFolder; + +/** + * A folder. + */ +public interface IAbstractFolder extends IAbstractResource { + + /** + * Returns true if the receiver contains a file with a given name + * @param name the name of the file. This is the name without the path leading to the + * parent folder. + */ + boolean hasFile(String name); + + /** + * Returns the {@link IFolder} object that the receiver could represent. + * Can be <code>null</code> + */ + IFolder getIFolder(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java new file mode 100644 index 0000000..daf243d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFile; + +import java.io.File; + +/** + * Base representation of a file system resource.<p/> + * This somewhat limited interface is designed to let classes use file-system resources, without + * having the manually handle {@link IFile} and/or {@link File} manually. + */ +public interface IAbstractResource { + + /** + * Returns the name of the resource. + */ + String getName(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java new file mode 100644 index 0000000..f0f5f2d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; + +import java.io.InputStream; + +/** + * An implementation of {@link IAbstractFile} on top of an {@link IFile} object. + */ +public class IFileWrapper implements IAbstractFile { + + private IFile mFile; + + public IFileWrapper(IFile file) { + mFile = file; + } + + public InputStream getContents() throws CoreException { + return mFile.getContents(); + } + + public String getOsLocation() { + return mFile.getLocation().toOSString(); + } + + public String getName() { + return mFile.getName(); + } + + public IFile getIFile() { + return mFile; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof IFileWrapper) { + return mFile.equals(((IFileWrapper)obj).mFile); + } + + if (obj instanceof IFile) { + return mFile.equals(obj); + } + + return super.equals(obj); + } + + @Override + public int hashCode() { + return mFile.hashCode(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java new file mode 100644 index 0000000..b1fa3ef --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager.files; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; + +/** + * An implementation of {@link IAbstractFolder} on top of an {@link IFolder} object. + */ +public class IFolderWrapper implements IAbstractFolder { + + private IFolder mFolder; + + public IFolderWrapper(IFolder folder) { + mFolder = folder; + } + + public String getName() { + return mFolder.getName(); + } + + public boolean hasFile(String name) { + try { + IResource[] files = mFolder.members(); + for (IResource file : files) { + if (name.equals(file.getName())) { + return true; + } + } + } catch (CoreException e) { + // we'll return false below. + } + + return false; + } + + public IFolder getIFolder() { + return mFolder; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof IFolderWrapper) { + return mFolder.equals(((IFolderWrapper)obj).mFolder); + } + + if (obj instanceof IFolder) { + return mFolder.equals(obj); + } + + return super.equals(obj); + } + + @Override + public int hashCode() { + return mFolder.hashCode(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java new file mode 100644 index 0000000..29453e9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.uimodel; + +import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.editors.uimodel.UiTextValueNode; + +import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.widgets.Text; + +import java.util.regex.Pattern; + +/** + * Displays and edits a color XML element value with a custom validator. + * <p/> + * See {@link UiAttributeNode} for more information. + */ +public class UiColorValueNode extends UiTextValueNode { + + /** Accepted RGBA formats are one of #RGB, #ARGB, #RRGGBB or #AARRGGBB. */ + private static final Pattern RGBA_REGEXP = Pattern.compile( + "#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})"); //$NON-NLS-1$ + + public UiColorValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (non-java doc) + * + * Add a modify listener that will check colors have the proper format, + * that is one of #RGB, #ARGB, #RRGGBB or #AARRGGBB. + */ + @Override + protected void onAddValidators(final Text text) { + ModifyListener listener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + String color = text.getText(); + if (RGBA_REGEXP.matcher(color).matches()) { + getManagedForm().getMessageManager().removeMessage(text, text); + } else { + getManagedForm().getMessageManager().addMessage(text, + "Accepted color formats are one of #RGB, #ARGB, #RRGGBB or #AARRGGBB.", + null /* data */, IMessageProvider.ERROR, text); + } + } + }; + + text.addModifyListener(listener); + + // Make sure the validator removes its message(s) when the widget is disposed + text.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + getManagedForm().getMessageManager().removeMessage(text, text); + } + }); + + // Finally call the validator once to make sure the initial value is processed + listener.modifyText(null); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java new file mode 100644 index 0000000..89649f5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.uimodel; + +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.resources.descriptors.ItemElementDescriptor; +import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * {@link UiItemElementNode} is apecial version of {@link UiElementNode} that + * customizes the element display to include the item type attribute if present. + */ +public class UiItemElementNode extends UiElementNode { + + /** + * Creates a new {@link UiElementNode} described by a given {@link ItemElementDescriptor}. + * + * @param elementDescriptor The {@link ItemElementDescriptor} for the XML node. Cannot be null. + */ + public UiItemElementNode(ItemElementDescriptor elementDescriptor) { + super(elementDescriptor); + } + + @Override + public String getShortDescription() { + Node xmlNode = getXmlNode(); + if (xmlNode != null && xmlNode instanceof Element && xmlNode.hasAttributes()) { + + Element elem = (Element) xmlNode; + String type = elem.getAttribute(ResourcesDescriptors.TYPE_ATTR); + String name = elem.getAttribute(ResourcesDescriptors.NAME_ATTR); + if (type != null && name != null && type.length() > 0 && name.length() > 0) { + type = DescriptorsUtils.capitalize(type); + return String.format("%1$s (%2$s %3$s)", name, type, getDescriptor().getUiName()); + } + } + + return super.getShortDescription(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java new file mode 100644 index 0000000..5fb479f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.viewers.DialogCellEditor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Text; + +import java.text.MessageFormat; + +/** + * Custom DialogCellEditor, replacing the Label with an editable {@link Text} widget. + * <p/>Also set the button to {@link SWT#FLAT} to make sure it looks good on MacOS X. + * <p/>Most of the code comes from TextCellEditor. + */ +public abstract class EditableDialogCellEditor extends DialogCellEditor { + + private Text text; + + private ModifyListener modifyListener; + + /** + * State information for updating action enablement + */ + private boolean isSelection = false; + + private boolean isDeleteable = false; + + private boolean isSelectable = false; + + EditableDialogCellEditor(Composite parent) { + super(parent); + } + + /* + * Re-implement this method only to properly set the style in the button, or it won't look + * good in MacOS X + */ + @Override + protected Button createButton(Composite parent) { + Button result = new Button(parent, SWT.DOWN | SWT.FLAT); + result.setText("..."); //$NON-NLS-1$ + return result; + } + + + @Override + protected Control createContents(Composite cell) { + text = new Text(cell, SWT.SINGLE); + text.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + handleDefaultSelection(e); + } + }); + text.addKeyListener(new KeyAdapter() { + // hook key pressed - see PR 14201 + @Override + public void keyPressed(KeyEvent e) { + keyReleaseOccured(e); + + // as a result of processing the above call, clients may have + // disposed this cell editor + if ((getControl() == null) || getControl().isDisposed()) { + return; + } + checkSelection(); // see explanation below + checkDeleteable(); + checkSelectable(); + } + }); + text.addTraverseListener(new TraverseListener() { + public void keyTraversed(TraverseEvent e) { + if (e.detail == SWT.TRAVERSE_ESCAPE + || e.detail == SWT.TRAVERSE_RETURN) { + e.doit = false; + } + } + }); + // We really want a selection listener but it is not supported so we + // use a key listener and a mouse listener to know when selection changes + // may have occurred + text.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent e) { + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + }); + text.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + EditableDialogCellEditor.this.focusLost(); + } + }); + text.setFont(cell.getFont()); + text.setBackground(cell.getBackground()); + text.setText("");//$NON-NLS-1$ + text.addModifyListener(getModifyListener()); + return text; + } + + /** + * Checks to see if the "deletable" state (can delete/ + * nothing to delete) has changed and if so fire an + * enablement changed notification. + */ + private void checkDeleteable() { + boolean oldIsDeleteable = isDeleteable; + isDeleteable = isDeleteEnabled(); + if (oldIsDeleteable != isDeleteable) { + fireEnablementChanged(DELETE); + } + } + + /** + * Checks to see if the "selectable" state (can select) + * has changed and if so fire an enablement changed notification. + */ + private void checkSelectable() { + boolean oldIsSelectable = isSelectable; + isSelectable = isSelectAllEnabled(); + if (oldIsSelectable != isSelectable) { + fireEnablementChanged(SELECT_ALL); + } + } + + /** + * Checks to see if the selection state (selection / + * no selection) has changed and if so fire an + * enablement changed notification. + */ + private void checkSelection() { + boolean oldIsSelection = isSelection; + isSelection = text.getSelectionCount() > 0; + if (oldIsSelection != isSelection) { + fireEnablementChanged(COPY); + fireEnablementChanged(CUT); + } + } + + /* (non-Javadoc) + * Method declared on CellEditor. + */ + @Override + protected void doSetFocus() { + if (text != null) { + text.selectAll(); + text.setFocus(); + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + } + + /* + * (non-Javadoc) + * @see org.eclipse.jface.viewers.DialogCellEditor#updateContents(java.lang.Object) + */ + @Override + protected void updateContents(Object value) { + Assert.isTrue(text != null && (value == null || (value instanceof String))); + if (value != null) { + text.removeModifyListener(getModifyListener()); + text.setText((String) value); + text.addModifyListener(getModifyListener()); + } + } + + /** + * The <code>TextCellEditor</code> implementation of + * this <code>CellEditor</code> framework method returns + * the text string. + * + * @return the text string + */ + @Override + protected Object doGetValue() { + return text.getText(); + } + + + /** + * Processes a modify event that occurred in this text cell editor. + * This framework method performs validation and sets the error message + * accordingly, and then reports a change via <code>fireEditorValueChanged</code>. + * Subclasses should call this method at appropriate times. Subclasses + * may extend or reimplement. + * + * @param e the SWT modify event + */ + protected void editOccured(ModifyEvent e) { + String value = text.getText(); + if (value == null) { + value = "";//$NON-NLS-1$ + } + Object typedValue = value; + boolean oldValidState = isValueValid(); + boolean newValidState = isCorrect(typedValue); + + if (!newValidState) { + // try to insert the current value into the error message. + setErrorMessage(MessageFormat.format(getErrorMessage(), + new Object[] { value })); + } + valueChanged(oldValidState, newValidState); + } + + /** + * Return the modify listener. + */ + private ModifyListener getModifyListener() { + if (modifyListener == null) { + modifyListener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + editOccured(e); + } + }; + } + return modifyListener; + } + + /** + * Handles a default selection event from the text control by applying the editor + * value and deactivating this cell editor. + * + * @param event the selection event + * + * @since 3.0 + */ + protected void handleDefaultSelection(SelectionEvent event) { + // same with enter-key handling code in keyReleaseOccured(e); + fireApplyEditorValue(); + deactivate(); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method returns <code>true</code> if + * the current selection is not empty. + */ + @Override + public boolean isCopyEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getSelectionCount() > 0; + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method returns <code>true</code> if + * the current selection is not empty. + */ + @Override + public boolean isCutEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getSelectionCount() > 0; + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method returns <code>true</code> + * if there is a selection or if the caret is not positioned + * at the end of the text. + */ + @Override + public boolean isDeleteEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getSelectionCount() > 0 + || text.getCaretPosition() < text.getCharCount(); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method always returns <code>true</code>. + */ + @Override + public boolean isPasteEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return true; + } + + /** + * Check if save all is enabled + * @return true if it is + */ + public boolean isSaveAllEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return true; + } + + /** + * Returns <code>true</code> if this cell editor is + * able to perform the select all action. + * <p> + * This default implementation always returns + * <code>false</code>. + * </p> + * <p> + * Subclasses may override + * </p> + * @return <code>true</code> if select all is possible, + * <code>false</code> otherwise + */ + @Override + public boolean isSelectAllEnabled() { + if (text == null || text.isDisposed()) { + return false; + } + return text.getCharCount() > 0; + } + + /** + * Processes a key release event that occurred in this cell editor. + * <p> + * The <code>TextCellEditor</code> implementation of this framework method + * ignores when the RETURN key is pressed since this is handled in + * <code>handleDefaultSelection</code>. + * An exception is made for Ctrl+Enter for multi-line texts, since + * a default selection event is not sent in this case. + * </p> + * + * @param keyEvent the key event + */ + @Override + protected void keyReleaseOccured(KeyEvent keyEvent) { + if (keyEvent.character == '\r') { // Return key + // Enter is handled in handleDefaultSelection. + // Do not apply the editor value in response to an Enter key event + // since this can be received from the IME when the intent is -not- + // to apply the value. + // See bug 39074 [CellEditors] [DBCS] canna input mode fires bogus event from Text Control + // + // An exception is made for Ctrl+Enter for multi-line texts, since + // a default selection event is not sent in this case. + if (text != null && !text.isDisposed() + && (text.getStyle() & SWT.MULTI) != 0) { + if ((keyEvent.stateMask & SWT.CTRL) != 0) { + super.keyReleaseOccured(keyEvent); + } + } + return; + } + super.keyReleaseOccured(keyEvent); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method copies the + * current selection to the clipboard. + */ + @Override + public void performCopy() { + text.copy(); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method cuts the + * current selection to the clipboard. + */ + @Override + public void performCut() { + text.cut(); + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method deletes the + * current selection or, if there is no selection, + * the character next character from the current position. + */ + @Override + public void performDelete() { + if (text.getSelectionCount() > 0) { + // remove the contents of the current selection + text.insert(""); //$NON-NLS-1$ + } else { + // remove the next character + int pos = text.getCaretPosition(); + if (pos < text.getCharCount()) { + text.setSelection(pos, pos + 1); + text.insert(""); //$NON-NLS-1$ + } + } + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method pastes the + * the clipboard contents over the current selection. + */ + @Override + public void performPaste() { + text.paste(); + checkSelection(); + checkDeleteable(); + checkSelectable(); + } + + /** + * The <code>TextCellEditor</code> implementation of this + * <code>CellEditor</code> method selects all of the + * current text. + */ + @Override + public void performSelectAll() { + text.selectAll(); + checkSelection(); + checkDeleteable(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java new file mode 100644 index 0000000..d095376 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java @@ -0,0 +1,47 @@ +package com.android.ide.eclipse.editors.ui; + +import org.eclipse.jface.resource.CompositeImageDescriptor; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.DecorationOverlayIcon; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; + +/** + * ImageDescriptor that adds a error marker. + * Based on {@link DecorationOverlayIcon} only available in Eclipse 3.3 + */ +public class ErrorImageComposite extends CompositeImageDescriptor { + + private Image mBaseImage; + private ImageDescriptor mErrorImageDescriptor; + private Point mSize; + + public ErrorImageComposite(Image baseImage) { + mBaseImage = baseImage; + mErrorImageDescriptor = PlatformUI.getWorkbench().getSharedImages().getImageDescriptor( + ISharedImages.IMG_OBJS_ERROR_TSK); + mSize = new Point(baseImage.getBounds().width, baseImage.getBounds().height); + } + + @Override + protected void drawCompositeImage(int width, int height) { + ImageData baseData = mBaseImage.getImageData(); + drawImage(baseData, 0, 0); + + ImageData overlayData = mErrorImageDescriptor.getImageData(); + if (overlayData.width == baseData.width && baseData.height == baseData.height) { + overlayData = overlayData.scaledTo(14, 14); + drawImage(overlayData, -3, mSize.y - overlayData.height + 3); + } else { + drawImage(overlayData, 0, mSize.y - overlayData.height); + } + } + + @Override + protected Point getSize() { + return mSize; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java new file mode 100644 index 0000000..ccae099 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** + * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)} + * method. + * <p/>The dialog box opened is the same as the one in the ui created by + * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)} + */ +public class FlagValueCellEditor extends EditableDialogCellEditor { + + private UiFlagAttributeNode mUiFlagAttribute; + + public FlagValueCellEditor(Composite parent) { + super(parent); + } + + @Override + protected Object openDialogBox(Control cellEditorWindow) { + if (mUiFlagAttribute != null) { + String currentValue = (String)getValue(); + return mUiFlagAttribute.showDialog(cellEditorWindow.getShell(), currentValue); + } + + return null; + } + + @Override + protected void doSetValue(Object value) { + if (value instanceof UiFlagAttributeNode) { + mUiFlagAttribute = (UiFlagAttributeNode)value; + super.doSetValue(mUiFlagAttribute.getCurrentValue()); + return; + } + + super.doSetValue(value); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java new file mode 100644 index 0000000..304dd14 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode; + +import org.eclipse.jface.viewers.ComboBoxCellEditor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** + * ComboBoxCellEditor able to receive a {@link UiListAttributeNode} in the {@link #setValue(Object)} + * method, and returning a {@link String} in {@link #getValue()} instead of an {@link Integer}. + */ +public class ListValueCellEditor extends ComboBoxCellEditor { + private String[] mItems; + private CCombo mCombo; + + public ListValueCellEditor(Composite parent) { + super(parent, new String[0], SWT.DROP_DOWN); + } + + @Override + protected Control createControl(Composite parent) { + mCombo = (CCombo) super.createControl(parent); + return mCombo; + } + + @Override + protected void doSetValue(Object value) { + if (value instanceof UiListAttributeNode) { + UiListAttributeNode uiListAttribute = (UiListAttributeNode)value; + + // set the possible values in the combo + String[] items = uiListAttribute.getPossibleValues(); + mItems = new String[items.length]; + System.arraycopy(items, 0, mItems, 0, items.length); + setItems(mItems); + + // now edit the current value of the attribute + String attrValue = uiListAttribute.getCurrentValue(); + mCombo.setText(attrValue); + + return; + } + + // default behavior + super.doSetValue(value); + } + + @Override + protected Object doGetValue() { + String comboText = mCombo.getText(); + if (comboText == null) { + return ""; //$NON-NLS-1$ + } + return comboText; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java new file mode 100644 index 0000000..4fc0ab3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** + * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)} + * method. + * <p/>The dialog box opened is the same as the one in the ui created by + * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)} + */ +public class ResourceValueCellEditor extends EditableDialogCellEditor { + + private UiResourceAttributeNode mUiResourceAttribute; + + public ResourceValueCellEditor(Composite parent) { + super(parent); + } + + @Override + protected Object openDialogBox(Control cellEditorWindow) { + if (mUiResourceAttribute != null) { + String currentValue = (String)getValue(); + return mUiResourceAttribute.showDialog(cellEditorWindow.getShell(), currentValue); + } + + return null; + } + + @Override + protected void doSetValue(Object value) { + if (value instanceof UiResourceAttributeNode) { + mUiResourceAttribute = (UiResourceAttributeNode)value; + super.doSetValue(mUiResourceAttribute.getCurrentValue()); + return; + } + + super.doSetValue(value); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java new file mode 100644 index 0000000..409e92f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.AndroidEditor; + +import org.eclipse.jface.text.DefaultInformationControl; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseTrackListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.SectionPart; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; +import org.eclipse.ui.forms.widgets.TableWrapData; +import org.eclipse.ui.forms.widgets.TableWrapLayout; + +import java.lang.reflect.Method; + +/** + * Helper for the AndroidManifest form editor. + * + * Helps create a new SectionPart with sensible default parameters, + * create default layout or add typical widgets. + * + * IMPORTANT: This is NOT a generic class. It makes a lot of assumptions on the + * UI as used by the form editor for the AndroidManifest. + * + * TODO: Consider moving to a common package. + */ +public final class SectionHelper { + + /** + * Utility class that derives from SectionPart, constructs the Section with + * sensible defaults (with a title and a description) and provide some shorthand + * methods for creating typically UI (label and text, form text.) + */ + static public class ManifestSectionPart extends SectionPart { + + /** + * Construct a SectionPart that uses a title bar and a description. + * It's up to the caller to call setText() and setDescription(). + * <p/> + * The section style includes a description and a title bar by default. + * + * @param body The parent (e.g. FormPage body) + * @param toolkit Form Toolkit + */ + public ManifestSectionPart(Composite body, FormToolkit toolkit) { + this(body, toolkit, 0, false); + } + + /** + * Construct a SectionPart that uses a title bar and a description. + * It's up to the caller to call setText() and setDescription(). + * <p/> + * The section style includes a description and a title bar by default. + * You can add extra styles, like Section.TWISTIE. + * + * @param body The parent (e.g. FormPage body). + * @param toolkit Form Toolkit. + * @param extra_style Extra styles (on top of description and title bar). + * @param use_description True if the Section.DESCRIPTION style should be added. + */ + public ManifestSectionPart(Composite body, FormToolkit toolkit, + int extra_style, boolean use_description) { + super(body, toolkit, extra_style | + Section.TITLE_BAR | + (use_description ? Section.DESCRIPTION : 0)); + } + + // Create non-static methods of helpers just for convenience + + /** + * Creates a new composite with a TableWrapLayout set with a given number of columns. + * + * If the parent composite is a Section, the new composite is set as a client. + * + * @param toolkit Form Toolkit + * @param numColumns Desired number of columns. + * @return The new composite. + */ + public Composite createTableLayout(FormToolkit toolkit, int numColumns) { + return SectionHelper.createTableLayout(getSection(), toolkit, numColumns); + } + + /** + * Creates a label widget. + * If the parent layout if a TableWrapLayout, maximize it to span over all columns. + * + * @param parent The parent (e.g. composite from CreateTableLayout()) + * @param toolkit Form Toolkit + * @param label The string for the label. + * @param tooltip An optional tooltip for the label and text. Can be null. + * @return The new created label + */ + public Label createLabel(Composite parent, FormToolkit toolkit, String label, + String tooltip) { + return SectionHelper.createLabel(parent, toolkit, label, tooltip); + } + + /** + * Creates two widgets: a label and a text field. + * + * This expects the parent composite to have a TableWrapLayout with 2 columns. + * + * @param parent The parent (e.g. composite from CreateTableLayout()) + * @param toolkit Form Toolkit + * @param label The string for the label. + * @param value The initial value of the text field. Can be null. + * @param tooltip An optional tooltip for the label and text. Can be null. + * @return The new created Text field (the label is not returned) + */ + public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label, + String value, String tooltip) { + return SectionHelper.createLabelAndText(parent, toolkit, label, value, tooltip); + } + + /** + * Creates a FormText widget. + * + * This expects the parent composite to have a TableWrapLayout with 2 columns. + * + * @param parent The parent (e.g. composite from CreateTableLayout()) + * @param toolkit Form Toolkit + * @param isHtml True if the form text will contain HTML that must be interpreted as + * rich text (i.e. parse tags & expand URLs). + * @param label The string for the label. + * @param setupLayoutData indicates whether the created form text receives a TableWrapData + * through the setLayoutData method. In some case, creating it will make the table parent + * huge, which we don't want. + * @return The new created FormText. + */ + public FormText createFormText(Composite parent, FormToolkit toolkit, boolean isHtml, + String label, boolean setupLayoutData) { + return SectionHelper.createFormText(parent, toolkit, isHtml, label, setupLayoutData); + } + + /** + * Forces the section to recompute its layout and redraw. + * This is needed after the content of the section has been changed at runtime. + */ + public void layoutChanged() { + Section section = getSection(); + + // Calls getSection().reflow(), which is the same that Section calls + // when the expandable state is changed and the height changes. + // Since this is protected, some reflection is needed to invoke it. + try { + Method reflow; + reflow = section.getClass().getDeclaredMethod("reflow", (Class<?>[])null); + reflow.setAccessible(true); + reflow.invoke(section); + } catch (Exception e) { + AdtPlugin.log(e, "Error when invoking Section.reflow"); + } + + section.layout(true /* changed */, true /* all */); + } + } + + /** + * Creates a new composite with a TableWrapLayout set with a given number of columns. + * + * If the parent composite is a Section, the new composite is set as a client. + * + * @param composite The parent (e.g. a Section or SectionPart) + * @param toolkit Form Toolkit + * @param numColumns Desired number of columns. + * @return The new composite. + */ + static public Composite createTableLayout(Composite composite, FormToolkit toolkit, + int numColumns) { + Composite table = toolkit.createComposite(composite); + TableWrapLayout layout = new TableWrapLayout(); + layout.numColumns = numColumns; + table.setLayout(layout); + toolkit.paintBordersFor(table); + if (composite instanceof Section) { + ((Section) composite).setClient(table); + } + return table; + } + + /** + * Creates a new composite with a GridLayout set with a given number of columns. + * + * If the parent composite is a Section, the new composite is set as a client. + * + * @param composite The parent (e.g. a Section or SectionPart) + * @param toolkit Form Toolkit + * @param numColumns Desired number of columns. + * @return The new composite. + */ + static public Composite createGridLayout(Composite composite, FormToolkit toolkit, + int numColumns) { + Composite grid = toolkit.createComposite(composite); + GridLayout layout = new GridLayout(); + layout.numColumns = numColumns; + grid.setLayout(layout); + toolkit.paintBordersFor(grid); + if (composite instanceof Section) { + ((Section) composite).setClient(grid); + } + return grid; + } + + /** + * Creates two widgets: a label and a text field. + * + * This expects the parent composite to have a TableWrapLayout with 2 columns. + * + * @param parent The parent (e.g. composite from CreateTableLayout()) + * @param toolkit Form Toolkit + * @param label_text The string for the label. + * @param value The initial value of the text field. Can be null. + * @param tooltip An optional tooltip for the label and text. Can be null. + * @return The new created Text field (the label is not returned) + */ + static public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label_text, + String value, String tooltip) { + Label label = toolkit.createLabel(parent, label_text); + label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); + Text text = toolkit.createText(parent, value); + text.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE)); + + addControlTooltip(label, tooltip); + return text; + } + + /** + * Creates a label widget. + * If the parent layout if a TableWrapLayout, maximize it to span over all columns. + * + * @param parent The parent (e.g. composite from CreateTableLayout()) + * @param toolkit Form Toolkit + * @param label_text The string for the label. + * @param tooltip An optional tooltip for the label and text. Can be null. + * @return The new created label + */ + static public Label createLabel(Composite parent, FormToolkit toolkit, String label_text, + String tooltip) { + Label label = toolkit.createLabel(parent, label_text); + + TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB); + if (parent.getLayout() instanceof TableWrapLayout) { + twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns; + } + label.setLayoutData(twd); + + addControlTooltip(label, tooltip); + return label; + } + + /** + * Associates a tooltip with a control. + * + * This mirrors the behavior from org.eclipse.pde.internal.ui.editor.text.PDETextHover + * + * @param control The control to which associate the tooltip. + * @param tooltip The tooltip string. Can use \n for multi-lines. Will not display if null. + */ + static public void addControlTooltip(final Control control, String tooltip) { + if (control == null || tooltip == null || tooltip.length() == 0) { + return; + } + + // Some kinds of controls already properly implement tooltip display. + if (control instanceof Button) { + control.setToolTipText(tooltip); + return; + } + + control.setToolTipText(null); + + final DefaultInformationControl ic = new DefaultInformationControl(control.getShell()); + ic.setInformation(tooltip); + Point sz = ic.computeSizeHint(); + ic.setSize(sz.x, sz.y); + ic.setVisible(false); // initially hidden + + control.addMouseTrackListener(new MouseTrackListener() { + public void mouseEnter(MouseEvent e) { + } + + public void mouseExit(MouseEvent e) { + ic.setVisible(false); + } + + public void mouseHover(MouseEvent e) { + ic.setLocation(control.toDisplay(10, 25)); // same offset as in PDETextHover + ic.setVisible(true); + } + }); + } + + /** + * Creates a FormText widget. + * + * This expects the parent composite to have a TableWrapLayout with 2 columns. + * + * @param parent The parent (e.g. composite from CreateTableLayout()) + * @param toolkit Form Toolkit + * @param isHtml True if the form text will contain HTML that must be interpreted as + * rich text (i.e. parse tags & expand URLs). + * @param label The string for the label. + * @param setupLayoutData indicates whether the created form text receives a TableWrapData + * through the setLayoutData method. In some case, creating it will make the table parent + * huge, which we don't want. + * @return The new created FormText. + */ + static public FormText createFormText(Composite parent, FormToolkit toolkit, + boolean isHtml, String label, boolean setupLayoutData) { + FormText text = toolkit.createFormText(parent, true /* track focus */); + if (setupLayoutData) { + TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB); + twd.maxWidth = AndroidEditor.TEXT_WIDTH_HINT; + if (parent.getLayout() instanceof TableWrapLayout) { + twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns; + } + text.setLayoutData(twd); + } + text.setWhitespaceNormalized(true); + text.setText(label, isHtml /* parseTags */, isHtml /* expandURLs */); + return text; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java new file mode 100644 index 0000000..2fe5783 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; + +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * TextCellEditor able to receive a {@link UiAttributeNode} in the {@link #setValue(Object)} + * method. + */ +public class TextValueCellEditor extends TextCellEditor { + + public TextValueCellEditor(Composite parent) { + super(parent); + } + + @Override + protected void doSetValue(Object value) { + if (value instanceof UiAttributeNode) { + super.doSetValue(((UiAttributeNode)value).getCurrentValue()); + return; + } + + super.doSetValue(value); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java new file mode 100644 index 0000000..66773bd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +/** + * Generic page's section part that displays all attributes of a given {@link UiElementNode}. + * <p/> + * This part is designed to be displayed in a page that has a table column layout. + * It is linked to a specific {@link UiElementNode} and automatically displays all of its + * attributes, manages its dirty state and commits the attributes when necessary. + * <p/> + * No derivation is needed unless the UI or workflow needs to be extended. + */ +public class UiElementPart extends ManifestSectionPart { + + /** A reference to the container editor */ + private ManifestEditor mEditor; + /** The {@link UiElementNode} manipulated by this SectionPart. It can be null. */ + private UiElementNode mUiElementNode; + /** Table that contains all the attributes */ + private Composite mTable; + + public UiElementPart(Composite body, FormToolkit toolkit, ManifestEditor editor, + UiElementNode uiElementNode, String sectionTitle, String sectionDescription, + int extra_style) { + super(body, toolkit, extra_style, sectionDescription != null); + mEditor = editor; + mUiElementNode = uiElementNode; + setupSection(sectionTitle, sectionDescription); + + if (uiElementNode == null) { + // This is serious and should never happen. Instead of crashing, simply abort. + // There will be no UI, which will prevent further damage. + AdtPlugin.log(IStatus.ERROR, "Missing node to edit!"); //$NON-NLS-1$ + return; + } + } + + /** + * Returns the Editor associated with this part. + */ + public ManifestEditor getEditor() { + return mEditor; + } + + /** + * Returns the {@link UiElementNode} associated with this part. + */ + public UiElementNode getUiElementNode() { + return mUiElementNode; + } + + /** + * Changes the element node handled by this part. + * + * @param uiElementNode The new element node for the part. + */ + public void setUiElementNode(UiElementNode uiElementNode) { + mUiElementNode = uiElementNode; + } + + /** + * Initializes the form part. + * <p/> + * This is called by the owning managed form as soon as the part is added to the form, + * which happens right after the part is actually created. + */ + @Override + public void initialize(IManagedForm form) { + super.initialize(form); + createFormControls(form); + } + + /** + * Setup the section that contains this part. + * <p/> + * This is called by the constructor to set the section's title and description + * with parameters given in the constructor. + * <br/> + * Derived class override this if needed, however in most cases the default + * implementation should be enough. + * + * @param sectionTitle The section part's title + * @param sectionDescription The section part's description + */ + protected void setupSection(String sectionTitle, String sectionDescription) { + Section section = getSection(); + section.setText(sectionTitle); + section.setDescription(sectionDescription); + } + + /** + * Create the controls to edit the attributes for the given ElementDescriptor. + * <p/> + * This MUST not be called by the constructor. Instead it must be called from + * <code>initialize</code> (i.e. right after the form part is added to the managed form.) + * <p/> + * Derived classes can override this if necessary. + * + * @param managedForm The owner managed form + */ + protected void createFormControls(IManagedForm managedForm) { + setTable(createTableLayout(managedForm.getToolkit(), 2 /* numColumns */)); + + createUiAttributes(managedForm); + } + + /** + * Sets the table where the attribute UI needs to be created. + */ + protected void setTable(Composite table) { + mTable = table; + } + + /** + * Returns the table where the attribute UI needs to be created. + */ + protected Composite getTable() { + return mTable; + } + + /** + * Add all the attribute UI widgets into the underlying table layout. + * + * @param managedForm The owner managed form + */ + protected void createUiAttributes(IManagedForm managedForm) { + Composite table = getTable(); + if (table == null || managedForm == null) { + return; + } + + // Remove any old UI controls + for (Control c : table.getChildren()) { + c.dispose(); + } + + fillTable(table, managedForm); + + // Tell the section that the layout has changed. + layoutChanged(); + } + + /** + * Actually fills the table. + * This is called by {@link #createUiAttributes(IManagedForm)} to populate the new + * table. The default implementation is to use + * {@link #insertUiAttributes(UiElementNode, Composite, IManagedForm)} to actually + * place the attributes of the default {@link UiElementNode} in the table. + * <p/> + * Derived classes can override this to add controls in the table before or after. + * + * @param table The table to fill. It must have 2 columns. + * @param managedForm The managed form for new controls. + */ + protected void fillTable(Composite table, IManagedForm managedForm) { + int inserted = insertUiAttributes(mUiElementNode, table, managedForm); + + if (inserted == 0) { + createLabel(table, managedForm.getToolkit(), + "No attributes to display, waiting for SDK to finish loading...", + null /* tooltip */ ); + } + } + + /** + * Insert the UI attributes of the given {@link UiElementNode} in the given table. + * + * @param uiNode The {@link UiElementNode} that contains the attributes to display. + * Must not be null. + * @param table The table to fill. It must have 2 columns. + * @param managedForm The managed form for new controls. + * @return The number of UI attributes inserted. It is >= 0. + */ + protected int insertUiAttributes(UiElementNode uiNode, Composite table, IManagedForm managedForm) { + if (uiNode == null || table == null || managedForm == null) { + return 0; + } + + // To iterate over all attributes, we use the {@link ElementDescriptor} instead + // of the {@link UiElementNode} because the attributes' order is guaranteed in the + // descriptor but not in the node itself. + AttributeDescriptor[] attr_desc_list = uiNode.getAttributeDescriptors(); + for (AttributeDescriptor attr_desc : attr_desc_list) { + if (attr_desc instanceof XmlnsAttributeDescriptor) { + // Do not show hidden attributes + continue; + } + + UiAttributeNode ui_attr = uiNode.findUiAttribute(attr_desc); + if (ui_attr != null) { + ui_attr.createUiControl(table, managedForm); + } else { + // The XML has an extra attribute which wasn't declared in + // AndroidManifestDescriptors. This is not a problem, we just ignore it. + AdtPlugin.log(IStatus.WARNING, + "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$ + attr_desc.getXmlLocalName(), + uiNode.getDescriptor().getXmlName()); + } + } + return attr_desc_list.length; + } + + /** + * Tests whether the part is dirty i.e. its widgets have state that is + * newer than the data in the model. + * <p/> + * This is done by iterating over all attributes and updating the super's + * internal dirty flag. Stop once at least one attribute is dirty. + * + * @return <code>true</code> if the part is dirty, <code>false</code> + * otherwise. + */ + @Override + public boolean isDirty() { + if (mUiElementNode != null && !super.isDirty()) { + for (UiAttributeNode ui_attr : mUiElementNode.getUiAttributes()) { + if (ui_attr.isDirty()) { + markDirty(); + break; + } + } + } + return super.isDirty(); + } + + /** + * If part is displaying information loaded from a model, this method + * instructs it to commit the new (modified) data back into the model. + * + * @param onSave + * indicates if commit is called during 'save' operation or for + * some other reason (for example, if form is contained in a + * wizard or a multi-page editor and the user is about to leave + * the page). + */ + @Override + public void commit(boolean onSave) { + if (mUiElementNode != null) { + mEditor.editXmlModel(new Runnable() { + public void run() { + for (UiAttributeNode ui_attr : mUiElementNode.getUiAttributes()) { + ui_attr.commit(); + } + } + }); + } + + // We need to call super's commit after we synchronized the nodes to make sure we + // reset the dirty flag after all the side effects from committing have occurred. + super.commit(onSave); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java new file mode 100644 index 0000000..2aad217 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.apache.xml.serialize.Method; +import org.apache.xml.serialize.OutputFormat; +import org.apache.xml.serialize.XMLSerializer; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.document.NodeContainer; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + + +/** + * Provides Cut and Copy actions for the tree nodes. + */ +public class CopyCutAction extends Action { + private List<UiElementNode> mUiNodes; + private boolean mPerformCut; + private final AndroidEditor mEditor; + private final Clipboard mClipboard; + private final ICommitXml mXmlCommit; + + /** + * Creates a new Copy or Cut action. + * + * @param selected The UI node to cut or copy. It *must* have a non-null XML node. + * @param perform_cut True if the operation is cut, false if it is copy. + */ + public CopyCutAction(AndroidEditor editor, Clipboard clipboard, ICommitXml xmlCommit, + UiElementNode selected, boolean perform_cut) { + this(editor, clipboard, xmlCommit, toList(selected), perform_cut); + } + + /** + * Creates a new Copy or Cut action. + * + * @param selected The UI nodes to cut or copy. They *must* have a non-null XML node. + * The list becomes owned by the {@link CopyCutAction}. + * @param perform_cut True if the operation is cut, false if it is copy. + */ + public CopyCutAction(AndroidEditor editor, Clipboard clipboard, ICommitXml xmlCommit, + List<UiElementNode> selected, boolean perform_cut) { + super(perform_cut ? "Cut" : "Copy"); + mEditor = editor; + mClipboard = clipboard; + mXmlCommit = xmlCommit; + + ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); + if (perform_cut) { + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT)); + setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT)); + setDisabledImageDescriptor( + images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_DISABLED)); + } else { + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY)); + setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY)); + setDisabledImageDescriptor( + images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_DISABLED)); + } + + mUiNodes = selected; + mPerformCut = perform_cut; + } + + /** + * Performs the cut or copy action. + * First an XML serializer is used to turn the existing XML node into a valid + * XML fragment, which is added as text to the clipboard. + */ + @Override + public void run() { + super.run(); + if (mUiNodes == null || mUiNodes.size() < 1) { + return; + } + + // Commit the current pages first, to make sure the XML is in sync. + // Committing may change the XML structure. + if (mXmlCommit != null) { + mXmlCommit.commitPendingXmlChanges(); + } + + StringBuilder allText = new StringBuilder(); + ArrayList<UiElementNode> nodesToCut = mPerformCut ? new ArrayList<UiElementNode>() : null; + + for (UiElementNode uiNode : mUiNodes) { + try { + Node xml_node = uiNode.getXmlNode(); + if (xml_node == null) { + return; + } + + String data = getXmlTextFromEditor(xml_node); + + // In the unlikely event that IStructuredDocument failed to extract the text + // directly from the editor, try to fall back on a direct XML serialization + // of the XML node. This uses the generic Node interface with no SSE tricks. + if (data == null) { + data = getXmlTextFromSerialization(xml_node); + } + + if (data != null) { + allText.append(data); + if (mPerformCut) { + // only remove notes to cut if we actually got some XML text from them + nodesToCut.add(uiNode); + } + } + + } catch (Exception e) { + AdtPlugin.log(e, "CopyCutAction failed for UI node %1$s", //$NON-NLS-1$ + uiNode.getBreadcrumbTrailDescription(true)); + } + } // for uiNode + + if (allText != null && allText.length() > 0) { + mClipboard.setContents( + new Object[] { allText.toString() }, + new Transfer[] { TextTransfer.getInstance() }); + if (mPerformCut) { + for (UiElementNode uiNode : nodesToCut) { + uiNode.deleteXmlNode(); + } + } + } + } + + /** Get the data directly from the editor. */ + private String getXmlTextFromEditor(Node xml_node) { + String data = null; + IStructuredModel model = mEditor.getModelForRead(); + try { + IStructuredDocument sse_doc = mEditor.getStructuredDocument(); + if (xml_node instanceof NodeContainer) { + // The easy way to get the source of an SSE XML node. + data = ((NodeContainer) xml_node).getSource(); + } else if (xml_node instanceof IndexedRegion && sse_doc != null) { + // Try harder. + IndexedRegion region = (IndexedRegion) xml_node; + int start = region.getStartOffset(); + int end = region.getEndOffset(); + + if (end > start) { + data = sse_doc.get(start, end - start); + } + } + } catch (BadLocationException e) { + // the region offset was invalid. ignore. + } finally { + model.releaseFromRead(); + } + return data; + } + + /** + * Direct XML serialization of the XML node. + * <p/> + * This uses the generic Node interface with no SSE tricks. It's however slower + * and doesn't respect formatting (since serialization is involved instead of reading + * the actual text buffer.) + */ + private String getXmlTextFromSerialization(Node xml_node) throws IOException { + String data; + StringWriter sw = new StringWriter(); + XMLSerializer serializer = new XMLSerializer(sw, + new OutputFormat(Method.XML, + OutputFormat.Defaults.Encoding /* utf-8 */, + true /* indent */)); + // Serialize will throw an IOException if it fails. + serializer.serialize((Element) xml_node); + data = sw.toString(); + return data; + } + + /** + * Static helper class to wrap on node into a list for the constructors. + */ + private static ArrayList<UiElementNode> toList(UiElementNode selected) { + ArrayList<UiElementNode> list = null; + if (selected != null) { + list = new ArrayList<UiElementNode>(1); + list.add(selected); + } + return list; + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java new file mode 100644 index 0000000..8b6aa0e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.ui.tree; + +/** + * Interface for an object that can commit its changes to the underlying XML model + */ +public interface ICommitXml { + + /** Commits pending data to the underlying XML model. */ + public void commitPendingXmlChanges(); + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java new file mode 100644 index 0000000..0729881 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog; +import org.eclipse.ui.dialogs.ISelectionStatusValidator; + +import java.util.Arrays; + +/** + * A selection dialog to select the type of the new element node to + * created, either in the application node or the selected sub node. + */ +public class NewItemSelectionDialog extends AbstractElementListSelectionDialog { + + /** The UI node selected in the tree view before creating the new item selection dialog. + * Can be null -- which means new items must be created in the root_node. */ + private UiElementNode mSelectedUiNode; + /** The root node chosen by the user, either root_node or the one passed + * to the constructor if not null */ + private UiElementNode mChosenRootNode; + private UiElementNode mLocalRootNode; + /** The descriptor of the elements to be displayed as root in this tree view. All elements + * of the same type in the root will be displayed. */ + private ElementDescriptor[] mDescriptorFilters; + + /** + * Creates the new item selection dialog. + * + * @param shell The parent shell for the list. + * @param labelProvider ILabelProvider for the list. + * @param descriptorFilters The element allows at the root of the tree + * @param ui_node The selected node, or null if none is selected. + * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node. + */ + public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider, + ElementDescriptor[] descriptorFilters, + UiElementNode ui_node, + UiElementNode root_node) { + super(shell, labelProvider); + mDescriptorFilters = descriptorFilters; + mLocalRootNode = root_node; + + // Only accept the UI node if it is not the UI root node and it can have children. + // If the node cannot have children, select its parent as a potential target. + if (ui_node != null && ui_node != mLocalRootNode) { + if (ui_node.getDescriptor().hasChildren()) { + mSelectedUiNode = ui_node; + } else { + UiElementNode parent = ui_node.getUiParent(); + if (parent != null && parent != mLocalRootNode) { + mSelectedUiNode = parent; + } + } + } + + setHelpAvailable(false); + setMultipleSelection(false); + + setValidator(new ISelectionStatusValidator() { + public IStatus validate(Object[] selection) { + if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) { + return new Status(IStatus.OK, // severity + AdtPlugin.PLUGIN_ID, //plugin id + IStatus.OK, // code + ((ViewElementDescriptor) selection[0]).getCanonicalClassName(), //msg + null); // exception + } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) { + return new Status(IStatus.OK, // severity + AdtPlugin.PLUGIN_ID, //plugin id + IStatus.OK, // code + "", //$NON-NLS-1$ // msg + null); // exception + } else { + return new Status(IStatus.ERROR, // severity + AdtPlugin.PLUGIN_ID, //plugin id + IStatus.ERROR, // code + "Invalid selection", // msg, translatable + null); // exception + } + } + }); + } + + /** + * @return The root node selected by the user, either root node or the + * one passed to the constructor if not null. + */ + public UiElementNode getChosenRootNode() { + return mChosenRootNode; + } + + /** + * Internal helper to compute the result. Returns the selection from + * the list view, if any. + */ + @Override + protected void computeResult() { + setResult(Arrays.asList(getSelectedElements())); + } + + /** + * Creates the dialog area. + * + * First add a radio area, which may be either 2 radio controls or + * just a message area if there's only one choice (the app root node). + * + * Then uses the default from the AbstractElementListSelectionDialog + * which is to add both a filter text and a filtered list. Adding both + * is necessary (since the base class accesses both internal directly + * fields without checking for null pointers.) + * + * Finally sets the initial selection list. + */ + @Override + protected Control createDialogArea(Composite parent) { + Composite contents = (Composite) super.createDialogArea(parent); + + createRadioControl(contents); + createFilterText(contents); + createFilteredList(contents); + + // Initialize the list state. + // This must be done after the filtered list as been created. + chooseNode(mChosenRootNode); + setSelection(getInitialElementSelections().toArray()); + return contents; + } + + /** + * Creates the message text widget and sets layout data. + * @param content the parent composite of the message area. + */ + private Composite createRadioControl(Composite content) { + + if (mSelectedUiNode != null) { + Button radio1 = new Button(content, SWT.RADIO); + radio1.setText(String.format("Create a new element at the top level, in %1$s.", + mLocalRootNode.getShortDescription())); + + Button radio2 = new Button(content, SWT.RADIO); + radio2.setText(String.format("Create a new element in the selected element, %1$s.", + mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */))); + + // Set the initial selection before adding the listeners + // (they can't be run till the filtered list has been created) + radio1.setSelection(false); + radio2.setSelection(true); + mChosenRootNode = mSelectedUiNode; + + radio1.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + chooseNode(mLocalRootNode); + } + }); + + radio2.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + chooseNode(mSelectedUiNode); + } + }); + } else { + setMessage(String.format("Create a new element at the top level, in %1$s.", + mLocalRootNode.getShortDescription())); + createMessageArea(content); + + mChosenRootNode = mLocalRootNode; + } + + return content; + } + + /** + * Internal helper to remember the root node choosen by the user. + * It also sets the list view to the adequate list of children that can + * be added to the chosen root node. + * + * If the chosen root node is mLocalRootNode and a descriptor filter was specified + * when creating the master-detail part, we use this as the set of nodes that + * can be created on the root node. + * + * @param ui_node The chosen root node, either mLocalRootNode or + * mSelectedUiNode. + */ + private void chooseNode(UiElementNode ui_node) { + mChosenRootNode = ui_node; + + if (ui_node == mLocalRootNode && + mDescriptorFilters != null && + mDescriptorFilters.length != 0) { + setListElements(mDescriptorFilters); + } else { + setListElements(ui_node.getDescriptor().getChildren()); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java new file mode 100644 index 0000000..8bb4ad2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; +import org.eclipse.wst.xml.core.internal.document.NodeContainer; +import org.w3c.dom.Node; + + +/** + * Provides Paste operation for the tree nodes + */ +public class PasteAction extends Action { + private UiElementNode mUiNode; + private final AndroidEditor mEditor; + private final Clipboard mClipboard; + + public PasteAction(AndroidEditor editor, Clipboard clipboard, UiElementNode ui_node) { + super("Paste"); + mEditor = editor; + mClipboard = clipboard; + + ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); + setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE)); + setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE)); + setDisabledImageDescriptor( + images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE_DISABLED)); + + mUiNode = ui_node; + } + + /** + * Performs the paste operation. + */ + @Override + public void run() { + super.run(); + + final String data = (String) mClipboard.getContents(TextTransfer.getInstance()); + if (data != null) { + IStructuredModel model = mEditor.getModelForEdit(); + try { + IStructuredDocument sse_doc = mEditor.getStructuredDocument(); + if (sse_doc != null) { + if (mUiNode.getDescriptor().hasChildren()) { + // This UI Node can have children. The new XML is + // inserted as the first child. + + if (mUiNode.getUiChildren().size() > 0) { + // There's already at least one child, so insert right before it. + Node xml_node = mUiNode.getUiChildren().get(0).getXmlNode(); + if (xml_node instanceof IndexedRegion) { // implies xml_node != null + IndexedRegion region = (IndexedRegion) xml_node; + sse_doc.replace(region.getStartOffset(), 0, data); + return; // we're done, no need to try the other cases + } + } + + // If there's no first XML node child. Create one by + // inserting at the end of the *start* tag. + Node xml_node = mUiNode.getXmlNode(); + if (xml_node instanceof NodeContainer) { + NodeContainer container = (NodeContainer) xml_node; + IStructuredDocumentRegion start_tag = + container.getStartStructuredDocumentRegion(); + if (start_tag != null) { + sse_doc.replace(start_tag.getEndOffset(), 0, data); + return; // we're done, no need to try the other case + } + } + } + + // This UI Node doesn't accept children. The new XML is inserted as the + // next sibling. This also serves as a fallback if all the previous + // attempts failed. However, this is not possible if the current node + // has for parent a document -- an XML document can only have one root, + // with no siblings. + if (!(mUiNode.getUiParent() instanceof UiDocumentNode)) { + Node xml_node = mUiNode.getXmlNode(); + if (xml_node instanceof IndexedRegion) { + IndexedRegion region = (IndexedRegion) xml_node; + sse_doc.replace(region.getEndOffset(), 0, data); + } + } + } + + } catch (BadLocationException e) { + AdtPlugin.log(e, "ParseAction failed for UI Node %2$s, content '%1$s'", //$NON-NLS-1$ + mUiNode.getBreadcrumbTrailDescription(true), data); + } finally { + model.releaseFromEdit(); + } + } + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java new file mode 100644 index 0000000..21180b1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.swt.widgets.Shell; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import java.util.List; + +/** + * Performs basic actions on an XML tree: add node, remove node, move up/down. + */ +public abstract class UiActions implements ICommitXml { + + public UiActions() { + } + + //--------------------- + // Actual implementations must override these to provide specific hooks + + /** Returns the UiDocumentNode for the current model. */ + abstract protected UiElementNode getRootNode(); + + /** Commits pending data before the XML model is modified. */ + abstract public void commitPendingXmlChanges(); + + /** + * Utility method to select an outline item based on its model node + * + * @param uiNode The node to select. Can be null (in which case nothing should happen) + */ + abstract protected void selectUiNode(UiElementNode uiNode); + + //--------------------- + + /** + * Called when the "Add..." button next to the tree view is selected. + * <p/> + * This simplified version of doAdd does not support descriptor filters and creates + * a new {@link UiModelTreeLabelProvider} for each call. + */ + public void doAdd(UiElementNode uiNode, Shell shell) { + doAdd(uiNode, null /* descriptorFilters */, shell, new UiModelTreeLabelProvider()); + } + + /** + * Called when the "Add..." button next to the tree view is selected. + * + * Displays a selection dialog that lets the user select which kind of node + * to create, depending on the current selection. + */ + public void doAdd(UiElementNode uiNode, + ElementDescriptor[] descriptorFilters, + Shell shell, ILabelProvider labelProvider) { + // If the root node is a document with already a root, use it as the root node + UiElementNode rootNode = getRootNode(); + if (rootNode instanceof UiDocumentNode && rootNode.getUiChildren().size() > 0) { + rootNode = rootNode.getUiChildren().get(0); + } + + NewItemSelectionDialog dlg = new NewItemSelectionDialog( + shell, + labelProvider, + descriptorFilters, + uiNode, rootNode); + dlg.open(); + Object[] results = dlg.getResult(); + if (results != null && results.length > 0) { + addElement(dlg.getChosenRootNode(), null, (ElementDescriptor) results[0], + true /*updateLayout*/); + } + } + + /** + * Adds a new XML element based on the {@link ElementDescriptor} to the given parent + * {@link UiElementNode}, and then select it. + * <p/> + * If the parent is a document root which already contains a root element, the inner + * root element is used as the actual parent. This ensure you can't create a broken + * XML file with more than one root element. + * <p/> + * If a sibling is given and that sibling has the same parent, the new node is added + * right after that sibling. Otherwise the new node is added at the end of the parent + * child list. + * + * @param uiParent An existing UI node or null to add to the tree root + * @param uiSibling An existing UI node before which to insert the new node. Can be null. + * @param descriptor The descriptor of the element to add + * @param updateLayout True if layout attributes should be set + * @return The new {@link UiElementNode} or null. + */ + public UiElementNode addElement(UiElementNode uiParent, + UiElementNode uiSibling, + ElementDescriptor descriptor, + boolean updateLayout) { + if (uiParent instanceof UiDocumentNode && uiParent.getUiChildren().size() > 0) { + uiParent = uiParent.getUiChildren().get(0); + } + if (uiSibling != null && uiSibling.getUiParent() != uiParent) { + uiSibling = null; + } + + UiElementNode uiNew = addNewTreeElement(uiParent, uiSibling, descriptor, updateLayout); + selectUiNode(uiNew); + + return uiNew; + } + + /** + * Called when the "Remove" button is selected. + * + * If the tree has a selection, remove it. + * This simply deletes the XML node attached to the UI node: when the XML model fires the + * update event, the tree will get refreshed. + */ + public void doRemove(final List<UiElementNode> nodes, Shell shell) { + + if (nodes == null || nodes.size() == 0) { + return; + } + + final int len = nodes.size(); + + StringBuilder sb = new StringBuilder(); + for (UiElementNode node : nodes) { + sb.append("\n- "); //$NON-NLS-1$ + sb.append(node.getBreadcrumbTrailDescription(false /* include_root */)); + } + + if (MessageDialog.openQuestion(shell, + len > 1 ? "Remove elements from Android XML" // title + : "Remove element from Android XML", + String.format("Do you really want to remove %1$s?", sb.toString()))) { + commitPendingXmlChanges(); + getRootNode().getEditor().editXmlModel(new Runnable() { + public void run() { + UiElementNode previous = null; + UiElementNode parent = null; + + for (int i = len - 1; i >= 0; i--) { + UiElementNode node = nodes.get(i); + previous = node.getUiPreviousSibling(); + parent = node.getUiParent(); + + // delete node + node.deleteXmlNode(); + } + + // try to select the last previous sibling or the last parent + if (previous != null) { + selectUiNode(previous); + } else if (parent != null) { + selectUiNode(parent); + } + } + }); + } + } + + /** + * Called when the "Up" button is selected. + * <p/> + * If the tree has a selection, move it up, either in the child list or as the last child + * of the previous parent. + */ + public void doUp(final List<UiElementNode> nodes) { + if (nodes == null || nodes.size() < 1) { + return; + } + + final Node[] select_xml_node = { null }; + UiElementNode last_node = null; + UiElementNode search_root = null; + + for (int i = 0; i < nodes.size(); i++) { + final UiElementNode node = last_node = nodes.get(i); + + // the node will move either up to its parent or grand-parent + search_root = node.getUiParent(); + if (search_root != null && search_root.getUiParent() != null) { + search_root = search_root.getUiParent(); + } + + commitPendingXmlChanges(); + getRootNode().getEditor().editXmlModel(new Runnable() { + public void run() { + Node xml_node = node.getXmlNode(); + if (xml_node != null) { + Node xml_parent = xml_node.getParentNode(); + if (xml_parent != null) { + UiElementNode ui_prev = node.getUiPreviousSibling(); + if (ui_prev != null && ui_prev.getXmlNode() != null) { + // This node is not the first one of the parent, so it can be + // removed and then inserted before its previous sibling. + // If the previous sibling can have children, though, then it + // is inserted at the end of the children list. + Node xml_prev = ui_prev.getXmlNode(); + if (ui_prev.getDescriptor().hasChildren()) { + xml_prev.appendChild(xml_parent.removeChild(xml_node)); + select_xml_node[0] = xml_node; + } else { + xml_parent.insertBefore( + xml_parent.removeChild(xml_node), + xml_prev); + select_xml_node[0] = xml_node; + } + } else if (!(xml_parent instanceof Document) && + xml_parent.getParentNode() != null && + !(xml_parent.getParentNode() instanceof Document)) { + // If the node is the first one of the child list of its + // parent, move it up in the hierarchy as previous sibling + // to the parent. This is only possible if the parent of the + // parent is not a document. + Node grand_parent = xml_parent.getParentNode(); + grand_parent.insertBefore(xml_parent.removeChild(xml_node), + xml_parent); + select_xml_node[0] = xml_node; + } + } + } + } + }); + } + + if (select_xml_node[0] == null) { + // The XML node has not been moved, we can just select the same UI node + selectUiNode(last_node); + } else { + // The XML node has moved. At this point the UI model has been reloaded + // and the XML node has been affected to a new UI node. Find that new UI + // node and select it. + if (search_root == null) { + search_root = last_node.getUiRoot(); + } + if (search_root != null) { + selectUiNode(search_root.findXmlNode(select_xml_node[0])); + } + } + } + + /** + * Called when the "Down" button is selected. + * + * If the tree has a selection, move it down, either in the same child list or as the + * first child of the next parent. + */ + public void doDown(final List<UiElementNode> nodes) { + if (nodes == null || nodes.size() < 1) { + return; + } + + final Node[] select_xml_node = { null }; + UiElementNode last_node = null; + UiElementNode search_root = null; + + for (int i = nodes.size() - 1; i >= 0; i--) { + final UiElementNode node = last_node = nodes.get(i); + // the node will move either down to its parent or grand-parent + search_root = node.getUiParent(); + if (search_root != null && search_root.getUiParent() != null) { + search_root = search_root.getUiParent(); + } + + commitPendingXmlChanges(); + getRootNode().getEditor().editXmlModel(new Runnable() { + public void run() { + Node xml_node = node.getXmlNode(); + if (xml_node != null) { + Node xml_parent = xml_node.getParentNode(); + if (xml_parent != null) { + UiElementNode uiNext = node.getUiNextSibling(); + if (uiNext != null && uiNext.getXmlNode() != null) { + // This node is not the last one of the parent, so it can be + // removed and then inserted after its next sibling. + // If the next sibling is a node that can have children, though, + // then the node is inserted as the first child. + Node xml_next = uiNext.getXmlNode(); + if (uiNext.getDescriptor().hasChildren()) { + // Note: insertBefore works as append if the ref node is + // null, i.e. when the node doesn't have children yet. + xml_next.insertBefore(xml_parent.removeChild(xml_node), + xml_next.getFirstChild()); + select_xml_node[0] = xml_node; + } else { + // Insert "before after next" ;-) + xml_parent.insertBefore(xml_parent.removeChild(xml_node), + xml_next.getNextSibling()); + select_xml_node[0] = xml_node; + } + } else if (!(xml_parent instanceof Document) && + xml_parent.getParentNode() != null && + !(xml_parent.getParentNode() instanceof Document)) { + // This node is the last node of its parent. + // If neither the parent nor the grandparent is a document, + // then the node can be insert right after the parent. + Node grand_parent = xml_parent.getParentNode(); + grand_parent.insertBefore(xml_parent.removeChild(xml_node), + xml_parent.getNextSibling()); + select_xml_node[0] = xml_node; + } + } + } + } + }); + } + + if (select_xml_node[0] == null) { + // The XML node has not been moved, we can just select the same UI node + selectUiNode(last_node); + } else { + // The XML node has moved. At this point the UI model has been reloaded + // and the XML node has been affected to a new UI node. Find that new UI + // node and select it. + if (search_root == null) { + search_root = last_node.getUiRoot(); + } + if (search_root != null) { + selectUiNode(search_root.findXmlNode(select_xml_node[0])); + } + } + } + + //--------------------- + + /** + * Adds a new element of the given descriptor's type to the given UI parent node. + * + * This actually creates the corresponding XML node in the XML model, which in turn + * will refresh the current tree view. + * + * @param uiParent An existing UI node or null to add to the tree root + * @param uiSibling An existing UI node to insert right before. Can be null. + * @param descriptor The descriptor of the element to add + * @param updateLayout True if layout attributes should be set + * @return The {@link UiElementNode} that has been added to the UI tree. + */ + private UiElementNode addNewTreeElement(UiElementNode uiParent, + final UiElementNode uiSibling, + ElementDescriptor descriptor, + final boolean updateLayout) { + commitPendingXmlChanges(); + + int index = 0; + for (UiElementNode uiChild : uiParent.getUiChildren()) { + if (uiChild == uiSibling) { + break; + } + index++; + } + + final UiElementNode uiNew = uiParent.insertNewUiChild(index, descriptor); + UiElementNode rootNode = getRootNode(); + + rootNode.getEditor().editXmlModel(new Runnable() { + public void run() { + DescriptorsUtils.setDefaultLayoutAttributes(uiNew, updateLayout); + Node xmlNode = uiNew.createXmlNode(); + } + }); + return uiNew; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java new file mode 100644 index 0000000..15c67c3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.ui.SectionHelper; +import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.forms.IDetailsPage; +import org.eclipse.ui.forms.IFormPart; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.events.ExpansionEvent; +import org.eclipse.ui.forms.events.IExpansionListener; +import org.eclipse.ui.forms.widgets.FormText; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; +import org.eclipse.ui.forms.widgets.SharedScrolledComposite; +import org.eclipse.ui.forms.widgets.TableWrapData; +import org.eclipse.ui.forms.widgets.TableWrapLayout; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Details page for the {@link UiElementNode} nodes in the tree view. + * <p/> + * See IDetailsBase for more details. + */ +class UiElementDetail implements IDetailsPage { + + /** The master-detail part, composed of a main tree and an auxiliary detail part */ + private ManifestSectionPart mMasterPart; + + private Section mMasterSection; + private UiElementNode mCurrentUiElementNode; + private Composite mCurrentTable; + private boolean mIsDirty; + + private IManagedForm mManagedForm; + + private final UiTreeBlock mTree; + + public UiElementDetail(UiTreeBlock tree) { + mTree = tree; + mMasterPart = mTree.getMasterPart(); + mManagedForm = mMasterPart.getManagedForm(); + } + + /* (non-java doc) + * Initializes the part. + */ + public void initialize(IManagedForm form) { + mManagedForm = form; + } + + /* (non-java doc) + * Creates the contents of the page in the provided parent. + */ + public void createContents(Composite parent) { + mMasterSection = createMasterSection(parent); + } + + /* (non-java doc) + * Called when the provided part has changed selection state. + * <p/> + * Only reply when our master part originates the selection. + */ + public void selectionChanged(IFormPart part, ISelection selection) { + if (part == mMasterPart && + !selection.isEmpty() && + selection instanceof ITreeSelection) { + ITreeSelection tree_selection = (ITreeSelection) selection; + + Object first = tree_selection.getFirstElement(); + if (first instanceof UiElementNode) { + UiElementNode ui_node = (UiElementNode) first; + createUiAttributeControls(mManagedForm, ui_node); + } + } + } + + /* (non-java doc) + * Instructs it to commit the new (modified) data back into the model. + */ + public void commit(boolean onSave) { + + IStructuredModel model = mTree.getEditor().getModelForEdit(); + try { + // Notify the model we're about to change data... + model.aboutToChangeModel(); + + if (mCurrentUiElementNode != null) { + mCurrentUiElementNode.commit(); + } + + // Finally reset the dirty flag if everything was saved properly + mIsDirty = false; + } catch (Exception e) { + AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$ + } finally { + // Notify the model we're done modifying it. This must *always* be executed. + model.changedModel(); + model.releaseFromEdit(); + } + } + + public void dispose() { + // pass + } + + + /* (non-java doc) + * Returns true if the part has been modified with respect to the data + * loaded from the model. + */ + public boolean isDirty() { + if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) { + markDirty(); + } + return mIsDirty; + } + + public boolean isStale() { + // pass + return false; + } + + /** + * Called by the master part when the tree is refreshed after the framework resources + * have been reloaded. + */ + public void refresh() { + if (mCurrentTable != null) { + mCurrentTable.dispose(); + mCurrentTable = null; + } + mCurrentUiElementNode = null; + mMasterSection.getParent().pack(true /* changed */); + } + + public void setFocus() { + // pass + } + + public boolean setFormInput(Object input) { + // pass + return false; + } + + /** + * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section. + * + * All the UI should be created in a layout which parent is the mSection itself. + * The hierarchy is: + * <pre> + * DetailPage + * + TableWrapLayout + * + Section (with title/description && fill_grab horizontal) + * + TableWrapLayout [*] + * + Labels/Forms/etc... [*] + * </pre> + * Both items marked with [*] are created by the derived classes to fit their needs. + * + * @param parent Parent of the mSection (from createContents) + * @return The new Section + */ + private Section createMasterSection(Composite parent) { + TableWrapLayout layout = new TableWrapLayout(); + layout.topMargin = 0; + parent.setLayout(layout); + + FormToolkit toolkit = mManagedForm.getToolkit(); + Section section = toolkit.createSection(parent, Section.TITLE_BAR); + section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP)); + return section; + } + + /** + * Create the ui attribute controls to edit the attributes for the given + * ElementDescriptor. + * <p/> + * This is called by the constructor. + * Derived classes can override this if necessary. + * + * @param managedForm The managed form + */ + private void createUiAttributeControls( + final IManagedForm managedForm, + final UiElementNode ui_node) { + + final ElementDescriptor elem_desc = ui_node.getDescriptor(); + mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription())); + + if (mCurrentUiElementNode != ui_node) { + // Before changing the table, commit all dirty state. + if (mIsDirty) { + commit(false); + } + if (mCurrentTable != null) { + mCurrentTable.dispose(); + mCurrentTable = null; + } + + // To iterate over all attributes, we use the {@link ElementDescriptor} instead + // of the {@link UiElementNode} because the attributes order is guaranteed in the + // descriptor but not in the node itself. + AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors(); + + // If the attribute list contains at least one SeparatorAttributeDescriptor, + // sub-sections will be used. This needs to be known early as it influences the + // creation of the master table. + boolean useSubsections = false; + for (AttributeDescriptor attr_desc : attr_desc_list) { + if (attr_desc instanceof SeparatorAttributeDescriptor) { + // Sub-sections will be used. The default sections should no longer be + useSubsections = true; + break; + } + } + + FormToolkit toolkit = managedForm.getToolkit(); + Composite masterTable = SectionHelper.createTableLayout(mMasterSection, + toolkit, useSubsections ? 1 : 2 /* numColumns */); + mCurrentTable = masterTable; + + mCurrentUiElementNode = ui_node; + + if (elem_desc.getTooltip() != null) { + String tooltip; + if (Sdk.getCurrent() != null && + Sdk.getCurrent().getDocumentationBaseUrl() != null) { + tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(), + elem_desc, + Sdk.getCurrent().getDocumentationBaseUrl()); + } else { + tooltip = elem_desc.getTooltip(); + } + + try { + FormText text = SectionHelper.createFormText(masterTable, toolkit, + true /* isHtml */, tooltip, true /* setupLayoutData */); + text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener()); + Image icon = elem_desc.getIcon(); + if (icon != null) { + text.setImage(DescriptorsUtils.IMAGE_KEY, icon); + } + } catch(Exception e) { + // The FormText parser is really really basic and will fail as soon as the + // HTML javadoc is ever so slightly malformatted. + AdtPlugin.log(e, + "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$ + ui_node.getDescriptor().getXmlName(), + tooltip); + + // Fallback to a pure text tooltip, no fancy HTML + tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip()); + Label label = SectionHelper.createLabel(masterTable, toolkit, + tooltip, tooltip); + } + } + + Composite table = useSubsections ? null : masterTable; + + for (AttributeDescriptor attr_desc : attr_desc_list) { + if (attr_desc instanceof XmlnsAttributeDescriptor) { + // Do not show hidden attributes + continue; + } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) { + String title = null; + if (attr_desc instanceof SeparatorAttributeDescriptor) { + // xmlName is actually the label of the separator + title = attr_desc.getXmlLocalName(); + } else { + title = String.format("Attributes from %1$s", elem_desc.getUiName()); + } + + table = createSubSectionTable(toolkit, masterTable, title); + if (attr_desc instanceof SeparatorAttributeDescriptor) { + continue; + } + } + + UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc); + + if (ui_attr != null) { + ui_attr.createUiControl(table, managedForm); + + if (ui_attr.getCurrentValue() != null && + ui_attr.getCurrentValue().length() > 0) { + ((Section) table.getParent()).setExpanded(true); + } + } else { + // The XML has an extra unknown attribute. + // This is not expected to happen so it is ignored. + AdtPlugin.log(IStatus.INFO, + "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$ + attr_desc.getXmlLocalName(), + ui_node.getDescriptor().getXmlName()); + } + } + + // Create a sub-section for the unknown attributes. + // It is initially hidden till there are some attributes to show here. + final Composite unknownTable = createSubSectionTable(toolkit, masterTable, + "Unknown XML Attributes"); + unknownTable.getParent().setVisible(false); // set section to not visible + final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>(); + + final IUiUpdateListener updateListener = new IUiUpdateListener() { + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + if (state == UiUpdateState.ATTR_UPDATED) { + updateUnknownAttributesSection(ui_node, unknownTable, managedForm, + reference); + } + } + }; + ui_node.addUpdateListener(updateListener); + + // remove the listener when the UI is disposed + unknownTable.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + ui_node.removeUpdateListener(updateListener); + } + }); + + updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference); + + mMasterSection.getParent().pack(true /* changed */); + } + } + + /** + * Create a sub Section and its embedding wrapper table with 2 columns. + * @return The table, child of a new section. + */ + private Composite createSubSectionTable(FormToolkit toolkit, + Composite masterTable, String title) { + + // The Section composite seems to ignore colspan when assigned a TableWrapData so + // if the parent is a table with more than one column an extra table with one column + // is inserted to respect colspan. + int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns; + if (parentNumCol > 1) { + masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1); + TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB); + twd.maxWidth = AndroidEditor.TEXT_WIDTH_HINT; + twd.colspan = parentNumCol; + masterTable.setLayoutData(twd); + } + + Composite table; + Section section = toolkit.createSection(masterTable, + Section.TITLE_BAR | Section.TWISTIE); + + // Add an expansion listener that will trigger a reflow on the parent + // ScrolledPageBook (which is actually a SharedScrolledComposite). This will + // recompute the correct size and adjust the scrollbar as needed. + section.addExpansionListener(new IExpansionListener() { + public void expansionStateChanged(ExpansionEvent e) { + reflowMasterSection(); + } + + public void expansionStateChanging(ExpansionEvent e) { + // pass + } + }); + + section.setText(title); + section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, + TableWrapData.TOP)); + table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */); + return table; + } + + /** + * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite). + * This will recompute the correct size and adjust the scrollbar as needed. + */ + private void reflowMasterSection() { + for(Composite c = mMasterSection; c != null; c = c.getParent()) { + if (c instanceof SharedScrolledComposite) { + ((SharedScrolledComposite) c).reflow(true /* flushCache */); + break; + } + } + } + + /** + * Updates the unknown attributes section for the UI Node. + */ + private void updateUnknownAttributesSection(UiElementNode ui_node, + final Composite unknownTable, final IManagedForm managedForm, + HashSet<UiAttributeNode> reference) { + Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes(); + Section section = ((Section) unknownTable.getParent()); + boolean needs_reflow = false; + + // The table was created hidden, show it if there are unknown attributes now + if (ui_attrs.size() > 0 && !section.isVisible()) { + section.setVisible(true); + needs_reflow = true; + } + + // Compare the new attribute set with the old "reference" one + boolean has_differences = ui_attrs.size() != reference.size(); + if (!has_differences) { + for (UiAttributeNode ui_attr : ui_attrs) { + if (!reference.contains(ui_attr)) { + has_differences = true; + break; + } + } + } + + if (has_differences) { + needs_reflow = true; + reference.clear(); + + // Remove all children of the table + for (Control c : unknownTable.getChildren()) { + c.dispose(); + } + + // Recreate all attributes UI + for (UiAttributeNode ui_attr : ui_attrs) { + reference.add(ui_attr); + ui_attr.createUiControl(unknownTable, managedForm); + + if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) { + section.setExpanded(true); + } + } + } + + if (needs_reflow) { + reflowMasterSection(); + } + } + + /** + * Marks the part dirty. Called as a result of user interaction with the widgets in the + * section. + */ + private void markDirty() { + if (!mIsDirty) { + mIsDirty = true; + mManagedForm.dirtyStateChanged(); + } + } +} + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java new file mode 100644 index 0000000..9f34d9e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import java.util.ArrayList; + +/** + * UiModelTreeContentProvider is a trivial implementation of {@link ITreeContentProvider} + * where elements are expected to be instances of {@link UiElementNode}. + */ +class UiModelTreeContentProvider implements ITreeContentProvider { + + /** The root {@link UiElementNode} which contains all the elements that are to be + * manipulated by this tree view. In general this is the manifest UI node. */ + private UiElementNode mUiRootNode; + /** The descriptor of the elements to be displayed as root in this tree view. All elements + * of the same type in the root will be displayed. */ + private ElementDescriptor[] mDescriptorFilters; + + public UiModelTreeContentProvider(UiElementNode uiRootNode, + ElementDescriptor[] descriptorFilters) { + mUiRootNode = uiRootNode; + mDescriptorFilters = descriptorFilters; + } + + /* (non-java doc) + * Returns all the UI node children of the given element or null if not the right kind + * of object. */ + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof UiElementNode) { + UiElementNode node = (UiElementNode) parentElement; + return node.getUiChildren().toArray(); + } + return null; + } + + /* (non-java doc) + * Returns the parent of a given UI node or null if it's a root node or it's not the + * right kind of node. */ + public Object getParent(Object element) { + if (element instanceof UiElementNode) { + UiElementNode node = (UiElementNode) element; + return node.getUiParent(); + } + return null; + } + + /* (non-java doc) + * Returns true if the UI node has any UI children nodes. */ + public boolean hasChildren(Object element) { + if (element instanceof UiElementNode) { + UiElementNode node = (UiElementNode) element; + return node.getUiChildren().size() > 0; + } + return false; + } + + /* (non-java doc) + * Get root elements for the tree. These are all the UI nodes that + * match the filter descriptor in the current root node. + * <p/> + * Although not documented, it seems this method should not return null. + * At worse, it should return new Object[0]. + * <p/> + * inputElement is not currently used. The root node and the filter are given + * by the enclosing class. + */ + public Object[] getElements(Object inputElement) { + ArrayList<UiElementNode> roots = new ArrayList<UiElementNode>(); + if (mUiRootNode != null) { + for (UiElementNode ui_node : mUiRootNode.getUiChildren()) { + if (mDescriptorFilters == null || mDescriptorFilters.length == 0) { + roots.add(ui_node); + } else { + for (ElementDescriptor filter : mDescriptorFilters) { + if (ui_node.getDescriptor() == filter) { + roots.add(ui_node); + } + } + } + } + } + + return roots.toArray(); + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java new file mode 100644 index 0000000..273a30b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.ui.ErrorImageComposite; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.swt.graphics.Image; + +/** + * UiModelTreeLabelProvider is a trivial implementation of {@link ILabelProvider} + * where elements are expected to derive from {@link UiElementNode} or + * from {@link ElementDescriptor}. + * + * It is used by both the master tree viewer and by the list in the Add... selection dialog. + */ +public class UiModelTreeLabelProvider implements ILabelProvider { + + public UiModelTreeLabelProvider() { + } + + /** + * Returns the element's logo with a fallback on the android logo. + */ + public Image getImage(Object element) { + ElementDescriptor desc = null; + if (element instanceof ElementDescriptor) { + Image img = ((ElementDescriptor) element).getIcon(); + if (img != null) { + return img; + } + } else if (element instanceof UiElementNode) { + UiElementNode node = (UiElementNode) element; + desc = node.getDescriptor(); + if (desc != null) { + Image img = desc.getIcon(); + if (img != null) { + if (node.hasError()) { + //TODO: cache image + return new ErrorImageComposite(img).createImage(); + } else { + return img; + } + } + } + } + return AdtPlugin.getAndroidLogo(); + } + + /** + * Uses UiElementNode.shortDescription for the label for this tree item. + */ + public String getText(Object element) { + if (element instanceof ElementDescriptor) { + ElementDescriptor desc = (ElementDescriptor) element; + return desc.getUiName(); + } else if (element instanceof UiElementNode) { + UiElementNode node = (UiElementNode) element; + return node.getShortDescription(); + } + return element.toString(); + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } +} + + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java new file mode 100644 index 0000000..fc384e8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java @@ -0,0 +1,882 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.ui.tree; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.ui.SectionHelper; +import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.forms.DetailsPart; +import org.eclipse.ui.forms.IDetailsPage; +import org.eclipse.ui.forms.IDetailsPageProvider; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.MasterDetailsBlock; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.Section; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for + * a specific set of {@link UiElementNode}. + * <p/> + * For a given UI element node, the tree view displays all first-level children that + * match a given type (given by an {@link ElementDescriptor}. All children from these + * nodes are also displayed. + * <p/> + * In the middle next to the tree are some controls to add or delete tree nodes. + * On the left is a details part that displays all the visible UI attributes for a given + * selected UI element node. + */ +public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml { + + /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */ + private static final int TREE_HEIGHT_HINT = 50; + + /** Container editor */ + AndroidEditor mEditor; + /** The root {@link UiElementNode} which contains all the elements that are to be + * manipulated by this tree view. In general this is the manifest UI node. */ + private UiElementNode mUiRootNode; + /** The descriptor of the elements to be displayed as root in this tree view. All elements + * of the same type in the root will be displayed. */ + private ElementDescriptor[] mDescriptorFilters; + /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */ + private String mTitle; + /** The description for the master-detail part (displayed on top of the tree view) */ + private String mDescription; + /** The master-detail part, composed of a main tree and an auxiliary detail part */ + private ManifestSectionPart mMasterPart; + /** The tree viewer in the master-detail part */ + private TreeViewer mTreeViewer; + /** The "add" button for the tree view */ + private Button mAddButton; + /** The "remove" button for the tree view */ + private Button mRemoveButton; + /** The "up" button for the tree view */ + private Button mUpButton; + /** The "down" button for the tree view */ + private Button mDownButton; + /** The Managed Form used to create the master part */ + private IManagedForm mManagedForm; + /** Reference to the details part of the tree master block. */ + private DetailsPart mDetailsPart; + /** Reference to the clipboard for copy-paste */ + private Clipboard mClipboard; + /** Listener to refresh the tree viewer when the parent's node has been updated */ + private IUiUpdateListener mUiRefreshListener; + /** Listener to enable/disable the UI based on the application node's presence */ + private IUiUpdateListener mUiEnableListener; + /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */ + private UiTreeActions mUiTreeActions; + /** + * True if the root node can be created on-demand (i.e. as needed as + * soon as children exist). False if an external entity controls the existence of the + * root node. In practise, this is false for the manifest application page (the actual + * "application" node is managed by the ApplicationToggle part) whereas it is true + * for all other tree pages. + */ + private final boolean mAutoCreateRoot; + + + /** + * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the + * given filter in the given root node. + * + * @param editor The parent manifest editor. + * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are + * to be manipulated by this tree view. In general this is the manifest UI node or the + * application UI node. This cannot be null. + * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as + * soon as children exist). False if an external entity controls the existence of the + * root node. In practise, this is false for the manifest application page (the actual + * "application" node is managed by the ApplicationToggle part) whereas it is true + * for all other tree pages. + * @param descriptorFilters A list of descriptors of the elements to be displayed as root in + * this tree view. Use null or an empty list to accept any kind of node. + * @param title Title for the section + * @param description Description for the section + */ + public UiTreeBlock(AndroidEditor editor, + UiElementNode uiRootNode, + boolean autoCreateRoot, + ElementDescriptor[] descriptorFilters, + String title, + String description) { + mEditor = editor; + mUiRootNode = uiRootNode; + mAutoCreateRoot = autoCreateRoot; + mDescriptorFilters = descriptorFilters; + mTitle = title; + mDescription = description; + } + + /** @returns The container editor */ + AndroidEditor getEditor() { + return mEditor; + } + + /** @returns The reference to the clipboard for copy-paste */ + Clipboard getClipboard() { + return mClipboard; + } + + /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */ + ManifestSectionPart getMasterPart() { + return mMasterPart; + } + + @Override + protected void createMasterPart(final IManagedForm managedForm, Composite parent) { + FormToolkit toolkit = managedForm.getToolkit(); + + mManagedForm = managedForm; + mMasterPart = new ManifestSectionPart(parent, toolkit); + Section section = mMasterPart.getSection(); + section.setText(mTitle); + section.setDescription(mDescription); + section.setLayout(new GridLayout()); + section.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Composite grid = SectionHelper.createGridLayout(section, toolkit, 2); + + Tree tree = createTreeViewer(toolkit, grid, managedForm); + createButtons(toolkit, grid); + createTreeContextMenu(tree); + createSectionActions(section, toolkit); + } + + private void createSectionActions(Section section, FormToolkit toolkit) { + ToolBarManager manager = new ToolBarManager(SWT.FLAT); + manager.removeAll(); + + ToolBar toolbar = manager.createControl(section); + section.setTextClient(toolbar); + + ElementDescriptor[] descs = mDescriptorFilters; + if (descs == null && mUiRootNode != null) { + descs = mUiRootNode.getDescriptor().getChildren(); + } + + if (descs != null && descs.length > 1) { + for (ElementDescriptor desc : descs) { + manager.add(new DescriptorFilterAction(desc)); + } + } + + manager.add(new TreeSortAction()); + + manager.update(true /*force*/); + } + + /** + * Creates the tree and its viewer + * @return The tree control + */ + private Tree createTreeViewer(FormToolkit toolkit, Composite grid, + final IManagedForm managedForm) { + // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here. + // However the class must be adapted to create an adapted toolkit tree. + final Tree tree = toolkit.createTree(grid, SWT.MULTI); + GridData gd = new GridData(GridData.FILL_BOTH); + gd.widthHint = AndroidEditor.TEXT_WIDTH_HINT; + gd.heightHint = TREE_HEIGHT_HINT; + tree.setLayoutData(gd); + + mTreeViewer = new TreeViewer(tree); + mTreeViewer.setContentProvider(new UiModelTreeContentProvider( + mUiRootNode, mDescriptorFilters)); + mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider()); + mTreeViewer.setInput("unused"); //$NON-NLS-1$ + + // Create a listener that reacts to selections on the tree viewer. + // When a selection is made, ask the managed form to propagate an event to + // all parts in the managed form. + // This is picked up by UiElementDetail.selectionChanged(). + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + managedForm.fireSelectionChanged(mMasterPart, event.getSelection()); + adjustTreeButtons(event.getSelection()); + } + }); + + // Create three listeners: + // - One to refresh the tree viewer when the parent's node has been updated + // - One to refresh the tree viewer when the framework resources have changed + // - One to enable/disable the UI based on the application node's presence. + mUiRefreshListener = new IUiUpdateListener() { + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + mTreeViewer.refresh(); + } + }; + + mUiEnableListener = new IUiUpdateListener() { + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { + // The UiElementNode for the application XML node always exists, even + // if there is no corresponding XML node in the XML file. + // + // Normally, we enable the UI here if the XML node is not null. + // + // However if mAutoCreateRoot is true, the root node will be created on-demand + // so the tree/block is always enabled. + boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null); + if (mMasterPart != null) { + Section section = mMasterPart.getSection(); + if (section.getEnabled() != exists) { + section.setEnabled(exists); + for (Control c : section.getChildren()) { + c.setEnabled(exists); + } + } + } + } + }; + + /** Listener to update the root node if the target of the file is changed because of a + * SDK location change or a project target change */ + final ITargetChangeListener targetListener = new ITargetChangeListener() { + public void onProjectTargetChange(IProject changedProject) { + if (changedProject == mEditor.getProject()) { + onTargetsLoaded(); + } + } + + public void onTargetsLoaded() { + // If a details part has been created, we need to "refresh" it too. + if (mDetailsPart != null) { + // The details part does not directly expose access to its internal + // page book. Instead it is possible to resize the page book to 0 and then + // back to its original value, which has the side effect of removing all + // existing cached pages. + int limit = mDetailsPart.getPageLimit(); + mDetailsPart.setPageLimit(0); + mDetailsPart.setPageLimit(limit); + } + // Refresh the tree, preserving the selection if possible. + mTreeViewer.refresh(); + } + }; + + // Setup the listeners + changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */); + + // Listen on resource framework changes to refresh the tree + AdtPlugin.getDefault().addTargetListener(targetListener); + + // Remove listeners when the tree widget gets disposed. + tree.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + UiElementNode node = mUiRootNode.getUiParent() != null ? + mUiRootNode.getUiParent() : + mUiRootNode; + + node.removeUpdateListener(mUiRefreshListener); + mUiRootNode.removeUpdateListener(mUiEnableListener); + + AdtPlugin.getDefault().removeTargetListener(targetListener); + if (mClipboard != null) { + mClipboard.dispose(); + mClipboard = null; + } + } + }); + + // Get a new clipboard reference. It is disposed when the tree is disposed. + mClipboard = new Clipboard(tree.getDisplay()); + + return tree; + } + + /** + * Changes the UI root node and the descriptor filters of the tree. + * <p/> + * This removes the listeners attached to the old root node and reattaches them to the + * new one. + * + * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are + * to be manipulated by this tree view. In general this is the manifest UI node or the + * application UI node. This cannot be null. + * @param descriptorFilters A list of descriptors of the elements to be displayed as root in + * this tree view. Use null or an empty list to accept any kind of node. + * @param forceRefresh If tree, forces the tree to refresh + */ + public void changeRootAndDescriptors(UiElementNode uiRootNode, + ElementDescriptor[] descriptorFilters, boolean forceRefresh) { + UiElementNode node; + + // Remove previous listeners if any + if (mUiRootNode != null) { + node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; + node.removeUpdateListener(mUiRefreshListener); + mUiRootNode.removeUpdateListener(mUiEnableListener); + } + + mUiRootNode = uiRootNode; + mDescriptorFilters = descriptorFilters; + + // Listen on structural changes on the root node of the tree + // If the node has a parent, listen on the parent instead. + node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; + node.addUpdateListener(mUiRefreshListener); + + // Use the root node to listen to its presence. + mUiRootNode.addUpdateListener(mUiEnableListener); + + // Initialize the enabled/disabled state + mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */); + + if (forceRefresh) { + mTreeViewer.refresh(); + } + + createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit()); + } + + /** + * Creates the buttons next to the tree. + */ + private void createButtons(FormToolkit toolkit, Composite grid) { + + mUiTreeActions = new UiTreeActions(); + + Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1); + button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING)); + mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH); + SectionHelper.addControlTooltip(mAddButton, "Adds a new element."); + mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | + GridData.VERTICAL_ALIGN_BEGINNING)); + + mAddButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doTreeAdd(); + } + }); + + mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH); + SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element."); + mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mRemoveButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doTreeRemove(); + } + }); + + mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH); + SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up."); + mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mUpButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doTreeUp(); + } + }); + + mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH); + SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down."); + mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mDownButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + doTreeDown(); + } + }); + + adjustTreeButtons(TreeSelection.EMPTY); + } + + private void createTreeContextMenu(Tree tree) { + MenuManager menuManager = new MenuManager(); + menuManager.setRemoveAllWhenShown(true); + menuManager.addMenuListener(new IMenuListener() { + /** + * The menu is about to be shown. The menu manager has already been + * requested to remove any existing menu item. This method gets the + * tree selection and if it is of the appropriate type it re-creates + * the necessary actions. + */ + public void menuAboutToShow(IMenuManager manager) { + ISelection selection = mTreeViewer.getSelection(); + if (!selection.isEmpty() && selection instanceof ITreeSelection) { + ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); + doCreateMenuAction(manager, selected); + return; + } + doCreateMenuAction(manager, null /* ui_node */); + } + }); + Menu contextMenu = menuManager.createContextMenu(tree); + tree.setMenu(contextMenu); + } + + /** + * Adds the menu actions to the context menu when the given UI node is selected in + * the tree view. + * + * @param manager The context menu manager + * @param selected The UI nodes selected in the tree. Can be null, in which case the root + * is to be modified. + */ + private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) { + if (selected != null) { + boolean hasXml = false; + for (UiElementNode uiNode : selected) { + if (uiNode.getXmlNode() != null) { + hasXml = true; + break; + } + } + + if (hasXml) { + manager.add(new CopyCutAction(getEditor(), getClipboard(), + null, selected, true /* cut */)); + manager.add(new CopyCutAction(getEditor(), getClipboard(), + null, selected, false /* cut */)); + + // Can't paste with more than one element selected (the selection is the target) + if (selected.size() <= 1) { + // Paste is not valid if it would add a second element on a terminal element + // which parent is a document -- an XML document can only have one child. This + // means paste is valid if the current UI node can have children or if the + // parent is not a document. + UiElementNode ui_root = selected.get(0).getUiRoot(); + if (ui_root.getDescriptor().hasChildren() || + !(ui_root.getUiParent() instanceof UiDocumentNode)) { + manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0))); + } + } + manager.add(new Separator()); + } + } + + // Append "add" and "remove" actions. They do the same thing as the add/remove + // buttons on the side. + Action action; + IconFactory factory = IconFactory.getInstance(); + + // "Add" makes sense only if there's 0 or 1 item selected since the + // one selected item becomes the target. + if (selected == null || selected.size() <= 1) { + manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$ + @Override + public void run() { + super.run(); + doTreeAdd(); + } + }); + } + + if (selected != null) { + if (selected != null) { + manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$ + @Override + public void run() { + super.run(); + doTreeRemove(); + } + }); + } + manager.add(new Separator()); + + manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$ + @Override + public void run() { + super.run(); + doTreeUp(); + } + }); + manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$ + @Override + public void run() { + super.run(); + doTreeDown(); + } + }); + } + } + + + /** + * This is called by the tree when a selection is made. + * It enables/disables the buttons associated with the tree depending on the current + * selection. + * + * @param selection The current tree selection (same as mTreeViewer.getSelection()) + */ + private void adjustTreeButtons(ISelection selection) { + mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection); + mUpButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection); + mDownButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection); + } + + /** + * An adapter/wrapper to use the add/remove/up/down tree edit actions. + */ + private class UiTreeActions extends UiActions { + @Override + protected UiElementNode getRootNode() { + return mUiRootNode; + } + + @Override + protected void selectUiNode(UiElementNode uiNodeToSelect) { + // Select the new item + if (uiNodeToSelect != null) { + LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>(); + for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode; + ui_node = ui_node.getUiParent()) { + segments.add(0, ui_node); + } + if (segments.size() > 0) { + mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray()))); + } else { + mTreeViewer.setSelection(null); + } + } + } + + @Override + public void commitPendingXmlChanges() { + commitManagedForm(); + } + } + + /** + * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's + * something else in there). + * + * @return A new list of {@link UiElementNode} with at least one item or null. + */ + @SuppressWarnings("unchecked") + private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) { + ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); + + for (Iterator it = selection.iterator(); it.hasNext(); ) { + Object selectedObj = it.next(); + + if (selectedObj instanceof UiElementNode) { + selected.add((UiElementNode) selectedObj); + } + } + + return selected.size() > 0 ? selected : null; + } + + /** + * Called when the "Add..." button next to the tree view is selected. + * + * Displays a selection dialog that lets the user select which kind of node + * to create, depending on the current selection. + */ + private void doTreeAdd() { + UiElementNode ui_node = mUiRootNode; + ISelection selection = mTreeViewer.getSelection(); + if (!selection.isEmpty() && selection instanceof ITreeSelection) { + ITreeSelection tree_selection = (ITreeSelection) selection; + Object first = tree_selection.getFirstElement(); + if (first != null && first instanceof UiElementNode) { + ui_node = (UiElementNode) first; + } + } + + mUiTreeActions.doAdd( + ui_node, + mDescriptorFilters, + mTreeViewer.getControl().getShell(), + (ILabelProvider) mTreeViewer.getLabelProvider()); + } + + /** + * Called when the "Remove" button is selected. + * + * If the tree has a selection, remove it. + * This simply deletes the XML node attached to the UI node: when the XML model fires the + * update event, the tree will get refreshed. + */ + protected void doTreeRemove() { + ISelection selection = mTreeViewer.getSelection(); + if (!selection.isEmpty() && selection instanceof ITreeSelection) { + ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); + mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell()); + } + } + + /** + * Called when the "Up" button is selected. + * <p/> + * If the tree has a selection, move it up, either in the child list or as the last child + * of the previous parent. + */ + protected void doTreeUp() { + ISelection selection = mTreeViewer.getSelection(); + if (!selection.isEmpty() && selection instanceof ITreeSelection) { + ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); + mUiTreeActions.doUp(selected); + } + } + + /** + * Called when the "Down" button is selected. + * + * If the tree has a selection, move it down, either in the same child list or as the + * first child of the next parent. + */ + protected void doTreeDown() { + ISelection selection = mTreeViewer.getSelection(); + if (!selection.isEmpty() && selection instanceof ITreeSelection) { + ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); + mUiTreeActions.doDown(selected); + } + } + + /** + * Commits the current managed form (the one associated with our master part). + * As a side effect, this will commit the current UiElementDetails page. + */ + void commitManagedForm() { + if (mManagedForm != null) { + mManagedForm.commit(false /* onSave */); + } + } + + /* Implements ICommitXml for CopyCutAction */ + public void commitPendingXmlChanges() { + commitManagedForm(); + } + + @Override + protected void createToolBarActions(IManagedForm managedForm) { + // Pass. Not used, toolbar actions are defined by createSectionActions(). + } + + @Override + protected void registerPages(DetailsPart detailsPart) { + // Keep a reference on the details part (the super class doesn't provide a getter + // for it.) + mDetailsPart = detailsPart; + + // The page selection mechanism does not use pages registered by association with + // a node class. Instead it uses a custom details page provider that provides a + // new UiElementDetail instance for each node instance. A limit of 5 pages is + // then set (the value is arbitrary but should be reasonable) for the internal + // page book. + detailsPart.setPageLimit(5); + + final UiTreeBlock tree = this; + + detailsPart.setPageProvider(new IDetailsPageProvider() { + public IDetailsPage getPage(Object key) { + if (key instanceof UiElementNode) { + return new UiElementDetail(tree); + } + return null; + } + + public Object getPageKey(Object object) { + return object; // use node object as key + } + }); + } + + /** + * An alphabetic sort action for the tree viewer. + */ + private class TreeSortAction extends Action { + + private ViewerComparator mComparator; + + public TreeSortAction() { + super("Sorts elements alphabetically.", AS_CHECK_BOX); + setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$ + + if (mTreeViewer != null) { + boolean is_sorted = mTreeViewer.getComparator() != null; + setChecked(is_sorted); + } + } + + /** + * Called when the button is selected. Toggles the tree viewer comparator. + */ + @Override + public void run() { + if (mTreeViewer == null) { + notifyResult(false /*success*/); + return; + } + + ViewerComparator comp = mTreeViewer.getComparator(); + if (comp != null) { + // Tree is currently sorted. + // Save currently comparator and remove it + mComparator = comp; + mTreeViewer.setComparator(null); + } else { + // Tree is not currently sorted. + // Reuse or add a new comparator. + if (mComparator == null) { + mComparator = new ViewerComparator(); + } + mTreeViewer.setComparator(mComparator); + } + + notifyResult(true /*success*/); + } + } + + /** + * A filter on descriptor for the tree viewer. + * <p/> + * The tree viewer will contain many of these actions and only one can be enabled at a + * given time. When no action is selected, everything is displayed. + * <p/> + * Since "radio"-like actions do not allow for unselecting all of them, we manually + * handle the exclusive radio button-like property: when an action is selected, it manually + * removes all other actions as needed. + */ + private class DescriptorFilterAction extends Action { + + private final ElementDescriptor mDescriptor; + private ViewerFilter mFilter; + + public DescriptorFilterAction(ElementDescriptor descriptor) { + super(String.format("Displays only %1$s elements.", descriptor.getUiName()), + AS_CHECK_BOX); + + mDescriptor = descriptor; + setImageDescriptor(descriptor.getImageDescriptor()); + } + + /** + * Called when the button is selected. + * <p/> + * Find any existing {@link DescriptorFilter}s and remove them. Install ours. + */ + @Override + public void run() { + super.run(); + + if (isChecked()) { + if (mFilter == null) { + // create filter when required + mFilter = new DescriptorFilter(this); + } + + // we add our filter first, otherwise the UI might show the full list + mTreeViewer.addFilter(mFilter); + + // Then remove the any other filters except ours. There should be at most + // one other filter, since that's how the actions are made to look like + // exclusive radio buttons. + for (ViewerFilter filter : mTreeViewer.getFilters()) { + if (filter instanceof DescriptorFilter && filter != mFilter) { + DescriptorFilterAction action = ((DescriptorFilter) filter).getAction(); + action.setChecked(false); + mTreeViewer.removeFilter(filter); + } + } + } else if (mFilter != null){ + mTreeViewer.removeFilter(mFilter); + } + } + + /** + * Filters the tree viewer for the given descriptor. + * <p/> + * The filter is linked to the action so that an action can iterate through the list + * of filters and un-select the actions. + */ + private class DescriptorFilter extends ViewerFilter { + + private final DescriptorFilterAction mAction; + + public DescriptorFilter(DescriptorFilterAction action) { + mAction = action; + } + + public DescriptorFilterAction getAction() { + return mAction; + } + + /** + * Returns true if an element should be displayed, that if the element or + * any of its parent matches the requested descriptor. + */ + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + while (element instanceof UiElementNode) { + UiElementNode uiNode = (UiElementNode)element; + if (uiNode.getDescriptor() == mDescriptor) { + return true; + } + element = uiNode.getUiParent(); + } + return false; + } + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java new file mode 100644 index 0000000..7fe44da --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.uimodel; + +/** + * This interface decoration indicates that a given UiAttributeNode can both + * set and get its current value. + */ +public interface IUiSettableAttributeNode { + + /** Returns the current value of the node. */ + public String getCurrentValue(); + + /** Sets the current value of the node. Cannot be null (use an empty string). */ + public void setCurrentValue(String value); + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java new file mode 100644 index 0000000..12cb31b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + + +/** + * Listen to update notifications in UI nodes. + */ +public interface IUiUpdateListener { + + /** Update state of the UI node */ + public enum UiUpdateState { + /** The node's attributes have been updated. They may or may not actually have changed. */ + ATTR_UPDATED, + /** The node sub-structure (i.e. child nodes) has changed */ + CHILDREN_CHANGED, + /** The XML counterpart for the UI node has just been created. */ + CREATED, + /** The XML counterpart for the UI node has just been deleted. + * Note that mandatory UI nodes are never actually deleted. */ + DELETED + } + + /** + * Indicates that an UiElementNode has been updated. + * <p/> + * This happens when an {@link UiElementNode} is refreshed to match the + * XML model. The actual UI element node may or may not have changed. + * + * @param ui_node The {@link UiElementNode} being updated. + */ + public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java new file mode 100644 index 0000000..4a9fbb1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; + +import org.w3c.dom.Node; + +/** + * Represents an XML attribute in that can be modified using a simple text field + * in the XML editor's user interface. + * <p/> + * The XML attribute has no default value. When unset, the text field is blank. + * When updating the XML, if the field is empty, the attribute will be removed + * from the XML element. + * <p/> + * See {@link UiAttributeNode} for more information. + */ +public abstract class UiAbstractTextAttributeNode extends UiAttributeNode + implements IUiSettableAttributeNode { + + protected static final String DEFAULT_VALUE = ""; //$NON-NLS-1$ + + /** Prevent internal listener from firing when internally modifying the text */ + private boolean mInternalTextModification; + /** Last value read from the XML model. Cannot be null. */ + private String mCurrentValue = DEFAULT_VALUE; + + public UiAbstractTextAttributeNode(AttributeDescriptor attributeDescriptor, + UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /** Returns the current value of the node. */ + @Override + public final String getCurrentValue() { + return mCurrentValue; + } + + /** Sets the current value of the node. Cannot be null (use an empty string). */ + public final void setCurrentValue(String value) { + mCurrentValue = value; + } + + /** Returns if the attribute node is valid, and its UI has been created. */ + public abstract boolean isValid(); + + /** Returns the text value present in the UI. */ + public abstract String getTextWidgetValue(); + + /** Sets the text value to be displayed in the UI. */ + public abstract void setTextWidgetValue(String value); + + + /** + * Updates the current text field's value when the XML has changed. + * <p/> + * The caller doesn't really know if attributes have changed, + * so it will call this to refresh the attribute anyway. The value + * is only set if it has changed. + * <p/> + * This also resets the "dirty" flag. + */ + @Override + public void updateValue(Node xml_attribute_node) { + mCurrentValue = DEFAULT_VALUE; + if (xml_attribute_node != null) { + mCurrentValue = xml_attribute_node.getNodeValue(); + } + + if (isValid() && !getTextWidgetValue().equals(mCurrentValue)) { + try { + mInternalTextModification = true; + setTextWidgetValue(mCurrentValue); + setDirty(false); + } finally { + mInternalTextModification = false; + } + } + } + + /* (non-java doc) + * Called by the user interface when the editor is saved or its state changed + * and the modified attributes must be committed (i.e. written) to the XML model. + */ + @Override + public void commit() { + UiElementNode parent = getUiParent(); + if (parent != null && isValid() && isDirty()) { + String value = getTextWidgetValue(); + if (parent.commitAttributeToXml(this, value)) { + mCurrentValue = value; + setDirty(false); + } + } + } + + protected final boolean isInInternalTextModification() { + return mInternalTextModification; + } + + protected final void setInInternalTextModification(boolean internalTextModification) { + mInternalTextModification = internalTextModification; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java new file mode 100644 index 0000000..5972f22 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.forms.IManagedForm; +import org.w3c.dom.Node; + +/** + * Represents an XML attribute that can be modified by the XML editor's user interface. + * <p/> + * The characteristics of an {@link UiAttributeNode} are declared by a + * corresponding {@link AttributeDescriptor}. + * <p/> + * This is an abstract class. Derived classes must implement the creation of the UI + * and manage its synchronization with the XML. + */ +public abstract class UiAttributeNode { + + private AttributeDescriptor mDescriptor; + private UiElementNode mUiParent; + private boolean mIsDirty; + private boolean mHasError; + + /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor} + * and the corresponding runtine {@link UiElementNode} parent. */ + public UiAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + mDescriptor = attributeDescriptor; + mUiParent = uiParent; + } + + /** Returns the {@link AttributeDescriptor} specific to this UI attribute node */ + public final AttributeDescriptor getDescriptor() { + return mDescriptor; + } + + /** Returns the {@link UiElementNode} that owns this {@link UiAttributeNode} */ + public final UiElementNode getUiParent() { + return mUiParent; + } + + /** Returns the current value of the node. */ + public abstract String getCurrentValue(); + + /** + * @return True if the attribute has been changed since it was last loaded + * from the XML model. + */ + public final boolean isDirty() { + return mIsDirty; + } + + /** + * Sets whether the attribute is dirty and also notifies the editor some part's dirty + * flag as changed. + * <p/> + * Subclasses should set the to true as a result of user interaction with the widgets in + * the section and then should set to false when the commit() method completed. + */ + public void setDirty(boolean isDirty) { + boolean old_value = mIsDirty; + mIsDirty = isDirty; + // TODO: for unknown attributes, getParent() != null && getParent().getEditor() != null + if (old_value != isDirty) { + getUiParent().getEditor().editorDirtyStateChanged(); + } + } + + /** + * Sets the error flag value. + * @param errorFlag the error flag + */ + public final void setHasError(boolean errorFlag) { + mHasError = errorFlag; + } + + /** + * Returns whether this node has errors. + */ + public final boolean hasError() { + return mHasError; + } + + /** + * Called once by the parent user interface to creates the necessary + * user interface to edit this attribute. + * <p/> + * This method can be called more than once in the life cycle of an UI node, + * typically when the UI is part of a master-detail tree, as pages are swapped. + * + * @param parent The composite where to create the user interface. + * @param managedForm The managed form owning this part. + */ + public abstract void createUiControl(Composite parent, IManagedForm managedForm); + + /** + * Used to get a list of all possible values for this UI attribute. + * <p/> + * This is used, among other things, by the XML Content Assists to complete values + * for an attribute. + * <p/> + * Implementations that do not have any known values should return null. + * + * @return A list of possible completion values or null. + */ + public abstract String[] getPossibleValues(); + + /** + * Called when the XML is being loaded or has changed to + * update the value held by this user interface attribute node. + * <p/> + * The XML Node <em>may</em> be null, which denotes that the attribute is not + * specified in the XML model. In general, this means the "default" value of the + * attribute should be used. + * <p/> + * The caller doesn't really know if attributes have changed, + * so it will call this to refresh the attribute anyway. It's up to the + * UI implementation to minimize refreshes. + * + * @param xml_attribute_node + */ + public abstract void updateValue(Node xml_attribute_node); + + /** + * Called by the user interface when the editor is saved or its state changed + * and the modified attributes must be committed (i.e. written) to the XML model. + * <p/> + * Important behaviors: + * <ul> + * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before. + * The implemented methods must assume it is safe to modify the XML model. + * <li>On success, the implementation *must* call setDirty(false). + * <li>On failure, the implementation can fail with an exception, which + * is trapped and logged by the caller, or do nothing, whichever is more + * appropriate. + * </ul> + */ + public abstract void commit(); +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java new file mode 100644 index 0000000..113738f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener.UiUpdateState; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +/** + * Represents an XML document node that can be modified by the user interface in the XML editor. + * <p/> + * The structure of a given {@link UiDocumentNode} is declared by a corresponding + * {@link DocumentDescriptor}. + */ +public class UiDocumentNode extends UiElementNode { + + /** + * Creates a new {@link UiDocumentNode} described by a given {@link DocumentDescriptor}. + * + * @param documentDescriptor The {@link DocumentDescriptor} for the XML node. Cannot be null. + */ + public UiDocumentNode(DocumentDescriptor documentDescriptor) { + super(documentDescriptor); + } + + /** + * Computes a short string describing the UI node suitable for tree views. + * Uses the element's attribute "android:name" if present, or the "android:label" one + * followed by the element's name. + * + * @return A short string describing the UI node suitable for tree views. + */ + @Override + public String getShortDescription() { + return "Document"; //$NON-NLS-1$ + } + + /** + * Computes a "breadcrumb trail" description for this node. + * + * @param include_root Whether to include the root (e.g. "Manifest") or not. Has no effect + * when called on the root node itself. + * @return The "breadcrumb trail" description for this node. + */ + @Override + public String getBreadcrumbTrailDescription(boolean include_root) { + return "Document"; //$NON-NLS-1$ + } + + /** + * This method throws an exception when attempted to assign a parent, since XML documents + * cannot have a parent. It is OK to assign null. + */ + @Override + protected void setUiParent(UiElementNode parent) { + if (parent != null) { + // DEBUG. Change to log warning. + throw new UnsupportedOperationException("Documents can't have UI parents"); //$NON-NLS-1$ + } + super.setUiParent(null); + } + + /** + * Populate this element node with all values from the given XML node. + * + * This fails if the given XML node has a different element name -- it won't change the + * type of this ui node. + * + * This method can be both used for populating values the first time and updating values + * after the XML model changed. + * + * @param xml_node The XML node to mirror + * @return Returns true if the XML structure has changed (nodes added, removed or replaced) + */ + @Override + public boolean loadFromXmlNode(Node xml_node) { + boolean structure_changed = (getXmlDocument() != xml_node); + setXmlDocument((Document) xml_node); + structure_changed |= super.loadFromXmlNode(xml_node); + if (structure_changed) { + invokeUiUpdateListeners(UiUpdateState.CHILDREN_CHANGED); + } + return structure_changed; + } + + /** + * This method throws an exception if there is no underlying XML document. + * <p/> + * XML documents cannot be created per se -- they are a by-product of the StructuredEditor + * XML parser. + * + * @return The current value of getXmlDocument(). + */ + @Override + public Node createXmlNode() { + if (getXmlDocument() == null) { + // By design, a document node cannot be created, it is owned by the XML parser. + // By "design" this should never happen since the XML parser always creates an XML + // document container, even for an empty file. + throw new UnsupportedOperationException("Documents cannot be created"); //$NON-NLS-1$ + } + return getXmlDocument(); + } + + /** + * This method throws an exception and does not even try to delete the XML document. + * <p/> + * XML documents cannot be deleted per se -- they are a by-product of the StructuredEditor + * XML parser. + * + * @return The removed node or null if it didn't exist in the firtst place. + */ + @Override + public Node deleteXmlNode() { + // DEBUG. Change to log warning. + throw new UnsupportedOperationException("Documents cannot be deleted"); //$NON-NLS-1$ + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java new file mode 100644 index 0000000..3728886 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java @@ -0,0 +1,1500 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.layout.descriptors.CustomViewDescriptorService; +import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors; +import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors; +import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors; +import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener.UiUpdateState; +import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.views.properties.IPropertyDescriptor; +import org.eclipse.ui.views.properties.IPropertySource; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.Text; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * Represents an XML node that can be modified by the user interface in the XML editor. + * <p/> + * Each tree viewer used in the application page's parts needs to keep a model representing + * each underlying node in the tree. This interface represents the base type for such a node. + * <p/> + * Each node acts as an intermediary model between the actual XML model (the real data support) + * and the tree viewers or the corresponding page parts. + * <p/> + * Element nodes don't contain data per se. Their data is contained in their attributes + * as well as their children's attributes, see {@link UiAttributeNode}. + * <p/> + * The structure of a given {@link UiElementNode} is declared by a corresponding + * {@link ElementDescriptor}. + * <p/> + * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when + * an element is selected. The {@link AttributeDescriptor} are used property descriptors. + */ +public class UiElementNode implements IPropertySource { + + /** List of prefixes removed from android:id strings when creating short descriptions. */ + private static String[] ID_PREFIXES = { + "@android:id/", //$NON-NLS-1$ + "@+id/", "@id/", "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + + /** The element descriptor for the node. Always present, never null. */ + private ElementDescriptor mDescriptor; + /** The parent element node in the UI model. It is null for a root element or until + * the node is attached to its parent. */ + private UiElementNode mUiParent; + /** The {@link AndroidEditor} handling the UI hierarchy. This is defined only for the + * root node. All children have the value set to null and query their parent. */ + private AndroidEditor mEditor; + /** The XML {@link Document} model that is being mirror by the UI model. This is defined + * only for the root node. All children have the value set to null and query their parent. */ + private Document mXmlDocument; + /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which + * have no corresponding XML node or for new UI nodes before their XML node is set. */ + private Node mXmlNode; + /** The list of all UI children nodes. Can be empty but never null. There's one UI children + * node per existing XML children node. */ + private ArrayList<UiElementNode> mUiChildren; + /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}. + * The list is always defined and never null. Unlike the UiElementNode children list, this + * is always defined, even for attributes that do not exist in the XML model -- that's because + * "missing" attributes in the XML model simply mean a default value is used. Also note that + * the underlying collection is a map, so order is not respected. To get the desired attribute + * order, iterate through the {@link ElementDescriptor}'s attribute list. */ + private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes; + private HashSet<UiAttributeNode> mUnknownUiAttributes; + /** A read-only view of the UI children node collection. */ + private List<UiElementNode> mReadOnlyUiChildren; + /** A read-only view of the UI attributes collection. */ + private Collection<UiAttributeNode> mReadOnlyUiAttributes; + /** A map of hidden attribute descriptors. Key is the XML name. */ + private Map<String, AttributeDescriptor> mCachedHiddenAttributes; + /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any + * listeners attached, so the list is only created on demand and can be null. */ + private ArrayList<IUiUpdateListener> mUiUpdateListeners; + /** Error Flag */ + private boolean mHasError; + /** Temporary data used by the editors. This data is not sync'ed with the XML */ + private Object mEditData; + + /** + * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}. + * + * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null. + */ + public UiElementNode(ElementDescriptor elementDescriptor) { + mDescriptor = elementDescriptor; + clearContent(); + } + + /** + * Clears the {@link UiElementNode} by resetting the children list and + * the {@link UiAttributeNode}s list. + * Also resets the attached XML node, document, editor if any. + * <p/> + * The parent {@link UiElementNode} node is not reset so that it's position + * in the hierarchy be left intact, if any. + */ + /* package */ void clearContent() { + mXmlNode = null; + mXmlDocument = null; + mEditor = null; + clearAttributes(); + mReadOnlyUiChildren = null; + if (mUiChildren == null) { + mUiChildren = new ArrayList<UiElementNode>(); + } else { + // We can't remove mandatory nodes, we just clear them. + for (int i = mUiChildren.size() - 1; i >= 0; --i) { + removeUiChildAtIndex(i); + } + } + } + + /** + * Clears the internal list of attributes, the read-only cached version of it + * and the read-only cached hidden attribute list. + */ + private void clearAttributes() { + mUiAttributes = null; + mReadOnlyUiAttributes = null; + mCachedHiddenAttributes = null; + mUnknownUiAttributes = new HashSet<UiAttributeNode>(); + } + + /** + * Gets or creates the internal UiAttributes list. + * <p/> + * When the descriptor derives from ViewElementDescriptor, this list depends on the + * current UiParent node. + * + * @return A new set of {@link UiAttributeNode} that matches the expected + * attributes for this node. + */ + private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() { + if (mUiAttributes == null) { + AttributeDescriptor[] attr_list = getAttributeDescriptors(); + mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attr_list.length); + for (AttributeDescriptor desc : attr_list) { + UiAttributeNode ui_node = desc.createUiNode(this); + if (ui_node != null) { // Some AttributeDescriptors do not have UI associated + mUiAttributes.put(desc, ui_node); + } + } + } + return mUiAttributes; + } + + /** + * Computes a short string describing the UI node suitable for tree views. + * Uses the element's attribute "android:name" if present, or the "android:label" one + * followed by the element's name. + * + * @return A short string describing the UI node suitable for tree views. + */ + public String getShortDescription() { + if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) { + + // Application and Manifest nodes have a special treatment: they are unique nodes + // so we don't bother trying to differentiate their strings and we fall back to + // just using the UI name below. + Element elem = (Element) mXmlNode; + + String attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES, + AndroidManifestDescriptors.ANDROID_NAME_ATTR); + if (attr == null || attr.length() == 0) { + attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES, + AndroidManifestDescriptors.ANDROID_LABEL_ATTR); + } + if (attr == null || attr.length() == 0) { + attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES, + XmlDescriptors.PREF_KEY_ATTR); + } + if (attr == null || attr.length() == 0) { + attr = elem.getAttribute(ResourcesDescriptors.NAME_ATTR); + } + if (attr == null || attr.length() == 0) { + attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES, + LayoutDescriptors.ID_ATTR); + + if (attr != null && attr.length() > 0) { + for (String prefix : ID_PREFIXES) { + if (attr.startsWith(prefix)) { + attr = attr.substring(prefix.length()); + break; + } + } + } + } + if (attr != null && attr.length() > 0) { + return String.format("%1$s (%2$s)", attr, mDescriptor.getUiName()); + } + } + + return String.format("%1$s", mDescriptor.getUiName()); + } + + /** + * Computes a "breadcrumb trail" description for this node. + * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter" + * + * @param include_root Whether to include the root (e.g. "Manifest") or not. Has no effect + * when called on the root node itself. + * @return The "breadcrumb trail" description for this node. + */ + public String getBreadcrumbTrailDescription(boolean include_root) { + StringBuilder sb = new StringBuilder(getShortDescription()); + + for (UiElementNode ui_node = getUiParent(); + ui_node != null; + ui_node = ui_node.getUiParent()) { + if (!include_root && ui_node.getUiParent() == null) { + break; + } + sb.insert(0, String.format("%1$s > ", ui_node.getShortDescription())); //$NON-NLS-1$ + } + + return sb.toString(); + } + + /** + * Sets the XML {@link Document}. + * <p/> + * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the + * UI root element node (this method takes care of that.) + */ + public void setXmlDocument(Document xml_doc) { + if (mUiParent == null) { + mXmlDocument = xml_doc; + } else { + mUiParent.setXmlDocument(xml_doc); + } + } + + /** + * Returns the XML {@link Document}. + * <p/> + * The value is initially null until the UI node is attached to its UI parent -- the value + * of the document is then propagated. + * + * @return the XML {@link Document} or the parent's XML {@link Document} or null. + */ + public Document getXmlDocument() { + if (mXmlDocument != null) { + return mXmlDocument; + } else if (mUiParent != null) { + return mUiParent.getXmlDocument(); + } + return null; + } + + /** + * Returns the XML node associated with this UI node. + * <p/> + * Some {@link ElementDescriptor} are declared as being "mandatory". This means the + * corresponding UI node will exist even if there is no corresponding XML node. Such structure + * is created and enforced by the parent of the tree, not the element themselves. However + * such nodes will likely not have an XML node associated, so getXmlNode() can return null. + * + * @return The associated XML node. Can be null for mandatory nodes. + */ + public Node getXmlNode() { + return mXmlNode; + } + + /** + * Returns the {@link ElementDescriptor} for this node. This is never null. + * <p/> + * Do not use this to call getDescriptor().getAttributes(), instead call + * getAttributeDescriptors() which can be overriden by derived classes. + */ + public ElementDescriptor getDescriptor() { + return mDescriptor; + } + + /** + * Returns the {@link AttributeDescriptor} array for the descriptor of this node. + * <p/> + * Use this instead of getDescriptor().getAttributes() -- derived classes can override + * this to manipulate the attribute descriptor list depending on the current UI node. + */ + public AttributeDescriptor[] getAttributeDescriptors() { + return mDescriptor.getAttributes(); + } + + /** + * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node. + * This is a subset of the getAttributeDescriptors() list. + * <p/> + * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes + * could override this to manipulate the attribute descriptor list depending on the current + * UI node. There's no need for it right now so keep it private. + */ + private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() { + if (mCachedHiddenAttributes == null) { + mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>(); + for (AttributeDescriptor attr_desc : getAttributeDescriptors()) { + if (attr_desc instanceof XmlnsAttributeDescriptor) { + mCachedHiddenAttributes.put( + ((XmlnsAttributeDescriptor) attr_desc).getXmlNsName(), + attr_desc); + } + } + } + return mCachedHiddenAttributes; + } + + /** + * Sets the parent of this UiElementNode. + * <p/> + * The root node has no parent. + */ + protected void setUiParent(UiElementNode parent) { + mUiParent = parent; + // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent. + clearAttributes(); + } + + /** + * @return The parent {@link UiElementNode} or null if this is the root node. + */ + public UiElementNode getUiParent() { + return mUiParent; + } + + /** + * Returns The root {@link UiElementNode}. + */ + public UiElementNode getUiRoot() { + UiElementNode root = this; + while (root.mUiParent != null) { + root = root.mUiParent; + } + + return root; + } + + /** + * Returns the previous UI sibling of this UI node. + * If the node does not have a previous sibling, returns null. + */ + public UiElementNode getUiPreviousSibling() { + if (mUiParent != null) { + List<UiElementNode> childlist = mUiParent.getUiChildren(); + if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) { + int index = childlist.indexOf(this); + return index > 0 ? childlist.get(index - 1) : null; + } + } + return null; + } + + /** + * Returns the next UI sibling of this UI node. + * If the node does not have a next sibling, returns null. + */ + public UiElementNode getUiNextSibling() { + if (mUiParent != null) { + List<UiElementNode> childlist = mUiParent.getUiChildren(); + if (childlist != null) { + int size = childlist.size(); + if (size > 1 && childlist.get(size - 1) != this) { + int index = childlist.indexOf(this); + return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null; + } + } + } + return null; + } + + /** + * Sets the {@link AndroidEditor} handling this {@link UiElementNode} hierarchy. + * <p/> + * The editor must always be set on the root node. This method takes care of that. + */ + public void setEditor(AndroidEditor editor) { + if (mUiParent == null) { + mEditor = editor; + } else { + mUiParent.setEditor(editor); + } + } + + /** + * Returns the {@link AndroidEditor} that embeds this {@link UiElementNode}. + * <p/> + * The value is initially null until the node is attached to its parent -- the value + * of the root node is then propagated. + * + * @return The embedding {@link AndroidEditor} or null. + */ + public AndroidEditor getEditor() { + return mUiParent == null ? mEditor : mUiParent.getEditor(); + } + + /** + * Returns the Android target data for the file being edited. + */ + public AndroidTargetData getAndroidTarget() { + return getEditor().getTargetData(); + } + + /** + * @return A read-only version of the children collection. + */ + public List<UiElementNode> getUiChildren() { + if (mReadOnlyUiChildren == null) { + mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren); + } + return mReadOnlyUiChildren; + } + + /** + * @return A read-only version of the attributes collection. + */ + public Collection<UiAttributeNode> getUiAttributes() { + if (mReadOnlyUiAttributes == null) { + mReadOnlyUiAttributes = Collections.unmodifiableCollection( + getInternalUiAttributes().values()); + } + return mReadOnlyUiAttributes; + } + + /** + * @return A read-only version of the unknown attributes collection. + */ + public Collection<UiAttributeNode> getUnknownUiAttributes() { + return Collections.unmodifiableCollection(mUnknownUiAttributes); + } + + /** + * Sets the error flag value. + * @param errorFlag the error flag + */ + public final void setHasError(boolean errorFlag) { + mHasError = errorFlag; + } + + /** + * Returns whether this node, its attributes, or one of the children nodes (and attributes) + * has errors. + */ + public final boolean hasError() { + if (mHasError) { + return true; + } + + // get the error value from the attributes. + Collection<UiAttributeNode> attributes = getInternalUiAttributes().values(); + for (UiAttributeNode attribute : attributes) { + if (attribute.hasError()) { + return true; + } + } + + // and now from the children. + for (UiElementNode child : mUiChildren) { + if (child.hasError()) { + return true; + } + } + + return false; + } + + /** + * Adds a new {@link IUiUpdateListener} to the internal update listener list. + */ + public void addUpdateListener(IUiUpdateListener listener) { + if (mUiUpdateListeners == null) { + mUiUpdateListeners = new ArrayList<IUiUpdateListener>(); + } + if (!mUiUpdateListeners.contains(listener)) { + mUiUpdateListeners.add(listener); + } + } + + /** + * Removes an existing {@link IUiUpdateListener} from the internal update listener list. + * Does nothing if the list is empty or the listener is not registered. + */ + public void removeUpdateListener(IUiUpdateListener listener) { + if (mUiUpdateListeners != null) { + mUiUpdateListeners.remove(listener); + } + } + + /** + * Finds a child node relative to this node using a path-like expression. + * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and + * returns the latter. If there are multiple nodes with the same name at the same + * level, always uses the first one found. + * + * @param path The path like expression to select a child node. + * @return The ui node found or null. + */ + public UiElementNode findUiChildNode(String path) { + String[] items = path.split("/"); //$NON-NLS-1$ + UiElementNode ui_node = this; + for (String item : items) { + boolean next_segment = false; + for (UiElementNode c : ui_node.mUiChildren) { + if (c.getDescriptor().getXmlName().equals(item)) { + ui_node = c; + next_segment = true; + break; + } + } + if (!next_segment) { + return null; + } + } + return ui_node; + } + + /** + * Finds an {@link UiElementNode} which contains the give XML {@link Node}. + * Looks recursively in all children UI nodes. + * + * @param xmlNode The XML node to look for. + * @return The {@link UiElementNode} that contains xmlNode or null if not found, + */ + public UiElementNode findXmlNode(Node xmlNode) { + if (xmlNode == null) { + return null; + } + if (getXmlNode() == xmlNode) { + return this; + } + + for (UiElementNode uiChild : mUiChildren) { + UiElementNode found = uiChild.findXmlNode(xmlNode); + if (found != null) { + return found; + } + } + + return null; + } + + /** + * Returns the {@link UiAttributeNode} matching this attribute descriptor or + * null if not found. + * + * @param attr_desc The {@link AttributeDescriptor} to match. + * @return the {@link UiAttributeNode} matching this attribute descriptor or null + * if not found. + */ + public UiAttributeNode findUiAttribute(AttributeDescriptor attr_desc) { + return getInternalUiAttributes().get(attr_desc); + } + + /** + * Populate this element node with all values from the given XML node. + * + * This fails if the given XML node has a different element name -- it won't change the + * type of this ui node. + * + * This method can be both used for populating values the first time and updating values + * after the XML model changed. + * + * @param xml_node The XML node to mirror + * @return Returns true if the XML structure has changed (nodes added, removed or replaced) + */ + public boolean loadFromXmlNode(Node xml_node) { + boolean structure_changed = (mXmlNode != xml_node); + mXmlNode = xml_node; + if (xml_node != null) { + updateAttributeList(xml_node); + structure_changed |= updateElementList(xml_node); + invokeUiUpdateListeners(structure_changed ? UiUpdateState.CHILDREN_CHANGED + : UiUpdateState.ATTR_UPDATED); + } + return structure_changed; + } + + /** + * Clears the UI node and reload it from the given XML node. + * <p/> + * This works by clearing all references to any previous XML or UI nodes and + * then reloads the XML document from scratch. The editor reference is kept. + * <p/> + * This is used in the special case where the ElementDescriptor structure has changed. + * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother + * and reload everything. This is not subtle and should be used very rarely. + * + * @param xml_node The XML node or document to reload. Can be null. + */ + public void reloadFromXmlNode(Node xml_node) { + // The editor needs to be preserved, it is not affected by an XML change. + AndroidEditor editor = getEditor(); + clearContent(); + setEditor(editor); + if (xml_node != null) { + setXmlDocument(xml_node.getOwnerDocument()); + } + // This will reload all the XML and recreate the UI structure from scratch. + loadFromXmlNode(xml_node); + } + + /** + * Called by attributes when they want to commit their value + * to an XML node. + * <p/> + * For mandatory nodes, this makes sure the underlying XML element node + * exists in the model. If not, it is created and assigned as the underlying + * XML node. + * </br> + * For non-mandatory nodes, simply return the underlying XML node, which + * must always exists. + * + * @return The XML node matching this {@link UiElementNode} or null. + */ + public Node prepareCommit() { + if (getDescriptor().isMandatory()) { + createXmlNode(); + // The new XML node has been created. + // We don't need to refresh using loadFromXmlNode() since there are + // no attributes or elements that need to be loading into this node. + } + return getXmlNode(); + } + + /** + * Commits the attributes (all internal, inherited from UI parent & unknown attributes). + * This is called by the UI when the embedding part needs to be committed. + */ + public void commit() { + for (UiAttributeNode ui_attr : getInternalUiAttributes().values()) { + ui_attr.commit(); + } + + for (UiAttributeNode ui_attr : mUnknownUiAttributes) { + ui_attr.commit(); + } + } + + /** + * Returns true if the part has been modified with respect to the data + * loaded from the model. + */ + public boolean isDirty() { + for (UiAttributeNode ui_attr : getInternalUiAttributes().values()) { + if (ui_attr.isDirty()) { + return true; + } + } + + for (UiAttributeNode ui_attr : mUnknownUiAttributes) { + if (ui_attr.isDirty()) { + return true; + } + } + + return false; + } + + /** + * Creates the underlying XML element node for this UI node if it doesn't already + * exists. + * + * @return The new value of getXmlNode() (can be null if creation failed) + */ + public Node createXmlNode() { + if (mXmlNode != null) { + return null; + } + Node parentXmlNode = null; + if (mUiParent != null) { + parentXmlNode = mUiParent.prepareCommit(); + if (parentXmlNode == null) { + // The parent failed to create its own backing XML node. Abort. + // No need to throw an exception, the parent will most likely + // have done so itself. + return null; + } + } + + String element_name = getDescriptor().getXmlName(); + Document doc = getXmlDocument(); + + // We *must* have a root node. If not, we need to abort. + if (doc == null) { + throw new RuntimeException( + String.format("Missing XML document for %1$s XML node.", element_name)); + } + + // If we get here and parent_xml_node is null, the node is to be created + // as the root node of the document (which can't be null, cf check above). + if (parentXmlNode == null) { + parentXmlNode = doc; + } + + mXmlNode = doc.createElement(element_name); + + Node xmlNextSibling = null; + + UiElementNode uiNextSibling = getUiNextSibling(); + if (uiNextSibling != null) { + xmlNextSibling = uiNextSibling.getXmlNode(); + } + + parentXmlNode.insertBefore(mXmlNode, xmlNextSibling); + + // Insert a separator after the tag, to make it easier to read + Text sep = doc.createTextNode("\n"); + parentXmlNode.appendChild(sep); + + // Set all initial attributes in the XML node if they are not empty. + // Iterate on the descriptor list to get the desired order and then use the + // internal values, if any. + for (AttributeDescriptor attr_desc : getAttributeDescriptors()) { + if (attr_desc instanceof XmlnsAttributeDescriptor) { + XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attr_desc; + Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI, + desc.getXmlNsName()); + attr.setValue(desc.getValue()); + attr.setPrefix(desc.getXmlNsPrefix()); + mXmlNode.getAttributes().setNamedItemNS(attr); + } else { + UiAttributeNode ui_attr = getInternalUiAttributes().get(attr_desc); + commitAttributeToXml(ui_attr, ui_attr.getCurrentValue()); + } + } + + invokeUiUpdateListeners(UiUpdateState.CREATED); + return mXmlNode; + } + + /** + * Removes the XML node corresponding to this UI node if it exists + * and also removes all mirrored information in this UI node (i.e. children, attributes) + * + * @return The removed node or null if it didn't exist in the firtst place. + */ + public Node deleteXmlNode() { + if (mXmlNode == null) { + return null; + } + + // First clear the internals of the node and *then* actually deletes the XML + // node (because doing so will generate an update even and this node may be + // revisited via loadFromXmlNode). + Node old_xml_node = mXmlNode; + clearContent(); + + Node xml_parent = old_xml_node.getParentNode(); + if (xml_parent == null) { + xml_parent = getXmlDocument(); + } + old_xml_node = xml_parent.removeChild(old_xml_node); + + invokeUiUpdateListeners(UiUpdateState.DELETED); + return old_xml_node; + } + + /** + * Updates the element list for this UiElementNode. + * At the end, the list of children UiElementNode here will match the one from the + * provided XML {@link Node}: + * <ul> + * <li> Walk both the current ui children list and the xml children list at the same time. + * <li> If we have a new xml child but already reached the end of the ui child list, add the + * new xml node. + * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so, + * move it here. It means the XML child list has been reordered. + * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list. + * <li> At the end, we may have finished walking the xml child list but still have remaining + * ui children, simply delete them as they matching trailing xml nodes that have been + * removed unless they are mandatory ui nodes. + * </ul> + * Note that only the first case is used when populating the ui list the first time. + * + * @param xml_node The XML node to mirror + * @return True when the XML structure has changed. + */ + protected boolean updateElementList(Node xml_node) { + boolean structure_changed = false; + int ui_index = 0; + Node xml_child = xml_node.getFirstChild(); + while (xml_child != null) { + if (xml_child.getNodeType() == Node.ELEMENT_NODE) { + String element_name = xml_child.getNodeName(); + UiElementNode ui_node = null; + if (mUiChildren.size() <= ui_index) { + // A new node is being added at the end of the list + ElementDescriptor desc = mDescriptor.findChildrenDescriptor(element_name, + false /* recursive */); + if (desc == null) { + // Unknown node. Create a temporary descriptor for it. + // most important we want to auto-add unknown attributes to it. + AndroidEditor editor = getEditor(); + IEditorInput editorInput = editor.getEditorInput(); + if (editorInput instanceof IFileEditorInput) { + IFileEditorInput fileInput = (IFileEditorInput)editorInput; + desc = CustomViewDescriptorService.getInstance().getDescriptor( + fileInput.getFile().getProject(), element_name); + if (desc == null) { + desc = new ElementDescriptor(element_name); + } + } else { + desc = new ElementDescriptor(element_name); + // TODO associate a new "?" icon to this descriptor. + } + } + structure_changed = true; + ui_node = appendNewUiChild(desc); + ui_index++; + } else { + // A new node is being inserted or moved. + // Note: mandatory nodes can be created without an XML node in which case + // getXmlNode() is null. + UiElementNode ui_child; + int n = mUiChildren.size(); + for (int j = ui_index; j < n; j++) { + ui_child = mUiChildren.get(j); + if (ui_child.getXmlNode() != null && ui_child.getXmlNode() == xml_child) { + if (j > ui_index) { + // Found the same XML node at some later index, now move it here. + mUiChildren.remove(j); + mUiChildren.add(ui_index, ui_child); + structure_changed = true; + } + ui_node = ui_child; + ui_index++; + break; + } + } + + if (ui_node == null) { + // Look for an unused mandatory node with no XML node attached + // referencing the same XML element name + for (int j = ui_index; j < n; j++) { + ui_child = mUiChildren.get(j); + if (ui_child.getXmlNode() == null && + ui_child.getDescriptor().isMandatory() && + ui_child.getDescriptor().getXmlName().equals(element_name)) { + if (j > ui_index) { + // Found it, now move it here + mUiChildren.remove(j); + mUiChildren.add(ui_index, ui_child); + } + // assign the XML node to this empty mandatory element. + ui_child.mXmlNode = xml_child; + structure_changed = true; + ui_node = ui_child; + ui_index++; + } + } + } + + if (ui_node == null) { + // Inserting new node + ElementDescriptor desc = mDescriptor.findChildrenDescriptor(element_name, + false /* recursive */); + if (desc == null) { + // Unknown element. Simply ignore it. + AdtPlugin.log(IStatus.WARNING, + "AndroidManifest: Ignoring unknown '%s' XML element", //$NON-NLS-1$ + element_name); + } else { + structure_changed = true; + ui_node = insertNewUiChild(ui_index, desc); + ui_index++; + } + } + } + if (ui_node != null) { + // If we touched an UI Node, even an existing one, refresh its content. + // For new nodes, this will populate them recursively. + structure_changed |= ui_node.loadFromXmlNode(xml_child); + } + } + xml_child = xml_child.getNextSibling(); + } + + // There might be extra UI nodes at the end if the XML node list got shorter. + for (int index = mUiChildren.size() - 1; index >= ui_index; --index) { + structure_changed |= removeUiChildAtIndex(index); + } + + return structure_changed; + } + + /** + * Internal helper to remove an UI child node given by its index in the + * internal child list. + * + * Also invokes the update listener on the node to be deleted. + * + * @param ui_index The index of the UI child to remove, range 0 .. mUiChildren.size()-1 + * @return True if the structure has changed + * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you + * know that could never happen unless the computer is on fire or something. + */ + private boolean removeUiChildAtIndex(int ui_index) { + UiElementNode ui_node = mUiChildren.get(ui_index); + invokeUiUpdateListeners(UiUpdateState.DELETED); + if (ui_node.getDescriptor().isMandatory()) { + // We can't remove a mandatory node, we just clear its content. + + // A mandatory node with no XML means it doesn't really exist, so it can't be + // deleted. + boolean xml_exists = (ui_node.getXmlNode() != null); + + ui_node.clearContent(); + return xml_exists; + } else { + mUiChildren.remove(ui_index); + return true; + } + } + + /** + * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor} + * and appends it to the end of the element children list. + * + * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node. + * @return The new UI node that has been appended + */ + public UiElementNode appendNewUiChild(ElementDescriptor descriptor) { + UiElementNode ui_node; + ui_node = descriptor.createUiNode(); + mUiChildren.add(ui_node); + ui_node.setUiParent(this); + ui_node.invokeUiUpdateListeners(UiUpdateState.CREATED); + return ui_node; + } + + /** + * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor} + * and inserts it in the element children list at the specified position. + * + * @param index The position where to insert in the element children list. + * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node. + * @return The new UI node. + */ + public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) { + UiElementNode ui_node; + ui_node = descriptor.createUiNode(); + mUiChildren.add(index, ui_node); + ui_node.setUiParent(this); + ui_node.invokeUiUpdateListeners(UiUpdateState.CREATED); + return ui_node; + } + + /** + * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}. + * <p/> + * For a given {@link UiElementNode}, the attribute list always exists in + * full and is totally independent of whether the XML model actually + * has the corresponding attributes. + * <p/> + * For each attribute declared in this {@link UiElementNode}, get + * the corresponding XML attribute. It may not exist, in which case the + * value will be null. We don't really know if a value has changed, so + * the updateValue() is called on the UI sattribute in all cases. + * + * @param xmlNode The XML node to mirror + */ + protected void updateAttributeList(Node xmlNode) { + NamedNodeMap xmlAttrMap = xmlNode.getAttributes(); + HashSet<Node> visited = new HashSet<Node>(); + + // For all known (i.e. expected) UI attributes, find an existing XML attribute of + // same (uri, local name) and update the internal Ui attribute value. + for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) { + AttributeDescriptor desc = uiAttr.getDescriptor(); + if (!(desc instanceof SeparatorAttributeDescriptor)) { + Node xmlAttr = xmlAttrMap == null ? null : + xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName()); + uiAttr.updateValue(xmlAttr); + visited.add(xmlAttr); + } + } + + // Clone the current list of unknown attributes. We'll then remove from this list when + // we still attributes which are still unknown. What will be left are the old unknown + // attributes that have been deleted in the current XML attribute list. + @SuppressWarnings("unchecked") //$NON-NLS-1$ + HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone(); + + // We need to ignore hidden attributes. + Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors(); + + // Traverse the actual XML attribute list to find unknown attributes + if (xmlAttrMap != null) { + for (int i = 0; i < xmlAttrMap.getLength(); i++) { + Node xmlAttr = xmlAttrMap.item(i); + // Ignore attributes which have actual descriptors + if (visited.contains(xmlAttr)) { + continue; + } + + String xmlFullName = xmlAttr.getNodeName(); + + // Ignore attributes which are hidden (based on the prefix:localName key) + if (hiddenAttrDesc.containsKey(xmlFullName)) { + continue; + } + + String xmlAttrLocalName = xmlAttr.getLocalName(); + String xmlNsUri = xmlAttr.getNamespaceURI(); + + UiAttributeNode uiAttr = null; + for (UiAttributeNode a : mUnknownUiAttributes) { + String aLocalName = a.getDescriptor().getXmlLocalName(); + String aNsUri = a.getDescriptor().getNamespaceUri(); + if (aLocalName.equals(xmlAttrLocalName) && + (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) { + // This attribute is still present in the unknown list + uiAttr = a; + // It has not been deleted + deleted.remove(a); + break; + } + } + if (uiAttr == null) { + // Create a new unknown attribute + TextAttributeDescriptor desc = new TextAttributeDescriptor( + xmlAttrLocalName, // xml name + xmlFullName, // ui name + xmlNsUri, // NS uri + "Unknown XML attribute"); // tooltip, translatable + uiAttr = desc.createUiNode(this); + mUnknownUiAttributes.add(uiAttr); + } + + uiAttr.updateValue(xmlAttr); + } + + // Remove from the internal list unknown attributes that have been deleted from the xml + for (UiAttributeNode a : deleted) { + mUnknownUiAttributes.remove(a); + } + } + } + + /** + * Invoke all registered {@link IUiUpdateListener} listening on this UI updates for this node. + */ + protected void invokeUiUpdateListeners(UiUpdateState state) { + if (mUiUpdateListeners != null) { + for (IUiUpdateListener listener : mUiUpdateListeners) { + try { + listener.uiElementNodeUpdated(this, state); + } catch (Exception e) { + // prevent a crashing listener from crashing the whole invocation chain + AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s", //$NON-NLS-1$ + getBreadcrumbTrailDescription(true), + state.toString()); + } + } + } + } + + // --- for derived implementations only --- + + // TODO doc + protected void setXmlNode(Node xml_node) { + mXmlNode = xml_node; + } + + /** + * Sets the temporary data used by the editors. + * @param data the data. + */ + public void setEditData(Object data) { + mEditData = data; + } + + /** + * Returns the temporary data used by the editors for this object. + * @return the data, or <code>null</code> if none has been set. + */ + public Object getEditData() { + return mEditData; + } + + public void refreshUi() { + invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED); + } + + + // ------------- Helpers + + /** + * Helper method to commit a single attribute value to XML. + * <p/> + * This method updates the XML regardless of the current XML value. + * Callers should check first if an update is needed. + * If the new value is empty, the XML attribute will be actually removed. + * <p/> + * Note that the caller MUST ensure that modifying the underlying XML model is + * safe and must take care of marking the model as dirty if necessary. + * + * @see AndroidEditor#editXmlModel(Runnable) + * + * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode. + * @param newValue The new value to set. + * @return True if the XML attribute was modified or removed, false if nothing changed. + */ + public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) { + // Get (or create) the underlying XML element node that contains the attributes. + Node element = prepareCommit(); + if (element != null && uiAttr != null) { + String attrLocalName = uiAttr.getDescriptor().getXmlLocalName(); + String attrNsUri = uiAttr.getDescriptor().getNamespaceUri(); + + NamedNodeMap attrMap = element.getAttributes(); + if (newValue == null || newValue.length() == 0) { + // Remove attribute if it's empty + if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) { + attrMap.removeNamedItemNS(attrNsUri, attrLocalName); + return true; + } + } else { + // Add or replace an attribute + Document doc = element.getOwnerDocument(); + if (doc != null) { + Attr attr = doc.createAttributeNS(attrNsUri, attrLocalName); + attr.setValue(newValue); + attr.setPrefix(lookupNamespacePrefix(element, attrNsUri)); + attrMap.setNamedItemNS(attr); + return true; + } + } + } + return false; + } + + /** + * Helper method to commit all dirty attributes values to XML. + * <p/> + * This method is useful if {@link #setAttributeValue(String, String, boolean)} has been + * called more than once and all the attributes marked as dirty must be commited to the + * XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty + * attribute. + * <p/> + * Note that the caller MUST ensure that modifying the underlying XML model is + * safe and must take care of marking the model as dirty if necessary. + * + * @see AndroidEditor#editXmlModel(Runnable) + * + * @return True if one or more values were actually modified or removed, + * false if nothing changed. + */ + public boolean commitDirtyAttributesToXml() { + boolean result = false; + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) { + UiAttributeNode ui_attr = entry.getValue(); + if (ui_attr.isDirty()) { + result |= commitAttributeToXml(ui_attr, ui_attr.getCurrentValue()); + ui_attr.setDirty(false); + } + } + return result; + } + + /** + * Returns the namespace prefix matching the requested namespace URI. + * If no such declaration is found, returns the default "android" prefix. + * + * @param node The current node. Must not be null. + * @param nsUri The namespace URI of which the prefix is to be found, + * e.g. SdkConstants.NS_RESOURCES + * @return The first prefix declared or the default "android" prefix. + */ + private String lookupNamespacePrefix(Node node, String nsUri) { + // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java + // The following code emulates this simple call: + // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES); + + // if the requested URI is null, it denotes an attribute with no namespace. + if (nsUri == null) { + return null; + } + + // per XML specification, the "xmlns" URI is reserved + if (XmlnsAttributeDescriptor.XMLNS_URI.equals(nsUri)) { + return "xmlns"; //$NON-NLS-1$ + } + + HashSet<String> visited = new HashSet<String>(); + Document doc = node == null ? null : node.getOwnerDocument(); + + for (; node != null && node.getNodeType() == Node.ELEMENT_NODE; + node = node.getParentNode()) { + NamedNodeMap attrs = node.getAttributes(); + for (int n = attrs.getLength() - 1; n >= 0; --n) { + Node attr = attrs.item(n); + if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$ + String uri = attr.getNodeValue(); + String nsPrefix = attr.getLocalName(); + if (SdkConstants.NS_RESOURCES.equals(uri)) { + return nsPrefix; + } + visited.add(nsPrefix); + } + } + } + + // Use a sensible default prefix if we can't find one. + // We need to make sure the prefix is not one that was declared in the scope + // visited above. + String prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$ + String base = prefix; + for (int i = 1; visited.contains(prefix); i++) { + prefix = base + Integer.toString(i); + } + + // Also create & define this prefix/URI in the XML document as an attribute in the + // first element of the document. + if (doc != null) { + node = doc.getFirstChild(); + while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { + node = node.getNextSibling(); + } + if (node != null) { + Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI, prefix); + attr.setValue(nsUri); + attr.setPrefix("xmlns"); //$NON-NLS-1$ + node.getAttributes().setNamedItemNS(attr); + } + } + + return prefix; + } + + /** + * Utility method to internally set the value of a text attribute for the current + * UiElementNode. + * <p/> + * This method is a helper. It silently ignores the errors such as the requested + * attribute not being present in the element or attribute not being settable. + * It accepts inherited attributes (such as layout). + * <p/> + * This does not commit to the XML model. It does mark the attribute node as dirty. + * This is up to the caller. + * + * @see #commitAttributeToXml(UiAttributeNode, String) + * @see #commitDirtyAttributesToXml() + * + * @param attrXmlName The XML name of the attribute to modify + * @param value The new value for the attribute. If set to null, the attribute is removed. + * @param override True if the value must be set even if one already exists. + * @return The {@link UiAttributeNode} that has been modified or null. + */ + public UiAttributeNode setAttributeValue(String attrXmlName, String value, boolean override) { + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + if (value == null) { + value = ""; //$NON-NLS-1$ -- this removes an attribute + } + + for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) { + AttributeDescriptor ui_desc = entry.getKey(); + if (ui_desc.getXmlLocalName().equals(attrXmlName)) { + UiAttributeNode ui_attr = entry.getValue(); + // Not all attributes are editable, ignore those which are not + if (ui_attr instanceof IUiSettableAttributeNode) { + String current = ui_attr.getCurrentValue(); + // Only update (and mark as dirty) if the attribute did not have any + // value or if the value was different. + if (override || current == null || !current.equals(value)) { + ((IUiSettableAttributeNode) ui_attr).setCurrentValue(value); + // mark the attribute as dirty since their internal content + // as been modified, but not the underlying XML model + ui_attr.setDirty(true); + return ui_attr; + } + } + break; + } + } + return null; + } + + /** + * Utility method to retrieve the internal value of an attribute. + * <p/> + * Note that this retrieves the *field* value if the attribute has some UI, and + * not the actual XML value. They may differ if the attribute is dirty. + * + * @param attrXmlName The XML name of the attribute to modify + * @return The current internal value for the attribute or null in case of error. + */ + public String getAttributeValue(String attrXmlName) { + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) { + AttributeDescriptor ui_desc = entry.getKey(); + if (ui_desc.getXmlLocalName().equals(attrXmlName)) { + UiAttributeNode ui_attr = entry.getValue(); + return ui_attr.getCurrentValue(); + } + } + return null; + } + + // ------ IPropertySource methods + + public Object getEditableValue() { + return null; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors() + * + * Returns the property descriptor for this node. Since the descriptors are not linked to the + * data, the AttributeDescriptor are used directly. + */ + public IPropertyDescriptor[] getPropertyDescriptors() { + List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>(); + + // get the standard descriptors + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + Set<AttributeDescriptor> keys = attributeMap.keySet(); + + + // we only want the descriptor that do implement the IPropertyDescriptor interface. + for (AttributeDescriptor key : keys) { + if (key instanceof IPropertyDescriptor) { + propDescs.add((IPropertyDescriptor)key); + } + } + + // now get the descriptor from the unknown attributes + for (UiAttributeNode unknownNode : mUnknownUiAttributes) { + if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) { + propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor()); + } + } + + // TODO cache this maybe, as it's not going to change (except for unknown descriptors) + return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]); + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object) + * + * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(), + * which return the AttributeDescriptor itself. + */ + public Object getPropertyValue(Object id) { + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + UiAttributeNode attribute = attributeMap.get(id); + + if (attribute == null) { + // look for the id in the unknown attributes. + for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { + if (id == unknownAttr.getDescriptor()) { + return unknownAttr; + } + } + } + + return attribute; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object) + * + * Returns whether the property is set. In our case this is if the string is non empty. + */ + public boolean isPropertySet(Object id) { + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + UiAttributeNode attribute = attributeMap.get(id); + + if (attribute != null) { + return attribute.getCurrentValue().length() > 0; + } + + // look for the id in the unknown attributes. + for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { + if (id == unknownAttr.getDescriptor()) { + return unknownAttr.getCurrentValue().length() > 0; + } + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object) + * + * Reset the property to its default value. For now we simply empty it. + */ + public void resetPropertyValue(Object id) { + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + UiAttributeNode attribute = attributeMap.get(id); + if (attribute != null) { + // TODO: reset the value of the attribute + + return; + } + + // look for the id in the unknown attributes. + for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { + if (id == unknownAttr.getDescriptor()) { + // TODO: reset the value of the attribute + + return; + } + } + } + + /* + * (non-Javadoc) + * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object) + * + * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the + * AttributeDescriptor itself. Value should be a String. + */ + public void setPropertyValue(Object id, Object value) { + HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes(); + + UiAttributeNode attribute = attributeMap.get(id); + + if (attribute == null) { + // look for the id in the unknown attributes. + for (UiAttributeNode unknownAttr : mUnknownUiAttributes) { + if (id == unknownAttr.getDescriptor()) { + attribute = unknownAttr; + break; + } + } + } + + if (attribute != null) { + final UiAttributeNode fAttribute = attribute; + + // get the current value and compare it to the new value + String oldValue = fAttribute.getCurrentValue(); + final String newValue = (String)value; + + if (oldValue.equals(newValue)) { + return; + } + + AndroidEditor editor = getEditor(); + editor.editXmlModel(new Runnable() { + public void run() { + commitAttributeToXml(fAttribute, newValue); + } + }); + } + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java new file mode 100644 index 0000000..ddcf0a0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.FlagAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.ui.SectionHelper; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.resource.FontDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.dialogs.SelectionStatusDialog; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.TableWrapData; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents an XML attribute that is defined by a set of flag values, + * i.e. enum names separated by pipe (|) characters. + * + * Note: in Android resources, a "flag" is a list of fixed values where one or + * more values can be selected using an "or", e.g. "align='left|top'". + * By contrast, an "enum" is a list of fixed values of which only one can be + * selected at a given time, e.g. "gravity='right'". + * <p/> + * This class handles the "flag" case. + * The "enum" case is done using {@link UiListAttributeNode}. + */ +public class UiFlagAttributeNode extends UiTextAttributeNode { + + public UiFlagAttributeNode(FlagAttributeDescriptor attributeDescriptor, + UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (non-java doc) + * Creates a label widget and an associated text field. + * <p/> + * As most other parts of the android manifest editor, this assumes the + * parent uses a table layout with 2 columns. + */ + @Override + public void createUiControl(Composite parent, IManagedForm managedForm) { + setManagedForm(managedForm); + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + Label label = toolkit.createLabel(parent, desc.getUiName()); + label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); + SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip())); + + Composite composite = toolkit.createComposite(parent); + composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE)); + GridLayout gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + composite.setLayout(gl); + // Fixes missing text borders under GTK... also requires adding a 1-pixel margin + // for the text field below + toolkit.paintBordersFor(composite); + + final Text text = toolkit.createText(composite, getCurrentValue()); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK + text.setLayoutData(gd); + final Button selectButton = toolkit.createButton(composite, "Select...", SWT.PUSH); + + setTextWidget(text); + + selectButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + + String currentText = getTextWidgetValue(); + + String result = showDialog(selectButton.getShell(), currentText); + + if (result != null) { + setTextWidgetValue(result); + } + } + }); + } + + /** + * Get the flag names, either from the initial names set in the attribute + * or by querying the framework resource parser. + */ + @Override + public String[] getPossibleValues() { + String attr_name = getDescriptor().getXmlLocalName(); + String element_name = getUiParent().getDescriptor().getXmlName(); + + String[] values = null; + + if (getDescriptor() instanceof FlagAttributeDescriptor && + ((FlagAttributeDescriptor) getDescriptor()).getNames() != null) { + // Get enum values from the descriptor + values = ((FlagAttributeDescriptor) getDescriptor()).getNames(); + } + + if (values == null) { + // or from the AndroidTargetData + UiElementNode uiNode = getUiParent(); + AndroidEditor editor = uiNode.getEditor(); + AndroidTargetData data = editor.getTargetData(); + if (data != null) { + values = data.getAttributeValues(element_name, attr_name); + } + } + + return values; + } + + /** + * Shows a dialog letting the user choose a set of enum, and returns a string + * containing the result. + */ + public String showDialog(Shell shell, String currentValue) { + FlagSelectionDialog dlg = new FlagSelectionDialog( + shell, currentValue.trim().split("\\s*\\|\\s*")); //$NON-NLS-1$ + dlg.open(); + Object[] result = dlg.getResult(); + if (result != null) { + StringBuilder buf = new StringBuilder(); + for (Object name : result) { + if (name instanceof String) { + if (buf.length() > 0) { + buf.append("|"); //$NON-NLS-1$ + } + buf.append(name); + } + } + + return buf.toString(); + } + + return null; + + } + + /** + * Displays a list of flag names with checkboxes. + */ + private class FlagSelectionDialog extends SelectionStatusDialog { + + private Set<String> mCurrentSet; + private Table mTable; + + public FlagSelectionDialog(Shell parentShell, String[] currentNames) { + super(parentShell); + + mCurrentSet = new HashSet<String>(); + for (String name : currentNames) { + if (name.length() > 0) { + mCurrentSet.add(name); + } + } + + int shellStyle = getShellStyle(); + setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE); + } + + @Override + protected void computeResult() { + if (mTable != null) { + ArrayList<String> results = new ArrayList<String>(); + + for (TableItem item : mTable.getItems()) { + if (item.getChecked()) { + results.add((String)item.getData()); + } + } + + setResult(results); + } + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite= new Composite(parent, SWT.NONE); + composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + composite.setLayout(new GridLayout(1, true)); + composite.setFont(parent.getFont()); + + Label label = new Label(composite, SWT.NONE); + label.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + label.setText(String.format("Select the flag values for attribute %1$s:", + ((FlagAttributeDescriptor) getDescriptor()).getUiName())); + + mTable = new Table(composite, SWT.CHECK | SWT.BORDER); + GridData data = new GridData(); + // The 60,18 hints are the ones used by AbstractElementListSelectionDialog + data.widthHint = convertWidthInCharsToPixels(60); + data.heightHint = convertHeightInCharsToPixels(18); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + + mTable.setHeaderVisible(false); + final TableColumn column = new TableColumn(mTable, SWT.NONE); + + // List all the expected flag names and check those which are currently used + String[] names = getPossibleValues(); + if (names != null) { + for (String name : names) { + TableItem item = new TableItem(mTable, SWT.NONE); + item.setText(name); + item.setData(name); + + boolean hasName = mCurrentSet.contains(name); + item.setChecked(hasName); + if (hasName) { + mCurrentSet.remove(name); + } + } + } + + // If there are unknown flag names currently used, display them at the end if the + // table already checked. + if (!mCurrentSet.isEmpty()) { + FontDescriptor fontDesc = JFaceResources.getDialogFontDescriptor(); + fontDesc = fontDesc.withStyle(SWT.ITALIC); + Font font = fontDesc.createFont(JFaceResources.getDialogFont().getDevice()); + + for (String name : mCurrentSet) { + TableItem item = new TableItem(mTable, SWT.NONE); + item.setText(String.format("%1$s (unknown flag)", name)); + item.setData(name); + item.setChecked(true); + item.setFont(font); + } + } + + // Add a listener that will resize the column to the full width of the table + // so that only one column appears in the table even if the dialog is resized. + ControlAdapter listener = new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = mTable.getClientArea(); + column.setWidth(r.width); + } + }; + + mTable.addControlListener(listener); + listener.controlResized(null /* event not used */); + + // Add a selection listener that will check/uncheck items when they are double-clicked + mTable.addSelectionListener(new SelectionAdapter() { + /** Default selection means double-click on "most" platforms */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + } + super.widgetDefaultSelected(e); + } + }); + + Dialog.applyDialogFont(composite); + setHelpAvailable(false); + + return composite; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java new file mode 100644 index 0000000..c5c10aa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.ListAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.ui.SectionHelper; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.TableWrapData; + +/** + * Represents an XML attribute which has possible built-in values, and can be modified by + * an editable Combo box. + * <p/> + * See {@link UiTextAttributeNode} for more information. + */ +public class UiListAttributeNode extends UiAbstractTextAttributeNode { + + protected Combo mCombo; + + public UiListAttributeNode(ListAttributeDescriptor attributeDescriptor, + UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (non-java doc) + * Creates a label widget and an associated text field. + * <p/> + * As most other parts of the android manifest editor, this assumes the + * parent uses a table layout with 2 columns. + */ + @Override + public final void createUiControl(final Composite parent, IManagedForm managedForm) { + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + Label label = toolkit.createLabel(parent, desc.getUiName()); + label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); + SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip())); + + int style = SWT.DROP_DOWN; + mCombo = new Combo(parent, style); + TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE); + twd.maxWidth = 100; + mCombo.setLayoutData(twd); + + fillCombo(); + + setTextWidgetValue(getCurrentValue()); + + mCombo.addModifyListener(new ModifyListener() { + /** + * Sent when the text is modified, whether by the user via manual + * input or programmatic input via setText(). + * <p/> + * Simply mark the attribute as dirty if it really changed. + * The container SectionPart will collect these flag and manage them. + */ + public void modifyText(ModifyEvent e) { + if (!isInInternalTextModification() && + !isDirty() && + mCombo != null && + getCurrentValue() != null && + !mCombo.getText().equals(getCurrentValue())) { + setDirty(true); + } + } + }); + + // Remove self-reference when the widget is disposed + mCombo.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mCombo = null; + } + }); + } + + protected void fillCombo() { + String[] values = getPossibleValues(); + + if (values == null) { + AdtPlugin.log(IStatus.ERROR, + "FrameworkResourceManager did not provide values yet for %1$s", + getDescriptor().getXmlLocalName()); + } else { + for (String value : values) { + mCombo.add(value); + } + } + } + + /** + * Get the list values, either from the initial values set in the attribute + * or by querying the framework resource parser. + */ + @Override + public String[] getPossibleValues() { + AttributeDescriptor descriptor = getDescriptor(); + UiElementNode uiParent = getUiParent(); + + String attr_name = descriptor.getXmlLocalName(); + String element_name = uiParent.getDescriptor().getXmlName(); + + // FrameworkResourceManager expects a specific prefix for the attribute. + String prefix = ""; + if (SdkConstants.NS_RESOURCES.equals(descriptor.getNamespaceUri())) { + prefix = "android:"; //$NON-NLS-1$ + } else if (XmlnsAttributeDescriptor.XMLNS_URI.equals(descriptor.getNamespaceUri())) { + prefix = "xmlns:"; //$NON-NLS-1$ + } + attr_name = prefix + attr_name; + + String[] values = null; + + if (descriptor instanceof ListAttributeDescriptor && + ((ListAttributeDescriptor) descriptor).getValues() != null) { + // Get enum values from the descriptor + values = ((ListAttributeDescriptor) descriptor).getValues(); + } + + if (values == null) { + // or from the AndroidTargetData + UiElementNode uiNode = getUiParent(); + AndroidEditor editor = uiNode.getEditor(); + AndroidTargetData data = editor.getTargetData(); + if (data != null) { + // get the great-grand-parent descriptor. + + // the parent should always exist. + UiElementNode grandParentNode = uiParent.getUiParent(); + + String greatGrandParentNodeName = null; + if (grandParentNode != null) { + UiElementNode greatGrandParentNode = grandParentNode.getUiParent(); + if (greatGrandParentNode != null) { + greatGrandParentNodeName = + greatGrandParentNode.getDescriptor().getXmlName(); + } + } + + values = data.getAttributeValues(element_name, attr_name, greatGrandParentNodeName); + } + } + + return values; + } + + @Override + public String getTextWidgetValue() { + if (mCombo != null) { + return mCombo.getText(); + } + + return null; + } + + @Override + public final boolean isValid() { + return mCombo != null; + } + + @Override + public void setTextWidgetValue(String value) { + if (mCombo != null) { + mCombo.setText(value); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java new file mode 100644 index 0000000..1c1e1bd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; +import com.android.ide.eclipse.editors.ui.SectionHelper; +import com.android.ide.eclipse.editors.wizards.ReferenceChooserDialog; +import com.android.ide.eclipse.editors.wizards.ResourceChooser; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.TableWrapData; + +/** + * Represents an XML attribute for a resource that can be modified using a simple text field or + * a dialog to choose an existing resource. + * <p/> + * It can be configured to represent any kind of resource, by providing the desired + * {@link ResourceType} in the constructor. + * <p/> + * See {@link UiTextAttributeNode} for more information. + */ +public class UiResourceAttributeNode extends UiTextAttributeNode { + + private ResourceType mType; + + public UiResourceAttributeNode(ResourceType type, + AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + + mType = type; + } + + /* (non-java doc) + * Creates a label widget and an associated text field. + * <p/> + * As most other parts of the android manifest editor, this assumes the + * parent uses a table layout with 2 columns. + */ + @Override + public void createUiControl(final Composite parent, IManagedForm managedForm) { + setManagedForm(managedForm); + FormToolkit toolkit = managedForm.getToolkit(); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + + Label label = toolkit.createLabel(parent, desc.getUiName()); + label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); + SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip())); + + Composite composite = toolkit.createComposite(parent); + composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE)); + GridLayout gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + composite.setLayout(gl); + // Fixes missing text borders under GTK... also requires adding a 1-pixel margin + // for the text field below + toolkit.paintBordersFor(composite); + + final Text text = toolkit.createText(composite, getCurrentValue()); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK + text.setLayoutData(gd); + Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH); + + setTextWidget(text); + + // TODO Add a validator using onAddModifyListener + + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + String result = showDialog(parent.getShell(), text.getText().trim()); + if (result != null) { + text.setText(result); + } + } + }); + } + + /** + * Shows a dialog letting the user choose a set of enum, and returns a string + * containing the result. + */ + public String showDialog(Shell shell, String currentValue) { + // we need to get the project of the file being edited. + UiElementNode uiNode = getUiParent(); + AndroidEditor editor = uiNode.getEditor(); + IProject project = editor.getProject(); + if (project != null) { + // get the resource repository for this project and the system resources. + IResourceRepository projectRepository = + ResourceManager.getInstance().getProjectResources(project); + + if (mType != null) { + // get the Target Data to get the system resources + AndroidTargetData data = editor.getTargetData(); + IResourceRepository systemRepository = data.getSystemResources(); + + // open a resource chooser dialog for specified resource type. + ResourceChooser dlg = new ResourceChooser(mType, + projectRepository, systemRepository, shell); + + dlg.setCurrentResource(currentValue); + + if (dlg.open() == Window.OK) { + return dlg.getCurrentResource(); + } + } else { + ReferenceChooserDialog dlg = new ReferenceChooserDialog(projectRepository, + shell); + + dlg.setCurrentResource(currentValue); + + if (dlg.open() == Window.OK) { + return dlg.getCurrentResource(); + } + } + } + + return null; + } + + @Override + public String[] getPossibleValues() { + // TODO: compute a list of existing resources for content assist completion + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java new file mode 100644 index 0000000..192f752 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.FormToolkit; +import org.eclipse.ui.forms.widgets.TableWrapData; +import org.eclipse.ui.forms.widgets.TableWrapLayout; +import org.w3c.dom.Node; + +/** + * {@link UiSeparatorAttributeNode} does not represent any real attribute. + * <p/> + * It is used to separate groups of attributes visually. + */ +public class UiSeparatorAttributeNode extends UiAttributeNode { + + /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor} */ + public UiSeparatorAttributeNode(SeparatorAttributeDescriptor attrDesc, + UiElementNode uiParent) { + super(attrDesc, uiParent); + } + + /** Returns the current value of the node. */ + @Override + public String getCurrentValue() { + // There is no value here. + return null; + } + + /** + * Sets whether the attribute is dirty and also notifies the editor some part's dirty + * flag as changed. + * <p/> + * Subclasses should set the to true as a result of user interaction with the widgets in + * the section and then should set to false when the commit() method completed. + */ + @Override + public void setDirty(boolean isDirty) { + // This is never dirty. + } + + /** + * Called once by the parent user interface to creates the necessary + * user interface to edit this attribute. + * <p/> + * This method can be called more than once in the life cycle of an UI node, + * typically when the UI is part of a master-detail tree, as pages are swapped. + * + * @param parent The composite where to create the user interface. + * @param managedForm The managed form owning this part. + */ + @Override + public void createUiControl(Composite parent, IManagedForm managedForm) { + FormToolkit toolkit = managedForm.getToolkit(); + Composite row = toolkit.createComposite(parent); + + TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB); + if (parent.getLayout() instanceof TableWrapLayout) { + twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns; + } + row.setLayoutData(twd); + row.setLayout(new GridLayout(3, false /* equal width */)); + + Label sep = toolkit.createSeparator(row, SWT.HORIZONTAL); + GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false); + gd.widthHint = 16; + sep.setLayoutData(gd); + + Label label = toolkit.createLabel(row, getDescriptor().getXmlLocalName()); + label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + sep = toolkit.createSeparator(row, SWT.HORIZONTAL); + sep.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + } + + /** No completion values for this UI attribute. */ + @Override + public String[] getPossibleValues() { + return null; + } + + /** + * Called when the XML is being loaded or has changed to + * update the value held by this user interface attribute node. + * <p/> + * The XML Node <em>may</em> be null, which denotes that the attribute is not + * specified in the XML model. In general, this means the "default" value of the + * attribute should be used. + * <p/> + * The caller doesn't really know if attributes have changed, + * so it will call this to refresh the attribute anyway. It's up to the + * UI implementation to minimize refreshes. + * + * @param xml_attribute_node + */ + @Override + public void updateValue(Node xml_attribute_node) { + // No value to update. + } + + /** + * Called by the user interface when the editor is saved or its state changed + * and the modified attributes must be committed (i.e. written) to the XML model. + * <p/> + * Important behaviors: + * <ul> + * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before. + * The implemented methods must assume it is safe to modify the XML model. + * <li>On success, the implementation *must* call setDirty(false). + * <li>On failure, the implementation can fail with an exception, which + * is trapped and logged by the caller, or do nothing, whichever is more + * appropriate. + * </ul> + */ + @Override + public void commit() { + // No value to commit. + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java new file mode 100644 index 0000000..4c53f4c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.ui.SectionHelper; + +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.widgets.TableWrapData; + +/** + * Represents an XML attribute in that can be modified using a simple text field + * in the XML editor's user interface. + * <p/> + * The XML attribute has no default value. When unset, the text field is blank. + * When updating the XML, if the field is empty, the attribute will be removed + * from the XML element. + * <p/> + * See {@link UiAttributeNode} for more information. + */ +public class UiTextAttributeNode extends UiAbstractTextAttributeNode { + + /** Text field */ + private Text mText; + /** The managed form, set only once createUiControl has been called. */ + private IManagedForm mManagedForm; + + public UiTextAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /* (non-java doc) + * Creates a label widget and an associated text field. + * <p/> + * As most other parts of the android manifest editor, this assumes the + * parent uses a table layout with 2 columns. + */ + @Override + public void createUiControl(Composite parent, IManagedForm managedForm) { + setManagedForm(managedForm); + TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); + Text text = SectionHelper.createLabelAndText(parent, managedForm.getToolkit(), + desc.getUiName(), getCurrentValue(), + DescriptorsUtils.formatTooltip(desc.getTooltip())); + + setTextWidget(text); + } + + /** No completion values for this UI attribute. */ + @Override + public String[] getPossibleValues() { + return null; + } + + /** + * Sets the internal managed form. + * This is usually set by createUiControl. + */ + protected void setManagedForm(IManagedForm managedForm) { + mManagedForm = managedForm; + } + + /** + * @return The managed form, set only once createUiControl has been called. + */ + protected IManagedForm getManagedForm() { + return mManagedForm; + } + + /* (non-java doc) + * Returns if the attribute node is valid, and its UI has been created. + */ + @Override + public boolean isValid() { + return mText != null; + } + + @Override + public String getTextWidgetValue() { + if (mText != null) { + return mText.getText(); + } + + return null; + } + + @Override + public void setTextWidgetValue(String value) { + if (mText != null) { + mText.setText(value); + } + } + + /** + * Sets the Text widget object, and prepares it to handle modification and synchronization + * with the XML node. + * @param textWidget + */ + protected final void setTextWidget(Text textWidget) { + mText = textWidget; + + if (textWidget != null) { + // Sets the with hint for the text field. Derived classes can always override it. + // This helps the grid layout to resize correctly on smaller screen sizes. + Object data = textWidget.getLayoutData(); + if (data == null) { + } else if (data instanceof GridData) { + ((GridData)data).widthHint = AndroidEditor.TEXT_WIDTH_HINT; + } else if (data instanceof TableWrapData) { + ((TableWrapData)data).maxWidth = 100; + } + + mText.addModifyListener(new ModifyListener() { + /** + * Sent when the text is modified, whether by the user via manual + * input or programmatic input via setText(). + * <p/> + * Simply mark the attribute as dirty if it really changed. + * The container SectionPart will collect these flag and manage them. + */ + public void modifyText(ModifyEvent e) { + if (!isInInternalTextModification() && + !isDirty() && + mText != null && + getCurrentValue() != null && + !mText.getText().equals(getCurrentValue())) { + setDirty(true); + } + } + }); + + // Remove self-reference when the widget is disposed + mText.addDisposeListener(new DisposeListener() { + public void widgetDisposed(DisposeEvent e) { + mText = null; + } + }); + } + + onAddValidators(mText); + } + + /** + * Called after the text widget as been created. + * <p/> + * Derived classes typically want to: + * <li> Create a new {@link ModifyListener} and attach it to the given {@link Text} widget. + * <li> In the modify listener, call getManagedForm().getMessageManager().addMessage() + * and getManagedForm().getMessageManager().removeMessage() as necessary. + * <li> Call removeMessage in a new text.addDisposeListener. + * <li> Call the validator once to setup the initial messages as needed. + * <p/> + * The base implementation does nothing. + * + * @param text The {@link Text} widget to validate. + */ + protected void onAddValidators(Text text) { + } + + /** + * Returns the text widget. + */ + protected final Text getTextWidget() { + return mText; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java new file mode 100644 index 0000000..5c1db05 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.uimodel; + +import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.Text; + +/** + * Represents an XML element value in that can be modified using a simple text field + * in the XML editor's user interface. + */ +public class UiTextValueNode extends UiTextAttributeNode { + + public UiTextValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) { + super(attributeDescriptor, uiParent); + } + + /** + * Updates the current text field's value when the XML has changed. + * <p/> + * The caller doesn't really know if value of the element has changed, + * so it will call this to refresh the value anyway. The value + * is only set if it has changed. + * <p/> + * This also resets the "dirty" flag. + */ + @Override + public void updateValue(Node xml_attribute_node) { + setCurrentValue(DEFAULT_VALUE); + + // The argument xml_attribute_node is not used here. It should always be + // null since this is not an attribute. What we want is the "text value" of + // the parent element, which is actually the first text node of the element. + + UiElementNode parent = getUiParent(); + if (parent != null) { + Node xml_node = parent.getXmlNode(); + if (xml_node != null) { + for (Node xml_child = xml_node.getFirstChild(); + xml_child != null; + xml_child = xml_child.getNextSibling()) { + if (xml_child.getNodeType() == Node.TEXT_NODE) { + setCurrentValue(xml_child.getNodeValue()); + break; + } + } + } + } + + if (isValid() && !getTextWidgetValue().equals(getCurrentValue())) { + try { + setInInternalTextModification(true); + setTextWidgetValue(getCurrentValue()); + setDirty(false); + } finally { + setInInternalTextModification(false); + } + } + } + + /* (non-java doc) + * Called by the user interface when the editor is saved or its state changed + * and the modified "attributes" must be committed (i.e. written) to the XML model. + */ + @Override + public void commit() { + UiElementNode parent = getUiParent(); + if (parent != null && isValid() && isDirty()) { + // Get (or create) the underlying XML element node that contains the value. + Node element = parent.prepareCommit(); + if (element != null) { + String value = getTextWidgetValue(); + + // Try to find an existing text child to update. + boolean updated = false; + + for (Node xml_child = element.getFirstChild(); + xml_child != null; + xml_child = xml_child.getNextSibling()) { + if (xml_child.getNodeType() == Node.TEXT_NODE) { + xml_child.setNodeValue(value); + updated = true; + break; + } + } + + // If we didn't find a text child to update, we need to create one. + if (!updated) { + Document doc = element.getOwnerDocument(); + if (doc != null) { + Text text = doc.createTextNode(value); + element.appendChild(text); + } + } + + setCurrentValue(value); + } + } + setDirty(false); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java new file mode 100644 index 0000000..4a05b1e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java @@ -0,0 +1,1278 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.editors.resources.configurations.CountryCodeQualifier; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier; +import com.android.ide.eclipse.editors.resources.configurations.LanguageQualifier; +import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier; +import com.android.ide.eclipse.editors.resources.configurations.NetworkCodeQualifier; +import com.android.ide.eclipse.editors.resources.configurations.PixelDensityQualifier; +import com.android.ide.eclipse.editors.resources.configurations.RegionQualifier; +import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; +import com.android.ide.eclipse.editors.resources.configurations.ScreenDimensionQualifier; +import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier; +import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier; +import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier; +import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier.KeyboardState; +import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier.NavigationMethod; +import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier.ScreenOrientation; +import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier.TextInputMethod; +import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier.TouchScreenType; +import com.android.ide.eclipse.editors.resources.manager.ResourceManager; + +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.FocusAdapter; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; + +import java.util.HashMap; + +/** + * Custom UI widget to let user build a Folder configuration. + * <p/> + * To use this, instantiate somewhere in the UI and then: + * <ul> + * <li>Use {@link #setConfiguration(String)} or {@link #setConfiguration(FolderConfiguration)}. + * <li>Retrieve the configuration using {@link #getConfiguration(FolderConfiguration)}. + * </ul> + */ +public class ConfigurationSelector extends Composite { + + public static final int WIDTH_HINT = 600; + public static final int HEIGHT_HINT = 250; + + private Runnable mOnChangeListener; + + private TableViewer mFullTableViewer; + private TableViewer mSelectionTableViewer; + private Button mAddButton; + private Button mRemoveButton; + private StackLayout mStackLayout; + + private boolean mOnRefresh = false; + + private final FolderConfiguration mBaseConfiguration = new FolderConfiguration(); + private final FolderConfiguration mSelectedConfiguration = new FolderConfiguration(); + + private final HashMap<Class<? extends ResourceQualifier>, QualifierEditBase> mUiMap = + new HashMap<Class<? extends ResourceQualifier>, QualifierEditBase>(); + private Composite mQualifierEditParent; + + /** + * Basic of {@link VerifyListener} to only accept digits. + */ + private static class DigitVerifier implements VerifyListener { + public void verifyText(VerifyEvent e) { + // check for digit only. + for (int i = 0 ; i < e.text.length(); i++) { + char letter = e.text.charAt(i); + if (letter < '0' || letter > '9') { + e.doit = false; + return; + } + } + } + } + + /** + * Implementation of {@link VerifyListener} for Country Code qualifiers. + */ + public static class MobileCodeVerifier extends DigitVerifier { + @Override + public void verifyText(VerifyEvent e) { + super.verifyText(e); + + // basic tests passed? + if (e.doit) { + // check the max 3 digits. + if (e.text.length() - e.end + e.start + + ((Text)e.getSource()).getText().length() > 3) { + e.doit = false; + } + } + } + } + + /** + * Implementation of {@link VerifyListener} for the Language and Region qualifiers. + */ + public static class LanguageRegionVerifier implements VerifyListener { + public void verifyText(VerifyEvent e) { + // check for length + if (e.text.length() - e.end + e.start + ((Combo)e.getSource()).getText().length() > 2) { + e.doit = false; + return; + } + + // check for lower case only. + for (int i = 0 ; i < e.text.length(); i++) { + char letter = e.text.charAt(i); + if ((letter < 'a' || letter > 'z') && (letter < 'A' || letter > 'Z')) { + e.doit = false; + return; + } + } + } + } + + /** + * Implementation of {@link VerifyListener} for the Pixel Density qualifier. + */ + public static class DensityVerifier extends DigitVerifier { } + + /** + * Implementation of {@link VerifyListener} for the Screen Dimension qualifier. + */ + public static class DimensionVerifier extends DigitVerifier { } + + /** + * Enum for the state of the configuration being created. + */ + public enum ConfigurationState { + OK, INVALID_CONFIG, REGION_WITHOUT_LANGUAGE; + } + + public ConfigurationSelector(Composite parent) { + super(parent, SWT.NONE); + + mBaseConfiguration.createDefault(); + + GridLayout gl = new GridLayout(4, false); + gl.marginWidth = gl.marginHeight = 0; + setLayout(gl); + + // first column is the first table + final Table fullTable = new Table(this, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); + fullTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + fullTable.setHeaderVisible(true); + fullTable.setLinesVisible(true); + + // create the column + final TableColumn fullTableColumn = new TableColumn(fullTable, SWT.LEFT); + // set the header + fullTableColumn.setText("Available Qualifiers"); + + fullTable.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = fullTable.getClientArea(); + fullTableColumn.setWidth(r.width); + } + }); + + mFullTableViewer = new TableViewer(fullTable); + mFullTableViewer.setContentProvider(new QualifierContentProvider()); + mFullTableViewer.setLabelProvider(new QualifierLabelProvider( + false /* showQualifierValue */)); + mFullTableViewer.setInput(mBaseConfiguration); + mFullTableViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structSelection = (IStructuredSelection)selection; + Object first = structSelection.getFirstElement(); + + if (first instanceof ResourceQualifier) { + mAddButton.setEnabled(true); + return; + } + } + + mAddButton.setEnabled(false); + } + }); + + // 2nd column is the left/right arrow button + Composite buttonComposite = new Composite(this, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginWidth = gl.marginHeight = 0; + buttonComposite.setLayout(gl); + buttonComposite.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + new Composite(buttonComposite, SWT.NONE); + mAddButton = new Button(buttonComposite, SWT.BORDER | SWT.PUSH); + mAddButton.setText("->"); + mAddButton.setEnabled(false); + mAddButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IStructuredSelection selection = + (IStructuredSelection)mFullTableViewer.getSelection(); + + Object first = selection.getFirstElement(); + if (first instanceof ResourceQualifier) { + ResourceQualifier qualifier = (ResourceQualifier)first; + + mBaseConfiguration.removeQualifier(qualifier); + mSelectedConfiguration.addQualifier(qualifier); + + mFullTableViewer.refresh(); + mSelectionTableViewer.refresh(); + mSelectionTableViewer.setSelection(new StructuredSelection(qualifier), true); + + onChange(false /* keepSelection */); + } + } + }); + + mRemoveButton = new Button(buttonComposite, SWT.BORDER | SWT.PUSH); + mRemoveButton.setText("<-"); + mRemoveButton.setEnabled(false); + mRemoveButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IStructuredSelection selection = + (IStructuredSelection)mSelectionTableViewer.getSelection(); + + Object first = selection.getFirstElement(); + if (first instanceof ResourceQualifier) { + ResourceQualifier qualifier = (ResourceQualifier)first; + + mSelectedConfiguration.removeQualifier(qualifier); + mBaseConfiguration.addQualifier(qualifier); + + mFullTableViewer.refresh(); + mSelectionTableViewer.refresh(); + + onChange(false /* keepSelection */); + } + } + }); + + // 3rd column is the selected config table + final Table selectionTable = new Table(this, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); + selectionTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + selectionTable.setHeaderVisible(true); + selectionTable.setLinesVisible(true); + + // create the column + final TableColumn selectionTableColumn = new TableColumn(selectionTable, SWT.LEFT); + // set the header + selectionTableColumn.setText("Chosen Qualifiers"); + + selectionTable.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = selectionTable.getClientArea(); + selectionTableColumn.setWidth(r.width); + } + }); + mSelectionTableViewer = new TableViewer(selectionTable); + mSelectionTableViewer.setContentProvider(new QualifierContentProvider()); + mSelectionTableViewer.setLabelProvider(new QualifierLabelProvider( + true /* showQualifierValue */)); + mSelectionTableViewer.setInput(mSelectedConfiguration); + mSelectionTableViewer.addSelectionChangedListener(new ISelectionChangedListener() { + public void selectionChanged(SelectionChangedEvent event) { + // ignore selection changes during resfreshes in some cases. + if (mOnRefresh) { + return; + } + + ISelection selection = event.getSelection(); + if (selection instanceof IStructuredSelection) { + IStructuredSelection structSelection = (IStructuredSelection)selection; + + if (structSelection.isEmpty() == false) { + Object first = structSelection.getFirstElement(); + + if (first instanceof ResourceQualifier) { + mRemoveButton.setEnabled(true); + + QualifierEditBase composite = mUiMap.get(first.getClass()); + + if (composite != null) { + composite.setQualifier((ResourceQualifier)first); + } + + mStackLayout.topControl = composite; + mQualifierEditParent.layout(); + + return; + } + } else { + mStackLayout.topControl = null; + mQualifierEditParent.layout(); + } + } + + mRemoveButton.setEnabled(false); + } + }); + + // 4th column is the detail of the selected qualifier + mQualifierEditParent = new Composite(this, SWT.NONE); + mQualifierEditParent.setLayout(mStackLayout = new StackLayout()); + mQualifierEditParent.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + // create the UI for all the qualifiers, and associate them to the ResourceQualifer class. + mUiMap.put(CountryCodeQualifier.class, new MCCEdit(mQualifierEditParent)); + mUiMap.put(NetworkCodeQualifier.class, new MNCEdit(mQualifierEditParent)); + mUiMap.put(LanguageQualifier.class, new LanguageEdit(mQualifierEditParent)); + mUiMap.put(RegionQualifier.class, new RegionEdit(mQualifierEditParent)); + mUiMap.put(ScreenOrientationQualifier.class, new OrientationEdit(mQualifierEditParent)); + mUiMap.put(PixelDensityQualifier.class, new PixelDensityEdit(mQualifierEditParent)); + mUiMap.put(TouchScreenQualifier.class, new TouchEdit(mQualifierEditParent)); + mUiMap.put(KeyboardStateQualifier.class, new KeyboardEdit(mQualifierEditParent)); + mUiMap.put(TextInputMethodQualifier.class, new TextInputEdit(mQualifierEditParent)); + mUiMap.put(NavigationMethodQualifier.class, new NavigationEdit(mQualifierEditParent)); + mUiMap.put(ScreenDimensionQualifier.class, new ScreenDimensionEdit(mQualifierEditParent)); + } + + /** + * Sets a listener to be notified when the configuration changes. + * @param listener A {@link Runnable} whose <code>run()</code> method is called when the + * configuration is changed. The method is called from the UI thread. + */ + public void setOnChangeListener(Runnable listener) { + mOnChangeListener = listener; + } + + /** + * Initialize the UI with a given {@link FolderConfiguration}. This must + * be called from the UI thread. + * @param config The configuration. + */ + public void setConfiguration(FolderConfiguration config) { + mSelectedConfiguration.set(config); + mSelectionTableViewer.refresh(); + + // create the base config, which is the default config minus the qualifiers + // in SelectedConfiguration + mBaseConfiguration.substract(mSelectedConfiguration); + mFullTableViewer.refresh(); + } + + /** + * Initialize the UI with the configuration represented by a resource folder name. + * This must be called from the UI thread. + * + * @param folderSegments the segments of the folder name, + * split using {@link FolderConfiguration#QUALIFIER_SEP}. + * @return true if success, or false if the folder name is not a valid name. + */ + public boolean setConfiguration(String[] folderSegments) { + FolderConfiguration config = ResourceManager.getInstance().getConfig(folderSegments); + + if (config == null) { + return false; + } + + setConfiguration(config); + + return true; + } + + /** + * Initialize the UI with the configuration represented by a resource folder name. + * This must be called from the UI thread. + * @param folderName the name of the folder. + * @return true if success, or false if the folder name is not a valid name. + */ + public boolean setConfiguration(String folderName) { + // split the name of the folder in segments. + String[] folderSegments = folderName.split(FolderConfiguration.QUALIFIER_SEP); + + return setConfiguration(folderSegments); + } + + /** + * Gets the configuration as setup by the widget. + * @param config the {@link FolderConfiguration} object to be filled with the information + * from the UI. + */ + public void getConfiguration(FolderConfiguration config) { + config.set(mSelectedConfiguration); + } + + /** + * Returns the state of the configuration being edited/created. + */ + public ConfigurationState getState() { + if (mSelectedConfiguration.getInvalidQualifier() != null) { + return ConfigurationState.INVALID_CONFIG; + } + + if (mSelectedConfiguration.checkRegion() == false) { + return ConfigurationState.REGION_WITHOUT_LANGUAGE; + } + + return ConfigurationState.OK; + } + + /** + * Returns the first invalid qualifier of the configuration being edited/created, + * or <code>null<code> if they are all valid (or if none exists). + * <p/>If {@link #getState()} return {@link ConfigurationState#INVALID_CONFIG} then this will + * not return <code>null</code>. + */ + public ResourceQualifier getInvalidQualifier() { + return mSelectedConfiguration.getInvalidQualifier(); + } + + /** + * Handle changes in the configuration. + * @param keepSelection if <code>true</code> attemps to avoid triggering selection change in + * {@link #mSelectedConfiguration}. + */ + private void onChange(boolean keepSelection) { + ISelection selection = null; + if (keepSelection) { + mOnRefresh = true; + selection = mSelectionTableViewer.getSelection(); + } + + mSelectionTableViewer.refresh(true); + + if (keepSelection) { + mSelectionTableViewer.setSelection(selection); + mOnRefresh = false; + } + + if (mOnChangeListener != null) { + mOnChangeListener.run(); + } + } + + /** + * Content provider around a {@link FolderConfiguration}. + */ + private static class QualifierContentProvider implements IStructuredContentProvider { + + private FolderConfiguration mInput; + + public QualifierContentProvider() { + } + + public void dispose() { + // pass + } + + public Object[] getElements(Object inputElement) { + return mInput.getQualifiers(); + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + mInput = null; + if (newInput instanceof FolderConfiguration) { + mInput = (FolderConfiguration)newInput; + } + } + } + + /** + * Label provider for {@link ResourceQualifier} objects. + */ + private static class QualifierLabelProvider implements ITableLabelProvider { + + private final boolean mShowQualifierValue; + + public QualifierLabelProvider(boolean showQualifierValue) { + mShowQualifierValue = showQualifierValue; + } + + public String getColumnText(Object element, int columnIndex) { + // only one column, so we can ignore columnIndex + if (element instanceof ResourceQualifier) { + if (mShowQualifierValue) { + String value = ((ResourceQualifier)element).getStringValue(); + if (value.length() == 0) { + return String.format("%1$s (?)", + ((ResourceQualifier)element).getShortName()); + } else { + return value; + } + + } else { + return ((ResourceQualifier)element).getShortName(); + } + } + + return null; + } + + public Image getColumnImage(Object element, int columnIndex) { + // only one column, so we can ignore columnIndex + if (element instanceof ResourceQualifier) { + return ((ResourceQualifier)element).getIcon(); + } + + return null; + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + // pass + } + + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Base class for Edit widget for {@link ResourceQualifier}. + */ + private abstract static class QualifierEditBase extends Composite { + + public QualifierEditBase(Composite parent, String title) { + super(parent, SWT.NONE); + setLayout(new GridLayout(1, false)); + + new Label(this, SWT.NONE).setText(title); + } + + public abstract void setQualifier(ResourceQualifier qualifier); + } + + /** + * Edit widget for {@link CountryCodeQualifier}. + */ + private class MCCEdit extends QualifierEditBase { + + private Text mText; + + public MCCEdit(Composite parent) { + super(parent, CountryCodeQualifier.NAME); + + mText = new Text(this, SWT.BORDER); + mText.addVerifyListener(new MobileCodeVerifier()); + mText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onTextChange(); + } + }); + + mText.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + onTextChange(); + } + }); + + new Label(this, SWT.NONE).setText("(3 digit code)"); + } + + private void onTextChange() { + String value = mText.getText(); + + if (value.length() == 0) { + // empty string, means a qualifier with no value. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setCountryCodeQualifier(new CountryCodeQualifier()); + } else { + try { + CountryCodeQualifier qualifier = CountryCodeQualifier.getQualifier( + CountryCodeQualifier.getFolderSegment(Integer.parseInt(value))); + if (qualifier != null) { + mSelectedConfiguration.setCountryCodeQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong + // (for instance not exactly 3 digits). + mSelectedConfiguration.setCountryCodeQualifier(new CountryCodeQualifier()); + } + } catch (NumberFormatException nfe) { + // Looks like the code is not a number. This should not happen since the text + // field has a VerifyListener that prevents it. + mSelectedConfiguration.setCountryCodeQualifier(new CountryCodeQualifier()); + } + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + CountryCodeQualifier q = (CountryCodeQualifier)qualifier; + + mText.setText(Integer.toString(q.getCode())); + } + } + + /** + * Edit widget for {@link NetworkCodeQualifier}. + */ + private class MNCEdit extends QualifierEditBase { + private Text mText; + + public MNCEdit(Composite parent) { + super(parent, NetworkCodeQualifier.NAME); + + mText = new Text(this, SWT.BORDER); + mText.addVerifyListener(new MobileCodeVerifier()); + mText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onTextChange(); + } + }); + mText.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + onTextChange(); + } + }); + + new Label(this, SWT.NONE).setText("(1-3 digit code)"); + } + + private void onTextChange() { + String value = mText.getText(); + + if (value.length() == 0) { + // empty string, means a qualifier with no value. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setNetworkCodeQualifier(new NetworkCodeQualifier()); + } else { + try { + NetworkCodeQualifier qualifier = NetworkCodeQualifier.getQualifier( + NetworkCodeQualifier.getFolderSegment(Integer.parseInt(value))); + if (qualifier != null) { + mSelectedConfiguration.setNetworkCodeQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong + // (for instance not exactly 3 digits). + mSelectedConfiguration.setNetworkCodeQualifier(new NetworkCodeQualifier()); + } + } catch (NumberFormatException nfe) { + // Looks like the code is not a number. This should not happen since the text + // field has a VerifyListener that prevents it. + mSelectedConfiguration.setNetworkCodeQualifier(new NetworkCodeQualifier()); + } + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + NetworkCodeQualifier q = (NetworkCodeQualifier)qualifier; + + mText.setText(Integer.toString(q.getCode())); + } + } + + /** + * Edit widget for {@link LanguageQualifier}. + */ + private class LanguageEdit extends QualifierEditBase { + private Combo mLanguage; + + public LanguageEdit(Composite parent) { + super(parent, LanguageQualifier.NAME); + + mLanguage = new Combo(this, SWT.DROP_DOWN); + mLanguage.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mLanguage.addVerifyListener(new LanguageRegionVerifier()); + mLanguage.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onLanguageChange(); + } + public void widgetSelected(SelectionEvent e) { + onLanguageChange(); + } + }); + mLanguage.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onLanguageChange(); + } + }); + + new Label(this, SWT.NONE).setText("(2 letter code)"); + } + + private void onLanguageChange() { + // update the current config + String value = mLanguage.getText(); + + if (value.length() == 0) { + // empty string, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setLanguageQualifier(new LanguageQualifier()); + } else { + LanguageQualifier qualifier = null; + String segment = LanguageQualifier.getFolderSegment(value); + if (segment != null) { + qualifier = LanguageQualifier.getQualifier(segment); + } + + if (qualifier != null) { + mSelectedConfiguration.setLanguageQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + mSelectedConfiguration.setLanguageQualifier(new LanguageQualifier()); + } + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + LanguageQualifier q = (LanguageQualifier)qualifier; + + String value = q.getValue(); + if (value != null) { + mLanguage.setText(value); + } + } + } + + /** + * Edit widget for {@link RegionQualifier}. + */ + private class RegionEdit extends QualifierEditBase { + private Combo mRegion; + + public RegionEdit(Composite parent) { + super(parent, RegionQualifier.NAME); + + mRegion = new Combo(this, SWT.DROP_DOWN); + mRegion.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRegion.addVerifyListener(new LanguageRegionVerifier()); + mRegion.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onRegionChange(); + } + public void widgetSelected(SelectionEvent e) { + onRegionChange(); + } + }); + mRegion.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onRegionChange(); + } + }); + + new Label(this, SWT.NONE).setText("(2 letter code)"); + } + + private void onRegionChange() { + // update the current config + String value = mRegion.getText(); + + if (value.length() == 0) { + // empty string, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setRegionQualifier(new RegionQualifier()); + } else { + RegionQualifier qualifier = null; + String segment = RegionQualifier.getFolderSegment(value); + if (segment != null) { + qualifier = RegionQualifier.getQualifier(segment); + } + + if (qualifier != null) { + mSelectedConfiguration.setRegionQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong (for instance a one letter string). + mSelectedConfiguration.setRegionQualifier(new RegionQualifier()); + } + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + RegionQualifier q = (RegionQualifier)qualifier; + + String value = q.getValue(); + if (value != null) { + mRegion.setText(q.getValue()); + } + } + } + + /** + * Edit widget for {@link ScreenOrientationQualifier}. + */ + private class OrientationEdit extends QualifierEditBase { + + private Combo mOrientation; + + public OrientationEdit(Composite parent) { + super(parent, ScreenOrientationQualifier.NAME); + + mOrientation = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); + ScreenOrientation[] soValues = ScreenOrientation.values(); + for (ScreenOrientation value : soValues) { + mOrientation.add(value.getDisplayValue()); + } + + mOrientation.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mOrientation.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onOrientationChange(); + } + public void widgetSelected(SelectionEvent e) { + onOrientationChange(); + } + }); + } + + protected void onOrientationChange() { + // update the current config + int index = mOrientation.getSelectionIndex(); + + if (index != -1) { + mSelectedConfiguration.setScreenOrientationQualifier(new ScreenOrientationQualifier( + ScreenOrientation.getByIndex(index))); + } else { + // empty selection, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setScreenOrientationQualifier( + new ScreenOrientationQualifier()); + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + ScreenOrientationQualifier q = (ScreenOrientationQualifier)qualifier; + + ScreenOrientation value = q.getValue(); + if (value == null) { + mOrientation.clearSelection(); + } else { + mOrientation.select(ScreenOrientation.getIndex(value)); + } + } + } + + /** + * Edit widget for {@link PixelDensityQualifier}. + */ + private class PixelDensityEdit extends QualifierEditBase { + private Text mText; + + public PixelDensityEdit(Composite parent) { + super(parent, PixelDensityQualifier.NAME); + + mText = new Text(this, SWT.BORDER); + mText.addVerifyListener(new DensityVerifier()); + mText.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onTextChange(); + } + }); + mText.addFocusListener(new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + onTextChange(); + } + }); + } + + private void onTextChange() { + String value = mText.getText(); + + if (value.length() == 0) { + // empty string, means a qualifier with no value. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setPixelDensityQualifier(new PixelDensityQualifier()); + } else { + try { + PixelDensityQualifier qualifier = PixelDensityQualifier.getQualifier( + PixelDensityQualifier.getFolderSegment(Integer.parseInt(value))); + if (qualifier != null) { + mSelectedConfiguration.setPixelDensityQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong + // (for instance a one letter string). + // We do nothing in this case. + return; + } + } catch (NumberFormatException nfe) { + // Looks like the code is not a number. This should not happen since the text + // field has a VerifyListener that prevents it. + // We do nothing in this case. + return; + } + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + PixelDensityQualifier q = (PixelDensityQualifier)qualifier; + + mText.setText(Integer.toString(q.getValue())); + } + } + + /** + * Edit widget for {@link TouchScreenQualifier}. + */ + private class TouchEdit extends QualifierEditBase { + + private Combo mTouchScreen; + + public TouchEdit(Composite parent) { + super(parent, TouchScreenQualifier.NAME); + + mTouchScreen = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); + TouchScreenType[] tstValues = TouchScreenType.values(); + for (TouchScreenType value : tstValues) { + mTouchScreen.add(value.getDisplayValue()); + } + + mTouchScreen.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTouchScreen.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onTouchChange(); + } + public void widgetSelected(SelectionEvent e) { + onTouchChange(); + } + }); + } + + protected void onTouchChange() { + // update the current config + int index = mTouchScreen.getSelectionIndex(); + + if (index != -1) { + mSelectedConfiguration.setTouchTypeQualifier(new TouchScreenQualifier( + TouchScreenType.getByIndex(index))); + } else { + // empty selection, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setTouchTypeQualifier(new TouchScreenQualifier()); + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + TouchScreenQualifier q = (TouchScreenQualifier)qualifier; + + TouchScreenType value = q.getValue(); + if (value == null) { + mTouchScreen.clearSelection(); + } else { + mTouchScreen.select(TouchScreenType.getIndex(value)); + } + } + } + + /** + * Edit widget for {@link KeyboardStateQualifier}. + */ + private class KeyboardEdit extends QualifierEditBase { + + private Combo mKeyboard; + + public KeyboardEdit(Composite parent) { + super(parent, KeyboardStateQualifier.NAME); + + mKeyboard = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); + KeyboardState[] ksValues = KeyboardState.values(); + for (KeyboardState value : ksValues) { + mKeyboard.add(value.getDisplayValue()); + } + + mKeyboard.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mKeyboard.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onKeyboardChange(); + } + public void widgetSelected(SelectionEvent e) { + onKeyboardChange(); + } + }); + } + + protected void onKeyboardChange() { + // update the current config + int index = mKeyboard.getSelectionIndex(); + + if (index != -1) { + mSelectedConfiguration.setKeyboardStateQualifier(new KeyboardStateQualifier( + KeyboardState.getByIndex(index))); + } else { + // empty selection, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setKeyboardStateQualifier( + new KeyboardStateQualifier()); + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + KeyboardStateQualifier q = (KeyboardStateQualifier)qualifier; + + KeyboardState value = q.getValue(); + if (value == null) { + mKeyboard.clearSelection(); + } else { + mKeyboard.select(KeyboardState.getIndex(value)); + } + } + } + + /** + * Edit widget for {@link TextInputMethodQualifier}. + */ + private class TextInputEdit extends QualifierEditBase { + + private Combo mTextInput; + + public TextInputEdit(Composite parent) { + super(parent, TextInputMethodQualifier.NAME); + + mTextInput = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); + TextInputMethod[] timValues = TextInputMethod.values(); + for (TextInputMethod value : timValues) { + mTextInput.add(value.getDisplayValue()); + } + + mTextInput.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextInput.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onTextInputChange(); + } + public void widgetSelected(SelectionEvent e) { + onTextInputChange(); + } + }); + } + + protected void onTextInputChange() { + // update the current config + int index = mTextInput.getSelectionIndex(); + + if (index != -1) { + mSelectedConfiguration.setTextInputMethodQualifier(new TextInputMethodQualifier( + TextInputMethod.getByIndex(index))); + } else { + // empty selection, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setTextInputMethodQualifier( + new TextInputMethodQualifier()); + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + TextInputMethodQualifier q = (TextInputMethodQualifier)qualifier; + + TextInputMethod value = q.getValue(); + if (value == null) { + mTextInput.clearSelection(); + } else { + mTextInput.select(TextInputMethod.getIndex(value)); + } + } + } + + /** + * Edit widget for {@link NavigationMethodQualifier}. + */ + private class NavigationEdit extends QualifierEditBase { + + private Combo mNavigation; + + public NavigationEdit(Composite parent) { + super(parent, NavigationMethodQualifier.NAME); + + mNavigation = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); + NavigationMethod[] nmValues = NavigationMethod.values(); + for (NavigationMethod value : nmValues) { + mNavigation.add(value.getDisplayValue()); + } + + mNavigation.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNavigation.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + onNavigationChange(); + } + public void widgetSelected(SelectionEvent e) { + onNavigationChange(); + } + }); + } + + protected void onNavigationChange() { + // update the current config + int index = mNavigation.getSelectionIndex(); + + if (index != -1) { + mSelectedConfiguration.setNavigationMethodQualifier(new NavigationMethodQualifier( + NavigationMethod.getByIndex(index))); + } else { + // empty selection, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setNavigationMethodQualifier( + new NavigationMethodQualifier()); + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + NavigationMethodQualifier q = (NavigationMethodQualifier)qualifier; + + NavigationMethod value = q.getValue(); + if (value == null) { + mNavigation.clearSelection(); + } else { + mNavigation.select(NavigationMethod.getIndex(value)); + } + } + } + + /** + * Edit widget for {@link ScreenDimensionQualifier}. + */ + private class ScreenDimensionEdit extends QualifierEditBase { + + private Text mSize1; + private Text mSize2; + + public ScreenDimensionEdit(Composite parent) { + super(parent, ScreenDimensionQualifier.NAME); + + ModifyListener modifyListener = new ModifyListener() { + public void modifyText(ModifyEvent e) { + onSizeChange(); + } + }; + + FocusAdapter focusListener = new FocusAdapter() { + @Override + public void focusLost(FocusEvent e) { + onSizeChange(); + } + }; + + mSize1 = new Text(this, SWT.BORDER); + mSize1.addVerifyListener(new DimensionVerifier()); + mSize1.addModifyListener(modifyListener); + mSize1.addFocusListener(focusListener); + + mSize2 = new Text(this, SWT.BORDER); + mSize2.addVerifyListener(new DimensionVerifier()); + mSize2.addModifyListener(modifyListener); + mSize2.addFocusListener(focusListener); + } + + private void onSizeChange() { + // update the current config + String size1 = mSize1.getText(); + String size2 = mSize2.getText(); + + if (size1.length() == 0 || size2.length() == 0) { + // if one of the strings is empty, reset to no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setScreenDimensionQualifier(new ScreenDimensionQualifier()); + } else { + ScreenDimensionQualifier qualifier = ScreenDimensionQualifier.getQualifier(size1, + size2); + + if (qualifier != null) { + mSelectedConfiguration.setScreenDimensionQualifier(qualifier); + } else { + // Failure! Looks like the value is wrong, reset the qualifier + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setScreenDimensionQualifier( + new ScreenDimensionQualifier()); + } + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + ScreenDimensionQualifier q = (ScreenDimensionQualifier)qualifier; + + mSize1.setText(Integer.toString(q.getValue1())); + mSize2.setText(Integer.toString(q.getValue2())); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java new file mode 100644 index 0000000..5781938 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java @@ -0,0 +1,1159 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.project.ProjectChooserHelper; +import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors; +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; +import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors; +import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType; +import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.ConfigurationState; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +/** + * This is the single page of the {@link NewXmlFileWizard} which provides the ability to create + * skeleton XML resources files for Android projects. + * <p/> + * This page is used to select the project, the resource folder, resource type and file name. + */ +class NewXmlFileCreationPage extends WizardPage { + + /** + * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.) + */ + static class TypeInfo { + private final String mUiName; + private final ResourceFolderType mResFolderType; + private final String mTooltip; + private final Object mRootSeed; + private Button mWidget; + private ArrayList<String> mRoots = new ArrayList<String>(); + private final String mXmlns; + private final String mDefaultAttrs; + private final String mDefaultRoot; + private final int mTargetApiLevel; + + public TypeInfo(String uiName, + String tooltip, + ResourceFolderType resFolderType, + Object rootSeed, + String defaultRoot, + String xmlns, + String defaultAttrs, + int targetApiLevel) { + mUiName = uiName; + mResFolderType = resFolderType; + mTooltip = tooltip; + mRootSeed = rootSeed; + mDefaultRoot = defaultRoot; + mXmlns = xmlns; + mDefaultAttrs = defaultAttrs; + mTargetApiLevel = targetApiLevel; + } + + /** Returns the UI name for the resource type. Unique. Never null. */ + String getUiName() { + return mUiName; + } + + /** Returns the tooltip for the resource type. Can be null. */ + String getTooltip() { + return mTooltip; + } + + /** + * Returns the name of the {@link ResourceFolderType}. + * Never null but not necessarily unique, + * e.g. two types use {@link ResourceFolderType#XML}. + */ + String getResFolderName() { + return mResFolderType.getName(); + } + + /** + * Returns the matching {@link ResourceFolderType}. + * Never null but not necessarily unique, + * e.g. two types use {@link ResourceFolderType#XML}. + */ + ResourceFolderType getResFolderType() { + return mResFolderType; + } + + /** Sets the radio button associate with the resource type. Can be null. */ + void setWidget(Button widget) { + mWidget = widget; + } + + /** Returns the radio button associate with the resource type. Can be null. */ + Button getWidget() { + return mWidget; + } + + /** + * Returns the seed used to fill the root element values. + * The seed might be either a String, a String array, an {@link ElementDescriptor}, + * a {@link DocumentDescriptor} or null. + */ + Object getRootSeed() { + return mRootSeed; + } + + /** Returns the default root element that should be selected by default. Can be null. */ + String getDefaultRoot() { + return mDefaultRoot; + } + + /** + * Returns the list of all possible root elements for the resource type. + * This can be an empty ArrayList but not null. + * <p/> + * TODO: the root list SHOULD depend on the currently selected project, to include + * custom classes. + */ + ArrayList<String> getRoots() { + return mRoots; + } + + /** + * If the generated resource XML file requires an "android" XMLNS, this should be set + * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated. + */ + String getXmlns() { + return mXmlns; + } + + /** + * When not null, this represent extra attributes that must be specified in the + * root element of the generated XML file. When null, no extra attributes are inserted. + */ + String getDefaultAttrs() { + return mDefaultAttrs; + } + + /** + * The minimum API level required by the current SDK target to support this feature. + */ + public int getTargetApiLevel() { + return mTargetApiLevel; + } + } + + /** + * TypeInfo, information for each "type" of file that can be created. + */ + private static final TypeInfo[] sTypes = { + new TypeInfo( + "Layout", // UI name + "An XML file that describes a screen layout.", // tooltip + ResourceFolderType.LAYOUT, // folder type + AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed + "LinearLayout", // default root + SdkConstants.NS_RESOURCES, // xmlns + "android:layout_width=\"wrap_content\"\n" + // default attributes + "android:layout_height=\"wrap_content\"", + 1 // target API level + ), + new TypeInfo("Values", // UI name + "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip + ResourceFolderType.VALUES, // folder type + ResourcesDescriptors.ROOT_ELEMENT, // root seed + null, // default root + null, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Menu", // UI name + "An XML file that describes an menu.", // tooltip + ResourceFolderType.MENU, // folder type + MenuDescriptors.MENU_ROOT_ELEMENT, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Gadget Provider", // UI name + "An XML file that describes a gadget provider.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_GADGET_PROVIDER, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 3 // target API level + ), + new TypeInfo("Preference", // UI name + "An XML file that describes preferences.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed + AndroidConstants.CLASS_PREFERENCE_SCREEN, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Searchable", // UI name + "An XML file that describes a searchable.", // tooltip + ResourceFolderType.XML, // folder type + AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed + null, // default root + SdkConstants.NS_RESOURCES, // xmlns + null, // default attributes + 1 // target API level + ), + new TypeInfo("Animation", // UI name + "An XML file that describes an animation.", // tooltip + ResourceFolderType.ANIM, // folder type + // TODO reuse constants if we ever make an editor with descriptors for animations + new String[] { // root seed + "set", //$NON-NLS-1$ + "alpha", //$NON-NLS-1$ + "scale", //$NON-NLS-1$ + "translate", //$NON-NLS-1$ + "rotate" //$NON-NLS-1$ + }, + "set", //$NON-NLS-1$ // default root + null, // xmlns + null, // default attributes + 1 // target API level + ), + }; + + /** Number of columns in the grid layout */ + final static int NUM_COL = 4; + + /** Absolute destination folder root, e.g. "/res/" */ + private static String sResFolderAbs = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP; + /** Relative destination folder root, e.g. "res/" */ + private static String sResFolderRel = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP; + + private IProject mProject; + private Text mProjectTextField; + private Button mProjectBrowseButton; + private Text mFileNameTextField; + private Text mWsFolderPathTextField; + private Combo mRootElementCombo; + private IStructuredSelection mInitialSelection; + private ConfigurationSelector mConfigSelector; + private FolderConfiguration mTempConfig = new FolderConfiguration(); + private boolean mInternalWsFolderPathUpdate; + private boolean mInternalTypeUpdate; + private boolean mInternalConfigSelectorUpdate; + private ProjectChooserHelper mProjectChooserHelper; + + + // --- UI creation --- + + /** + * Constructs a new {@link NewXmlFileCreationPage}. + * <p/> + * Called by {@link NewXmlFileWizard#createMainPage()}. + */ + protected NewXmlFileCreationPage(String pageName) { + super(pageName); + setPageComplete(false); + } + + public void setInitialSelection(IStructuredSelection initialSelection) { + mInitialSelection = initialSelection; + } + + /** + * Called by the parent Wizard to create the UI for this Wizard Page. + * + * {@inheritDoc} + * + * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite) + */ + public void createControl(Composite parent) { + Composite composite = new Composite(parent, SWT.NULL); + composite.setFont(parent.getFont()); + + initializeDialogUnits(parent); + + composite.setLayout(new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/)); + composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createProjectGroup(composite); + createTypeGroup(composite); + createRootGroup(composite); + + // Show description the first time + setErrorMessage(null); + setMessage(null); + setControl(composite); + + // Update state the first time + initializeFromSelection(mInitialSelection); + initializeRootValues(); + enableTypesBasedOnApi(); + validatePage(); + } + + /** + * Returns the target project or null. + */ + public IProject getProject() { + return mProject; + } + + /** + * Returns the destination filename or an empty string. + */ + public String getFileName() { + return mFileNameTextField == null ? "" : mFileNameTextField.getText(); //$NON-NLS-1$ + } + + /** + * Returns the destination folder path relative to the project or an empty string. + */ + public String getWsFolderPath() { + return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$ + } + + + /** + * Returns an {@link IFile} on the destination file. + * <p/> + * Uses {@link #getProject()}, {@link #getWsFolderPath()} and {@link #getFileName()}. + * <p/> + * Returns null if the project, filename or folder are invalid and the destination file + * cannot be determined. + * <p/> + * The {@link IFile} is a resource. There might or might not be an actual real file. + */ + public IFile getDestinationFile() { + IProject project = getProject(); + String wsFolderPath = getWsFolderPath(); + String fileName = getFileName(); + if (project != null && wsFolderPath.length() > 0 && fileName.length() > 0) { + IPath dest = new Path(wsFolderPath).append(fileName); + IFile file = project.getFile(dest); + return file; + } + return null; + } + + /** + * Returns the {@link TypeInfo} for the currently selected type radio button. + * Returns null if no radio button is selected. + * + * @return A {@link TypeInfo} or null. + */ + public TypeInfo getSelectedType() { + TypeInfo type = null; + for (TypeInfo ti : sTypes) { + if (ti.getWidget().getSelection()) { + type = ti; + break; + } + } + return type; + } + + /** + * Returns the selected root element string, if any. + * + * @return The selected root element string or null. + */ + public String getRootElement() { + int index = mRootElementCombo.getSelectionIndex(); + if (index >= 0) { + return mRootElementCombo.getItem(index); + } + return null; + } + + // --- UI creation --- + + /** + * Helper method to create a new GridData with an horizontal span. + * + * @param horizSpan The number of cells for the horizontal span. + * @return A new GridData with the horizontal span. + */ + private GridData newGridData(int horizSpan) { + GridData gd = new GridData(); + gd.horizontalSpan = horizSpan; + return gd; + } + + /** + * Helper method to create a new GridData with an horizontal span and a style. + * + * @param horizSpan The number of cells for the horizontal span. + * @param style The style, e.g. {@link GridData#FILL_HORIZONTAL} + * @return A new GridData with the horizontal span and the style. + */ + private GridData newGridData(int horizSpan, int style) { + GridData gd = new GridData(style); + gd.horizontalSpan = horizSpan; + return gd; + } + + /** + * Helper method that creates an empty cell in the parent composite. + * + * @param parent The parent composite. + */ + private void emptyCell(Composite parent) { + new Label(parent, SWT.NONE); + } + + /** + * Pads the parent with empty cells to match the number of columns of the parent grid. + * + * @param parent A grid layout with NUM_COL columns + * @param col The current number of columns used. + * @return 0, the new number of columns used, for convenience. + */ + private int padWithEmptyCells(Composite parent, int col) { + for (; col < NUM_COL; ++col) { + emptyCell(parent); + } + col = 0; + return col; + } + + /** + * Creates the project & filename fields. + * <p/> + * The parent must be a GridLayout with NUM_COL colums. + */ + private void createProjectGroup(Composite parent) { + int col = 0; + + // project name + String tooltip = "The Android Project where the new resource file will be created."; + Label label = new Label(parent, SWT.NONE); + label.setText("Project"); + label.setToolTipText(tooltip); + ++col; + + mProjectTextField = new Text(parent, SWT.BORDER); + mProjectTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mProjectTextField.setToolTipText(tooltip); + mProjectTextField.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onProjectFieldUpdated(); + } + }); + ++col; + + mProjectBrowseButton = new Button(parent, SWT.NONE); + mProjectBrowseButton.setText("Browse..."); + mProjectBrowseButton.setToolTipText("Allows you to select the Android project to modify."); + mProjectBrowseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onProjectBrowse(); + } + }); + mProjectChooserHelper = new ProjectChooserHelper(parent.getShell()); + ++col; + + col = padWithEmptyCells(parent, col); + + // file name + tooltip = "The name of the resource file to create."; + label = new Label(parent, SWT.NONE); + label.setText("File"); + label.setToolTipText(tooltip); + ++col; + + mFileNameTextField = new Text(parent, SWT.BORDER); + mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFileNameTextField.setToolTipText(tooltip); + mFileNameTextField.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + validatePage(); + } + }); + ++col; + + padWithEmptyCells(parent, col); + } + + /** + * Creates the type field, {@link ConfigurationSelector} and the folder field. + * <p/> + * The parent must be a GridLayout with NUM_COL colums. + */ + private void createTypeGroup(Composite parent) { + // separator + Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL)); + + // label before type radios + label = new Label(parent, SWT.NONE); + label.setText("What type of resource would you like to create?"); + label.setLayoutData(newGridData(NUM_COL)); + + // display the types on three columns of radio buttons. + emptyCell(parent); + Composite grid = new Composite(parent, SWT.NONE); + padWithEmptyCells(parent, 2); + + grid.setLayout(new GridLayout(NUM_COL, true /*makeColumnsEqualWidth*/)); + + SelectionListener radioListener = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // single-click. Only do something if activated. + if (e.getSource() instanceof Button) { + onRadioTypeUpdated((Button) e.getSource()); + } + } + }; + + int n = sTypes.length; + int num_lines = (n + NUM_COL/2) / NUM_COL; + for (int line = 0, k = 0; line < num_lines; line++) { + for (int i = 0; i < NUM_COL; i++, k++) { + if (k < n) { + TypeInfo type = sTypes[k]; + Button radio = new Button(grid, SWT.RADIO); + type.setWidget(radio); + radio.setSelection(false); + radio.setText(type.getUiName()); + radio.setToolTipText(type.getTooltip()); + radio.addSelectionListener(radioListener); + } else { + emptyCell(grid); + } + } + } + + // label before configuration selector + label = new Label(parent, SWT.NONE); + label.setText("What type of resource configuration would you like?"); + label.setLayoutData(newGridData(NUM_COL)); + + // configuration selector + emptyCell(parent); + mConfigSelector = new ConfigurationSelector(parent); + GridData gd = newGridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); + gd.widthHint = ConfigurationSelector.WIDTH_HINT; + gd.heightHint = ConfigurationSelector.HEIGHT_HINT; + mConfigSelector.setLayoutData(gd); + mConfigSelector.setOnChangeListener(new onConfigSelectorUpdated()); + emptyCell(parent); + + // folder name + String tooltip = "The folder where the file will be generated, relative to the project."; + label = new Label(parent, SWT.NONE); + label.setText("Folder"); + label.setToolTipText(tooltip); + + mWsFolderPathTextField = new Text(parent, SWT.BORDER); + mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mWsFolderPathTextField.setToolTipText(tooltip); + mWsFolderPathTextField.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + onWsFolderPathUpdated(); + } + }); + } + + /** + * Creates the root element combo. + * <p/> + * The parent must be a GridLayout with NUM_COL colums. + */ + private void createRootGroup(Composite parent) { + // separator + Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL)); + + // label before the root combo + String tooltip = "The root element to create in the XML file."; + label = new Label(parent, SWT.NONE); + label.setText("Select the root element for the XML file:"); + label.setLayoutData(newGridData(NUM_COL)); + label.setToolTipText(tooltip); + + // root combo + emptyCell(parent); + + mRootElementCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + mRootElementCombo.setEnabled(false); + mRootElementCombo.select(0); + mRootElementCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRootElementCombo.setToolTipText(tooltip); + + padWithEmptyCells(parent, 2); + } + + /** + * Called by {@link NewXmlFileWizard} to initialize the page with the selection + * received by the wizard -- typically the current user workbench selection. + * <p/> + * Things we expect to find out from the selection: + * <ul> + * <li>The project name, valid if it's an android nature.</li> + * <li>The current folder, valid if it's a folder under /res</li> + * <li>An existing filename, in which case the user will be asked whether to override it.</li> + * <ul> + * + * @param selection The selection when the wizard was initiated. + */ + private void initializeFromSelection(IStructuredSelection selection) { + if (selection == null) { + return; + } + + + // Find the best match in the element list. In case there are multiple selected elements + // select the one that provides the most information and assign them a score, + // e.g. project=1 + folder=2 + file=4. + IProject targetProject = null; + String targetWsFolderPath = null; + String targetFileName = null; + int targetScore = 0; + for (Object element : selection.toList()) { + if (element instanceof IAdaptable) { + IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class); + IProject project = res != null ? res.getProject() : null; + + // Is this an Android project? + try { + if (project == null || !project.hasNature(AndroidConstants.NATURE)) { + continue; + } + } catch (CoreException e) { + // checking the nature failed, ignore this resource + continue; + } + + int score = 1; // we have a valid project at least + + IPath wsFolderPath = null; + String fileName = null; + if (res.getType() == IResource.FOLDER) { + wsFolderPath = res.getProjectRelativePath(); + } else if (res.getType() == IResource.FILE) { + fileName = res.getName(); + wsFolderPath = res.getParent().getProjectRelativePath(); + } + + // Disregard this folder selection if it doesn't point to /res/something + if (wsFolderPath != null && + wsFolderPath.segmentCount() > 1 && + SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) { + score += 2; + } else { + wsFolderPath = null; + fileName = null; + } + + score += fileName != null ? 4 : 0; + + if (score > targetScore) { + targetScore = score; + targetProject = project; + targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null; + targetFileName = fileName; + } + } + } + + // Now set the UI accordingly + if (targetScore > 0) { + mProject = targetProject; + mProjectTextField.setText(targetProject != null ? targetProject.getName() : ""); //$NON-NLS-1$ + mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$ + mWsFolderPathTextField.setText(targetWsFolderPath != null ? targetWsFolderPath : ""); //$NON-NLS-1$ + } + } + + /** + * Initialize the root values of the type infos based on the current framework values. + */ + private void initializeRootValues() { + for (TypeInfo type : sTypes) { + // Clear all the roots for this type + ArrayList<String> roots = type.getRoots(); + if (roots.size() > 0) { + roots.clear(); + } + + // depending of the type of the seed, initialize the root in different ways + Object rootSeed = type.getRootSeed(); + + if (rootSeed instanceof String) { + // The seed is a single string, Add it as-is. + roots.add((String) rootSeed); + } else if (rootSeed instanceof String[]) { + // The seed is an array of strings. Add them as-is. + for (String value : (String[]) rootSeed) { + roots.add(value); + } + } else if (rootSeed instanceof Integer && mProject != null) { + // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_* + // In this case add all the children element descriptors defined, recursively, + // and avoid infinite recursion by keeping track of what has already been added. + + // Note: if project is null, the root list will be empty since it has been + // cleared above. + + // get the AndroidTargetData from the project + IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + + IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed); + ElementDescriptor descriptor = provider.getDescriptor(); + if (descriptor != null) { + HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>(); + initRootElementDescriptor(roots, descriptor, visited); + } + + // Sort alphabetically. + Collections.sort(roots); + } + } + } + + /** + * Helper method to recursively insert all XML names for the given {@link ElementDescriptor} + * into the roots array list. Keeps track of visited nodes to avoid infinite recursion. + * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic + * and not a valid root element. + */ + private void initRootElementDescriptor(ArrayList<String> roots, + ElementDescriptor desc, HashSet<ElementDescriptor> visited) { + if (!(desc instanceof DocumentDescriptor)) { + String xmlName = desc.getXmlName(); + if (xmlName != null && xmlName.length() > 0) { + roots.add(xmlName); + } + } + + visited.add(desc); + + for (ElementDescriptor child : desc.getChildren()) { + if (!visited.contains(child)) { + initRootElementDescriptor(roots, child, visited); + } + } + } + + /** + * Callback called when the user edits the project text field. + */ + private void onProjectFieldUpdated() { + String project = mProjectTextField.getText(); + + // Is this a valid project? + IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/); + IProject found = null; + for (IJavaProject p : projects) { + if (p.getProject().getName().equals(project)) { + found = p.getProject(); + break; + } + } + + if (found != mProject) { + changeProject(found); + } + } + + /** + * Callback called when the user uses the "Browse Projects" button. + */ + private void onProjectBrowse() { + IJavaProject p = mProjectChooserHelper.chooseJavaProject(mProjectTextField.getText()); + if (p != null) { + changeProject(p.getProject()); + mProjectTextField.setText(mProject.getName()); + } + } + + /** + * Changes mProject to the given new project and update the UI accordingly. + */ + private void changeProject(IProject newProject) { + mProject = newProject; + + // enable types based on new API level + enableTypesBasedOnApi(); + + // update the Type with the new descriptors. + initializeRootValues(); + + // update the combo + updateRootCombo(getSelectedType()); + + validatePage(); + } + + /** + * Callback called when the Folder text field is changed, either programmatically + * or by the user. + */ + private void onWsFolderPathUpdated() { + if (mInternalWsFolderPathUpdate) { + return; + } + + String wsFolderPath = mWsFolderPathTextField.getText(); + + // This is a custom path, we need to sanitize it. + // First it should start with "/res/". Then we need to make sure there are no + // relative paths, things like "../" or "./" or even "//". + wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$ + wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>(); + + // We get "res/foo" from selections relative to the project when we want a "/res/foo" path. + if (wsFolderPath.startsWith(sResFolderRel)) { + wsFolderPath = sResFolderAbs + wsFolderPath.substring(sResFolderRel.length()); + + mInternalWsFolderPathUpdate = true; + mWsFolderPathTextField.setText(wsFolderPath); + mInternalWsFolderPathUpdate = false; + } + + if (wsFolderPath.startsWith(sResFolderAbs)) { + wsFolderPath = wsFolderPath.substring(sResFolderAbs.length()); + + int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR); + if (pos >= 0) { + wsFolderPath = wsFolderPath.substring(0, pos); + } + + String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP); + + if (folderSegments.length > 0) { + String folderName = folderSegments[0]; + + // update config selector + mInternalConfigSelectorUpdate = true; + mConfigSelector.setConfiguration(folderSegments); + mInternalConfigSelectorUpdate = false; + + boolean selected = false; + for (TypeInfo type : sTypes) { + if (type.getResFolderName().equals(folderName)) { + matches.add(type); + selected |= type.getWidget().getSelection(); + } + } + + if (matches.size() == 1) { + // If there's only one match, select it if it's not already selected + if (!selected) { + selectType(matches.get(0)); + } + } else if (matches.size() > 1) { + // There are multiple type candidates for this folder. This can happen + // for /res/xml for example. Check to see if one of them is currently + // selected. If yes, leave the selection unchanged. If not, deselect all type. + if (!selected) { + selectType(null); + } + } else { + // Nothing valid was selected. + selectType(null); + } + } + } + + validatePage(); + } + + /** + * Callback called when one of the type radio button is changed. + * + * @param typeWidget The type radio button that changed. + */ + private void onRadioTypeUpdated(Button typeWidget) { + // Do nothing if this is an internal modification or if the widget has been + // de-selected. + if (mInternalTypeUpdate || !typeWidget.getSelection()) { + return; + } + + // Find type info that has just been enabled. + TypeInfo type = null; + for (TypeInfo ti : sTypes) { + if (ti.getWidget() == typeWidget) { + type = ti; + break; + } + } + + if (type == null) { + return; + } + + // update the combo + + updateRootCombo(type); + + // update the folder path + + String wsFolderPath = mWsFolderPathTextField.getText(); + String newPath = null; + + mConfigSelector.getConfiguration(mTempConfig); + ResourceQualifier qual = mTempConfig.getInvalidQualifier(); + if (qual == null) { + // The configuration is valid. Reformat the folder path using the canonical + // value from the configuration. + + newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType()); + } else { + // The configuration is invalid. We still update the path but this time + // do it manually on the string. + if (wsFolderPath.startsWith(sResFolderAbs)) { + wsFolderPath.replaceFirst( + "^(" + sResFolderAbs +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$ + "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType()); + } + } + + if (newPath != null && !newPath.equals(wsFolderPath)) { + mInternalWsFolderPathUpdate = true; + mWsFolderPathTextField.setText(newPath); + mInternalWsFolderPathUpdate = false; + } + + validatePage(); + } + + /** + * Helper method that fills the values of the "root element" combo box based + * on the currently selected type radio button. Also disables the combo is there's + * only one choice. Always select the first root element for the given type. + * + * @param type The currently selected {@link TypeInfo}. Cannot be null. + */ + private void updateRootCombo(TypeInfo type) { + // reset all the values in the combo + mRootElementCombo.removeAll(); + + if (type != null) { + // get the list of roots. The list can be empty but not null. + ArrayList<String> roots = type.getRoots(); + + // enable the combo if there's more than one choice + mRootElementCombo.setEnabled(roots != null && roots.size() > 1); + + for (String root : roots) { + mRootElementCombo.add(root); + } + + int index = 0; // default is to select the first one + String defaultRoot = type.getDefaultRoot(); + if (defaultRoot != null) { + index = roots.indexOf(defaultRoot); + } + mRootElementCombo.select(index < 0 ? 0 : index); + } + } + + /** + * Callback called when the configuration has changed in the {@link ConfigurationSelector}. + */ + private class onConfigSelectorUpdated implements Runnable { + public void run() { + if (mInternalConfigSelectorUpdate) { + return; + } + + TypeInfo type = getSelectedType(); + + if (type != null) { + mConfigSelector.getConfiguration(mTempConfig); + StringBuffer sb = new StringBuffer(sResFolderAbs); + sb.append(mTempConfig.getFolderName(type.getResFolderType())); + + mInternalWsFolderPathUpdate = true; + mWsFolderPathTextField.setText(sb.toString()); + mInternalWsFolderPathUpdate = false; + + validatePage(); + } + } + } + + /** + * Helper method to select on of the type radio buttons. + * + * @param type The TypeInfo matching the radio button to selected or null to deselect them all. + */ + private void selectType(TypeInfo type) { + if (type == null || !type.getWidget().getSelection()) { + mInternalTypeUpdate = true; + for (TypeInfo type2 : sTypes) { + type2.getWidget().setSelection(type2 == type); + } + updateRootCombo(type); + mInternalTypeUpdate = false; + } + } + + /** + * Helper method to enable the type radio buttons depending on the current API level. + * <p/> + * A type radio button is enabled either if: + * - if mProject is null, API level 1 is considered valid + * - if mProject is !null, the project->target->API must be >= to the type's API level. + */ + private void enableTypesBasedOnApi() { + + IAndroidTarget target = mProject != null ? Sdk.getCurrent().getTarget(mProject) : null; + int currentApiLevel = 1; + if (target != null) { + currentApiLevel = target.getApiVersionNumber(); + } + + for (TypeInfo type : sTypes) { + type.getWidget().setEnabled(type.getTargetApiLevel() <= currentApiLevel); + } + } + + /** + * Validates the fields, displays errors and warnings. + * Enables the finish button if there are no errors. + */ + private void validatePage() { + String error = null; + String warning = null; + + // -- validate project + if (getProject() == null) { + error = "Please select an Android project."; + } + + // -- validate filename + if (error == null) { + String fileName = getFileName(); + if (fileName == null || fileName.length() == 0) { + error = "A destination file name is required."; + } else if (!fileName.endsWith(AndroidConstants.DOT_XML)) { + error = String.format("The filename must end with %1$s.", AndroidConstants.DOT_XML); + } + } + + // -- validate type + if (error == null) { + TypeInfo type = getSelectedType(); + + if (type == null) { + error = "One of the types must be selected (e.g. layout, values, etc.)"; + } + } + + // -- validate type API level + if (error == null) { + IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); + int currentApiLevel = 1; + if (target != null) { + currentApiLevel = target.getApiVersionNumber(); + } + + TypeInfo type = getSelectedType(); + + if (type.getTargetApiLevel() > currentApiLevel) { + error = "The API level of the selected type (e.g. gadget, etc.) is not " + + "compatible with the API level of the project."; + } + } + + // -- validate folder configuration + if (error == null) { + ConfigurationState state = mConfigSelector.getState(); + if (state == ConfigurationState.INVALID_CONFIG) { + ResourceQualifier qual = mConfigSelector.getInvalidQualifier(); + if (qual != null) { + error = String.format("The qualifier '%1$s' is invalid in the folder configuration.", + qual.getName()); + } + } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) { + error = "The Region qualifier requires the Language qualifier."; + } + } + + // -- validate generated path + if (error == null) { + String wsFolderPath = getWsFolderPath(); + if (!wsFolderPath.startsWith(sResFolderAbs)) { + error = String.format("Target folder must start with %1$s.", sResFolderAbs); + } + } + + // -- validate destination file doesn't exist + if (error == null) { + IFile file = getDestinationFile(); + if (file != null && file.exists()) { + warning = "The destination file already exists"; + } + } + + // -- update UI & enable finish if there's no error + setPageComplete(error == null); + if (error != null) { + setMessage(error, WizardPage.ERROR); + } else if (warning != null) { + setMessage(warning, WizardPage.WARNING); + } else { + setErrorMessage(null); + setMessage(null); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java new file mode 100644 index 0000000..125102b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.IconFactory; +import com.android.ide.eclipse.editors.wizards.NewXmlFileCreationPage.TypeInfo; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.INewWizard; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +/** + * The "New Android XML File Wizard" provides the ability to create skeleton XML + * resources files for Android projects. + * <p/> + * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project, + * the resource folder, resource type and file name. It then creates the XML file. + */ +public class NewXmlFileWizard extends Wizard implements INewWizard { + + private static final String PROJECT_LOGO_LARGE = "android_large"; //$NON-NLS-1$ + + protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$ + + private NewXmlFileCreationPage mMainPage; + + public void init(IWorkbench workbench, IStructuredSelection selection) { + setHelpAvailable(false); // TODO have help + setWindowTitle("New Android XML File"); + setImageDescriptor(); + + mMainPage = createMainPage(); + mMainPage.setTitle("New Android XML File"); + mMainPage.setDescription("Creates a new Android XML file."); + mMainPage.setInitialSelection(selection); + } + + /** + * Creates the wizard page. + * <p/> + * Please do NOT override this method. + * <p/> + * This is protected so that it can be overridden by unit tests. + * However the contract of this class is private and NO ATTEMPT will be made + * to maintain compatibility between different versions of the plugin. + */ + protected NewXmlFileCreationPage createMainPage() { + return new NewXmlFileCreationPage(MAIN_PAGE_NAME); + } + + // -- Methods inherited from org.eclipse.jface.wizard.Wizard -- + // + // The Wizard class implements most defaults and boilerplate code needed by + // IWizard + + /** + * Adds pages to this wizard. + */ + @Override + public void addPages() { + addPage(mMainPage); + } + + /** + * Performs any actions appropriate in response to the user having pressed + * the Finish button, or refuse if finishing now is not permitted: here, it + * actually creates the workspace project and then switch to the Java + * perspective. + * + * @return True + */ + @Override + public boolean performFinish() { + IFile file = createXmlFile(); + if (file == null) { + return false; + } else { + // Open the file in an editor + IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (win != null) { + IWorkbenchPage page = win.getActivePage(); + if (page != null) { + try { + IDE.openEditor(page, file); + } catch (PartInitException e) { + AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$ + file.getFullPath().toString()); + } + } + } + return true; + } + } + + // -- Custom Methods -- + + private IFile createXmlFile() { + IFile file = mMainPage.getDestinationFile(); + String name = file.getFullPath().toString(); + boolean need_delete = false; + + if (file.exists()) { + if (!AdtPlugin.displayPrompt("New Android XML File", + String.format("Do you want to overwrite the file %1$s ?", name))) { + // abort if user selects cancel. + return null; + } + need_delete = true; + } else { + createWsParentDirectory(file.getParent()); + } + + TypeInfo type = mMainPage.getSelectedType(); + if (type == null) { + // this is not expected to happen + AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name); //$NON-NLS-1$ + return null; + } + String xmlns = type.getXmlns(); + String root = mMainPage.getRootElement(); + if (root == null) { + // this is not expected to happen + AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$ + file.toString()); + return null; + } + + StringBuilder sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$ + + sb.append('<').append(root); + if (xmlns != null) { + sb.append('\n').append(" xmlns:android=\"").append(xmlns).append("\""); //$NON-NLS-1$ //$NON-NLS-2$ + } + + String attrs = type.getDefaultAttrs(); + if (attrs != null) { + sb.append("\n "); //$NON-NLS-1$ + sb.append(attrs.replace("\n", "\n ")); //$NON-NLS-1$ //$NON-NLS-2$ + } + + sb.append(">\n"); //$NON-NLS-1$ + sb.append("</").append(root).append(">\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + String result = sb.toString(); + String error = null; + try { + byte[] buf = result.getBytes("UTF8"); + InputStream stream = new ByteArrayInputStream(buf); + if (need_delete) { + file.delete(IFile.KEEP_HISTORY | IFile.FORCE, null /*monitor*/); + } + file.create(stream, true /*force*/, null /*progres*/); + return file; + } catch (UnsupportedEncodingException e) { + error = e.getMessage(); + } catch (CoreException e) { + error = e.getMessage(); + } + + error = String.format("Failed to generate %1$s: %2$s", name, error); + AdtPlugin.displayError("New Android XML File", error); + return null; + } + + private boolean createWsParentDirectory(IContainer wsPath) { + if (wsPath.getType() == IContainer.FOLDER) { + if (wsPath == null || wsPath.exists()) { + return true; + } + + IFolder folder = (IFolder) wsPath; + try { + if (createWsParentDirectory(wsPath.getParent())) { + folder.create(true /* force */, true /* local */, null /* monitor */); + return true; + } + } catch (CoreException e) { + e.printStackTrace(); + } + } + + return false; + } + + /** + * Returns an image descriptor for the wizard logo. + */ + private void setImageDescriptor() { + ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE); + setDefaultPageImageDescriptor(desc); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java new file mode 100644 index 0000000..6913ce0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.DialogSettings; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.ui.dialogs.FilteredTree; +import org.eclipse.ui.dialogs.PatternFilter; +import org.eclipse.ui.dialogs.SelectionStatusDialog; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A dialog to let the user choose a reference to a resource. + * + */ +public class ReferenceChooserDialog extends SelectionStatusDialog { + + private static Pattern sResourcePattern = Pattern.compile("@(.*)/(.+)"); //$NON-NLS-1$ + private static Pattern sInlineIdResourcePattern = Pattern.compile("@\\+id/(.+)"); //$NON-NLS-1$ + + private static IDialogSettings sDialogSettings = new DialogSettings(""); + + private IResourceRepository mResources; + private String mCurrentResource; + + private FilteredTree mFilteredTree; + + /** + * @param parent + */ + public ReferenceChooserDialog(IResourceRepository resources, Shell parent) { + super(parent); + + int shellStyle = getShellStyle(); + setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE); + + setTitle("Reference Dialog"); + setMessage(String.format("Choose a resource")); + mResources = resources; + + setDialogBoundsSettings(sDialogSettings, getDialogBoundsStrategy()); + } + + public void setCurrentResource(String resource) { + mCurrentResource = resource; + } + + public String getCurrentResource() { + return mCurrentResource; + } + + + /* (non-Javadoc) + * @see org.eclipse.ui.dialogs.SelectionStatusDialog#computeResult() + */ + @Override + protected void computeResult() { + // get the selection + TreePath treeSelection = getSelection(); + if (treeSelection != null) { + if (treeSelection.getSegmentCount() == 2) { + // get the resource type and the resource item + ResourceType resourceType = (ResourceType)treeSelection.getFirstSegment(); + ResourceItem resourceItem = (ResourceItem)treeSelection.getLastSegment(); + + mCurrentResource = resourceType.getXmlString(resourceItem, false /* system */); + } + } + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = (Composite)super.createDialogArea(parent); + + // create the standard message area + createMessageArea(top); + + // create the filtered tree + createFilteredTree(top); + + // setup the initial selection + setupInitialSelection(); + + return top; + } + + private void createFilteredTree(Composite parent) { + mFilteredTree = new FilteredTree(parent, SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION, + new PatternFilter()); + + GridData data = new GridData(); + data.widthHint = convertWidthInCharsToPixels(60); + data.heightHint = convertHeightInCharsToPixels(18); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mFilteredTree.setLayoutData(data); + mFilteredTree.setFont(parent.getFont()); + + TreeViewer treeViewer = mFilteredTree.getViewer(); + Tree tree = treeViewer.getTree(); + + tree.addSelectionListener(new SelectionListener() { + public void widgetDefaultSelected(SelectionEvent e) { + handleDoubleClick(); + } + + public void widgetSelected(SelectionEvent e) { + handleSelection(); + } + }); + + treeViewer.setLabelProvider(new ResourceLabelProvider()); + treeViewer.setContentProvider(new ResourceContentProvider(false /* fullLevels */)); + treeViewer.setInput(mResources); + } + + protected void handleSelection() { + validateCurrentSelection(); + } + + protected void handleDoubleClick() { + if (validateCurrentSelection()) { + buttonPressed(IDialogConstants.OK_ID); + } + } + + /** + * Returns the selected item in the tree as a {@link TreePath} object. + * @return the <code>TreePath</code> object or <code>null</code> if there was no selection. + */ + private TreePath getSelection() { + ISelection selection = mFilteredTree.getViewer().getSelection(); + if (selection instanceof TreeSelection) { + TreeSelection treeSelection = (TreeSelection)selection; + TreePath[] treePaths = treeSelection.getPaths(); + + // the selection mode is SWT.SINGLE, so we just get the first one. + if (treePaths.length > 0) { + return treePaths[0]; + } + } + + return null; + } + + private boolean validateCurrentSelection() { + TreePath treeSelection = getSelection(); + + IStatus status; + if (treeSelection != null) { + if (treeSelection.getSegmentCount() == 2) { + status = new Status(IStatus.OK, AdtPlugin.PLUGIN_ID, + IStatus.OK, "", //$NON-NLS-1$ + null); + } else { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + IStatus.ERROR, "You must select a Resource Item", + null); + } + } else { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + IStatus.ERROR, "", //$NON-NLS-1$ + null); + } + + updateStatus(status); + + return status.isOK(); + } + + /** + * Sets up the initial selection. + * <p/> + * This parses {@link #mCurrentResource} to find out the resource type and the resource name. + */ + private void setupInitialSelection() { + // checks the inline id pattern first as it's more restrictive than the other one. + Matcher m = sInlineIdResourcePattern.matcher(mCurrentResource); + if (m.matches()) { + // get the matching name + String resourceName = m.group(1); + + // setup initial selection + setupInitialSelection(ResourceType.ID, resourceName); + } else { + // attempts the inline id pattern + m = sResourcePattern.matcher(mCurrentResource); + if (m.matches()) { + // get the resource type. + ResourceType resourceType = ResourceType.getEnum(m.group(1)); + if (resourceType != null) { + // get the matching name + String resourceName = m.group(2); + + // setup initial selection + setupInitialSelection(resourceType, resourceName); + } + } + } + } + + /** + * Sets up the initial selection based on a {@link ResourceType} and a resource name. + * @param resourceType the resource type. + * @param resourceName the resource name. + */ + private void setupInitialSelection(ResourceType resourceType, String resourceName) { + // get all the resources of this type + ResourceItem[] resourceItems = mResources.getResources(resourceType); + + for (ResourceItem resourceItem : resourceItems) { + if (resourceName.equals(resourceItem.getName())) { + // name of the resource match, we select it, + TreePath treePath = new TreePath(new Object[] { resourceType, resourceItem }); + mFilteredTree.getViewer().setSelection(new TreeSelection(treePath)); + + // and we're done. + return; + } + } + + // if we get here, the resource type is valid, but the resource is missing. + // we select and expand the resource type element. + TreePath treePath = new TreePath(new Object[] { resourceType }); + mFilteredTree.getViewer().setSelection(new TreeSelection(treePath)); + mFilteredTree.getViewer().setExpandedState(resourceType, true /* expanded */); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java new file mode 100644 index 0000000..60a627b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A dialog to let the user select a resource based on a resource type. + */ +public class ResourceChooser extends AbstractElementListSelectionDialog { + + private Pattern mProjectResourcePattern; + + private ResourceType mResourceType; + + private IResourceRepository mProjectResources; + + // TODO: enable when we can display the system resources. + // private Pattern mSystemResourcePattern; + // private IResourceRepository mSystemResources; + // private Button mProjectButton; + // private Button mSystemButton; + + private String mCurrentResource; + + /** + * Creates a Resource Chooser dialog. + * @param type The type of the resource to choose + * @param project The repository for the project + * @param system The System resource repository + * @param parent the parent shell + */ + public ResourceChooser(ResourceType type, IResourceRepository project, + IResourceRepository system, Shell parent) { + super(parent, new ResourceLabelProvider()); + + mResourceType = type; + mProjectResources = project; + // TODO: enable when we can display the system resources. + // mSystemResources = system; + + mProjectResourcePattern = Pattern.compile( + "@" + mResourceType.getName() + "/(.+)"); //$NON-NLS-1$ //$NON-NLS-2$ + // TODO: enable when we can display the system resources. + // mSystemResourcePattern = Pattern.compile( + // "@android:" + mResourceType.getName() + "/(.+)"); //$NON-NLS-1$ //$NON-NLS-2$ + + setTitle("Resource Chooser"); + setMessage(String.format("Choose a %1$s resource", + mResourceType.getDisplayName().toLowerCase())); + } + + public void setCurrentResource(String resource) { + mCurrentResource = resource; + } + + public String getCurrentResource() { + return mCurrentResource; + } + + @Override + protected void computeResult() { + Object[] elements = getSelectedElements(); + if (elements.length == 1 && elements[0] instanceof ResourceItem) { + ResourceItem item = (ResourceItem)elements[0]; + + mCurrentResource = mResourceType.getXmlString(item, + // TODO: enable when we can display the system resources. + false /*mSystemButton.getSelection()*/); + } + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = (Composite)super.createDialogArea(parent); + + createMessageArea(top); + + // TODO: enable when we can display the system resources. + // createButtons(top); + + createFilterText(top); + createFilteredList(top); + + setupResourceListAndCurrent(); + + return top; + } + + /** + * Creates the radio button to switch between project and system resources. + * @param top the parent composite + */ + /* TODO: enable when we can display the system resources. + private void createButtons(Composite top) { + mProjectButton = new Button(top, SWT.RADIO); + mProjectButton.setText("Project Resources"); + mProjectButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + setListElements(mProjectResources.getResources(mResourceType)); + } + }); + mSystemButton = new Button(top, SWT.RADIO); + mSystemButton.setText("System Resources"); + mSystemButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + setListElements(mSystemResources.getResources(mResourceType)); + } + }); + } + */ + + /** + * Setups the current list based on the current resource. + */ + private void setupResourceListAndCurrent() { + if (setupInitialSelection(mProjectResourcePattern, mProjectResources) == false) { + // if we couldn't understand the current value, we default to the project resources + ResourceItem[] items = mProjectResources.getResources(mResourceType); + setListElements(items); + } + /* + * TODO: enable when we can display the system resources. + if (setupInitialSelection(mProjectResourcePattern, mProjectResources) == false) { + if (setupInitialSelection(mSystemResourcePattern, mSystemResources) == false) { + // if we couldn't understand the current value, we default to the project resources + IResourceItem[] items = mProjectResources.getResources(mResourceType); + setListElements(items); + mProjectButton.setSelection(true); + } else { + mSystemButton.setSelection(true); + } + } else { + mProjectButton.setSelection(true); + }*/ + } + + /** + * Attempts to setup the list of element from a repository if the current resource + * matches the provided pattern. + * @param pattern the pattern to test the current value + * @param repository the repository to use if the pattern matches. + * @return true if success. + */ + private boolean setupInitialSelection(Pattern pattern, IResourceRepository repository) { + Matcher m = pattern.matcher(mCurrentResource); + if (m.matches()) { + // we have a project resource, let's setup the list + ResourceItem[] items = repository.getResources(mResourceType); + setListElements(items); + + // and let's look for the item we found + String name = m.group(1); + + for (ResourceItem item : items) { + if (name.equals(item.getName())) { + setSelection(new Object[] { item }); + break; + } + } + return true; + } + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java new file mode 100644 index 0000000..7c6a539 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.common.resources.IResourceRepository; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.manager.ConfigurableResourceItem; +import com.android.ide.eclipse.editors.resources.manager.ResourceFile; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** + * Content provider for the Resource Explorer TreeView. + * Each level of the tree is represented by a different class. + * <ul> + * <li>{@link ResourceType}. This represents the list of existing Resource Type present + * in the resources. This can be matched to the subclasses inside the class <code>R</code> + * </li> + * <ul> + * <li>{@link ResourceItem}. This represents one resource (which can existing in various alternate + * versions). This is similar to the resource Ids defined as <code>R.sometype.id</code>. + * </li> + * <ul> + * <li>{@link ResourceFile}. (optional) This represents a particular version of the + * {@link ResourceItem}. It is displayed as a list of resource qualifier. + * </li> + * </ul> + * </ul> + * </ul> + * + * @see ResourceLabelProvider + */ +public class ResourceContentProvider implements ITreeContentProvider { + + /** + * The current ProjectResources being displayed. + */ + private IResourceRepository mResources; + + private boolean mFullLevels; + + /** + * Constructs a new content providers for resource display. + * @param fullLevels if <code>true</code> the content provider will suppport all 3 levels. If + * <code>false</code>, only two levels are provided. + */ + public ResourceContentProvider(boolean fullLevels) { + mFullLevels = fullLevels; + } + + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof ResourceType) { + return mResources.getResources((ResourceType)parentElement); + } else if (mFullLevels && parentElement instanceof ConfigurableResourceItem) { + return ((ConfigurableResourceItem)parentElement).getSourceFileArray(); + } + return null; + } + + public Object getParent(Object element) { + // pass + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof ResourceType) { + return mResources.hasResources((ResourceType)element); + } else if (mFullLevels && element instanceof ConfigurableResourceItem) { + return ((ConfigurableResourceItem)element).hasAlternates(); + } + return false; + } + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof IResourceRepository) { + if ((IResourceRepository)inputElement == mResources) { + // get the top level resources. + return mResources.getAvailableResourceTypes(); + } + } + + return new Object[0]; + } + + public void dispose() { + // pass + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + if (newInput instanceof IResourceRepository) { + mResources = (IResourceRepository)newInput; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java new file mode 100644 index 0000000..024d084 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.wizards; + +import com.android.ide.eclipse.common.resources.IIdResourceItem; +import com.android.ide.eclipse.common.resources.ResourceItem; +import com.android.ide.eclipse.common.resources.ResourceType; +import com.android.ide.eclipse.editors.resources.manager.ConfigurableResourceItem; +import com.android.ide.eclipse.editors.resources.manager.IdResourceItem; +import com.android.ide.eclipse.editors.resources.manager.ResourceFile; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; + +/** + * Label provider for the Resource Explorer TreeView. + * Each level of the tree is represented by a different class. + * <ul> + * <li>{@link ResourceType}. This represents the list of existing Resource Type present + * in the resources. This can be matched to the subclasses inside the class <code>R</code> + * </li> + * <ul> + * <li>{@link ResourceItem}. This represents one resource. The actual type can be + * {@link ConfigurableResourceItem} (which can exist in various alternate versions), + * or {@link IdResourceItem}. + * This is similar to the resource Ids defined as <code>R.sometype.id</code>. + * </li> + * <ul> + * <li>{@link ResourceFile}. This represents a particular version of the {@link ResourceItem}. + * It is displayed as a list of resource qualifier. + * </li> + * </ul> + * </ul> + * </ul> + * + * @see ResourceContentProvider + */ +public class ResourceLabelProvider implements ILabelProvider, ITableLabelProvider { + private Image mWarningImage; + + public ResourceLabelProvider() { + mWarningImage = PlatformUI.getWorkbench().getSharedImages().getImageDescriptor( + ISharedImages.IMG_OBJS_WARN_TSK).createImage(); + } + + /** + * @see #getColumnImage(Object, int) + */ + public Image getImage(Object element) { + // pass + return null; + } + + /** + * @see #getColumnText(Object, int) + */ + public String getText(Object element) { + return getColumnText(element, 0); + } + + public void addListener(ILabelProviderListener listener) { + // pass + } + + public void dispose() { + mWarningImage.dispose(); + } + + public boolean isLabelProperty(Object element, String property) { + return false; + } + + public void removeListener(ILabelProviderListener listener) { + // pass + } + + public Image getColumnImage(Object element, int columnIndex) { + if (columnIndex == 1) { + if (element instanceof ConfigurableResourceItem) { + ConfigurableResourceItem item = (ConfigurableResourceItem)element; + if (item.hasDefault() == false) { + return mWarningImage; + } + } + } + return null; + } + + public String getColumnText(Object element, int columnIndex) { + switch (columnIndex) { + case 0: + if (element instanceof ResourceType) { + return ((ResourceType)element).getDisplayName(); + } else if (element instanceof ResourceItem) { + return ((ResourceItem)element).getName(); + } else if (element instanceof ResourceFile) { + return ((ResourceFile)element).getFolder().getConfiguration().toDisplayString(); + } + break; + case 1: + if (element instanceof ConfigurableResourceItem) { + ConfigurableResourceItem item = (ConfigurableResourceItem)element; + int count = item.getAlternateCount(); + if (count > 0) { + if (item.hasDefault()) { + count++; + } + return String.format("%1$d version(s)", count); + } + } else if (element instanceof IIdResourceItem) { + IIdResourceItem idResource = (IIdResourceItem)element; + if (idResource.isDeclaredInline()) { + return "Declared inline"; + } + } + return null; + } + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java new file mode 100644 index 0000000..f28b523 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.xml; + +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.editors.AndroidContentAssist; + +/** + * Content Assist Processor for /res/xml XML files + */ +class XmlContentAssist extends AndroidContentAssist { + + /** + * Constructor for LayoutContentAssist + */ + public XmlContentAssist() { + super(AndroidTargetData.DESCRIPTOR_XML); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java new file mode 100644 index 0000000..d7f6119 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.xml; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.sdk.Sdk; +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.editors.AndroidEditor; +import com.android.ide.eclipse.editors.FirstElementParser; +import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.uimodel.UiDocumentNode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.FileEditorInput; +import org.w3c.dom.Document; + +/** + * Multi-page form editor for /res/xml XML files. + */ +public class XmlEditor extends AndroidEditor { + + public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".xml.XmlEditor"; //$NON-NLS-1$ + + /** Root node of the UI element hierarchy */ + private UiDocumentNode mUiRootNode; + + /** + * Creates the form editor for resources XML files. + */ + public XmlEditor() { + super(); + } + + /** + * Returns the root node of the UI element hierarchy, which here + * is the document node. + */ + @Override + public UiDocumentNode getUiRootNode() { + return mUiRootNode; + } + + // ---- Static ---- + + /** + * Indicates if this is a file that this {@link XmlEditor} can handle. + * <p/> + * The {@link XmlEditor} can handle XML files that have a <searchable> or + * <Preferences> root XML element with the adequate xmlns:android attribute. + * + * @return True if the {@link XmlEditor} can handle that file. + */ + public static boolean canHandleFile(IFile file) { + // we need the target of the file's project to access the descriptors. + IProject project = file.getProject(); + IAndroidTarget target = Sdk.getCurrent().getTarget(project); + if (target != null) { + AndroidTargetData data = Sdk.getCurrent().getTargetData(target); + + FirstElementParser.Result result = FirstElementParser.parse( + file.getLocation().toOSString(), + SdkConstants.NS_RESOURCES); + + if (result != null) { + String name = result.getElement(); + if (name != null && result.getXmlnsPrefix() != null) { + DocumentDescriptor desc = data.getXmlDescriptors().getDescriptor(); + for (ElementDescriptor elem : desc.getChildren()) { + if (elem.getXmlName().equals(name)) { + // This is an element that this document can handle + return true; + } + } + } + } + } + + return false; + } + + // ---- Base Class Overrides ---- + + /** + * Returns whether the "save as" operation is supported by this editor. + * <p/> + * Save-As is a valid operation for the ManifestEditor since it acts on a + * single source file. + * + * @see IEditorPart + */ + @Override + public boolean isSaveAsAllowed() { + return true; + } + + /** + * Create the various form pages. + */ + @Override + protected void createFormPages() { + try { + addPage(new XmlTreePage(this)); + } catch (PartInitException e) { + AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$ + } + + } + + /* (non-java doc) + * Change the tab/title name to include the project name. + */ + @Override + protected void setInput(IEditorInput input) { + super.setInput(input); + if (input instanceof FileEditorInput) { + FileEditorInput fileInput = (FileEditorInput) input; + IFile file = fileInput.getFile(); + setPartName(String.format("%1$s", file.getName())); + } + } + + /** + * Processes the new XML Model, which XML root node is given. + * + * @param xml_doc The XML document, if available, or null if none exists. + */ + @Override + protected void xmlModelChanged(Document xml_doc) { + // init the ui root on demand + initUiRootNode(false /*force*/); + + mUiRootNode.loadFromXmlNode(xml_doc); + + super.xmlModelChanged(xml_doc); + } + + /** + * Creates the initial UI Root Node, including the known mandatory elements. + * @param force if true, a new UiRootNode is recreated even if it already exists. + */ + @Override + protected void initUiRootNode(boolean force) { + // The root UI node is always created, even if there's no corresponding XML node. + if (mUiRootNode == null || force) { + Document doc = null; + if (mUiRootNode != null) { + doc = mUiRootNode.getXmlDocument(); + } + + // get the target data from the opened file (and its project) + AndroidTargetData data = getTargetData(); + + DocumentDescriptor desc; + if (data == null) { + desc = new DocumentDescriptor("temp", null /*children*/); + } else { + desc = data.getXmlDescriptors().getDescriptor(); + } + + mUiRootNode = (UiDocumentNode) desc.createUiNode(); + mUiRootNode.setEditor(this); + + onDescriptorsChanged(doc); + } + } + + // ---- Local Methods ---- + + /** + * Reloads the UI manifest node from the XML, and calls the pages to update. + */ + private void onDescriptorsChanged(Document document) { + if (document != null) { + mUiRootNode.loadFromXmlNode(document); + } else { + mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlNode()); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java new file mode 100644 index 0000000..d25c812 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.xml; + + +import com.android.ide.eclipse.editors.AndroidSourceViewerConfig; + +/** + * Source Viewer Configuration that calls in XmlContentAssist. + */ +public class XmlSourceViewerConfig extends AndroidSourceViewerConfig { + + public XmlSourceViewerConfig() { + super(new XmlContentAssist()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java new file mode 100644 index 0000000..91ce6dd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.xml; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.eclipse.ui.forms.IManagedForm; +import org.eclipse.ui.forms.editor.FormPage; +import org.eclipse.ui.forms.widgets.ScrolledForm; + +/** + * Page for the xml form editor. + */ +public final class XmlTreePage extends FormPage { + /** Page id used for switching tabs programmatically */ + public final static String PAGE_ID = "xml_tree_page"; //$NON-NLS-1$ + + /** Container editor */ + XmlEditor mEditor; + + public XmlTreePage(XmlEditor editor) { + super(editor, PAGE_ID, "Structure"); // tab's label, keep it short + mEditor = editor; + } + + /** + * Creates the content in the form hosted in this page. + * + * @param managedForm the form hosted in this page. + */ + @Override + protected void createFormContent(IManagedForm managedForm) { + super.createFormContent(managedForm); + ScrolledForm form = managedForm.getForm(); + form.setText("Android Xml"); + form.setImage(AdtPlugin.getAndroidLogo()); + + UiElementNode rootNode = mEditor.getUiRootNode(); + UiTreeBlock block = new UiTreeBlock(mEditor, rootNode, + true /* autoCreateRoot */, + null /* no element filters */, + "Xml Elements", + "List of all xml elements in this XML file."); + block.createContent(managedForm); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java new file mode 100644 index 0000000..7929b5a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.xml.descriptors; + +import com.android.ide.eclipse.common.AndroidConstants; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo; +import com.android.ide.eclipse.common.resources.ViewClassInfo; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils; +import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider; +import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor; +import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor; +import com.android.sdklib.SdkConstants; + +import java.util.ArrayList; +import java.util.Map; + + +/** + * Description of the /res/xml structure. + * Currently supports the <searchable> and <preferences> root nodes. + */ +public final class XmlDescriptors implements IDescriptorProvider { + + // Public attributes names, attributes descriptors and elements descriptors referenced + // elsewhere. + public static final String PREF_KEY_ATTR = "key"; //$NON-NLS-1$ + + /** The root document descriptor for both searchable and preferences. */ + private DocumentDescriptor mDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$ + + /** The root document descriptor for searchable. */ + private DocumentDescriptor mSearchDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$ + + /** The root document descriptor for preferences. */ + private DocumentDescriptor mPrefDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$ + + /** The root document descriptor for gadget provider. */ + private DocumentDescriptor mGadgetDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$ + + /** @return the root descriptor for both searchable and preferences. */ + public DocumentDescriptor getDescriptor() { + return mDescriptor; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mDescriptor.getChildren(); + } + + /** @return the root descriptor for searchable. */ + public DocumentDescriptor getSearchableDescriptor() { + return mSearchDescriptor; + } + + /** @return the root descriptor for preferences. */ + public DocumentDescriptor getPreferencesDescriptor() { + return mPrefDescriptor; + } + + /** @return the root descriptor for gadget providers. */ + public DocumentDescriptor getGadgetDescriptor() { + return mGadgetDescriptor; + } + + public IDescriptorProvider getSearchableProvider() { + return new IDescriptorProvider() { + public ElementDescriptor getDescriptor() { + return mSearchDescriptor; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mSearchDescriptor.getChildren(); + } + }; + } + + public IDescriptorProvider getPreferencesProvider() { + return new IDescriptorProvider() { + public ElementDescriptor getDescriptor() { + return mPrefDescriptor; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mPrefDescriptor.getChildren(); + } + }; + } + + public IDescriptorProvider getGadgetProvider() { + return new IDescriptorProvider() { + public ElementDescriptor getDescriptor() { + return mGadgetDescriptor; + } + + public ElementDescriptor[] getRootElementDescriptors() { + return mGadgetDescriptor.getChildren(); + } + }; + } + + /** + * Updates the document descriptor. + * <p/> + * It first computes the new children of the descriptor and then updates them + * all at once. + * + * @param searchableStyleMap The map style=>attributes for <searchable> from the attrs.xml file + * @param gadgetStyleMap The map style=>attributes for <gadget-provider> from the attrs.xml file + * @param prefs The list of non-group preference descriptions + * @param prefGroups The list of preference group descriptions + */ + public synchronized void updateDescriptors( + Map<String, DeclareStyleableInfo> searchableStyleMap, + Map<String, DeclareStyleableInfo> gadgetStyleMap, + ViewClassInfo[] prefs, ViewClassInfo[] prefGroups) { + + XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor( + "android", //$NON-NLS-1$ + SdkConstants.NS_RESOURCES); + + ElementDescriptor searchable = createSearchable(searchableStyleMap, xmlns); + ElementDescriptor gadget = createGadgetProviderInfo(gadgetStyleMap, xmlns); + ElementDescriptor preferences = createPreference(prefs, prefGroups, xmlns); + ArrayList<ElementDescriptor> list = new ArrayList<ElementDescriptor>(); + if (searchable != null) { + list.add(searchable); + mSearchDescriptor.setChildren(new ElementDescriptor[]{ searchable }); + } + if (gadget != null) { + list.add(gadget); + mGadgetDescriptor.setChildren(new ElementDescriptor[]{ gadget }); + } + if (preferences != null) { + list.add(preferences); + mPrefDescriptor.setChildren(new ElementDescriptor[]{ preferences }); + } + + if (list.size() > 0) { + mDescriptor.setChildren(list.toArray(new ElementDescriptor[list.size()])); + } + } + + //------------------------- + // Creation of <searchable> + //------------------------- + + /** + * Returns the new ElementDescriptor for <searchable> + */ + private ElementDescriptor createSearchable( + Map<String, DeclareStyleableInfo> searchableStyleMap, + XmlnsAttributeDescriptor xmlns) { + + ElementDescriptor action_key = createElement(searchableStyleMap, + "SearchableActionKey", //$NON-NLS-1$ styleName + "actionkey", //$NON-NLS-1$ xmlName + "Action Key", // uiName + null, // sdk url + null, // extraAttribute + null, // childrenElements + false /* mandatory */ ); + + ElementDescriptor searchable = createElement(searchableStyleMap, + "Searchable", //$NON-NLS-1$ styleName + "searchable", //$NON-NLS-1$ xmlName + "Searchable", // uiName + null, // sdk url + xmlns, // extraAttribute + new ElementDescriptor[] { action_key }, // childrenElements + false /* mandatory */ ); + return searchable; + } + + /** + * Returns the new ElementDescriptor for <gadget-provider> + */ + private ElementDescriptor createGadgetProviderInfo( + Map<String, DeclareStyleableInfo> gadgetStyleMap, + XmlnsAttributeDescriptor xmlns) { + + if (gadgetStyleMap == null) { + return null; + } + + ElementDescriptor gadget = createElement(gadgetStyleMap, + "GadgetProviderInfo", //$NON-NLS-1$ styleName + "gadget-provider", //$NON-NLS-1$ xmlName + "Gadget Provider", // uiName + null, // sdk url + xmlns, // extraAttribute + null, // childrenElements + false /* mandatory */ ); + return gadget; + } + + /** + * Returns a new ElementDescriptor constructed from the information given here + * and the javadoc & attributes extracted from the style map if any. + */ + private ElementDescriptor createElement( + Map<String, DeclareStyleableInfo> styleMap, String styleName, + String xmlName, String uiName, String sdkUrl, + AttributeDescriptor extraAttribute, + ElementDescriptor[] childrenElements, boolean mandatory) { + + ElementDescriptor element = new ElementDescriptor(xmlName, uiName, null, sdkUrl, + null, childrenElements, mandatory); + + return updateElement(element, styleMap, styleName, extraAttribute); + } + + /** + * Updates an ElementDescriptor with the javadoc & attributes extracted from the style + * map if any. + */ + private ElementDescriptor updateElement(ElementDescriptor element, + Map<String, DeclareStyleableInfo> styleMap, + String styleName, + AttributeDescriptor extraAttribute) { + ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>(); + + DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null; + if (style != null) { + DescriptorsUtils.appendAttributes(descs, + null, // elementName + SdkConstants.NS_RESOURCES, + style.getAttributes(), + null, // requiredAttributes + null); // overrides + element.setTooltip(style.getJavaDoc()); + } + + if (extraAttribute != null) { + descs.add(extraAttribute); + } + + element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()])); + return element; + } + + //-------------------------- + // Creation of <Preferences> + //-------------------------- + + /** + * Returns the new ElementDescriptor for <Preferences> + */ + private ElementDescriptor createPreference(ViewClassInfo[] prefs, + ViewClassInfo[] prefGroups, XmlnsAttributeDescriptor xmlns) { + + ArrayList<ElementDescriptor> newPrefs = new ArrayList<ElementDescriptor>(); + if (prefs != null) { + for (ViewClassInfo info : prefs) { + ElementDescriptor desc = convertPref(info); + newPrefs.add(desc); + } + } + + ElementDescriptor topPreferences = null; + + ArrayList<ElementDescriptor> newGroups = new ArrayList<ElementDescriptor>(); + if (prefGroups != null) { + for (ViewClassInfo info : prefGroups) { + ElementDescriptor desc = convertPref(info); + newGroups.add(desc); + + if (info.getCanonicalClassName() == AndroidConstants.CLASS_PREFERENCES) { + topPreferences = desc; + } + } + } + + ArrayList<ElementDescriptor> everything = new ArrayList<ElementDescriptor>(); + everything.addAll(newGroups); + everything.addAll(newPrefs); + ElementDescriptor[] newArray = everything.toArray(new ElementDescriptor[everything.size()]); + + // Link all groups to everything else here.. recursively + for (ElementDescriptor layoutDesc : newGroups) { + layoutDesc.setChildren(newArray); + } + + // The "top" element to be returned corresponds to the class "Preferences". + // Its descriptor has already been created. However the root one also needs + // the hidden xmlns:android definition.. + if (topPreferences != null) { + AttributeDescriptor[] attrs = topPreferences.getAttributes(); + AttributeDescriptor[] newAttrs = new AttributeDescriptor[attrs.length + 1]; + System.arraycopy(attrs, 0, newAttrs, 0, attrs.length); + newAttrs[attrs.length] = xmlns; + return new ElementDescriptor( + topPreferences.getXmlName(), + topPreferences.getUiName(), + topPreferences.getTooltip(), + topPreferences.getSdkUrl(), + newAttrs, + topPreferences.getChildren(), + false /* mandatory */); + } else { + return null; + } + } + + /** + * Creates an element descriptor from a given {@link ViewClassInfo}. + */ + private ElementDescriptor convertPref(ViewClassInfo info) { + String xml_name = info.getShortClassName(); + String tooltip = info.getJavaDoc(); + + // Process all Preference attributes + ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>(); + DescriptorsUtils.appendAttributes(attributes, + null, // elementName + SdkConstants.NS_RESOURCES, + info.getAttributes(), + null, // requiredAttributes + null); // overrides + + for (ViewClassInfo link = info.getSuperClass(); + link != null; + link = link.getSuperClass()) { + AttributeInfo[] attrList = link.getAttributes(); + if (attrList.length > 0) { + attributes.add(new SeparatorAttributeDescriptor( + String.format("Attributes from %1$s", link.getShortClassName()))); + DescriptorsUtils.appendAttributes(attributes, + null, // elementName + SdkConstants.NS_RESOURCES, + attrList, + null, // requiredAttributes + null); // overrides + } + } + + return new ViewElementDescriptor(xml_name, + xml_name, // ui_name + info.getCanonicalClassName(), + tooltip, + null, // sdk_url + attributes.toArray(new AttributeDescriptor[attributes.size()]), + null, + null, // children + false /* mandatory */); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template new file mode 100644 index 0000000..b43e75f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="PACKAGE" + android:versionCode="1" + android:versionName="1.0"> + <application android:icon="@drawable/icon" android:label="APPLICATION_NAME"> +ACTIVITIES + </application> +USES-SDK +</manifest> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template new file mode 100644 index 0000000..e91d602 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template @@ -0,0 +1,7 @@ + <activity android:name=".ACTIVITY_NAME" + android:label="APPLICATION_NAME"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> +INTENT_FILTERS + </intent-filter> + </activity>
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.png Binary files differnew file mode 100644 index 0000000..7502484 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template new file mode 100644 index 0000000..173ff96 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template @@ -0,0 +1,13 @@ +package PACKAGE; + +import android.app.Activity; +import android.os.Bundle; + +public class ACTIVITY_NAME extends Activity { + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template new file mode 100644 index 0000000..f5681be --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template @@ -0,0 +1 @@ + <category android:name="android.intent.category.LAUNCHER" />
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template new file mode 100644 index 0000000..5f4c383 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + > +<TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="@string/hello" + /> +</LinearLayout> + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template new file mode 100644 index 0000000..609bb2c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template @@ -0,0 +1 @@ + <category android:name="android.intent.category.PREFERENCE" />
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template new file mode 100644 index 0000000..3a6e4b2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template @@ -0,0 +1 @@ + <string name="STRING_NAME">STRING_CONTENT</string>
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template new file mode 100644 index 0000000..b980ace --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +STRINGS +</resources> + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template new file mode 100644 index 0000000..8adae71 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template @@ -0,0 +1 @@ + <uses-sdk android:minSdkVersion="MIN_SDK_VERSION" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/.classpath b/eclipse/plugins/com.android.ide.eclipse.ddms/.classpath new file mode 100644 index 0000000..280621c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/.classpath @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="lib" path="libs/jfreechart-1.0.9.jar"/> + <classpathentry kind="lib" path="libs/jcommon-1.0.12.jar"/> + <classpathentry kind="lib" path="libs/jfreechart-1.0.9-swt.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/.project b/eclipse/plugins/com.android.ide.eclipse.ddms/.project new file mode 100644 index 0000000..2e9f996 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/.project @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddms-plugin</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF new file mode 100644 index 0000000..09b8085 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF @@ -0,0 +1,23 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Dalvik Debug Monitor Service +Bundle-SymbolicName: com.android.ide.eclipse.ddms;singleton:=true +Bundle-Version: 0.9.0.qualifier +Bundle-Activator: com.android.ide.eclipse.ddms.DdmsPlugin +Bundle-Vendor: The Android Open Source Project +Bundle-Localization: plugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.ui.console +Eclipse-LazyStart: true +Export-Package: com.android.ddmlib, + com.android.ddmlib.log, + com.android.ddmlib.testrunner, + com.android.ddmuilib, + com.android.ddmuilib.console, + com.android.ide.eclipse.ddms, + com.android.ide.eclipse.ddms.views +Bundle-ClassPath: libs/jcommon-1.0.12.jar, + libs/jfreechart-1.0.9.jar, + libs/jfreechart-1.0.9-swt.jar, + . diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2 b/eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2 diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/build.properties b/eclipse/plugins/com.android.ide.eclipse.ddms/build.properties new file mode 100644 index 0000000..1c4c896 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/build.properties @@ -0,0 +1,10 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + icons/,\ + plugin.xml,\ + ., \ + libs/jcommon-1.0.12.jar,\ + libs/jfreechart-1.0.9-swt.jar,\ + libs/jfreechart-1.0.9.jar + diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png Binary files differnew file mode 100644 index 0000000..3779d4d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png Binary files differnew file mode 100644 index 0000000..d75e7a9 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml new file mode 100644 index 0000000..27fadf2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.2"?> +<plugin> + <extension + point="org.eclipse.ui.views"> + <category + name="Android" + id="com.android.ide.eclipse.ddms.views.category"> + </category> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.DeviceView" + icon="icons/device.png" + id="com.android.ide.eclipse.ddms.views.DeviceView" + name="Devices"> + </view> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.LogCatView" + icon="icons/android.png" + id="com.android.ide.eclipse.ddms.views.LogCatView" + name="LogCat"/> + <!-- Disabled for now due to AWT/SWT bridge issue on Leopard. + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.EventLogView" + icon="icons/android.png" + id="com.android.ide.eclipse.ddms.views.EventLogView" + name="Event Log"/> --> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.ThreadView" + icon="icons/thread.png" + id="com.android.ide.eclipse.ddms.views.ThreadView" + name="Threads"/> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.HeapView" + icon="icons/heap.png" + id="com.android.ide.eclipse.ddms.views.HeapView" + name="Heap"/> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.FileExplorerView" + icon="icons/android.png" + id="com.android.ide.eclipse.ddms.views.FileExplorerView" + name="File Explorer"/> + <view + allowMultiple="false" + category="com.android.ide.eclipse.ddms.views.category" + class="com.android.ide.eclipse.ddms.views.EmulatorControlView" + icon="icons/emulator.png" + id="com.android.ide.eclipse.ddms.views.EmulatorControlView" + name="Emulator Control"/> + </extension> + <extension + point="org.eclipse.ui.perspectives"> + <perspective + class="com.android.ide.eclipse.ddms.Perspective" + icon="icons/android.png" + id="com.android.ide.eclipse.ddms.Perspective" + name="DDMS"/> + </extension> + <extension + point="org.eclipse.core.runtime.preferences"> + <initializer class="com.android.ide.eclipse.ddms.preferences.PreferenceInitializer"/> + </extension> + <extension + point="org.eclipse.ui.perspectiveExtensions"> + <perspectiveExtension targetID="org.eclipse.jdt.ui.JavaPerspective"> + <perspectiveShortcut id="com.android.ide.eclipse.ddms.Perspective"/> + </perspectiveExtension> + <perspectiveExtension targetID="org.eclipse.ui.resourcePerspective"> + <perspectiveShortcut id="com.android.ide.eclipse.ddms.Perspective"/> + </perspectiveExtension> + <perspectiveExtension targetID="org.eclipse.debug.ui.DebugPerspective"> + <perspectiveShortcut id="com.android.ide.eclipse.ddms.Perspective"/> + <view id="com.android.ide.eclipse.ddms.views.LogCatView" + relative="org.eclipse.ui.views.ProblemView" + relationship="stack" /> + </perspectiveExtension> + </extension> + <extension + point="org.eclipse.ui.preferencePages"> + <page + category="com.android.ide.eclipse.preferences.main" + class="com.android.ide.eclipse.ddms.preferences.PreferencePage" + id="com.android.ide.eclipse.ddms.preferences.PreferencePage" + name="DDMS"/> + <page + category="com.android.ide.eclipse.preferences.main" + class="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage" + id="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage" + name="LogCat"/> + </extension> +</plugin> diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java new file mode 100644 index 0000000..30ca4cb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.ddms; + +import com.android.ddmuilib.actions.ICommonAction; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; + +/** + * Basic action extending the jFace Action class in order to implement + * ICommonAction. + */ +public class CommonAction extends Action implements ICommonAction { + + private Runnable mRunnable; + + public CommonAction() { + super(); + } + + public CommonAction(String text) { + super(text); + } + + /** + * @param text + * @param image + */ + public CommonAction(String text, ImageDescriptor image) { + super(text, image); + } + + /** + * @param text + * @param style + */ + public CommonAction(String text, int style) { + super(text, style); + } + + @Override + public void run() { + if (mRunnable != null) { + mRunnable.run(); + } + } + + /** + * Sets the {@link Runnable}. + * @see ICommonAction#setRunnable(Runnable) + */ + public void setRunnable(Runnable runnable) { + mRunnable = runnable; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java new file mode 100644 index 0000000..ccadce6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.Device; +import com.android.ddmlib.Log; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Log.ILogOutput; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ide.eclipse.ddms.preferences.PreferenceInitializer; +import com.android.ide.eclipse.ddms.views.DeviceView; + +import org.eclipse.core.runtime.Preferences; +import org.eclipse.core.runtime.Preferences.IPropertyChangeListener; +import org.eclipse.core.runtime.Preferences.PropertyChangeEvent; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +import java.util.ArrayList; +import java.util.Calendar; + +/** + * The activator class controls the plug-in life cycle + */ +public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener, + IUiSelectionListener { + + // The plug-in ID + public static final String PLUGIN_ID = "com.android.ide.eclipse.ddms"; // $NON-NLS-1$ + + private static final String ADB_LOCATION = PLUGIN_ID + ".adb"; // $NON-NLS-1$ + + /** The singleton instance */ + private static DdmsPlugin sPlugin; + + /** Location of the adb command line executable */ + private static String sAdbLocation; + + /** + * Debug Launcher for already running apps + */ + private static IDebugLauncher sRunningAppDebugLauncher; + + /** Console for DDMS log message */ + private MessageConsole mDdmsConsole; + + /** Image loader object */ + private ImageLoader mLoader; + + private Device mCurrentDevice; + private Client mCurrentClient; + private boolean mListeningToUiSelection = false; + + private final ArrayList<ISelectionListener> mListeners = new ArrayList<ISelectionListener>(); + + private Color mRed; + + private boolean mDdmlibInitialized; + + /** + * Interface to provide debugger launcher for running apps. + */ + public interface IDebugLauncher { + public boolean debug(String packageName, int port); + } + + /** + * Classes which implement this interface provide methods that deals + * with {@link Device} and {@link Client} selectionchanges. + */ + public interface ISelectionListener { + + /** + * Sent when a new {@link Client} is selected. + * @param selectedClient The selected client. If null, no clients are selected. + */ + public void selectionChanged(Client selectedClient); + + /** + * Sent when a new {@link Device} is selected. + * @param selectedDevice the selected device. If null, no devices are selected. + */ + public void selectionChanged(Device selectedDevice); + } + + /** + * The constructor + */ + public DdmsPlugin() { + sPlugin = this; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + final Display display = getDisplay(); + + // get the eclipse store + final IPreferenceStore eclipseStore = getPreferenceStore(); + + AndroidDebugBridge.addDeviceChangeListener(this); + + DdmUiPreferences.setStore(eclipseStore); + + //DdmUiPreferences.displayCharts(); + + // set the consoles. + mDdmsConsole = new MessageConsole("DDMS", null); // $NON-NLS-1$ + ConsolePlugin.getDefault().getConsoleManager().addConsoles( + new IConsole[] { + mDdmsConsole + }); + + final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream(); + final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream(); + mRed = new Color(display, 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non UI thread, and because + // changing the console properties update the UI, we need to make this change + // in the UI thread. + display.asyncExec(new Runnable() { + public void run() { + errorConsoleStream.setColor(mRed); + } + }); + + // set up the ddms log to use the ddms console. + Log.setLogOutput(new ILogOutput() { + public void printLog(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { + printToStream(errorConsoleStream, tag, message); + ConsolePlugin.getDefault().getConsoleManager().showConsoleView(mDdmsConsole); + } else { + printToStream(consoleStream, tag, message); + } + } + + public void printAndPromptLog(final LogLevel logLevel, final String tag, + final String message) { + printLog(logLevel, tag, message); + // dialog box only run in UI thread.. + display.asyncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + if (logLevel == LogLevel.ERROR) { + MessageDialog.openError(shell, tag, message); + } else { + MessageDialog.openWarning(shell, tag, message); + } + } + }); + } + + }); + + // create the loader that's able to load the images + mLoader = new ImageLoader(this); + + // set the listener for the preference change + Preferences prefs = getPluginPreferences(); + prefs.addPropertyChangeListener(new IPropertyChangeListener() { + public void propertyChange(PropertyChangeEvent event) { + // get the name of the property that changed. + String property = event.getProperty(); + + if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) { + DdmPreferences.setDebugPortBase( + eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE)); + } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) { + DdmPreferences.setSelectedDebugPort( + eclipseStore.getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT)); + } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) { + DdmUiPreferences.setThreadRefreshInterval( + eclipseStore.getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL)); + } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) { + DdmPreferences.setLogLevel( + eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL)); + } + } + }); + + // read the adb location from the prefs to attempt to start it properly without + // having to wait for ADT to start + sAdbLocation = eclipseStore.getString(ADB_LOCATION); + + // start it in a thread to return from start() asap. + new Thread() { + @Override + public void run() { + // init ddmlib if needed + getDefault().initDdmlib(); + + // create and start the first bridge + AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */); + } + }.start(); + } + + public static Display getDisplay() { + IWorkbench bench = sPlugin.getWorkbench(); + if (bench != null) { + return bench.getDisplay(); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + @Override + public void stop(BundleContext context) throws Exception { + AndroidDebugBridge.removeDeviceChangeListener(this); + + AndroidDebugBridge.terminate(); + + mRed.dispose(); + + sPlugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static DdmsPlugin getDefault() { + return sPlugin; + } + + /** Return the image loader for the plugin */ + public static ImageLoader getImageLoader() { + if (sPlugin != null) { + return sPlugin.mLoader; + } + return null; + } + + public static String getAdb() { + return sAdbLocation; + } + + /** + * Set the location of the adb executable and optionally starts adb + * @param adb location of adb + * @param startAdb flag to start adb + */ + public static void setAdb(String adb, boolean startAdb) { + sAdbLocation = adb; + + // store the location for future ddms only start. + sPlugin.getPreferenceStore().setValue(ADB_LOCATION, sAdbLocation); + + // starts the server in a thread in case this is blocking. + if (startAdb) { + new Thread() { + @Override + public void run() { + // init ddmlib if needed + getDefault().initDdmlib(); + + // create and start the bridge + AndroidDebugBridge.createBridge(sAdbLocation, false /* forceNewBridge */); + } + }.start(); + } + } + + private synchronized void initDdmlib() { + if (mDdmlibInitialized == false) { + // set the preferences. + PreferenceInitializer.setupPreferences(); + + // init the lib + AndroidDebugBridge.init(true /* debugger support */); + + mDdmlibInitialized = true; + } + } + + /** + * Sets the launcher responsible for connecting the debugger to running applications. + * @param launcher The launcher. + */ + public static void setRunningAppDebugLauncher(IDebugLauncher launcher) { + sRunningAppDebugLauncher = launcher; + + // if the process view is already running, give it the launcher. + // This method could be called from a non ui thread, so we make sure to do that + // in the ui thread. + Display display = getDisplay(); + if (display != null && display.isDisposed() == false) { + display.asyncExec(new Runnable() { + public void run() { + DeviceView dv = DeviceView.getInstance(); + if (dv != null) { + dv.setDebugLauncher(sRunningAppDebugLauncher); + } + } + }); + } + } + + public static IDebugLauncher getRunningAppDebugLauncher() { + return sRunningAppDebugLauncher; + } + + public synchronized void addSelectionListener(ISelectionListener listener) { + mListeners.add(listener); + + // notify the new listener of the current selection + listener.selectionChanged(mCurrentDevice); + listener.selectionChanged(mCurrentClient); + } + + public synchronized void removeSelectionListener(ISelectionListener listener) { + mListeners.remove(listener); + } + + public synchronized void setListeningState(boolean state) { + mListeningToUiSelection = state; + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(Device) + */ + public void deviceConnected(Device device) { + // if we are listening to selection coming from the ui, then we do nothing, as + // any change in the devices/clients, will be handled by the UI, and we'll receive + // selection notification through our implementation of IUiSelectionListener. + if (mListeningToUiSelection == false) { + if (mCurrentDevice == null) { + handleDefaultSelection(device); + } + } + } + + /** + * Sent when the a device is disconnected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(Device) + */ + public void deviceDisconnected(Device device) { + // if we are listening to selection coming from the ui, then we do nothing, as + // any change in the devices/clients, will be handled by the UI, and we'll receive + // selection notification through our implementation of IUiSelectionListener. + if (mListeningToUiSelection == false) { + // test if the disconnected device was the default selection. + if (mCurrentDevice == device) { + // try to find a new device + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + // get the device list + Device[] devices = bridge.getDevices(); + + // check if we still have devices + if (devices.length == 0) { + handleDefaultSelection((Device)null); + } else { + handleDefaultSelection(devices[0]); + } + } else { + handleDefaultSelection((Device)null); + } + } + } + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(Device) + */ + public void deviceChanged(Device device, int changeMask) { + // if we are listening to selection coming from the ui, then we do nothing, as + // any change in the devices/clients, will be handled by the UI, and we'll receive + // selection notification through our implementation of IUiSelectionListener. + if (mListeningToUiSelection == false) { + + // check if this is our device + if (device == mCurrentDevice) { + if (mCurrentClient == null) { + handleDefaultSelection(device); + } else { + // get the clients and make sure ours is still in there. + Client[] clients = device.getClients(); + boolean foundClient = false; + for (Client client : clients) { + if (client == mCurrentClient) { + foundClient = true; + break; + } + } + + // if we haven't found our client, lets look for a new one + if (foundClient == false) { + mCurrentClient = null; + handleDefaultSelection(device); + } + } + } + } + } + + /** + * Sent when a new {@link Device} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + */ + public synchronized void selectionChanged(Device selectedDevice, Client selectedClient) { + if (mCurrentDevice != selectedDevice) { + mCurrentDevice = selectedDevice; + + // notify of the new default device + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentDevice); + } + } + + if (mCurrentClient != selectedClient) { + mCurrentClient = selectedClient; + + // notify of the new default client + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentClient); + } + } + } + + /** + * Handles a default selection of a {@link Device} and {@link Client}. + * @param device the selected device + */ + private void handleDefaultSelection(final Device device) { + // because the listener expect to receive this from the UI thread, and this is called + // from the AndroidDebugBridge notifications, we need to run this in the UI thread. + try { + Display display = getDisplay(); + + display.asyncExec(new Runnable() { + public void run() { + // set the new device if different. + boolean newDevice = false; + if (mCurrentDevice != device) { + mCurrentDevice = device; + newDevice = true; + + // notify of the new default device + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentDevice); + } + } + + if (device != null) { + // if this is a device switch or the same device but we didn't find a valid + // client the last time, we go look for a client to use again. + if (newDevice || mCurrentClient == null) { + // now get the new client + Client[] clients = device.getClients(); + if (clients.length > 0) { + handleDefaultSelection(clients[0]); + } else { + handleDefaultSelection((Client)null); + } + } + } else { + handleDefaultSelection((Client)null); + } + } + }); + } catch (SWTException e) { + // display is disposed. Do nothing since we're quitting anyway. + } + } + + private void handleDefaultSelection(Client client) { + mCurrentClient = client; + + // notify of the new default client + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentClient); + } + } + + /** + * Prints a message, associated with a project to the specified stream + * @param stream The stream to write to + * @param tag The tag associated to the message. Can be null + * @param message The message to print. + */ + private static synchronized void printToStream(MessageConsoleStream stream, String tag, + String message) { + String dateTag = getMessageTag(tag); + + stream.print(dateTag); + stream.println(message); + } + + /** + * Creates a string containing the current date/time, and the tag + * @param tag The tag associated to the message. Can be null + * @return The dateTag + */ + private static String getMessageTag(String tag) { + Calendar c = Calendar.getInstance(); + + if (tag == null) { + return String.format("[%1$tF %1$tT]", c); + } + + return String.format("[%1$tF %1$tT - %2$s]", c, tag); + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java new file mode 100644 index 0000000..a70405d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.ddms; + +import com.android.ddmuilib.IImageLoader; + +import org.eclipse.core.runtime.Plugin; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Implementation of the IImageLoader interface for the eclipse plugin. + */ +public class ImageLoader implements IImageLoader { + + private URL mBaseUrl; + + public ImageLoader(Plugin plugin) { + mBaseUrl = plugin.getBundle().getEntry("/"); // $NON-NLS-1$ + } + + /** + * default method. only need a filename. the 2 interface methods call this one. + * @param filename the filename of the image to load. The filename is searched for under /icons. + * @return + */ + public ImageDescriptor loadDescriptor(String filename) { + try { + URL newUrl = new URL(mBaseUrl, "/icons/" + filename); // $NON-NLS-1$ + return ImageDescriptor.createFromURL(newUrl); + } catch (MalformedURLException e) { + // we'll just return null; + } + return null; + } + + public ImageDescriptor loadDescriptor(String filename, Display display) { + return loadDescriptor(filename); + } + + + public Image loadImage(String filename, Display display) { + ImageDescriptor descriptor = loadDescriptor(filename); + if (descriptor !=null) { + return descriptor.createImage(); + } + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java new file mode 100644 index 0000000..4c01e9b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.ddms; + +import com.android.ide.eclipse.ddms.views.DeviceView; +import com.android.ide.eclipse.ddms.views.EmulatorControlView; +import com.android.ide.eclipse.ddms.views.FileExplorerView; +import com.android.ide.eclipse.ddms.views.HeapView; +import com.android.ide.eclipse.ddms.views.LogCatView; +import com.android.ide.eclipse.ddms.views.ThreadView; + +import org.eclipse.ui.IFolderLayout; +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IPerspectiveFactory; + +public class Perspective implements IPerspectiveFactory { + + public void createInitialLayout(IPageLayout layout) { + // create a default layout that looks like the stand alone DDMS. + + // no editor window + layout.setEditorAreaVisible(false); + + String editorArea = layout.getEditorArea(); + IFolderLayout folder; + + folder = layout.createFolder("logcat", IPageLayout.BOTTOM, 0.8f, //$NON-NLS-1$ + editorArea); + folder.addPlaceholder(LogCatView.ID + ":*"); //$NON-NLS-1$ + folder.addView(LogCatView.ID); + + folder = layout.createFolder("devices", IPageLayout.LEFT, 0.3f, //$NON-NLS-1$ + editorArea); + folder.addPlaceholder(DeviceView.ID + ":*"); //$NON-NLS-1$ + folder.addView(DeviceView.ID); + + folder = layout.createFolder("emulator", IPageLayout.BOTTOM, 0.5f, //$NON-NLS-1$ + "devices"); + folder.addPlaceholder(EmulatorControlView.ID + ":*"); //$NON-NLS-1$ + folder.addView(EmulatorControlView.ID); + + folder = layout.createFolder("ddms-detail", IPageLayout.RIGHT, 0.5f, //$NON-NLS-1$ + editorArea); + folder.addPlaceholder(ThreadView.ID + ":*"); //$NON-NLS-1$ + folder.addView(ThreadView.ID); + folder.addView(HeapView.ID); + folder.addView(FileExplorerView.ID); + + layout.addPerspectiveShortcut("org.eclipse.ui.resourcePerspective"); //$NON-NLS-1$ + layout.addPerspectiveShortcut("org.eclipse.debug.ui.DebugPerspective"); //$NON-NLS-1$ + layout.addPerspectiveShortcut("org.eclipse.jdt.ui.JavaPerspective"); //$NON-NLS-1$ + + layout.addShowViewShortcut(DeviceView.ID); + layout.addShowViewShortcut(FileExplorerView.ID); + layout.addShowViewShortcut(HeapView.ID); + layout.addShowViewShortcut(LogCatView.ID); + layout.addShowViewShortcut(ThreadView.ID); + + layout.addShowViewShortcut(IPageLayout.ID_RES_NAV); + layout.addShowViewShortcut(IPageLayout.ID_BOOKMARKS); + layout.addShowViewShortcut(IPageLayout.ID_OUTLINE); + layout.addShowViewShortcut(IPageLayout.ID_PROP_SHEET); + layout.addShowViewShortcut(IPageLayout.ID_PROBLEM_VIEW); + layout.addShowViewShortcut(IPageLayout.ID_PROGRESS_VIEW); + layout.addShowViewShortcut(IPageLayout.ID_TASK_LIST); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java new file mode 100644 index 0000000..909207d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.preferences; + +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.views.LogCatView; + +import org.eclipse.core.runtime.Preferences; +import org.eclipse.core.runtime.Preferences.IPropertyChangeListener; +import org.eclipse.core.runtime.Preferences.PropertyChangeEvent; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.FontFieldEditor; +import org.eclipse.swt.SWTError; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +/** + * Preference Pane for LogCat. + */ +public class LogCatPreferencePage extends FieldEditorPreferencePage implements + IWorkbenchPreferencePage { + + public LogCatPreferencePage() { + super(GRID); + setPreferenceStore(DdmsPlugin.getDefault().getPreferenceStore()); + } + + @Override + protected void createFieldEditors() { + FontFieldEditor ffe = new FontFieldEditor(PreferenceInitializer.ATTR_LOGCAT_FONT, + "Display Font:", getFieldEditorParent()); + addField(ffe); + + Preferences prefs = DdmsPlugin.getDefault().getPluginPreferences(); + prefs.addPropertyChangeListener(new IPropertyChangeListener() { + public void propertyChange(PropertyChangeEvent event) { + // get the name of the property that changed. + String property = event.getProperty(); + + if (PreferenceInitializer.ATTR_LOGCAT_FONT.equals(property)) { + try { + FontData fdat = new FontData((String)event.getNewValue()); + LogCatView.setFont(new Font(getFieldEditorParent().getDisplay(), fdat)); + } catch (IllegalArgumentException e) { + // Looks like the data from the store is not valid. + // We do nothing (default font will be used). + } catch (SWTError e2) { + // Looks like the Font() constructor failed. + // We do nothing in this case, the logcat view will use the default font. + } + } + } + }); + } + + public void init(IWorkbench workbench) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java new file mode 100644 index 0000000..b53d85c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.preferences; + +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmuilib.DdmUiPreferences; + +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.FontData; + +/** + * Class used to initialize default preference values. + */ +public class PreferenceInitializer extends AbstractPreferenceInitializer { + + public final static String ATTR_LOG_LEVEL = + DdmsPlugin.PLUGIN_ID + ".logLevel"; //$NON-NLS-1$ + + public final static String ATTR_DEBUG_PORT_BASE = + DdmsPlugin.PLUGIN_ID + ".adbDebugBasePort"; //$NON-NLS-1$ + + public final static String ATTR_SELECTED_DEBUG_PORT = + DdmsPlugin.PLUGIN_ID + ".debugSelectedPort"; //$NON-NLS-1$ + + public final static String ATTR_DEFAULT_THREAD_UPDATE = + DdmsPlugin.PLUGIN_ID + ".defaultThreadUpdateEnabled"; //$NON-NLS-1$ + + public final static String ATTR_DEFAULT_HEAP_UPDATE = + DdmsPlugin.PLUGIN_ID + ".defaultHeapUpdateEnabled"; //$NON-NLS-1$ + + public final static String ATTR_THREAD_INTERVAL = + DdmsPlugin.PLUGIN_ID + ".threadStatusInterval"; //$NON-NLS-1$ + + public final static String ATTR_IMAGE_SAVE_DIR = + DdmsPlugin.PLUGIN_ID + ".imageSaveDir"; //$NON-NLS-1$ + + public final static String ATTR_LAST_IMAGE_SAVE_DIR = + DdmsPlugin.PLUGIN_ID + ".lastImageSaveDir"; //$NON-NLS-1$ + + public final static String ATTR_LOGCAT_FONT = + DdmsPlugin.PLUGIN_ID + ".logcatFont"; //$NON-NLS-1$ + + /* + * (non-Javadoc) + * + * @see org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer + * #initializeDefaultPreferences() + */ + @Override + public void initializeDefaultPreferences() { + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + + store.setDefault(ATTR_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE); + + store.setDefault(ATTR_SELECTED_DEBUG_PORT, DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT); + + store.setDefault(ATTR_DEFAULT_THREAD_UPDATE, DdmPreferences.DEFAULT_INITIAL_THREAD_UPDATE); + store.setDefault(ATTR_DEFAULT_HEAP_UPDATE, + DdmPreferences.DEFAULT_INITIAL_HEAP_UPDATE); + + store.setDefault(ATTR_THREAD_INTERVAL, DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL); + + String homeDir = System.getProperty("user.home"); //$NON-NLS-1$ + store.setDefault(ATTR_IMAGE_SAVE_DIR, homeDir); + + store.setDefault(ATTR_LOG_LEVEL, DdmPreferences.DEFAULT_LOG_LEVEL.getStringValue()); + + store.setDefault(ATTR_LOGCAT_FONT, + new FontData("Courier", 10, SWT.NORMAL).toString()); //$NON-NLS-1$ + } + + /** + * Initializes the preferences of ddmlib and ddmuilib with values from the eclipse store. + */ + public synchronized static void setupPreferences() { + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + + DdmPreferences.setDebugPortBase(store.getInt(ATTR_DEBUG_PORT_BASE)); + DdmPreferences.setSelectedDebugPort(store.getInt(ATTR_SELECTED_DEBUG_PORT)); + DdmPreferences.setLogLevel(store.getString(ATTR_LOG_LEVEL)); + DdmPreferences.setInitialThreadUpdate(store.getBoolean(ATTR_DEFAULT_THREAD_UPDATE)); + DdmPreferences.setInitialHeapUpdate(store.getBoolean(ATTR_DEFAULT_HEAP_UPDATE)); + DdmUiPreferences.setThreadRefreshInterval(store.getInt(ATTR_THREAD_INTERVAL)); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java new file mode 100644 index 0000000..86e87c7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.preferences; + +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.PortFieldEditor; + +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.jface.preference.RadioGroupFieldEditor; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +public class PreferencePage extends FieldEditorPreferencePage implements + IWorkbenchPreferencePage { + + public PreferencePage() { + super(GRID); + setPreferenceStore(DdmsPlugin.getDefault().getPreferenceStore()); + } + + /** + * Creates the field editors. Field editors are abstractions of the common + * GUI blocks needed to manipulate various types of preferences. Each field + * editor knows how to save and restore itself. + */ + @Override + public void createFieldEditors() { + IntegerFieldEditor ife; + + ife = new PortFieldEditor(PreferenceInitializer.ATTR_DEBUG_PORT_BASE, + "ADB debugger base port:", getFieldEditorParent()); + addField(ife); + + BooleanFieldEditor bfe; + + bfe = new BooleanFieldEditor(PreferenceInitializer.ATTR_DEFAULT_THREAD_UPDATE, + "Thread updates enabled by default", getFieldEditorParent()); + addField(bfe); + + bfe = new BooleanFieldEditor(PreferenceInitializer.ATTR_DEFAULT_HEAP_UPDATE, + "Heap updates enabled by default", getFieldEditorParent()); + addField(bfe); + + ife = new IntegerFieldEditor(PreferenceInitializer.ATTR_THREAD_INTERVAL, + "Thread status refresh interval (seconds):", getFieldEditorParent()); + ife.setValidRange(1, 60); + addField(ife); + + RadioGroupFieldEditor rgfe = new RadioGroupFieldEditor(PreferenceInitializer.ATTR_LOG_LEVEL, + "Logging Level", 1, new String[][] { + { "Verbose", LogLevel.VERBOSE.getStringValue() }, + { "Debug", LogLevel.DEBUG.getStringValue() }, + { "Info", LogLevel.INFO.getStringValue() }, + { "Warning", LogLevel.WARN.getStringValue() }, + { "Error", LogLevel.ERROR.getStringValue() }, + { "Assert", LogLevel.ASSERT.getStringValue() } + }, + getFieldEditorParent(), true); + addField(rgfe); + + } + + public void init(IWorkbench workbench) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java new file mode 100644 index 0000000..62a528a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Device; +import com.android.ddmuilib.DevicePanel; +import com.android.ddmuilib.ScreenShotDialog; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.DdmsPlugin.IDebugLauncher; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; + +public class DeviceView extends ViewPart implements IUiSelectionListener { + + private final static boolean USE_SELECTED_DEBUG_PORT = true; + + public static final String ID = + "com.android.ide.eclipse.ddms.views.DeviceView"; //$NON-NLS-1$ + + private DevicePanel mDeviceList; + private Action mResetAdbAction; + private Action mCaptureAction; + private Action mUpdateThreadAction; + private Action mUpdateHeapAction; + private Action mGcAction; + private Action mKillAppAction; + private Action mDebugAction; + private IDebugLauncher mDebugLauncher; + + private static DeviceView sThis; + + public DeviceView() { + // the view is declared with allowMultiple="false" so we + // can safely do this. + sThis = this; + } + + public static DeviceView getInstance() { + return sThis; + } + + /** + * Sets the {@link IDebugLauncher}. + * @param debugLauncher + */ + public void setDebugLauncher(DdmsPlugin.IDebugLauncher debugLauncher) { + mDebugLauncher = debugLauncher; + if (mDebugAction != null && mDeviceList != null) { + Client currentClient = mDeviceList.getSelectedClient(); + if (currentClient != null) { + mDebugAction.setEnabled(true); + } + } + } + + @Override + public void createPartControl(Composite parent) { + mDeviceList = new DevicePanel(DdmsPlugin.getImageLoader(), USE_SELECTED_DEBUG_PORT); + mDeviceList.createPanel(parent); + mDeviceList.addSelectionListener(this); + + DdmsPlugin plugin = DdmsPlugin.getDefault(); + mDeviceList.addSelectionListener(plugin); + plugin.setListeningState(true); + + mCaptureAction = new Action("Screen Capture") { + @Override + public void run() { + ScreenShotDialog dlg = new ScreenShotDialog( + DdmsPlugin.getDisplay().getActiveShell()); + dlg.open(mDeviceList.getSelectedDevice()); + } + }; + mCaptureAction.setToolTipText("Screen Capture"); + mCaptureAction.setImageDescriptor( + DdmsPlugin.getImageLoader().loadDescriptor("capture.png")); //$NON-NLS-1$ + + mResetAdbAction = new Action("Reset adb") { + @Override + public void run() { + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + if (bridge.restart() == false) { + // get the current Display + final Display display = DdmsPlugin.getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, "Adb Error", + "Adb failed to restart!\n\nMake sure the plugin is properly configured."); + } + }); + } + } + } + }; + mResetAdbAction.setToolTipText("Reset the adb host daemon"); + mResetAdbAction.setImageDescriptor(PlatformUI.getWorkbench() + .getSharedImages().getImageDescriptor( + ISharedImages.IMG_OBJS_WARN_TSK)); + + mKillAppAction = new Action() { + @Override + public void run() { + mDeviceList.killSelectedClient(); + } + }; + + mKillAppAction.setText("Stop Process"); + mKillAppAction.setToolTipText("Stop Process"); + mKillAppAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor(DevicePanel.ICON_HALT)); + + mGcAction = new Action() { + @Override + public void run() { + mDeviceList.forceGcOnSelectedClient(); + } + }; + + mGcAction.setText("Cause GC"); + mGcAction.setToolTipText("Cause GC"); + mGcAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor(DevicePanel.ICON_GC)); + + mUpdateHeapAction = new Action("Update Heap", IAction.AS_CHECK_BOX) { + @Override + public void run() { + boolean enable = mUpdateHeapAction.isChecked(); + mDeviceList.setEnabledHeapOnSelectedClient(enable); + } + }; + mUpdateHeapAction.setToolTipText("Update Heap"); + mUpdateHeapAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor(DevicePanel.ICON_HEAP)); + + mUpdateThreadAction = new Action("Update Threads", IAction.AS_CHECK_BOX) { + @Override + public void run() { + boolean enable = mUpdateThreadAction.isChecked(); + mDeviceList.setEnabledThreadOnSelectedClient(enable); + } + }; + mUpdateThreadAction.setToolTipText("Update Threads"); + mUpdateThreadAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor(DevicePanel.ICON_THREAD)); + + // check if there's already a debug launcher set up in the plugin class + mDebugLauncher = DdmsPlugin.getRunningAppDebugLauncher(); + + mDebugAction = new Action("Debug Process") { + @Override + public void run() { + if (mDebugLauncher != null) { + Client currentClient = mDeviceList.getSelectedClient(); + if (currentClient != null) { + ClientData clientData = currentClient.getClientData(); + + // make sure the client can be debugged + switch (clientData.getDebuggerConnectionStatus()) { + case ClientData.DEBUGGER_ERROR: { + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, "Process Debug", + "The process debug port is already in use!"); + return; + } + case ClientData.DEBUGGER_ATTACHED: { + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, "Process Debug", + "The process is already being debugged!"); + return; + } + } + + // get the name of the client + String packageName = clientData.getClientDescription(); + if (packageName != null) { + if (mDebugLauncher.debug(packageName, + currentClient.getDebuggerListenPort()) == false) { + + // if we get to this point, then we failed to find a project + // that matched the application to debug + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, "Process Debug", + String.format( + "No opened project found for %1$s. Debug session failed!", + packageName)); + } + } + } + } + } + }; + mDebugAction.setToolTipText("Debug the selected process, provided its source project is present and opened in the workspace."); + mDebugAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor("debug-attach.png")); //$NON-NLS-1$ + if (mDebugLauncher == null) { + mDebugAction.setEnabled(false); + } + + placeActions(); + } + + @Override + public void setFocus() { + mDeviceList.setFocus(); + } + + /** + * Sent when a new {@link Device} and {@link Client} are selected. + * @param selectedDevice the selected device. If null, no devices are selected. + * @param selectedClient The selected client. If null, no clients are selected. + */ + public void selectionChanged(Device selectedDevice, Client selectedClient) { + // update the buttons + doSelectionChanged(selectedClient); + doSelectionChanged(selectedDevice); + } + + private void doSelectionChanged(Client selectedClient) { + // update the buttons + if (selectedClient != null) { + if (USE_SELECTED_DEBUG_PORT) { + // set the client as the debug client + selectedClient.setAsSelectedClient(); + } + + mDebugAction.setEnabled(mDebugLauncher != null); + mKillAppAction.setEnabled(true); + mGcAction.setEnabled(true); + + mUpdateHeapAction.setEnabled(true); + mUpdateHeapAction.setChecked(selectedClient.isHeapUpdateEnabled()); + + mUpdateThreadAction.setEnabled(true); + mUpdateThreadAction.setChecked(selectedClient.isThreadUpdateEnabled()); + } else { + if (USE_SELECTED_DEBUG_PORT) { + // set the client as the debug client + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + bridge.setSelectedClient(null); + } + } + + mDebugAction.setEnabled(false); + mKillAppAction.setEnabled(false); + mGcAction.setEnabled(false); + mUpdateHeapAction.setChecked(false); + mUpdateHeapAction.setEnabled(false); + mUpdateThreadAction.setEnabled(false); + mUpdateThreadAction.setChecked(false); + } + } + + private void doSelectionChanged(Device selectedDevice) { + mCaptureAction.setEnabled(selectedDevice != null); + } + + /** + * Place the actions in the ui. + */ + private final void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(mDebugAction); + menuManager.add(new Separator()); + menuManager.add(mUpdateThreadAction); + menuManager.add(mUpdateHeapAction); + menuManager.add(new Separator()); + menuManager.add(mGcAction); + menuManager.add(new Separator()); + menuManager.add(mKillAppAction); + menuManager.add(new Separator()); + menuManager.add(mCaptureAction); + menuManager.add(new Separator()); + menuManager.add(mResetAdbAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.add(mDebugAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mUpdateThreadAction); + toolBarManager.add(mUpdateHeapAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mKillAppAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mCaptureAction); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java new file mode 100644 index 0000000..ca9a691 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmuilib.EmulatorControlPanel; +import com.android.ide.eclipse.ddms.DdmsPlugin; + +import org.eclipse.swt.widgets.Composite; + +public class EmulatorControlView extends SelectionDependentViewPart { + + public static final String ID = + "com.android.ide.eclipse.ddms.views.EmulatorControlView"; //$NON-NLS-1$ + + private EmulatorControlPanel mPanel; + + @Override + public void createPartControl(Composite parent) { + mPanel = new EmulatorControlPanel(DdmsPlugin.getImageLoader()); + mPanel.createPanel(parent); + setSelectionDependentPanel(mPanel); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java new file mode 100644 index 0000000..3a74e42 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmuilib.log.event.EventLogPanel; +import com.android.ide.eclipse.ddms.CommonAction; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.ImageLoader; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; + +public class EventLogView extends SelectionDependentViewPart { + + private EventLogPanel mLogPanel; + + @Override + public void createPartControl(Composite parent) { + ImageLoader loader = DdmsPlugin.getImageLoader(); + + // create the external actions + CommonAction optionsAction = new CommonAction("Options..."); + optionsAction.setToolTipText("Opens the options panel"); + optionsAction.setImageDescriptor(loader + .loadDescriptor("edit.png")); // $NON-NLS-1$ + + CommonAction clearLogAction = new CommonAction("Clear Log"); + clearLogAction.setToolTipText("Clears the event log"); + clearLogAction.setImageDescriptor(loader + .loadDescriptor("clear.png")); // $NON-NLS-1$ + + CommonAction saveAction = new CommonAction("Save Log"); + saveAction.setToolTipText("Saves the event log"); + saveAction.setImageDescriptor(loader + .loadDescriptor("save.png")); // $NON-NLS-1$ + + CommonAction loadAction = new CommonAction("Load Log"); + loadAction.setToolTipText("Loads an event log"); + loadAction.setImageDescriptor(loader + .loadDescriptor("load.png")); // $NON-NLS-1$ + + CommonAction importBugAction = new CommonAction("Import Bug Report Log"); + importBugAction.setToolTipText("Imports a bug report."); + importBugAction.setImageDescriptor(loader + .loadDescriptor("importBug.png")); // $NON-NLS-1$ + + placeActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); + + mLogPanel = new EventLogPanel(DdmsPlugin.getImageLoader()); + mLogPanel.setActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); + mLogPanel.createPanel(parent); + setSelectionDependentPanel(mLogPanel); + } + + @Override + public void setFocus() { + mLogPanel.setFocus(); + } + + @Override + public void dispose() { + if (mLogPanel != null) { + mLogPanel.stopEventLog(true); + } + } + + /** + * Places the actions in the toolbar and in the menu. + * @param importBugAction + */ + private void placeActions(IAction optionAction, IAction clearAction, IAction saveAction, + IAction loadAction, CommonAction importBugAction) { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(clearAction); + menuManager.add(new Separator()); + menuManager.add(saveAction); + menuManager.add(loadAction); + menuManager.add(importBugAction); + menuManager.add(new Separator()); + menuManager.add(optionAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.add(clearAction); + toolBarManager.add(new Separator()); + toolBarManager.add(saveAction); + toolBarManager.add(loadAction); + toolBarManager.add(importBugAction); + toolBarManager.add(new Separator()); + toolBarManager.add(optionAction); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java new file mode 100644 index 0000000..4f0dd2e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; +import com.android.ddmuilib.explorer.DeviceExplorer; +import com.android.ide.eclipse.ddms.CommonAction; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.DdmsPlugin.ISelectionListener; + +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; + +public class FileExplorerView extends ViewPart implements ISelectionListener { + + public static final String ID = + "com.android.ide.eclipse.ddms.views.FileExplorerView"; //$NON-NLS-1$ + + private final static String COLUMN_NAME = + DdmsPlugin.PLUGIN_ID + ".explorer.name"; //$NON-NLS-1S + private final static String COLUMN_SIZE = + DdmsPlugin.PLUGIN_ID + ".explorer.size"; //$NON-NLS-1S + private final static String COLUMN_DATE = + DdmsPlugin.PLUGIN_ID + ".explorer.data"; //$NON-NLS-1S + private final static String COLUMN_TIME = + DdmsPlugin.PLUGIN_ID + ".explorer.time"; //$NON-NLS-1S + private final static String COLUMN_PERMISSIONS = + DdmsPlugin.PLUGIN_ID +".explorer.permissions"; //$NON-NLS-1S + private final static String COLUMN_INFO = + DdmsPlugin.PLUGIN_ID + ".explorer.info"; //$NON-NLS-1$ + + private DeviceExplorer mExplorer; + + public FileExplorerView() { + } + + @Override + public void createPartControl(Composite parent) { + DeviceExplorer.COLUMN_NAME = COLUMN_NAME; + DeviceExplorer.COLUMN_SIZE = COLUMN_SIZE; + DeviceExplorer.COLUMN_DATE = COLUMN_DATE; + DeviceExplorer.COLUMN_TIME = COLUMN_TIME; + DeviceExplorer.COLUMN_PERMISSIONS = COLUMN_PERMISSIONS; + DeviceExplorer.COLUMN_INFO = COLUMN_INFO; + + // device explorer + mExplorer = new DeviceExplorer(); + + + mExplorer.setImages(PlatformUI.getWorkbench() + .getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE), + PlatformUI.getWorkbench() .getSharedImages().getImage( + ISharedImages.IMG_OBJ_FOLDER), + DdmsPlugin.getImageLoader().loadDescriptor("android.png") //$NON-NLS-1$ + .createImage(), + PlatformUI.getWorkbench() .getSharedImages().getImage( + ISharedImages.IMG_OBJ_ELEMENT)); + + // creates the actions + CommonAction pushAction = new CommonAction("Push File...") { + @Override + public void run() { + mExplorer.pushIntoSelection(); + } + }; + pushAction.setToolTipText("Push a file onto the device"); + pushAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor("push.png")); //$NON-NLS-1$ + pushAction.setEnabled(false); + + CommonAction pullAction = new CommonAction("Pull File...") { + @Override + public void run() { + mExplorer.pullSelection(); + } + }; + pullAction.setToolTipText("Pull a file from the device"); + pullAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor("pull.png")); //$NON-NLS-1$ + pullAction.setEnabled(false); + + CommonAction deleteAction = new CommonAction("Delete") { + @Override + public void run() { + mExplorer.deleteSelection(); + } + }; + deleteAction.setToolTipText("Delete the selection"); + deleteAction.setImageDescriptor(DdmsPlugin.getImageLoader() + .loadDescriptor("delete.png")); //$NON-NLS-1$ + deleteAction.setEnabled(false); + + // set up the actions in the explorer + mExplorer.setActions(pushAction, pullAction, deleteAction); + + // and in the ui + IActionBars actionBars = getViewSite().getActionBars(); + IMenuManager menuManager = actionBars.getMenuManager(); + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + + menuManager.add(pullAction); + menuManager.add(pushAction); + menuManager.add(new Separator()); + menuManager.add(deleteAction); + + toolBarManager.add(pullAction); + toolBarManager.add(pushAction); + toolBarManager.add(new Separator()); + toolBarManager.add(deleteAction); + + mExplorer.createPanel(parent); + + DdmsPlugin.getDefault().addSelectionListener(this); + } + + @Override + public void setFocus() { + mExplorer.setFocus(); + } + + /** + * Sent when a new {@link Client} is selected. + * @param selectedClient The selected client. + */ + public void selectionChanged(Client selectedClient) { + // pass + } + + /** + * Sent when a new {@link Device} is selected. + * @param selectedDevice the selected device. + */ + public void selectionChanged(Device selectedDevice) { + mExplorer.switchDevice(selectedDevice); + } + + /** + * Sent when there is no current selection. + */ + public void selectionRemoved() { + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java new file mode 100644 index 0000000..5745e8e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmuilib.HeapPanel; + +import org.eclipse.swt.widgets.Composite; + +public class HeapView extends TableView { + + public static final String ID = "com.android.ide.eclipse.ddms.views.HeapView"; //$NON-NLS-1$ + private HeapPanel mPanel; + + public HeapView() { + } + + @Override + public void createPartControl(Composite parent) { + mPanel = new HeapPanel(); + mPanel.createPanel(parent); + + setSelectionDependentPanel(mPanel); + + // listen to focus changes for table(s) of the panel. + setupTableFocusListener(mPanel, parent); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java new file mode 100644 index 0000000..d3053f1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ide.eclipse.ddms.CommonAction; +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.ImageLoader; +import com.android.ide.eclipse.ddms.preferences.PreferenceInitializer; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmuilib.logcat.LogColors; +import com.android.ddmuilib.logcat.LogFilter; +import com.android.ddmuilib.logcat.LogPanel; +import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.actions.ActionFactory; + +import java.util.ArrayList; + +/** + * The log cat view displays log output from the current device selection. + * + */ +public final class LogCatView extends SelectionDependentViewPart { + + public static final String ID = + "com.android.ide.eclipse.ddms.views.LogCatView"; // $NON-NLS-1$ + + private static final String PREFS_COL_TIME = + DdmsPlugin.PLUGIN_ID + ".logcat.time"; // $NON-NLS-1$ + private static final String PREFS_COL_LEVEL = + DdmsPlugin.PLUGIN_ID + ".logcat.level"; // $NON-NLS-1$ + private static final String PREFS_COL_PID = + DdmsPlugin.PLUGIN_ID + ".logcat.pid"; // $NON-NLS-1$ + private static final String PREFS_COL_TAG = + DdmsPlugin.PLUGIN_ID + ".logcat.tag"; // $NON-NLS-1$ + private static final String PREFS_COL_MESSAGE = + DdmsPlugin.PLUGIN_ID + ".logcat.message"; // $NON-NLS-1$ + + private static final String PREFS_FILTERS = + DdmsPlugin.PLUGIN_ID + ".logcat.filters"; // $NON-NLS-1$ + + private static LogCatView sThis; + private LogPanel mLogPanel; + + private CommonAction mCreateFilterAction; + private CommonAction mDeleteFilterAction; + private CommonAction mEditFilterAction; + private CommonAction mExportAction; + + private CommonAction[] mLogLevelActions; + private String[] mLogLevelIcons = { + "v.png", //$NON-NLS-1S + "d.png", //$NON-NLS-1S + "i.png", //$NON-NLS-1S + "w.png", //$NON-NLS-1S + "e.png", //$NON-NLS-1S + }; + + private Action mClearAction; + + private Clipboard mClipboard; + + /** + * An implementation of {@link ILogFilterStorageManager} to bridge to the eclipse preference + * store, and saves the log filters. + */ + private final class FilterStorage implements ILogFilterStorageManager { + + public LogFilter[] getFilterFromStore() { + String filterPrefs = DdmsPlugin.getDefault().getPreferenceStore().getString( + PREFS_FILTERS); + + // split in a string per filter + String[] filters = filterPrefs.split("\\|"); // $NON-NLS-1$ + + ArrayList<LogFilter> list = + new ArrayList<LogFilter>(filters.length); + + for (String f : filters) { + if (f.length() > 0) { + LogFilter logFilter = new LogFilter(); + if (logFilter.loadFromString(f)) { + list.add(logFilter); + } + } + } + + return list.toArray(new LogFilter[list.size()]); + } + + public void saveFilters(LogFilter[] filters) { + StringBuilder sb = new StringBuilder(); + for (LogFilter f : filters) { + String filterString = f.toString(); + sb.append(filterString); + sb.append('|'); + } + + DdmsPlugin.getDefault().getPreferenceStore().setValue(PREFS_FILTERS, sb.toString()); + } + + public boolean requiresDefaultFilter() { + return true; + } + } + + public LogCatView() { + sThis = this; + LogPanel.PREFS_TIME = PREFS_COL_TIME; + LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL; + LogPanel.PREFS_PID = PREFS_COL_PID; + LogPanel.PREFS_TAG = PREFS_COL_TAG; + LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE; + } + + /** + * Returns the singleton instance. + */ + public static LogCatView getInstance() { + return sThis; + } + + /** + * Sets the display font. + * @param font The font. + */ + public static void setFont(Font font) { + if (sThis != null && sThis.mLogPanel != null) { + sThis.mLogPanel.setFont(font); + } + } + + @Override + public void createPartControl(Composite parent) { + Display d = parent.getDisplay(); + LogColors colors = new LogColors(); + + ImageLoader loader = DdmsPlugin.getImageLoader(); + + colors.infoColor = new Color(d, 0, 127, 0); + colors.debugColor = new Color(d, 0, 0, 127); + colors.errorColor = new Color(d, 255, 0, 0); + colors.warningColor = new Color(d, 255, 127, 0); + colors.verboseColor = new Color(d, 0, 0, 0); + + mCreateFilterAction = new CommonAction("Create Filter") { + @Override + public void run() { + mLogPanel.addFilter(); + } + }; + mCreateFilterAction.setToolTipText("Create Filter"); + mCreateFilterAction.setImageDescriptor(loader + .loadDescriptor("add.png")); // $NON-NLS-1$ + + mEditFilterAction = new CommonAction("Edit Filter") { + @Override + public void run() { + mLogPanel.editFilter(); + } + }; + mEditFilterAction.setToolTipText("Edit Filter"); + mEditFilterAction.setImageDescriptor(loader + .loadDescriptor("edit.png")); // $NON-NLS-1$ + + mDeleteFilterAction = new CommonAction("Delete Filter") { + @Override + public void run() { + mLogPanel.deleteFilter(); + } + }; + mDeleteFilterAction.setToolTipText("Delete Filter"); + mDeleteFilterAction.setImageDescriptor(loader + .loadDescriptor("delete.png")); // $NON-NLS-1$ + + mExportAction = new CommonAction("Export Selection As Text...") { + @Override + public void run() { + mLogPanel.save(); + } + }; + mExportAction.setToolTipText("Export Selection As Text..."); + mExportAction.setImageDescriptor(loader.loadDescriptor("save.png")); // $NON-NLS-1$ + + LogLevel[] levels = LogLevel.values(); + mLogLevelActions = new CommonAction[mLogLevelIcons.length]; + for (int i = 0 ; i < mLogLevelActions.length; i++) { + String name = levels[i].getStringValue(); + mLogLevelActions[i] = new CommonAction(name, IAction.AS_CHECK_BOX) { + @Override + public void run() { + // disable the other actions and record current index + for (int i = 0 ; i < mLogLevelActions.length; i++) { + Action a = mLogLevelActions[i]; + if (a == this) { + a.setChecked(true); + + // set the log level + mLogPanel.setCurrentFilterLogLevel(i+2); + } else { + a.setChecked(false); + } + } + } + }; + + mLogLevelActions[i].setToolTipText(name); + mLogLevelActions[i].setImageDescriptor(loader.loadDescriptor(mLogLevelIcons[i])); + } + + mClearAction = new Action("Clear Log") { + @Override + public void run() { + mLogPanel.clear(); + } + }; + mClearAction.setImageDescriptor(loader + .loadDescriptor("clear.png")); // $NON-NLS-1$ + + + // now create the log view + mLogPanel = new LogPanel(loader, colors, new FilterStorage(), LogPanel.FILTER_MANUAL); + mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); + + // get the font + String fontStr = DdmsPlugin.getDefault().getPreferenceStore().getString( + PreferenceInitializer.ATTR_LOGCAT_FONT); + if (fontStr != null) { + FontData data = new FontData(fontStr); + + if (fontStr != null) { + mLogPanel.setFont(new Font(parent.getDisplay(), data)); + } + } + + mLogPanel.createPanel(parent); + setSelectionDependentPanel(mLogPanel); + + // place the actions. + placeActions(); + + // setup the copy action + mClipboard = new Clipboard(d); + IActionBars actionBars = getViewSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action("Copy") { + @Override + public void run() { + mLogPanel.copy(mClipboard); + } + }); + + // setup the select all action + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), + new Action("Select All") { + @Override + public void run() { + mLogPanel.selectAll(); + } + }); + } + + @Override + public void dispose() { + mLogPanel.stopLogCat(true); + mClipboard.dispose(); + } + + @Override + public void setFocus() { + mLogPanel.setFocus(); + } + + /** + * Place the actions in the ui. + */ + private void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(mCreateFilterAction); + menuManager.add(mEditFilterAction); + menuManager.add(mDeleteFilterAction); + menuManager.add(new Separator()); + menuManager.add(mClearAction); + menuManager.add(new Separator()); + menuManager.add(mExportAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + for (CommonAction a : mLogLevelActions) { + toolBarManager.add(a); + } + toolBarManager.add(new Separator()); + toolBarManager.add(mCreateFilterAction); + toolBarManager.add(mEditFilterAction); + toolBarManager.add(mDeleteFilterAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mClearAction); + } + } + diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java new file mode 100644 index 0000000..ed5aacb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmuilib.NativeHeapPanel; + +import org.eclipse.swt.widgets.Composite; + +public class NativeHeapView extends TableView { + + public static final String ID = + "com.android.ide.eclipse.ddms.views.NativeHeapView"; // $NON-NLS-1$ + private NativeHeapPanel mPanel; + + public NativeHeapView() { + } + + @Override + public void createPartControl(Composite parent) { + mPanel = new NativeHeapPanel(); + mPanel.createPanel(parent); + + setSelectionDependentPanel(mPanel); + + // listen to focus changes for table(s) of the panel. + setupTableFocusListener(mPanel, parent); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java new file mode 100644 index 0000000..48b2689 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ide.eclipse.ddms.DdmsPlugin; +import com.android.ide.eclipse.ddms.DdmsPlugin.ISelectionListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.Device; +import com.android.ddmuilib.SelectionDependentPanel; + +import org.eclipse.ui.part.ViewPart; + +/** + * A Workbench {@link ViewPart} that requires {@link Device}/{@link Client} selection notifications + * from {@link DdmsPlugin} through the {@link ISelectionListener} interface. + */ +public abstract class SelectionDependentViewPart extends ViewPart implements ISelectionListener { + + private SelectionDependentPanel mPanel; + + protected final void setSelectionDependentPanel(SelectionDependentPanel panel) { + // remember the panel + mPanel = panel; + + // and add ourself as listener of selection events. + DdmsPlugin.getDefault().addSelectionListener(this); + } + + @Override + public void dispose() { + DdmsPlugin.getDefault().removeSelectionListener(this); + super.dispose(); + } + + /** + * Sent when a new {@link Client} is selected. + * @param selectedClient The selected client. + * + * @see ISelectionListener + */ + public final void selectionChanged(Client selectedClient) { + mPanel.clientSelected(selectedClient); + } + + /** + * Sent when a new {@link Device} is selected. + * @param selectedDevice the selected device. + * + * @see ISelectionListener + */ + public final void selectionChanged(Device selectedDevice) { + mPanel.deviceSelected(selectedDevice); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java new file mode 100644 index 0000000..0fda35d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.TablePanel; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.actions.ActionFactory; + +/** + * Base class for view containing Table that needs to support copy, and select all. + */ +public abstract class TableView extends SelectionDependentViewPart { + + /** Activator for the current Table that has the focus */ + IFocusedTableActivator mActivator = null; + + private Clipboard mClipboard; + + private Action mCopyAction; + private Action mSelectAllAction; + + /** + * Setup the listener for the Table objects of <code>Panel</code>, and setup + * the copy and select all actions. + * @param panel The panel to setup + * @param parent The parent composite of the Panel's content. + */ + void setupTableFocusListener(TablePanel panel, Composite parent) { + panel.setTableFocusListener(new ITableFocusListener() { + public void focusGained(IFocusedTableActivator activator) { + mActivator = activator; + mCopyAction.setEnabled(true); + mSelectAllAction.setEnabled(true); + } + + public void focusLost(IFocusedTableActivator activator) { + if (activator == mActivator) { + mActivator = null; + mCopyAction.setEnabled(false); + mSelectAllAction.setEnabled(false); + } + } + }); + + // setup the copy action + mClipboard = new Clipboard(parent.getDisplay()); + IActionBars actionBars = getViewSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), + mCopyAction = new Action("Copy") { + @Override + public void run() { + if (mActivator != null) { + mActivator.copy(mClipboard); + } + } + }); + + // setup the select all action + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), + mSelectAllAction = new Action("Select All") { + @Override + public void run() { + if (mActivator != null) { + mActivator.selectAll(); + } + } + }); + + } + + @Override + public void dispose() { + super.dispose(); + mClipboard.dispose(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java new file mode 100644 index 0000000..cd24458 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.ddms.views; + +import com.android.ddmuilib.ThreadPanel; + +import org.eclipse.swt.widgets.Composite; + +public class ThreadView extends TableView { + + public static final String ID = + "com.android.ide.eclipse.ddms.views.ThreadView"; // $NON-NLS-1$ + private ThreadPanel mPanel; + + public ThreadView() { + } + + @Override + public void createPartControl(Composite parent) { + mPanel = new ThreadPanel(); + mPanel.createPanel(parent); + + setSelectionDependentPanel(mPanel); + + // listen to focus changes for table(s) of the panel. + setupTableFocusListener(mPanel, parent); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/.classpath b/eclipse/plugins/com.android.ide.eclipse.tests/.classpath new file mode 100644 index 0000000..4088683 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/.classpath @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="src" path="unittests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="lib" path="kxml2-2.3.0.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/.project b/eclipse/plugins/com.android.ide.eclipse.tests/.project new file mode 100644 index 0000000..99e4964 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/.project @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>adt-tests</name> + <comment></comment> + <projects> + <project>SdkLib</project> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF new file mode 100644 index 0000000..266008c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF @@ -0,0 +1,19 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Android Plugin Tests +Bundle-SymbolicName: com.android.ide.eclipse.tests +Bundle-Version: 0.9.0.qualifier +Bundle-Activator: com.android.ide.eclipse.tests.AndroidTestPlugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.resources, + com.android.ide.eclipse.adt, + org.junit, + org.eclipse.jdt.core, + org.eclipse.jdt.launching, + org.eclipse.ui.views, + com.android.ide.eclipse.ddms +Eclipse-LazyStart: true +Bundle-Vendor: The Android Open Source Project +Bundle-ClassPath: kxml2-2.3.0.jar, + . diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL b/eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/NOTICE b/eclipse/plugins/com.android.ide.eclipse.tests/NOTICE new file mode 100644 index 0000000..49c101d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/NOTICE @@ -0,0 +1,224 @@ +*Eclipse Public License - v 1.0* + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +*1. DEFINITIONS* + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and +documentation distributed under this Agreement, and +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and +are distributed by that particular Contributor. A Contribution +'originates' from a Contributor if it was added to the Program by such +Contributor itself or anyone acting on such Contributor's behalf. +Contributions do not include additions to the Program which: (i) are +separate modules of software distributed in conjunction with the Program +under their own license agreement, and (ii) are not derivative works of +the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +*2. GRANT OF RIGHTS* + +a) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free copyright +license to reproduce, prepare derivative works of, publicly display, +publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and +object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free patent license +under Licensed Patents to make, use, sell, offer to sell, import and +otherwise transfer the Contribution of such Contributor, if any, in +source code and object code form. This patent license shall apply to the +combination of the Contribution and the Program if, at the time the +Contribution is added by the Contributor, such addition of the +Contribution causes such combination to be covered by the Licensed +Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are +provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility to +secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow Recipient +to distribute the Program, it is Recipient's responsibility to acquire +that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright +license set forth in this Agreement. + +*3. REQUIREMENTS* + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties +and conditions, express and implied, including warranties or conditions +of title and non-infringement, and implied warranties or conditions of +merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and +consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable +manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained +within the Program. + +Each Contributor must identify itself as the originator of its +Contribution, if any, in a manner that reasonably allows subsequent +Recipients to identify the originator of the Contribution. + +*4. COMMERCIAL DISTRIBUTION* + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, the +Contributor who includes the Program in a commercial product offering +should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in +a commercial product offering, such Contributor ("Commercial +Contributor") hereby agrees to defend and indemnify every other +Contributor ("Indemnified Contributor") against any losses, damages and +costs (collectively "Losses") arising from claims, lawsuits and other +legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the +Program in a commercial product offering. The obligations in this +section do not apply to any claims or Losses relating to any actual or +alleged intellectual property infringement. In order to qualify, an +Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial +Contributor to control, and cooperate with the Commercial Contributor +in, the defense and any related settlement negotiations. The Indemnified +Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + +*5. NO WARRANTY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED +ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES +OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR +A PARTICULAR PURPOSE. Each Recipient is solely responsible for +determining the appropriateness of using and distributing the Program +and assumes all risks associated with its exercise of rights under this +Agreement , including but not limited to the risks and costs of program +errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +*6. DISCLAIMER OF LIABILITY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +*7. GENERAL* + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including +a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails +to comply with any of the material terms or conditions of this Agreement +and does not cure such failure in a reasonable period of time after +becoming aware of such noncompliance. If all Recipient's rights under +this Agreement terminate, Recipient agrees to cease use and distribution +of the Program as soon as reasonably practicable. However, Recipient's +obligations under this Agreement and any licenses granted by Recipient +relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and may +only be modified in the following manner. The Agreement Steward reserves +the right to publish new versions (including revisions) of this +Agreement from time to time. No one other than the Agreement Steward has +the right to modify this Agreement. The Eclipse Foundation is the +initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is +published, Contributor may elect to distribute the Program (including +its Contributions) under the new version. Except as expressly stated in +Sections 2(a) and 2(b) above, Recipient receives no rights or licenses +to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in +the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to +this Agreement will bring a legal action under this Agreement more than +one year after the cause of action arose. Each party waives its rights +to a jury trial in any resulting litigation. + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/README.txt b/eclipse/plugins/com.android.ide.eclipse.tests/README.txt new file mode 100644 index 0000000..f5899e3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/README.txt @@ -0,0 +1,72 @@ +This project contains the tests for the Android Eclipse Plugins. + +You can do two things: +1- Run the full "eclipse plugin" suite +2- Run independent JUnit tests (not as plugin) + +------------------------------------------ +1- Running the full "eclipse plugin" suite +------------------------------------------ + +Steps to run the test suite: + +A- In Eclipse, import following projects from //device/tools/eclipse/plugins: + - adt-tests + - adt + - common + - editors + +B- Create a new "JUnit Plug-in Test" run configuration via the "Run > Open Run Dialog..." menu +Set the launch configuration's data as follows: +i. "Test" tab: + Select "Run a single test" + Project: adt-tests + Test class: com.android.ide.eclipse.tests.UnitTests + Test runner: JUnit 3 +ii. "Arguments" tab: + Set "VM Arguments" to +"-Dtest_data=<adt>/plugins/com.android.ide.eclipse.tests/unittests/data/" +replacing "<adt>" with absolute filesystem path to the android plugin source location + +All other fields can be left with their default values + +C. Run the newly created launch configuration + +Running the tests will run a secondary instance of Eclipse. + +Please note the following constraints to be aware of when writing tests to run within a plugin environment: + +a. Access restrictions: cannot access package or protected members in a different +plugin, even if they are in the same declared package +b. Using classloader.getResource or getResourceAsStream to access test data will +likely fail in the plugin environment. Instead, use AdtTestData to access test files +in conjunction with the "test_data" environment variable mentioned above + + +------------------------------------------- +2- Run independent JUnit tests (not plugin) +------------------------------------------- + +A- In Eclipse, import following projects from //device/tools/eclipse/plugins: + - adt-tests + - adt + - common + - editors + +B- Select the "unittests" source folder, right-click and select + "Run As > JUnit Test" (i.e. not the plugin tests) + +This creates a debug configuration of type "JUnit Test" running all tests +in the source folder "unittests". The runtime must be JUnit 3. + +Note: this method runs the tests within a regular JVM environment (ie not within +an Eclipse instance). This method has the advantage of being quicker than running +as a JUnit plugin test, and requires less potential set-up, but has the +disadvantage of not properly replicating how the tests will be run in the +continuous test environment. Tests that pass when run as "JUnit Tests" can +fail when run as "JUnit Plugin Tests", due to the extra constraints imposed by +running within an Eclipse plug-in noted in section 1. + + + + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/build.properties b/eclipse/plugins/com.android.ide.eclipse.tests/build.properties new file mode 100644 index 0000000..cbfd993 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/build.properties @@ -0,0 +1,10 @@ +source.. = src/,\ + unittests/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + test.xml,\ + prefs.template,\ + unittest.xml,\ + kxml2-2.3.0.jar,\ + unittests/data/ diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java new file mode 100644 index 0000000..3cf1027 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jar.example; + +public class Class1 { + + public static final int sStaticField = 1; + + /** constructor */ + public Class1() { + int a = 1; + } + + public static class InnerStaticClass1 { + + } + + public class InnerClass2 { + + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java new file mode 100644 index 0000000..4d15c47 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jar.example; + +public class Class2 extends Class1 { + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/prefs.template b/eclipse/plugins/com.android.ide.eclipse.tests/prefs.template new file mode 100644 index 0000000..e0037de --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/prefs.template @@ -0,0 +1,3 @@ +#Wed Feb 20 16:56:40 PST 2008 +com.android.ide.eclipse.adt.sdk=sdk_home +eclipse.preferences.version=1 diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java new file mode 100644 index 0000000..42f8df0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.adt.wizards.newproject; + +import java.io.File; + +/** + * Stub class for project creation page Returns canned responses for creating a + * sample project + */ +public class StubSampleProjectCreationPage extends NewProjectCreationPage { + + private String mSampleProjectName; + private String mOsSdkLocation; + + public StubSampleProjectCreationPage(String pageName, + String sampleProjectName, String osSdkLocation) { + super(pageName); + this.mSampleProjectName = sampleProjectName; + this.mOsSdkLocation = osSdkLocation; + } + + @Override + public String getProjectName() { + return mSampleProjectName; + } + + @Override + public String getPackageName() { + return "com.android.samples"; + } + + @Override + public String getActivityName() { + return mSampleProjectName; + } + + @Override + public String getApplicationName() { + return mSampleProjectName; + } + + @Override + public boolean isNewProject() { + return false; + } + + @Override + public String getProjectLocation() { + return mOsSdkLocation + File.separator + "samples" + File.separator + mSampleProjectName; + } + + @Override + public String getSourceFolder() { + return "src"; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java new file mode 100644 index 0000000..40cd636 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.adt.wizards.newproject; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.wizard.IWizardContainer; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.widgets.Shell; + +import java.lang.reflect.InvocationTargetException; + +/** + * Stub class for project creation wizard Created so project creation logic can + * be run without UI creation/manipulation Returns canned responses for creating + * a sample project + */ +public class StubSampleProjectWizard extends NewProjectWizard { + + private final String mSampleProjectName; + private final String mOsSdkLocation; + + /** + * Constructor + * + * @param sampleProjectName + * @param osSdkLocation + */ + public StubSampleProjectWizard(String sampleProjectName, String osSdkLocation) { + this.mSampleProjectName = sampleProjectName; + this.mOsSdkLocation = osSdkLocation; + } + + /** + * Override parent to return stub page + */ + @Override + protected NewProjectCreationPage createMainPage() { + return new StubSampleProjectCreationPage(MAIN_PAGE_NAME, + mSampleProjectName, mOsSdkLocation); + } + + /** + * Overrides parent to return dummy wizard container + */ + @Override + public IWizardContainer getContainer() { + return new IWizardContainer() { + + public IWizardPage getCurrentPage() { + return null; + } + + public Shell getShell() { + return null; + } + + public void showPage(IWizardPage page) { + // pass + } + + public void updateButtons() { + // pass + } + + public void updateMessage() { + // pass + } + + public void updateTitleBar() { + // pass + } + + public void updateWindowTitle() { + // pass + } + + /** + * Executes runnable on current thread + */ + public void run(boolean fork, boolean cancelable, + IRunnableWithProgress runnable) + throws InvocationTargetException, InterruptedException { + runnable.run(new NullProgressMonitor()); + } + + }; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java new file mode 100644 index 0000000..262ef65 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + +import java.io.File; +import java.net.URL; +import java.util.logging.Logger; + +/** + * Helper class for retrieving test data + * + * All tests which need to retrieve test data files should go through this class + * + */ +public class AdtTestData { + + /** singleton instance */ + private static AdtTestData sInstance = null; + private static final Logger sLogger = Logger.getLogger(AdtTestData.class.getName()); + + /** the absolute file path to the /data directory in this test + * environment. + */ + private String mOsRootDataPath; + + + private AdtTestData() { + // can set test_data env variable to override default behavior of + // finding data using class loader + // useful when running in plugin environment, where test data is inside + // bundled jar, and must be extracted to temp filesystem location to be + // accessed normally + mOsRootDataPath = System.getProperty("test_data"); + if (mOsRootDataPath == null) { + sLogger.info("Cannot find test_data directory, init to class loader"); + URL url = this.getClass().getClassLoader().getResource("data"); //$NON-NLS-1$ + mOsRootDataPath = url.getFile(); + } + if (!mOsRootDataPath.endsWith(File.separator)) { + sLogger.info("Fixing test_data env variable does not end with path separator"); + mOsRootDataPath = mOsRootDataPath.concat(File.separator); + } + } + + /** Get the singleton instance of AdtTestData */ + public static AdtTestData getInstance() { + if (sInstance == null) { + sInstance = new AdtTestData(); + } + return sInstance; + } + + /** Returns the absolute file path to a file located in this plugins + * "data" directory + * @param osRelativePath - string path to file contained in /data. Must + * use path separators appropriate to host OS + * @return String + */ + public String getTestFilePath(String osRelativePath) { + return mOsRootDataPath + osRelativePath; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java new file mode 100644 index 0000000..fb5504c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + +import junit.framework.TestSuite; + + +/** + * Container TestSuite for all eclipse tests, both functional and unit + */ +public class AllTests extends TestSuite { + + public AllTests() { + + } + + /** + * Returns a suite of test cases to be run. + */ + public static TestSuite suite() { + TestSuite suite = new TestSuite(); + suite.addTest(FuncTests.suite()); + suite.addTest(UnitTests.suite()); + return suite; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java new file mode 100644 index 0000000..45a6fbc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle + */ +public class AndroidTestPlugin extends AbstractUIPlugin { + + // The plug-in ID + public static final String PLUGIN_ID = "com.android.ide.eclipse.adt.tests"; + + // The shared instance + private static AndroidTestPlugin sPlugin; + + /** + * The constructor + */ + public AndroidTestPlugin() { + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + sPlugin = this; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + @Override + public void stop(BundleContext context) throws Exception { + sPlugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static AndroidTestPlugin getDefault() { + return sPlugin; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java new file mode 100644 index 0000000..29538bb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.eclipse.core.runtime.Plugin; + +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.Enumeration; +import java.util.logging.Logger; + +/** + * Class for collecting all test cases in an eclipse plugin + * + */ +public class EclipseTestCollector { + + private static final Logger sLogger = Logger.getLogger(EclipseTestCollector.class.getName()); + + /** + * Constructor + */ + public EclipseTestCollector() { + + } + + /** + * Searches through given plugin, adding all TestCase classes to given suite + * @param suite - TestSuite to add to + * @param plugin - Plugin to search for tests + * @param expectedPackage - expected package for tests. Only test classes + * that start with this package name will be added to suite + */ + public void addTestCases(TestSuite suite, Plugin plugin, String expectedPackage) { + if (plugin != null) { + Enumeration entries = plugin.getBundle().findEntries("/", "*.class", true); + + while (entries.hasMoreElements()) { + URL entry = (URL)entries.nextElement(); + String filePath = entry.getPath().replace(".class", ""); + try { + Class testClass = getClass(filePath, expectedPackage); + if (isTestClass(testClass)) { + suite.addTestSuite(testClass); + } + } + catch (ClassNotFoundException e) { + // ignore, this is not the class we're looking for + //sLogger.log(Level.INFO, "Could not load class " + filePath); + } + } + } + } + + /** + * Returns true if given class shouk\ld be added to suite + * @param testClass + * @return + */ + protected boolean isTestClass(Class testClass) { + return TestCase.class.isAssignableFrom(testClass) && + Modifier.isPublic(testClass.getModifiers()) && + hasPublicConstructor(testClass); + } + + /** + * Returns true if given class has a public constructor + * @param testClass + * @return + */ + protected boolean hasPublicConstructor(Class testClass) { + try { + TestSuite.getTestConstructor(testClass); + } catch(NoSuchMethodException e) { + return false; + } + return true; + } + + /** + * Load the class given by the plugin aka bundle file path + * @param filePath - path of class in bundle + * @param expectedPackage - expected package of class + * @return + * @throws ClassNotFoundException + */ + protected Class getClass(String filePath, String expectedPackage) throws ClassNotFoundException { + String dotPath = filePath.replace('/', '.'); + // remove the output folders, by finding where package name starts + int index = dotPath.indexOf(expectedPackage); + if (index == -1) { + throw new ClassNotFoundException(); + } + String packagePath = dotPath.substring(index); + return Class.forName(packagePath); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java new file mode 100644 index 0000000..63f17ab --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + +import junit.framework.TestCase; + +/** + * Generic superclass for Eclipse Android functional test cases, that provides + * common facilities + */ +public class FuncTestCase extends TestCase { + + private String mOsSdkLocation; + + /** + * Constructor + * + * @throws IllegalArgumentException if environment variable "sdk_home" is + * not set + */ + protected FuncTestCase() { + mOsSdkLocation = System.getProperty("sdk_home"); + if (mOsSdkLocation == null || mOsSdkLocation.length() < 1) { + throw new IllegalArgumentException( + "Environment variable sdk_home is not set"); + } + } + + /** + * Returns the absolute file system path of the Android SDK location to use + * for this test + */ + protected String getOsSdkLocation() { + return mOsSdkLocation; + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java new file mode 100644 index 0000000..08405e8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + +import com.android.ide.eclipse.tests.functests.sampleProjects.SampleProjectTest; + +import junit.framework.TestSuite; + +/** + * Container TestSuite for all eclipse tests to be run + */ + +public class FuncTests extends TestSuite { + + static final String FUNC_TEST_PACKAGE = "com.android.ide.eclipse.tests.functests"; + + public FuncTests() { + + } + + /** + * Returns a suite of test cases to be run. + * Needed for JUnit3 compliant command line test runner + */ + public static TestSuite suite() { + TestSuite suite = new TestSuite(); + + suite.addTestSuite(SampleProjectTest.class); + + return suite; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java new file mode 100644 index 0000000..ac928db --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests; + + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Container TestSuite for all eclipse unit tests to be run + * + * Uses Eclipse OSGI to find and then run all junit.junit.framework.Tests in + * this plugin, excluding tests in the FuncTests.FUNC_TEST_PACKAGE package + * + * Since it uses Eclipse OSGI, it must be run in a Eclipse plugin environment + * i.e. from Eclipse workbench, this suite must be run using the + * "JUnit Plug-in Test" launch configuration as opposed to as a "JUnit Test" + * + */ +public class UnitTests { + private static final String TEST_PACKAGE = "com.android.ide.eclipse"; + + public static Test suite() { + TestSuite suite = new TestSuite(); + + UnitTestCollector collector = new UnitTestCollector(); + collector.addTestCases(suite, AndroidTestPlugin.getDefault(), TEST_PACKAGE); + return suite; + } + + /** + * Specialized test collector which will skip adding functional tests + */ + private static class UnitTestCollector extends EclipseTestCollector { + /** + * Override parent class to exclude functional tests + */ + @Override + protected boolean isTestClass(Class testClass) { + return super.isTestClass(testClass) && + !testClass.getPackage().getName().startsWith(FuncTests.FUNC_TEST_PACKAGE); + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java new file mode 100644 index 0000000..98817c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.ide.eclipse.tests.functests.sampleProjects; + +import com.android.ide.eclipse.adt.project.ProjectHelper; +import com.android.ide.eclipse.adt.wizards.newproject.StubSampleProjectWizard; +import com.android.ide.eclipse.tests.FuncTestCase; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.widgets.Display; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Test case that verifies all SDK sample projects can be imported, built in + * Eclipse + * + * TODO: add support for deploying apps onto emulator and verifying successful + * execution there + * + */ +public class SampleProjectTest extends FuncTestCase { + + private static final Logger sLogger = Logger.getLogger(SampleProjectTest.class.getName()); + + /** + * Tests the sample project with the given name + * + * @param name - name of sample project to test + */ + protected void doTestSampleProject(String name) { + try { + + StubSampleProjectWizard newProjCreator = new StubSampleProjectWizard( + name, getOsSdkLocation()); + newProjCreator.init(null, null); + newProjCreator.performFinish(); + + IProject iproject = validateProjectExists(name); + + validateNoProblems(iproject); + + } + catch (CoreException e) { + fail("Unexpected exception when creating sample project: " + e.toString()); + } + } + + public void testApiDemos() { + doTestSampleProject("ApiDemos"); + } + + public void testHelloActivity() { + doTestSampleProject("HelloActivity"); + } + + public void testLunarLander() { + doTestSampleProject("LunarLander"); + } + + public void testNotePad() { + doTestSampleProject("NotePad"); + } + + public void testSkeletonApp() { + doTestSampleProject("SkeletonApp"); + } + + public void testSnake() { + doTestSampleProject("Snake"); + } + + private IProject validateProjectExists(String name) { + IProject iproject = getIProject(name); + assertTrue(iproject.exists()); + assertTrue(iproject.isOpen()); + return iproject; + } + + private IProject getIProject(String name) { + IProject iproject = ResourcesPlugin.getWorkspace().getRoot() + .getProject(name); + return iproject; + } + + private void validateNoProblems(IProject iproject) throws CoreException { + waitForBuild(iproject); + assertFalse(ProjectHelper.hasError(iproject, true)); + } + + + /** + * Waits for build to complete. + * + * @param iproject + */ + private void waitForBuild(final IProject iproject) { + + final BuiltProjectDeltaVisitor deltaVisitor = new BuiltProjectDeltaVisitor(iproject); + IResourceChangeListener newBuildListener = new IResourceChangeListener() { + + public void resourceChanged(IResourceChangeEvent event) { + try { + event.getDelta().accept(deltaVisitor); + } + catch (CoreException e) { + fail(); + } + } + + }; + iproject.getWorkspace().addResourceChangeListener(newBuildListener, + IResourceChangeEvent.POST_BUILD); + + // poll build listener to determine when build is done + // loop max of 1200 times * 50 ms = 60 seconds + final int maxWait = 1200; + for (int i=0; i < maxWait; i++) { + if (deltaVisitor.isProjectBuilt()) { + return; + } + try { + Thread.sleep(50); + } + catch (InterruptedException e) { + + } + if (Display.getCurrent() != null) { + Display.getCurrent().readAndDispatch(); + } + } + + sLogger.log(Level.SEVERE, "expected build event never happened?"); + fail("expected build event never happened for " + iproject.getName()); + + } + + /** + * Scans a given IResourceDelta looking for a "build event" change for given IProject + * + */ + private class BuiltProjectDeltaVisitor implements IResourceDeltaVisitor { + + private IProject mIProject; + private boolean mIsBuilt; + + public BuiltProjectDeltaVisitor(IProject iproject) { + mIProject = iproject; + mIsBuilt = false; + } + + public boolean visit(IResourceDelta delta) { + if (mIProject.equals(delta.getResource())) { + setBuilt(true); + return false; + } + return true; + } + + private synchronized void setBuilt(boolean b) { + mIsBuilt = b; + } + + public synchronized boolean isProjectBuilt() { + return mIsBuilt; + } + + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/test.xml b/eclipse/plugins/com.android.ide.eclipse.tests/test.xml new file mode 100644 index 0000000..792ebc2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/test.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- test launcher file for Android Eclipse functional tests --> +<project name="testsuite" default="run" basedir="."> + <!--The following properties should be passed into this script, set to some default value for now --> + <property name="eclipse.home" value="/opt/eclipse" /> + <property name="sdk_home" value="/tmp/sdk" /> + <property name="eclipse_test" value="${eclipse.home}/plugins/org.eclipse.test_3.2.0" /> + + <!-- eclipse scripts use an annoying mixture of eclipse-home and eclipse.home --> + <!-- lets define both...--> + <property name="eclipse-home" value="${eclipse.home}" /> + <property name="test-folder" value="${eclipse.home}/test_folder" /> + + <!-- sets the properties eclipse.home, and library-file --> + <property name="plugin-name" value="com.android.ide.eclipse.tests" /> + <property name="library-file" value="${eclipse_test}/library.xml" /> + + <!-- location of adt preference file (within workspace) --> + <property name="prefs_path" value="${test-folder}/.metadata/.plugins/org.eclipse.core.runtime/.settings/com.android.ide.eclipse.adt.prefs" /> + <!-- location of pref template file relative to this file --> + + <property name="prefs_template" value="prefs.template" /> + + <!-- This target holds all initialization code that needs to be done for --> + <!-- all tests that are to be run. --> + <target name="init"> + <tstamp /> + <echo message="eclipse.home: ${eclipse.home}" /> + <echo message="libfile: ${library-file}" /> + + <!-- delete test eclipse workspace --> + <delete dir="${test-folder}" quiet="true" /> + + <!-- delete test results dir --> + <delete dir="${eclipse.home}/results" quiet="true" /> + + <!-- Copy a preference file into the test workspace. --> + <!-- This is done to ensure Android SDK preference is set on startup --> + <copy file="${prefs_template}" tofile="${prefs_path}" /> + <!-- replace sdk path placeholder token with actual sdk path --> + <replace file="${prefs_path}" token="sdk_home" value="${sdk_home}" />
+
+ <!-- if this is on windows, escape the drive and file separators -->
+ <replace file="${prefs_path}" token="\" value="\\" />
+ <replace file="${prefs_path}" token=":" value="\:" /> + </target> + + <!-- This target defines the tests that need to be run. --> + <target name="suite"> + <!-- launch as ui-test ie as a test which needs Eclipse workbench --> + <ant target="ui-test" antfile="${library-file}" dir="${eclipse.home}"> + <property name="data-dir" value="${test-folder}" /> + <property name="plugin-name" value="${plugin-name}" /> + <property name="classname" value="com.android.ide.eclipse.tests.FuncTests" /> + <!-- pass extra vm arg to set sdk_home env variable --> + <property name="extraVMargs" value="-Dsdk_home=${sdk_home}" /> + </ant> + </target> + + <!-- This target holds code to cleanup the testing environment after --> + <!-- after all of the tests have been run. You can use this target to --> + <!-- delete temporary files that have been created. --> + <target name="cleanup"> + </target> + + <!-- This target runs the test suite. Any actions that need to happen --> + <!-- after all the tests have been run should go here. --> + <target name="run" depends="init,suite,cleanup"> + <ant target="collect" antfile="${library-file}" dir="${eclipse.home}/results"> + <property name="includes" value="com*.xml" /> + <property name="output-file" value="${plugin-name}.xml" /> + </ant> + </target> +</project> + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml b/eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml new file mode 100644 index 0000000..83e00ec --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- test launcher file for Android Eclipse unit tests --> +<project name="testsuite" default="run" basedir="."> + <!--The following properties should be passed into this script, set to some default value for now --> + <property name="eclipse.home" value="/opt/eclipse" /> + <property name="eclipse_test" value="${eclipse.home}/plugins/org.eclipse.test_3.2.0" /> + + <!-- eclipse scripts use an annoying mixture of eclipse-home and eclipse.home --> + <!-- lets define both...--> + <property name="eclipse-home" value="${eclipse.home}" /> + <property name="test-folder" value="${eclipse.home}/unittest_ws" /> + + <!-- sets the properties eclipse.home, and library-file --> + <property name="plugin-name" value="com.android.ide.eclipse.tests" /> + <property name="library-file" value="${eclipse_test}/library.xml" /> + + <!-- This target holds all initialization code that needs to be done for --> + <!-- all tests that are to be run. --> + <target name="init"> + <ant antfile="test.xml" target="init"> + <property name="test-folder" value="${test-folder}" /> + </ant> + </target> + + <!-- This target defines the tests that need to be run. --> + <target name="suite"> + <!-- need to launch as ui-test since all ADT plugins depend on ui--> + <!-- otherwise other plugins will not load --> + <ant target="ui-test" antfile="${library-file}" dir="${eclipse.home}"> + <property name="data-dir" value="${test-folder}" /> + <property name="plugin-name" value="${plugin-name}" /> + <property name="classname" value="com.android.ide.eclipse.tests.UnitTests" /> + <!-- pass extra vm arg to set test_data env variable --> + <property name="extraVMargs" value="-Dtest_data=${test_data}" /> + </ant> + </target> + + <!-- This target holds code to cleanup the testing environment after --> + <!-- after all of the tests have been run. You can use this target to --> + <!-- delete temporary files that have been created. --> + <target name="cleanup"> + </target> + + <!-- This target runs the test suite. Any actions that need to happen --> + <!-- after all the tests have been run should go here. --> + <target name="run" depends="init,suite,cleanup"> + <ant target="collect" antfile="${library-file}" dir="${eclipse.home}/results"> + <property name="includes" value="com*.xml" /> + <property name="output-file" value="${plugin-name}.xml" /> + </ant> + </target> +</project> + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java new file mode 100644 index 0000000..0860e40 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.build; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import junit.framework.TestCase; + +public class BaseBuilderTest extends TestCase { + + public void testParseAaptOutput() { + Pattern p = Pattern.compile( "^(.+):(\\d+):\\s(.+)$"); //$NON-NLS-1$ + String s = "C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml:11: WARNING: empty 'some warning text"; + + Matcher m = p.matcher(s); + assertEquals(true, m.matches()); + assertEquals("C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml", m.group(1)); + assertEquals("11", m.group(2)); + assertEquals("WARNING: empty 'some warning text", m.group(3)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java new file mode 100644 index 0000000..8c52d81 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.project; + +import com.android.ide.eclipse.mock.ClasspathEntryMock; +import com.android.ide.eclipse.mock.JavaProjectMock; + +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.JavaModelException; + +import junit.framework.TestCase; + +public class ProjectHelperTest extends TestCase { + + /** The old container id */ + private final static String OLD_CONTAINER_ID = + "com.android.ide.eclipse.adt.project.AndroidClasspathContainerInitializer"; //$NON-NLS-1$ + + /** The container id for the android framework jar file */ + private final static String CONTAINER_ID = + "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ + + @Override + public void setUp() throws Exception { + // pass for now + } + + @Override + public void tearDown() throws Exception { + // pass for now + } + + public final void testFixProjectClasspathEntriesFromOldContainer() throws JavaModelException { + // create a project with a path to an android .zip + JavaProjectMock javaProject = new JavaProjectMock( + new IClasspathEntry[] { + new ClasspathEntryMock(new Path("Project/src"), //$NON-NLS-1$ + IClasspathEntry.CPE_SOURCE), + new ClasspathEntryMock(new Path(OLD_CONTAINER_ID), + IClasspathEntry.CPE_CONTAINER), + }, + new Path("Project/bin")); + + ProjectHelper.fixProjectClasspathEntries(javaProject); + + IClasspathEntry[] fixedEntries = javaProject.getRawClasspath(); + assertEquals(3, fixedEntries.length); + assertEquals("Project/src", fixedEntries[0].getPath().toString()); + assertEquals(OLD_CONTAINER_ID, fixedEntries[1].getPath().toString()); + assertEquals(CONTAINER_ID, fixedEntries[2].getPath().toString()); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java new file mode 100644 index 0000000..8af7e02 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.adt.sdk.IAndroidClassLoader.IClassDescriptor; +import com.android.ide.eclipse.tests.AdtTestData; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; + +import junit.framework.TestCase; + +/** + * Unit Test for {@link AndroidJarLoader}. + * + * Uses the classes jar.example.Class1/Class2 stored in tests/data/jar_example.jar. + */ +public class AndroidJarLoaderTest extends TestCase { + + private AndroidJarLoader mFrameworkClassLoader; + + /** Creates an instance of {@link AndroidJarLoader} on our test data JAR */ + @Override + public void setUp() throws Exception { + String jarfilePath = AdtTestData.getInstance().getTestFilePath("jar_example.jar"); //$NON-NLS-1$ + mFrameworkClassLoader = new AndroidJarLoader(jarfilePath); + } + + @Override + public void tearDown() throws Exception { + mFrameworkClassLoader = null; + System.gc(); + } + + /** Preloads classes. They should load just fine. */ + public final void testPreLoadClasses() throws Exception { + mFrameworkClassLoader.preLoadClasses("jar.example.", null, null); //$NON-NLS-1$ + HashMap<String, Class<?>> map = getPrivateClassCache(); + assertEquals(0, map.size()); + HashMap<String,byte[]> data = getPrivateEntryCache(); + assertTrue(data.containsKey("jar.example.Class1")); //$NON-NLS-1$ + assertTrue(data.containsKey("jar.example.Class2")); //$NON-NLS-1$ + assertTrue(data.containsKey("jar.example.Class1$InnerStaticClass1")); //$NON-NLS-1$ + assertTrue(data.containsKey("jar.example.Class1$InnerClass2")); //$NON-NLS-1$ + assertEquals(4, data.size()); + } + + /** Preloads a class not in the JAR. Preloading does nothing in this case. */ + public final void testPreLoadClasses_classNotFound() throws Exception { + mFrameworkClassLoader.preLoadClasses("not.a.package.", null, null); //$NON-NLS-1$ + HashMap<String, Class<?>> map = getPrivateClassCache(); + assertEquals(0, map.size()); + HashMap<String,byte[]> data = getPrivateEntryCache(); + assertEquals(0, data.size()); + } + + /** Finds a class we just preloaded. It should work. */ + public final void testFindClass_classFound() throws Exception { + Class<?> c = _findClass(mFrameworkClassLoader, "jar.example.Class2"); //$NON-NLS-1$ + assertEquals("jar.example.Class2", c.getName()); //$NON-NLS-1$ + HashMap<String, Class<?>> map = getPrivateClassCache(); + assertTrue(map.containsKey("jar.example.Class1")); //$NON-NLS-1$ + assertTrue(map.containsKey("jar.example.Class2")); //$NON-NLS-1$ + assertEquals(2, map.size()); + } + + /** call the protected method findClass */ + private Class<?> _findClass(AndroidJarLoader jarLoader, String name) throws Exception { + Method findClassMethod = AndroidJarLoader.class.getDeclaredMethod( + "findClass", String.class); //$NON-NLS-1$ + findClassMethod.setAccessible(true); + try { + return (Class<?>)findClassMethod.invoke(jarLoader, name); + } + catch (InvocationTargetException e) { + throw (Exception)e.getCause(); + } + } + + /** Trying to find a class that we fail to preload should throw a CNFE. */ + public final void testFindClass_classNotFound() throws Exception { + try { + // Will throw ClassNotFoundException + _findClass(mFrameworkClassLoader, "not.a.valid.ClassName"); //$NON-NLS-1$ + } catch (ClassNotFoundException e) { + // check the message in the CNFE + assertEquals("not.a.valid.ClassName", e.getMessage()); //$NON-NLS-1$ + return; + } + // Exception not thrown - this is a failure + fail("Expected ClassNotFoundException not thrown"); + } + + public final void testFindClassesDerivingFrom() throws Exception { + HashMap<String, ArrayList<IClassDescriptor>> found = + mFrameworkClassLoader.findClassesDerivingFrom("jar.example.", new String[] { //$NON-NLS-1$ + "jar.example.Class1", //$NON-NLS-1$ + "jar.example.Class2" }); //$NON-NLS-1$ + + assertTrue(found.containsKey("jar.example.Class1")); //$NON-NLS-1$ + assertTrue(found.containsKey("jar.example.Class2")); //$NON-NLS-1$ + assertEquals(2, found.size()); + // Only Class2 derives from Class1.. + // Class1 and Class1$InnerStaticClass1 derive from Object and are thus ignored. + // Class1$InnerClass2 should never be seen either. + assertEquals("jar.example.Class2", //$NON-NLS-1$ + found.get("jar.example.Class1").get(0).getCanonicalName()); //$NON-NLS-1$ + assertEquals(1, found.get("jar.example.Class1").size()); //$NON-NLS-1$ + assertEquals(0, found.get("jar.example.Class2").size()); //$NON-NLS-1$ + } + + // --- Utilities --- + + /** + * Retrieves the private mFrameworkClassLoader.mClassCache field using reflection. + * + * @throws NoSuchFieldException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + */ + @SuppressWarnings("unchecked") + private HashMap<String, Class<?> > getPrivateClassCache() + throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { + Field field = AndroidJarLoader.class.getDeclaredField("mClassCache"); //$NON-NLS-1$ + field.setAccessible(true); + return (HashMap<String, Class<?>>) field.get(mFrameworkClassLoader); + } + + /** + * Retrieves the private mFrameworkClassLoader.mEntryCache field using reflection. + * + * @throws NoSuchFieldException + * @throws SecurityException + * @throws IllegalAccessException + * @throws IllegalArgumentException + */ + @SuppressWarnings("unchecked") + private HashMap<String,byte[]> getPrivateEntryCache() + throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { + Field field = AndroidJarLoader.class.getDeclaredField("mEntryCache"); //$NON-NLS-1$ + field.setAccessible(true); + return (HashMap<String, byte[]>) field.get(mFrameworkClassLoader); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java new file mode 100644 index 0000000..cedf4d4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.sdk; + +import com.android.ide.eclipse.adt.sdk.AndroidJarLoader.ClassWrapper; +import com.android.ide.eclipse.adt.sdk.IAndroidClassLoader.IClassDescriptor; +import com.android.ide.eclipse.common.resources.AttrsXmlParser; +import com.android.ide.eclipse.common.resources.ViewClassInfo; +import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.TreeMap; + +import junit.framework.TestCase; + +/** + * Test the inner private methods of PlatformDataParser. + * + * Convention: method names that start with an underscore are actually local wrappers + * that call private methods from {@link AndroidTargetParser} using reflection. + * This is inspired by the Python coding rule which mandates underscores prefixes for + * "private" methods. + */ +public class LayoutParamsParserTest extends TestCase { + + private static class MockFrameworkClassLoader extends AndroidJarLoader { + MockFrameworkClassLoader() { + super(null /* osFrameworkLocation */); + } + + @Override + public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom( + String rootPackage, String[] superClasses) throws ClassFormatError { + return new HashMap<String, ArrayList<IClassDescriptor>>(); + } + } + + private static class MockAttrsXmlPath { + public String getPath() { + ClassLoader cl = this.getClass().getClassLoader(); + URL res = cl.getResource("data/mock_attrs.xml"); //$NON-NLS-1$ + return res.getFile(); + } + } + + private static class MockLayoutParamsParser extends LayoutParamsParser { + public MockLayoutParamsParser() { + super(new MockFrameworkClassLoader(), + new AttrsXmlParser(new MockAttrsXmlPath().getPath()).preload()); + + mTopViewClass = new ClassWrapper(mock_android.view.View.class); + mTopGroupClass = new ClassWrapper(mock_android.view.ViewGroup.class); + mTopLayoutParamsClass = new ClassWrapper(mock_android.view.ViewGroup.LayoutParams.class); + + mViewList = new ArrayList<IClassDescriptor>(); + mGroupList = new ArrayList<IClassDescriptor>(); + mViewMap = new TreeMap<String, ExtViewClassInfo>(); + mGroupMap = new TreeMap<String, ExtViewClassInfo>(); + mLayoutParamsMap = new HashMap<String, LayoutParamsInfo>(); + } + } + + private MockLayoutParamsParser mParser; + + @Override + public void setUp() throws Exception { + mParser = new MockLayoutParamsParser(); + } + + @Override + public void tearDown() throws Exception { + } + + public final void testFindLayoutParams() throws Exception { + assertEquals(mock_android.view.ViewGroup.LayoutParams.class, + ((ClassWrapper)_findLayoutParams(mock_android.view.ViewGroup.class)).wrappedClass()); + + assertEquals(mock_android.widget.LinearLayout.LayoutParams.class, + ((ClassWrapper)_findLayoutParams(mock_android.widget.LinearLayout.class)).wrappedClass()); + + assertEquals(mock_android.widget.TableLayout.LayoutParams.class, + ((ClassWrapper)_findLayoutParams(mock_android.widget.TableLayout.class)).wrappedClass()); + } + + public final void testGetLayoutParamsInfo() throws Exception { + LayoutParamsInfo info1 = _getLayoutParamsInfo( + mock_android.view.ViewGroup.LayoutParams.class); + assertNotNull(info1); + // ViewGroup.LayoutData has Object for superClass, which we don't map + assertNull(info1.getSuperClass()); + + LayoutParamsInfo info2 = _getLayoutParamsInfo( + mock_android.widget.LinearLayout.LayoutParams.class); + assertNotNull(info2); + // LinearLayout.LayoutData links to ViewGroup.LayoutParams + assertSame(info1, info2.getSuperClass()); + + LayoutParamsInfo info3 = _getLayoutParamsInfo( + mock_android.widget.TableLayout.LayoutParams.class); + assertNotNull(info3); + // TableLayout.LayoutData does not link to ViewGroup.LayoutParams nor + // LinearLayout.LayoutParams + assertNotSame(info1, info3.getSuperClass()); + assertNotSame(info2, info3.getSuperClass()); + // TableLayout.LayoutParams => ViewGroup.MarginLayoutParams => ViewGroup.LayoutParams + assertSame(info1, info3.getSuperClass().getSuperClass()); + } + + public final void testGetLayoutClasses() throws Exception { + // _getLayoutClasses(); + } + + //---- access to private methods + + /** Calls the private constructor of the parser */ + @SuppressWarnings("unused") + private AndroidTargetParser _Constructor(String osJarPath) throws Exception { + Constructor<AndroidTargetParser> constructor = + AndroidTargetParser.class.getDeclaredConstructor(String.class); + constructor.setAccessible(true); + return constructor.newInstance(osJarPath); + } + + /** calls the private getLayoutClasses() of the parser */ + @SuppressWarnings("unused") + private void _getLayoutClasses() throws Exception { + Method method = AndroidTargetParser.class.getDeclaredMethod("getLayoutClasses"); //$NON-NLS-1$ + method.setAccessible(true); + method.invoke(mParser); + } + + /** calls the private addGroup() of the parser */ + @SuppressWarnings("unused") + private ViewClassInfo _addGroup(Class<?> groupClass) throws Exception { + Method method = LayoutParamsParser.class.getDeclaredMethod("addGroup", //$NON-NLS-1$ + IClassDescriptor.class); + method.setAccessible(true); + return (ViewClassInfo) method.invoke(mParser, new ClassWrapper(groupClass)); + } + + /** calls the private addLayoutParams() of the parser */ + @SuppressWarnings("unused") + private LayoutParamsInfo _addLayoutParams(Class<?> groupClass) throws Exception { + Method method = LayoutParamsParser.class.getDeclaredMethod("addLayoutParams", //$NON-NLS-1$ + IClassDescriptor.class); + method.setAccessible(true); + return (LayoutParamsInfo) method.invoke(mParser, new ClassWrapper(groupClass)); + } + + /** calls the private getLayoutParamsInfo() of the parser */ + private LayoutParamsInfo _getLayoutParamsInfo(Class<?> layoutParamsClass) throws Exception { + Method method = LayoutParamsParser.class.getDeclaredMethod("getLayoutParamsInfo", //$NON-NLS-1$ + IClassDescriptor.class); + method.setAccessible(true); + return (LayoutParamsInfo) method.invoke(mParser, new ClassWrapper(layoutParamsClass)); + } + + /** calls the private findLayoutParams() of the parser */ + private IClassDescriptor _findLayoutParams(Class<?> groupClass) throws Exception { + Method method = LayoutParamsParser.class.getDeclaredMethod("findLayoutParams", //$NON-NLS-1$ + IClassDescriptor.class); + method.setAccessible(true); + return (IClassDescriptor) method.invoke(mParser, new ClassWrapper(groupClass)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java new file mode 100644 index 0000000..6604264 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.project; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import junit.framework.TestCase; + +public class AndroidManifestHelperTest extends TestCase { + private File mFile; + private AndroidManifestHelper mManifest; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mFile = File.createTempFile("androidManifest", "xml"); //$NON-NLS-1$ //$NON-NLS-2$ + assertNotNull(mFile); + + FileWriter fw = new FileWriter(mFile); + fw.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$ + fw.write("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"); //$NON-NLS-1$ + fw.write(" package=\"com.android.testapp\">\n"); //$NON-NLS-1$ + fw.write(" <application android:icon=\"@drawable/icon\">\n"); //$NON-NLS-1$ + fw.write(" <activity android:name=\".MainActivity\" android:label=\"@string/app_name\">\n"); //$NON-NLS-1$ + fw.write(" <intent-filter>\n"); //$NON-NLS-1$ + fw.write(" <action android:name=\"android.intent.action.MAIN\" />\n"); //$NON-NLS-1$ + fw.write(" <category android:name=\"android.intent.category.LAUNCHER\" />\"\n"); //$NON-NLS-1$ + fw.write(" <category android:name=\"android.intent.category.DEFAULT\" />\n"); //$NON-NLS-1$ + fw.write(" </intent-filter>\n"); //$NON-NLS-1$ + fw.write(" </activity>\n"); //$NON-NLS-1$ + fw.write(" <activity android:name=\".OptionsActivity\" android:label=\"@string/options\"\n"); //$NON-NLS-1$ + fw.write(" android:theme=\"@style/Theme.Floating\">\n"); //$NON-NLS-1$ + fw.write(" <intent-filter>\n"); //$NON-NLS-1$ + fw.write(" <action android:name=\"com.android.mandelbrot.action.EDIT_OPTIONS\" />\n"); //$NON-NLS-1$ + fw.write(" <category android:name=\"android.intent.category.PREFERENCE_CATEGORY\" />\n"); //$NON-NLS-1$ + fw.write(" </intent-filter>\n"); //$NON-NLS-1$ + fw.write(" </activity>\n"); //$NON-NLS-1$ + fw.write(" <activity android:name=\".InfoActivity\" android:label=\"@string/options\"\n"); //$NON-NLS-1$ + fw.write(" android:theme=\"@style/Theme.Floating\">\n"); //$NON-NLS-1$ + fw.write(" <intent-filter>\n"); //$NON-NLS-1$ + fw.write(" <action android:name=\"com.android.mandelbrot.action.DISPLAY_INFO\" />\n"); //$NON-NLS-1$ + fw.write(" </intent-filter>\n"); //$NON-NLS-1$ + fw.write(" </activity>\n"); //$NON-NLS-1$ + fw.write(" </application>\n"); //$NON-NLS-1$ + fw.write("</manifest>\n"); //$NON-NLS-1$ + fw.flush(); + fw.close(); + + mManifest = new AndroidManifestHelper(mFile.getAbsolutePath()); + } + + @Override + protected void tearDown() throws Exception { + assertTrue(mFile.delete()); + super.tearDown(); + } + + public void testExists() { + assertTrue(mManifest.exists()); + } + + public void testNotExists() throws IOException { + File f = File.createTempFile("androidManifest2", "xml"); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue(f.delete()); + AndroidManifestHelper manifest = new AndroidManifestHelper(f.getAbsolutePath()); + assertFalse(manifest.exists()); + } + + public void testGetPackageName() { + assertEquals("com.android.testapp", mManifest.getPackageName()); + } + + public void testGetActivityName() { + assertEquals("", mManifest.getActivityName(0)); //$NON-NLS-1$ + assertEquals(".MainActivity", mManifest.getActivityName(1)); //$NON-NLS-1$ + assertEquals(".OptionsActivity", mManifest.getActivityName(2)); //$NON-NLS-1$ + assertEquals(".InfoActivity", mManifest.getActivityName(3)); //$NON-NLS-1$ + assertEquals("", mManifest.getActivityName(4)); //$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java new file mode 100644 index 0000000..8338453 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.common.resources; + + +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo; +import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo.Format; +import com.android.ide.eclipse.tests.AdtTestData; + +import org.w3c.dom.Document; + +import java.lang.reflect.Method; +import java.util.Map; + +import junit.framework.TestCase; + +public class AttrsXmlParserTest extends TestCase { + + private AttrsXmlParser mParser; + private String mFilePath; + + @Override + public void setUp() throws Exception { + mFilePath = AdtTestData.getInstance().getTestFilePath("mock_attrs.xml"); //$NON-NLS-1$ + mParser = new AttrsXmlParser(mFilePath); + } + + @Override + public void tearDown() throws Exception { + } + + public final void testGetDocument() throws Exception { + assertNotNull(_getDocument()); + } + + public void testGetOsAttrsXmlPath() throws Exception { + assertEquals(mFilePath, mParser.getOsAttrsXmlPath()); + } + + public final void testPreload() throws Exception { + assertSame(mParser, mParser.preload()); + } + + + public final void testLoadViewAttributes() throws Exception { + mParser.preload(); + ViewClassInfo info = new ViewClassInfo( + false /* isLayout */, + "mock_android.something.Theme", //$NON-NLS-1$ + "Theme"); //$NON-NLS-1$ + mParser.loadViewAttributes(info); + + assertEquals("These are the standard attributes that make up a complete theme.", //$NON-NLS-1$ + info.getJavaDoc()); + AttributeInfo[] attrs = info.getAttributes(); + assertEquals(1, attrs.length); + assertEquals("scrollbarSize", info.getAttributes()[0].getName()); + assertEquals(1, info.getAttributes()[0].getFormats().length); + assertEquals(Format.DIMENSION, info.getAttributes()[0].getFormats()[0]); + } + + public final void testEnumFlagValues() throws Exception { + /* The XML being read contains: + <!-- Standard orientation constant. --> + <attr name="orientation"> + <!-- Defines an horizontal widget. --> + <enum name="horizontal" value="0" /> + <!-- Defines a vertical widget. --> + <enum name="vertical" value="1" /> + </attr> + */ + + mParser.preload(); + Map<String, Map<String, Integer>> attrMap = mParser.getEnumFlagValues(); + assertTrue(attrMap.containsKey("orientation")); + + Map<String, Integer> valueMap = attrMap.get("orientation"); + assertTrue(valueMap.containsKey("horizontal")); + assertTrue(valueMap.containsKey("vertical")); + assertEquals(Integer.valueOf(0), valueMap.get("horizontal")); + assertEquals(Integer.valueOf(1), valueMap.get("vertical")); + } + + public final void testDeprecated() throws Exception { + mParser.preload(); + + DeclareStyleableInfo dep = mParser.getDeclareStyleableList().get("DeprecatedTest"); + assertNotNull(dep); + + AttributeInfo[] attrs = dep.getAttributes(); + assertEquals(4, attrs.length); + + assertEquals("deprecated-inline", attrs[0].getName()); + assertEquals("In-line deprecated.", attrs[0].getDeprecatedDoc()); + assertEquals("Deprecated comments using delimiters.", attrs[0].getJavaDoc()); + + assertEquals("deprecated-multiline", attrs[1].getName()); + assertEquals("Multi-line version of deprecated that works till the next tag.", + attrs[1].getDeprecatedDoc()); + assertEquals("Deprecated comments on their own line.", attrs[1].getJavaDoc()); + + assertEquals("deprecated-not", attrs[2].getName()); + assertEquals(null, attrs[2].getDeprecatedDoc()); + assertEquals("This attribute is not deprecated.", attrs[2].getJavaDoc()); + + assertEquals("deprecated-no-javadoc", attrs[3].getName()); + assertEquals("There is no other javadoc here.", attrs[3].getDeprecatedDoc()); + assertEquals("", attrs[3].getJavaDoc()); + } + + //---- access to private methods + + private Document _getDocument() throws Exception { + Method method = AttrsXmlParser.class.getDeclaredMethod("getDocument"); //$NON-NLS-1$ + method.setAccessible(true); + return (Document) method.invoke(mParser); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java new file mode 100644 index 0000000..69c3ed8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.ide.eclipse.editors.descriptors; + +import junit.framework.TestCase; + +/** + * Unit tests for DescriptorsUtils in the editors plugin + */ +public class DescriptorsUtilsTest extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testPrettyAttributeUiName() { + assertEquals("", DescriptorsUtils.prettyAttributeUiName("")); + + assertEquals("Max width for view", + DescriptorsUtils.prettyAttributeUiName("maxWidthForView")); + + assertEquals("Layout width", + DescriptorsUtils.prettyAttributeUiName("layout_width")); + + // X Y and Z are capitalized when used as single words (so "T" becomes "t") + assertEquals("Axis X", DescriptorsUtils.prettyAttributeUiName("axisX")); + assertEquals("Axis Y", DescriptorsUtils.prettyAttributeUiName("axisY")); + assertEquals("Axis Z", DescriptorsUtils.prettyAttributeUiName("axisZ")); + assertEquals("Axis t", DescriptorsUtils.prettyAttributeUiName("axisT")); + + assertEquals("The X axis", DescriptorsUtils.prettyAttributeUiName("theXAxis")); + assertEquals("The Y axis", DescriptorsUtils.prettyAttributeUiName("theYAxis")); + assertEquals("The Z axis", DescriptorsUtils.prettyAttributeUiName("theZAxis")); + assertEquals("The t axis", DescriptorsUtils.prettyAttributeUiName("theTAxis")); + } + + public void testCapitalize() { + assertEquals("UPPER", DescriptorsUtils.capitalize("UPPER")); + assertEquals("Lower", DescriptorsUtils.capitalize("lower")); + assertEquals("Capital", DescriptorsUtils.capitalize("Capital")); + assertEquals("CamelCase", DescriptorsUtils.capitalize("camelCase")); + assertEquals("", DescriptorsUtils.capitalize("")); + } + + public void testFormatTooltip() { + assertEquals("", DescriptorsUtils.formatTooltip("")); + + assertEquals("\"application\"", + DescriptorsUtils.formatTooltip( + "<code>application</code>")); + + assertEquals("android.content.Intent", + DescriptorsUtils.formatTooltip( + "{@link android.content.Intent}")); + + assertEquals("FLAG_ACTIVITY_SINGLE_TOP", + DescriptorsUtils.formatTooltip( + "{@link android.content.Intent#FLAG_ACTIVITY_SINGLE_TOP}")); + + assertEquals("activity-alias", + DescriptorsUtils.formatTooltip( + "{@link \t #AndroidManifestActivityAlias \tactivity-alias }")); + + assertEquals("\"permission\"", + DescriptorsUtils.formatTooltip( + "{@link #AndroidManifestPermission <permission>}")); + + assertEquals("and etc.", + DescriptorsUtils.formatTooltip( + "{@link #IntentCategory <category> and etc. }")); + + assertEquals("Activity.onNewIntent()", + DescriptorsUtils.formatTooltip( + "{@link android.app.Activity#onNewIntent Activity.onNewIntent()}")); + } + + public void testFormatFormText() { + ElementDescriptor desc = new ElementDescriptor("application"); + desc.setSdkUrl(DescriptorsUtils.MANIFEST_SDK_URL + "TagApplication"); + String docBaseUrl = "http://base"; + assertEquals("<form><li style=\"image\" value=\"image\"></li></form>", DescriptorsUtils.formatFormText("", desc, docBaseUrl)); + + assertEquals("<form><li style=\"image\" value=\"image\"><a href=\"http://base/reference/android/R.styleable.html#TagApplication\">application</a></li></form>", + DescriptorsUtils.formatFormText( + "<code>application</code>", + desc, docBaseUrl)); + + assertEquals("<form><li style=\"image\" value=\"image\"><b>android.content.Intent</b></li></form>", + DescriptorsUtils.formatFormText( + "{@link android.content.Intent}", + desc, docBaseUrl)); + + assertEquals("<form><li style=\"image\" value=\"image\"><a href=\"http://base/reference/android/R.styleable.html#AndroidManifestPermission\">AndroidManifestPermission</a></li></form>", + DescriptorsUtils.formatFormText( + "{@link #AndroidManifestPermission}", + desc, docBaseUrl)); + + assertEquals("<form><li style=\"image\" value=\"image\"><a href=\"http://base/reference/android/R.styleable.html#AndroidManifestPermission\">\"permission\"</a></li></form>", + DescriptorsUtils.formatFormText( + "{@link #AndroidManifestPermission <permission>}", + desc, docBaseUrl)); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java new file mode 100644 index 0000000..b0deda0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.layout; + +import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.editors.mock.MockXmlNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; +import com.android.sdklib.SdkConstants; + +import org.w3c.dom.Node; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.util.HashMap; + +import junit.framework.TestCase; + +public class UiElementPullParserTest extends TestCase { + + private UiElementNode ui; + private HashMap<String, String> button1Map; + private HashMap<String, String> button2Map; + private HashMap<String, String> textMap; + + @Override + protected void setUp() throws Exception { + // set up some basic descriptors. + // We have button, textview, linear layout, relative layout. + // only the layouts have children (all 4 descriptors possible) + // Also add some dummy attributes. + ElementDescriptor buttonDescriptor = new ElementDescriptor("Button", "Button", "", "", + new AttributeDescriptor[] { + new TextAttributeDescriptor("name", "name", SdkConstants.NS_RESOURCES, ""), + new TextAttributeDescriptor("text", "text", SdkConstants.NS_RESOURCES, ""), + }, + new ElementDescriptor[] {}, false); + + ElementDescriptor textDescriptor = new ElementDescriptor("TextView", "TextView", "", "", + new AttributeDescriptor[] { + new TextAttributeDescriptor("name", "name", SdkConstants.NS_RESOURCES, ""), + new TextAttributeDescriptor("text", "text", SdkConstants.NS_RESOURCES, ""), }, + new ElementDescriptor[] {}, false); + + ElementDescriptor linearDescriptor = new ElementDescriptor("LinearLayout", "Linear Layout", + "", "", + new AttributeDescriptor[] { + new TextAttributeDescriptor("orientation", "orientation", + SdkConstants.NS_RESOURCES, ""), + }, + new ElementDescriptor[] { }, false); + + ElementDescriptor relativeDescriptor = new ElementDescriptor("RelativeLayout", + "Relative Layout", "", "", + new AttributeDescriptor[] { + new TextAttributeDescriptor("orientation", "orientation", + SdkConstants.NS_RESOURCES, ""), + }, + new ElementDescriptor[] { }, false); + + ElementDescriptor[] a = new ElementDescriptor[] { + buttonDescriptor, textDescriptor, linearDescriptor, relativeDescriptor + }; + + linearDescriptor.setChildren(a); + relativeDescriptor.setChildren(a); + + // document descriptor + ElementDescriptor rootDescriptor = new ElementDescriptor("root", "", "", "", + new AttributeDescriptor[] { }, a, false); + + + ui = new UiElementNode(rootDescriptor); + + /* create a dummy XML file. + * <LinearLayout android:orientation="vertical"> + * <Button android:name="button1" android:text="button1text"/> + * <RelativeLayout android:orientation="toto"> + * <Button android:name="button2" android:text="button2text"/> + * <TextView android:name="text1" android:text="text1text"/> + * </RelativeLayout> + * </LinearLayout> + */ + MockXmlNode button1 = new MockXmlNode(null /* namespace */, "Button", Node.ELEMENT_NODE, + null); + button1.addAttributes(SdkConstants.NS_RESOURCES, "name", "button1"); + button1.addAttributes(SdkConstants.NS_RESOURCES, "text", "button1text"); + + // create a map of the attributes we add to the multi-attribute nodes so that + // we can more easily test the values when we parse the XML. + // This is due to some attributes showing in a certain order for a node and in a different + // order in another node. Since the order doesn't matter, we just simplify the test. + button1Map = new HashMap<String, String>(); + button1Map.put("name", "button1"); + button1Map.put("text", "button1text"); + + MockXmlNode button2 = new MockXmlNode(null /* namespace */, "Button", Node.ELEMENT_NODE, + null); + button2.addAttributes(SdkConstants.NS_RESOURCES, "name", "button2"); + button2.addAttributes(SdkConstants.NS_RESOURCES, "text", "button2text"); + + button2Map = new HashMap<String, String>(); + button2Map.put("name", "button2"); + button2Map.put("text", "button2text"); + + MockXmlNode text = new MockXmlNode(null /* namespace */, "TextView", Node.ELEMENT_NODE, + null); + text.addAttributes(SdkConstants.NS_RESOURCES, "name", "text1"); + text.addAttributes(SdkConstants.NS_RESOURCES, "text", "text1text"); + + textMap = new HashMap<String, String>(); + textMap.put("name", "text1"); + textMap.put("text", "text1text"); + + MockXmlNode relative = new MockXmlNode(null /* namespace */, "RelativeLayout", + Node.ELEMENT_NODE, new MockXmlNode[] { button2, text }); + relative.addAttributes(SdkConstants.NS_RESOURCES, "orientation", "toto"); + + MockXmlNode linear = new MockXmlNode(null /* namespace */, "LinearLayout", + Node.ELEMENT_NODE, new MockXmlNode[] { button1, relative }); + linear.addAttributes(SdkConstants.NS_RESOURCES, "orientation", "vertical"); + + MockXmlNode root = new MockXmlNode(null /* namespace */, "root", Node.ELEMENT_NODE, + new MockXmlNode[] { linear }); + + // put the namespace/prefix in place + root.setPrefix(SdkConstants.NS_RESOURCES, "android"); + + // load the xml into the UiElementNode + ui.loadFromXmlNode(root); + + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testParser() { + try { + // wrap the parser around the ui element node, and start parsing + UiElementPullParser parser = new UiElementPullParser(ui); + + assertEquals(XmlPullParser.START_DOCUMENT, parser.getEventType()); + + // top level Linear layout + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("LinearLayout", parser.getName()); + assertEquals(1, parser.getAttributeCount()); + assertEquals("orientation", parser.getAttributeName(0)); + assertEquals(SdkConstants.NS_RESOURCES, parser.getAttributeNamespace(0)); + assertEquals("android", parser.getAttributePrefix(0)); + assertEquals("vertical", parser.getAttributeValue(0)); + + // Button + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("Button", parser.getName()); + assertEquals(2, parser.getAttributeCount()); + check(parser, 0, button1Map); + check(parser, 1, button1Map); + // end of button + assertEquals(XmlPullParser.END_TAG, parser.next()); + + // Relative Layout + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("RelativeLayout", parser.getName()); + assertEquals(1, parser.getAttributeCount()); + assertEquals("orientation", parser.getAttributeName(0)); + assertEquals(SdkConstants.NS_RESOURCES, parser.getAttributeNamespace(0)); + assertEquals("android", parser.getAttributePrefix(0)); + assertEquals("toto", parser.getAttributeValue(0)); + + // Button + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("Button", parser.getName()); + assertEquals(2, parser.getAttributeCount()); + check(parser, 0, button2Map); + check(parser, 1, button2Map); + // end of button + assertEquals(XmlPullParser.END_TAG, parser.next()); + + // TextView + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("TextView", parser.getName()); + assertEquals(2, parser.getAttributeCount()); + check(parser, 0, textMap); + check(parser, 1, textMap); + // end of TextView + assertEquals(XmlPullParser.END_TAG, parser.next()); + + // end of RelativeLayout + assertEquals(XmlPullParser.END_TAG, parser.next()); + + + // end of top level linear layout + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); + } catch (XmlPullParserException e) { + e.printStackTrace(); + assertTrue(false); + } + } + + /** + * Receives a {@link XmlPullParser} at the START_TAG level, and checks the i-th attribute + * to be present in the {@link HashMap} with the proper (name, value) + * @param parser + * @param i + * @param map + */ + private void check(UiElementPullParser parser, int i, HashMap<String, String> map) { + String name = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + + String referenceValue = map.get(name); + assertNotNull(referenceValue); + assertEquals(referenceValue, value); + + assertEquals(SdkConstants.NS_RESOURCES, parser.getAttributeNamespace(i)); + assertEquals("android", parser.getAttributePrefix(i)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java new file mode 100644 index 0000000..e75d9ef --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.manifest.model; + +import com.android.ide.eclipse.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.editors.mock.MockXmlNode; +import com.android.ide.eclipse.editors.uimodel.UiElementNode; + +import org.w3c.dom.Node; + +import java.util.Iterator; + +import junit.framework.TestCase; + +public class UiElementNodeTest extends TestCase { + + private ElementDescriptor e; + private UiElementNode ui; + + @Override + protected void setUp() throws Exception { + e = new ElementDescriptor("manifest", new ElementDescriptor[] { + new ElementDescriptor("application", new ElementDescriptor[] { + new ElementDescriptor("provider"), + new ElementDescriptor("activity", new ElementDescriptor[] { + new ElementDescriptor("intent-filter") + }), + }), + new ElementDescriptor("permission") + }); + + ui = new UiElementNode(e); + + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + // pass + } + + /** + * Check initialization values for ui node + */ + public void testInit() { + assertSame(e, ui.getDescriptor()); + assertNull(ui.getUiParent()); + assertEquals(0, ui.getUiChildren().size()); + assertEquals(0, ui.getUiAttributes().size()); + } + + /** + * loadFrom() does nothing if the root node doesn't match what's expected + */ + public void testLoadFrom_InvalidRoot() { + assertEquals(0, ui.getUiChildren().size()); + MockXmlNode root = new MockXmlNode(null /* namespace */, "blah", Node.ELEMENT_NODE, null); + ui.loadFromXmlNode(root); + assertEquals(0, ui.getUiChildren().size()); + } + + /** + * UiElementNode.loadFrom should be used to populate an empty ui node from an + * existing XML node tree. + */ + public void testLoadFrom_NewTree_1_Node() { + MockXmlNode root = new MockXmlNode(null /* namespace */, "manifest", Node.ELEMENT_NODE, + new MockXmlNode[] { + new MockXmlNode(null /* namespace */, "application", Node.ELEMENT_NODE, null) + }); + + // get /manifest + ui.loadFromXmlNode(root); + assertEquals("manifest", ui.getDescriptor().getXmlName()); + assertEquals(1, ui.getUiChildren().size()); + assertEquals(0, ui.getUiAttributes().size()); + + // get /manifest/application + Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator(); + UiElementNode application = ui_child_it.next(); + assertEquals("application", application.getDescriptor().getXmlName()); + assertEquals(0, application.getUiChildren().size()); + assertEquals(0, application.getUiAttributes().size()); + } + + + public void testLoadFrom_NewTree_2_Nodes() { + MockXmlNode root = new MockXmlNode(null /* namespace */, "manifest", Node.ELEMENT_NODE, + new MockXmlNode[] { + new MockXmlNode(null /* namespace */, "application", Node.ELEMENT_NODE, null), + new MockXmlNode(null /* namespace */, "permission", Node.ELEMENT_NODE, null), + }); + + // get /manifest + ui.loadFromXmlNode(root); + assertEquals("manifest", ui.getDescriptor().getXmlName()); + assertEquals(2, ui.getUiChildren().size()); + assertEquals(0, ui.getUiAttributes().size()); + + // get /manifest/application + Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator(); + UiElementNode application = ui_child_it.next(); + assertEquals("application", application.getDescriptor().getXmlName()); + assertEquals(0, application.getUiChildren().size()); + assertEquals(0, application.getUiAttributes().size()); + + // get /manifest/permission + UiElementNode first_permission = ui_child_it.next(); + assertEquals("permission", first_permission.getDescriptor().getXmlName()); + assertEquals(0, first_permission.getUiChildren().size()); + assertEquals(0, first_permission.getUiAttributes().size()); + } + + public void testLoadFrom_NewTree_N_Nodes() { + MockXmlNode root = new MockXmlNode(null /* namespace */, "manifest", Node.ELEMENT_NODE, + new MockXmlNode[] { + new MockXmlNode(null /* namespace */, "application", Node.ELEMENT_NODE, + new MockXmlNode[] { + new MockXmlNode(null /* namespace */, "activity", Node.ELEMENT_NODE, + null), + new MockXmlNode(null /* namespace */, "activity", Node.ELEMENT_NODE, + new MockXmlNode[] { + new MockXmlNode(null /* namespace */, "intent-filter", + Node.ELEMENT_NODE, null), + }), + new MockXmlNode(null /* namespace */, "provider", Node.ELEMENT_NODE, + null), + new MockXmlNode(null /* namespace */, "provider", Node.ELEMENT_NODE, + null), + }), + new MockXmlNode(null /* namespace */, "permission", Node.ELEMENT_NODE, + null), + new MockXmlNode(null /* namespace */, "permission", Node.ELEMENT_NODE, + null), + }); + + // get /manifest + ui.loadFromXmlNode(root); + assertEquals("manifest", ui.getDescriptor().getXmlName()); + assertEquals(3, ui.getUiChildren().size()); + assertEquals(0, ui.getUiAttributes().size()); + + // get /manifest/application + Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator(); + UiElementNode application = ui_child_it.next(); + assertEquals("application", application.getDescriptor().getXmlName()); + assertEquals(4, application.getUiChildren().size()); + assertEquals(0, application.getUiAttributes().size()); + + // get /manifest/application/activity #1 + Iterator<UiElementNode> app_child_it = application.getUiChildren().iterator(); + UiElementNode first_activity = app_child_it.next(); + assertEquals("activity", first_activity.getDescriptor().getXmlName()); + assertEquals(0, first_activity.getUiChildren().size()); + assertEquals(0, first_activity.getUiAttributes().size()); + + // get /manifest/application/activity #2 + UiElementNode second_activity = app_child_it.next(); + assertEquals("activity", second_activity.getDescriptor().getXmlName()); + assertEquals(1, second_activity.getUiChildren().size()); + assertEquals(0, second_activity.getUiAttributes().size()); + + // get /manifest/application/activity #2/intent-filter #1 + Iterator<UiElementNode> activity_child_it = second_activity.getUiChildren().iterator(); + UiElementNode intent_filter = activity_child_it.next(); + assertEquals("intent-filter", intent_filter.getDescriptor().getXmlName()); + assertEquals(0, intent_filter.getUiChildren().size()); + assertEquals(0, intent_filter.getUiAttributes().size()); + + // get /manifest/application/provider #1 + UiElementNode first_provider = app_child_it.next(); + assertEquals("provider", first_provider.getDescriptor().getXmlName()); + assertEquals(0, first_provider.getUiChildren().size()); + assertEquals(0, first_provider.getUiAttributes().size()); + + // get /manifest/application/provider #2 + UiElementNode second_provider = app_child_it.next(); + assertEquals("provider", second_provider.getDescriptor().getXmlName()); + assertEquals(0, second_provider.getUiChildren().size()); + assertEquals(0, second_provider.getUiAttributes().size()); + + // get /manifest/permission #1 + UiElementNode first_permission = ui_child_it.next(); + assertEquals("permission", first_permission.getDescriptor().getXmlName()); + assertEquals(0, first_permission.getUiChildren().size()); + assertEquals(0, first_permission.getUiAttributes().size()); + + // get /manifest/permission #1 + UiElementNode second_permission = ui_child_it.next(); + assertEquals("permission", second_permission.getDescriptor().getXmlName()); + assertEquals(0, second_permission.getUiChildren().size()); + assertEquals(0, second_permission.getUiAttributes().size()); + } + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java new file mode 100644 index 0000000..b1f089b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.mock; + +import org.w3c.dom.DOMException; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.util.ArrayList; +import java.util.HashMap; + +class MockNamedNodeMap implements NamedNodeMap { + + /** map for access by namespace/name */ + private final HashMap<String, HashMap<String, Node>> mNodeMap = + new HashMap<String, HashMap<String, Node>>(); + + /** list for access by index */ + private final ArrayList<Node> mNodeList = new ArrayList<Node>(); + + public MockXmlNode addAttribute(String namespace, String localName, String value) { + MockXmlNode node = new MockXmlNode(namespace, localName, value); + + if (namespace == null) { + namespace = ""; // no namespace + } + + // get the map for the namespace + HashMap<String, Node> map = mNodeMap.get(namespace); + if (map == null) { + map = new HashMap<String, Node>(); + mNodeMap.put(namespace, map); + } + + + map.put(localName, node); + mNodeList.add(node); + + return node; + } + + // --------- NamedNodeMap ------- + + public int getLength() { + return mNodeList.size(); + } + + public Node getNamedItem(String name) { + HashMap<String, Node> map = mNodeMap.get(""); // no namespace + if (map != null) { + return map.get(name); + } + + return null; + } + + public Node getNamedItemNS(String namespaceURI, String localName) throws DOMException { + if (namespaceURI == null) { + namespaceURI = ""; //no namespace + } + + HashMap<String, Node> map = mNodeMap.get(namespaceURI); + if (map != null) { + return map.get(localName); + } + + return null; + } + + public Node item(int index) { + return mNodeList.get(index); + } + + public Node removeNamedItem(String name) throws DOMException { + throw new NotImplementedException(); + } + + public Node removeNamedItemNS(String namespaceURI, String localName) throws DOMException { + throw new NotImplementedException(); + } + + public Node setNamedItem(Node arg) throws DOMException { + throw new NotImplementedException(); + } + + public Node setNamedItemNS(Node arg) throws DOMException { + throw new NotImplementedException(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java new file mode 100644 index 0000000..d766af7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.mock; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; + + +/** + * A quick mock implementation of NodeList on top of ArrayList. + */ +public class MockNodeList implements NodeList { + + ArrayList<MockXmlNode> mChildren; + + /** + * Constructs a node list from a given children list. + * + * @param children The children list. Can be null. + */ + public MockNodeList(MockXmlNode[] children) { + mChildren = new ArrayList<MockXmlNode>(); + if (children != null) { + for (MockXmlNode n : children) { + mChildren.add(n); + } + } + } + + public int getLength() { + return mChildren.size(); + } + + public Node item(int index) { + if (index >= 0 && index < mChildren.size()) { + return mChildren.get(index); + } + return null; + } + + public ArrayList<MockXmlNode> getArrayList() { + return mChildren; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java new file mode 100644 index 0000000..4a9dbbd --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.mock; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.UserDataHandler; + +import java.util.HashMap; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + + +/** + * A mock XML node with only a minimal set of information. + */ +public class MockXmlNode implements Node { + + MockNodeList mNodeList; + private String mLocalName; + private String mNamespace; + private short mNodeType; + private MockXmlNode mParent; + private MockXmlNode mPreviousSibling; + private MockXmlNode mNextSibling; + private String mAttrValue; + private MockNamedNodeMap mAttributes; + + // namespace stuff only set in the root node + /** map from namespace to prefix. */ + private HashMap<String, String> mNsMap = null; + + /** + * Constructs a node from a given children list. + * + * @param namespace The namespace of the node or null if none + * @param localName The XML local node name. + * @param node_type One of Node.xxx_NODE constants, e.g. Node.ELEMENT_NODE + * @param children The children list. Can be null. + */ + public MockXmlNode(String namespace, String localName, short node_type, + MockXmlNode[] children) { + mLocalName = localName; + mNamespace = namespace; + mNodeType = node_type; + mNodeList = new MockNodeList(children); + fixNavigation(); + } + + /** + * Constructs an attribute node + * + * @param namespace The namespace of the node or null if none + * @param localName The XML local node name. + * @param value the value of the attribute + */ + public MockXmlNode(String namespace, String localName, String value) { + mLocalName = localName; + mNamespace = namespace; + mAttrValue = value; + mNodeType = Node.ATTRIBUTE_NODE; + mNodeList = new MockNodeList(new MockXmlNode[0]); + fixNavigation(); + } + + private void fixNavigation() { + MockXmlNode prev = null; + for (MockXmlNode n : mNodeList.getArrayList()) { + n.mParent = this; + n.mPreviousSibling = prev; + if (prev != null) { + prev.mNextSibling = n; + } + n.fixNavigation(); + prev = n; + } + } + + public void addAttributes(String namespaceURI, String localName, String value) { + if (mAttributes == null) { + mAttributes = new MockNamedNodeMap(); + } + + MockXmlNode node = mAttributes.addAttribute(namespaceURI, localName, value); + node.mParent = this; + } + + public void setPrefix(String namespace, String prefix) { + if (mNsMap == null) { + mNsMap = new HashMap<String, String>(); + } + + mNsMap.put(namespace, prefix); + } + + public String getPrefix(String namespace) { + if (mNsMap != null) { + return mNsMap.get(namespace); + } + + return mParent.getPrefix(namespace); + } + + + // ----------- Node methods + + public Node appendChild(Node newChild) throws DOMException { + mNodeList.getArrayList().add((MockXmlNode) newChild); + return newChild; + } + + public NamedNodeMap getAttributes() { + return mAttributes; + } + + public NodeList getChildNodes() { + return mNodeList; + } + + public Node getFirstChild() { + if (mNodeList.getLength() > 0) { + return mNodeList.item(0); + } + return null; + } + + public Node getLastChild() { + if (mNodeList.getLength() > 0) { + return mNodeList.item(mNodeList.getLength() - 1); + } + return null; + } + + public Node getNextSibling() { + return mNextSibling; + } + + public String getNodeName() { + return mLocalName; + } + + public String getLocalName() { + return mLocalName; + } + + public short getNodeType() { + return mNodeType; + } + + public Node getParentNode() { + return mParent; + } + + public Node getPreviousSibling() { + return mPreviousSibling; + } + + public boolean hasChildNodes() { + return mNodeList.getLength() > 0; + } + + public boolean hasAttributes() { + // TODO Auto-generated method stub + throw new NotImplementedException(); + //return false; + } + + public boolean isSameNode(Node other) { + return this == other; + } + + public String getNodeValue() throws DOMException { + return mAttrValue; + } + + public String getPrefix() { + return getPrefix(getNamespaceURI()); + } + + public String getNamespaceURI() { + return mNamespace; + } + + + // --- methods not implemented --- + + public Node cloneNode(boolean deep) { + throw new NotImplementedException(); + } + + public short compareDocumentPosition(Node other) throws DOMException { + throw new NotImplementedException(); + } + + public String getBaseURI() { + throw new NotImplementedException(); + } + + public Object getFeature(String feature, String version) { + throw new NotImplementedException(); + } + + public Document getOwnerDocument() { + throw new NotImplementedException(); + } + + public String getTextContent() throws DOMException { + throw new NotImplementedException(); + } + + public Object getUserData(String key) { + throw new NotImplementedException(); + } + + public Node insertBefore(Node newChild, Node refChild) + throws DOMException { + throw new NotImplementedException(); + } + + public boolean isDefaultNamespace(String namespaceURI) { + throw new NotImplementedException(); + } + + public boolean isEqualNode(Node arg) { + throw new NotImplementedException(); + } + + public boolean isSupported(String feature, String version) { + throw new NotImplementedException(); + } + + public String lookupNamespaceURI(String prefix) { + throw new NotImplementedException(); + } + + public String lookupPrefix(String namespaceURI) { + throw new NotImplementedException(); + } + + public void normalize() { + throw new NotImplementedException(); + } + + public Node removeChild(Node oldChild) throws DOMException { + throw new NotImplementedException(); + } + + public Node replaceChild(Node newChild, Node oldChild) + throws DOMException { + throw new NotImplementedException(); + } + + public void setNodeValue(String nodeValue) throws DOMException { + throw new NotImplementedException(); + } + + public void setPrefix(String prefix) throws DOMException { + throw new NotImplementedException(); + } + + public void setTextContent(String textContent) throws DOMException { + throw new NotImplementedException(); + } + + public Object setUserData(String key, Object data, + UserDataHandler handler) { + throw new NotImplementedException(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java new file mode 100644 index 0000000..bedaa0d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class CountryCodeQualifierTest extends TestCase { + + private CountryCodeQualifier mccq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mccq = new CountryCodeQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mccq = null; + config = null; + } + + public void testCheckAndSet() { + assertEquals(true, mccq.checkAndSet("mcc123", config));//$NON-NLS-1$ + assertTrue(config.getCountryCodeQualifier() != null); + assertEquals(123, config.getCountryCodeQualifier().getCode()); + assertEquals("mcc123", config.getCountryCodeQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, mccq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, mccq.checkAndSet("mcc", config));//$NON-NLS-1$ + assertEquals(false, mccq.checkAndSet("MCC123", config));//$NON-NLS-1$ + assertEquals(false, mccq.checkAndSet("123", config));//$NON-NLS-1$ + assertEquals(false, mccq.checkAndSet("mccsdf", config));//$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java new file mode 100644 index 0000000..e15434e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class KeyboardStateQualifierTest extends TestCase { + + private KeyboardStateQualifier ksq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ksq = new KeyboardStateQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + ksq = null; + config = null; + } + + public void testExposed() { + assertEquals(true, ksq.checkAndSet("keysexposed", config)); //$NON-NLS-1$ + assertTrue(config.getKeyboardStateQualifier() != null); + assertEquals(KeyboardStateQualifier.KeyboardState.EXPOSED, + config.getKeyboardStateQualifier().getValue()); + assertEquals("keysexposed", config.getKeyboardStateQualifier().toString()); //$NON-NLS-1$ + } + + public void testHidden() { + assertEquals(true, ksq.checkAndSet("keyshidden", config)); //$NON-NLS-1$ + assertTrue(config.getKeyboardStateQualifier() != null); + assertEquals(KeyboardStateQualifier.KeyboardState.HIDDEN, + config.getKeyboardStateQualifier().getValue()); + assertEquals("keyshidden", config.getKeyboardStateQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, ksq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, ksq.checkAndSet("KEYSEXPOSED", config));//$NON-NLS-1$ + assertEquals(false, ksq.checkAndSet("other", config));//$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java new file mode 100644 index 0000000..d00972f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class LanguageQualifierTest extends TestCase { + + private FolderConfiguration config; + private LanguageQualifier lq; + + @Override + public void setUp() throws Exception { + super.setUp(); + config = new FolderConfiguration(); + lq = new LanguageQualifier(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + config = null; + lq = null; + } + + public void testCheckAndSet() { + assertEquals(true, lq.checkAndSet("en", config)); //$NON-NLS-1$ + assertTrue(config.getLanguageQualifier() != null); + assertEquals("en", config.getLanguageQualifier().toString()); //$NON-NLS-1$ + + } + + public void testFailures() { + assertEquals(false, lq.checkAndSet("", config)); //$NON-NLS-1$ + assertEquals(false, lq.checkAndSet("EN", config)); //$NON-NLS-1$ + assertEquals(false, lq.checkAndSet("abc", config)); //$NON-NLS-1$ + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java new file mode 100644 index 0000000..d672f1f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class NavigationMethodQualifierTest extends TestCase { + + private FolderConfiguration config; + private NavigationMethodQualifier nmq; + + @Override + public void setUp() throws Exception { + super.setUp(); + config = new FolderConfiguration(); + nmq = new NavigationMethodQualifier(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + config = null; + nmq = null; + } + + public void testDPad() { + assertEquals(true, nmq.checkAndSet("dpad", config)); //$NON-NLS-1$ + assertTrue(config.getNavigationMethodQualifier() != null); + assertEquals(NavigationMethodQualifier.NavigationMethod.DPAD, + config.getNavigationMethodQualifier().getValue()); + assertEquals("dpad", config.getNavigationMethodQualifier().toString()); //$NON-NLS-1$ + } + + public void testTrackball() { + assertEquals(true, nmq.checkAndSet("trackball", config)); //$NON-NLS-1$ + assertTrue(config.getNavigationMethodQualifier() != null); + assertEquals(NavigationMethodQualifier.NavigationMethod.TRACKBALL, + config.getNavigationMethodQualifier().getValue()); + assertEquals("trackball", config.getNavigationMethodQualifier().toString()); //$NON-NLS-1$ + } + + public void testWheel() { + assertEquals(true, nmq.checkAndSet("wheel", config)); //$NON-NLS-1$ + assertTrue(config.getNavigationMethodQualifier() != null); + assertEquals(NavigationMethodQualifier.NavigationMethod.WHEEL, + config.getNavigationMethodQualifier().getValue()); + assertEquals("wheel", config.getNavigationMethodQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, nmq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, nmq.checkAndSet("WHEEL", config));//$NON-NLS-1$ + assertEquals(false, nmq.checkAndSet("other", config));//$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java new file mode 100644 index 0000000..4567dff --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class NetworkCodeQualifierTest extends TestCase { + + private NetworkCodeQualifier mncq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mncq = new NetworkCodeQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mncq = null; + config = null; + } + + public void testCheckAndSet() { + assertEquals(true, mncq.checkAndSet("mnc123", config));//$NON-NLS-1$ + assertTrue(config.getNetworkCodeQualifier() != null); + assertEquals(123, config.getNetworkCodeQualifier().getCode()); + assertEquals("mnc123", config.getNetworkCodeQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, mncq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, mncq.checkAndSet("mnc", config));//$NON-NLS-1$ + assertEquals(false, mncq.checkAndSet("MNC123", config));//$NON-NLS-1$ + assertEquals(false, mncq.checkAndSet("123", config));//$NON-NLS-1$ + assertEquals(false, mncq.checkAndSet("mncsdf", config));//$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java new file mode 100644 index 0000000..2c4cd2f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class PixelDensityQualifierTest extends TestCase { + + private PixelDensityQualifier pdq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + pdq = new PixelDensityQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + pdq = null; + config = null; + } + + public void testCheckAndSet() { + assertEquals(true, pdq.checkAndSet("123dpi", config));//$NON-NLS-1$ + assertTrue(config.getPixelDensityQualifier() != null); + assertEquals(123, config.getPixelDensityQualifier().getValue()); + assertEquals("123dpi", config.getPixelDensityQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, pdq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, pdq.checkAndSet("dpi", config));//$NON-NLS-1$ + assertEquals(false, pdq.checkAndSet("123DPI", config));//$NON-NLS-1$ + assertEquals(false, pdq.checkAndSet("123", config));//$NON-NLS-1$ + assertEquals(false, pdq.checkAndSet("sdfdpi", config));//$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java new file mode 100644 index 0000000..8a9e6f8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class RegionQualifierTest extends TestCase { + + private RegionQualifier rq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + rq = new RegionQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + rq = null; + config = null; + } + + public void testCheckAndSet() { + assertEquals(true, rq.checkAndSet("rUS", config));//$NON-NLS-1$ + assertTrue(config.getRegionQualifier() != null); + assertEquals("US", config.getRegionQualifier().getValue()); //$NON-NLS-1$ + assertEquals("rUS", config.getRegionQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, rq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, rq.checkAndSet("rus", config));//$NON-NLS-1$ + assertEquals(false, rq.checkAndSet("rUSA", config));//$NON-NLS-1$ + assertEquals(false, rq.checkAndSet("abc", config));//$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java new file mode 100644 index 0000000..681d4e0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class ScreenDimensionQualifierTest extends TestCase { + + private ScreenDimensionQualifier sdq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + sdq = new ScreenDimensionQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + sdq = null; + config = null; + } + + public void testCheckAndSet() { + assertEquals(true, sdq.checkAndSet("400x200", config));//$NON-NLS-1$ + assertTrue(config.getScreenDimensionQualifier() != null); + assertEquals(400, config.getScreenDimensionQualifier().getValue1()); + assertEquals(200, config.getScreenDimensionQualifier().getValue2()); + assertEquals("400x200", config.getScreenDimensionQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, sdq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, sdq.checkAndSet("400X200", config));//$NON-NLS-1$ + assertEquals(false, sdq.checkAndSet("x200", config));//$NON-NLS-1$ + assertEquals(false, sdq.checkAndSet("ax200", config));//$NON-NLS-1$ + assertEquals(false, sdq.checkAndSet("400x", config));//$NON-NLS-1$ + assertEquals(false, sdq.checkAndSet("400xa", config));//$NON-NLS-1$ + assertEquals(false, sdq.checkAndSet("other", config));//$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java new file mode 100644 index 0000000..28f9961 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class ScreenOrientationQualifierTest extends TestCase { + + private ScreenOrientationQualifier soq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + soq = new ScreenOrientationQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + soq = null; + config = null; + } + + public void testPortrait() { + assertEquals(true, soq.checkAndSet("port", config)); //$NON-NLS-1$ + assertTrue(config.getScreenOrientationQualifier() != null); + assertEquals(ScreenOrientationQualifier.ScreenOrientation.PORTRAIT, + config.getScreenOrientationQualifier().getValue()); + assertEquals("port", config.getScreenOrientationQualifier().toString()); //$NON-NLS-1$ + } + + public void testLanscape() { + assertEquals(true, soq.checkAndSet("land", config)); //$NON-NLS-1$ + assertTrue(config.getScreenOrientationQualifier() != null); + assertEquals(ScreenOrientationQualifier.ScreenOrientation.LANDSCAPE, + config.getScreenOrientationQualifier().getValue()); + assertEquals("land", config.getScreenOrientationQualifier().toString()); //$NON-NLS-1$ + } + + public void testSquare() { + assertEquals(true, soq.checkAndSet("square", config)); //$NON-NLS-1$ + assertTrue(config.getScreenOrientationQualifier() != null); + assertEquals(ScreenOrientationQualifier.ScreenOrientation.SQUARE, + config.getScreenOrientationQualifier().getValue()); + assertEquals("square", config.getScreenOrientationQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, soq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, soq.checkAndSet("PORT", config));//$NON-NLS-1$ + assertEquals(false, soq.checkAndSet("landscape", config));//$NON-NLS-1$ + assertEquals(false, soq.checkAndSet("portrait", config));//$NON-NLS-1$ + assertEquals(false, soq.checkAndSet("other", config));//$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java new file mode 100644 index 0000000..28f7871 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class TextInputMethodQualifierTest extends TestCase { + + private TextInputMethodQualifier timq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + timq = new TextInputMethodQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + timq = null; + config = null; + } + + public void testQuerty() { + assertEquals(true, timq.checkAndSet("qwerty", config)); //$NON-NLS-1$ + assertTrue(config.getTextInputMethodQualifier() != null); + assertEquals(TextInputMethodQualifier.TextInputMethod.QWERTY, + config.getTextInputMethodQualifier().getValue()); + assertEquals("qwerty", config.getTextInputMethodQualifier().toString()); //$NON-NLS-1$ + } + + public void test12Key() { + assertEquals(true, timq.checkAndSet("12key", config)); //$NON-NLS-1$ + assertTrue(config.getTextInputMethodQualifier() != null); + assertEquals(TextInputMethodQualifier.TextInputMethod.TWELVEKEYS, + config.getTextInputMethodQualifier().getValue()); + assertEquals("12key", config.getTextInputMethodQualifier().toString()); //$NON-NLS-1$ + } + + public void testNoKey() { + assertEquals(true, timq.checkAndSet("nokeys", config)); //$NON-NLS-1$ + assertTrue(config.getTextInputMethodQualifier() != null); + assertEquals(TextInputMethodQualifier.TextInputMethod.NOKEY, + config.getTextInputMethodQualifier().getValue()); + assertEquals("nokeys", config.getTextInputMethodQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, timq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, timq.checkAndSet("QWERTY", config));//$NON-NLS-1$ + assertEquals(false, timq.checkAndSet("12keys", config));//$NON-NLS-1$ + assertEquals(false, timq.checkAndSet("*12key", config));//$NON-NLS-1$ + assertEquals(false, timq.checkAndSet("other", config));//$NON-NLS-1$ + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java new file mode 100644 index 0000000..9a788ad --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.configurations; + +import junit.framework.TestCase; + +public class TouchScreenQualifierTest extends TestCase { + + private TouchScreenQualifier tsq; + private FolderConfiguration config; + + @Override + protected void setUp() throws Exception { + super.setUp(); + tsq = new TouchScreenQualifier(); + config = new FolderConfiguration(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + tsq = null; + config = null; + } + + public void testNoTouch() { + assertEquals(true, tsq.checkAndSet("notouch", config)); //$NON-NLS-1$ + assertTrue(config.getTouchTypeQualifier() != null); + assertEquals(TouchScreenQualifier.TouchScreenType.NOTOUCH, + config.getTouchTypeQualifier().getValue()); + assertEquals("notouch", config.getTouchTypeQualifier().toString()); //$NON-NLS-1$ + } + + public void testFinger() { + assertEquals(true, tsq.checkAndSet("finger", config)); //$NON-NLS-1$ + assertTrue(config.getTouchTypeQualifier() != null); + assertEquals(TouchScreenQualifier.TouchScreenType.FINGER, + config.getTouchTypeQualifier().getValue()); + assertEquals("finger", config.getTouchTypeQualifier().toString()); //$NON-NLS-1$ + } + + public void testStylus() { + assertEquals(true, tsq.checkAndSet("stylus", config)); //$NON-NLS-1$ + assertTrue(config.getTouchTypeQualifier() != null); + assertEquals(TouchScreenQualifier.TouchScreenType.STYLUS, + config.getTouchTypeQualifier().getValue()); + assertEquals("stylus", config.getTouchTypeQualifier().toString()); //$NON-NLS-1$ + } + + public void testFailures() { + assertEquals(false, tsq.checkAndSet("", config));//$NON-NLS-1$ + assertEquals(false, tsq.checkAndSet("STYLUS", config));//$NON-NLS-1$ + assertEquals(false, tsq.checkAndSet("other", config));//$NON-NLS-1$ + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java new file mode 100644 index 0000000..25a86c3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; +import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier.KeyboardState; +import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier.NavigationMethod; +import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier.ScreenOrientation; +import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier.TextInputMethod; +import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier.TouchScreenType; +import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder; +import com.android.ide.eclipse.editors.resources.manager.files.IFileWrapper; +import com.android.ide.eclipse.editors.resources.manager.files.IFolderWrapper; +import com.android.ide.eclipse.mock.FileMock; +import com.android.ide.eclipse.mock.FolderMock; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; + +import junit.framework.TestCase; + +public class ConfigMatchTest extends TestCase { + private static final String SEARCHED_FILENAME = "main.xml"; //$NON-NLS-1$ + private static final String MISC1_FILENAME = "foo.xml"; //$NON-NLS-1$ + private static final String MISC2_FILENAME = "bar.xml"; //$NON-NLS-1$ + + private ProjectResources mResources; + private ResourceQualifier[] mQualifierList; + private FolderConfiguration config4; + private FolderConfiguration config3; + private FolderConfiguration config2; + private FolderConfiguration config1; + + @SuppressWarnings("unchecked") + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create a Resource Manager to get a list of qualifier as instantiated by the real code. + // Thanks for QualifierListTest we know this contains all the qualifiers. + ResourceManager manager = ResourceManager.getInstance(); + Field qualifierListField = ResourceManager.class.getDeclaredField("mQualifiers"); + assertNotNull(qualifierListField); + qualifierListField.setAccessible(true); + + // get the actual list. + mQualifierList = (ResourceQualifier[])qualifierListField.get(manager); + + // create the project resources. + mResources = new ProjectResources(false /* isFrameworkRepository */); + + // create 2 arrays of IResource. one with the filename being looked up, and one without. + // Since the required API uses IResource, we can use MockFolder for them. + FileMock[] validMemberList = new FileMock[] { + new FileMock(MISC1_FILENAME), + new FileMock(SEARCHED_FILENAME), + new FileMock(MISC2_FILENAME), + }; + FileMock[] invalidMemberList = new FileMock[] { + new FileMock(MISC1_FILENAME), + new FileMock(MISC2_FILENAME), + }; + + // add multiple ResourceFolder to the project resource. + FolderConfiguration defaultConfig = getConfiguration( + null, // country code + null, // network code + null, // language + null, // region + null, // screen orientation + null, // dpi + null, // touch mode + null, // keyboard state + null, // text input + null, // navigation + null); // screen size + + addFolder(mResources, defaultConfig, validMemberList); + + config1 = getConfiguration( + null, // country code + null, // network code + "en", // language + null, // region + null, // screen orientation + null, // dpi + null, // touch mode + KeyboardState.EXPOSED.getValue(), // keyboard state + null, // text input + null, // navigation + null); // screen size + + addFolder(mResources, config1, validMemberList); + + config2 = getConfiguration( + null, // country code + null, // network code + "en", // language + null, // region + null, // screen orientation + null, // dpi + null, // touch mode + KeyboardState.HIDDEN.getValue(), // keyboard state + null, // text input + null, // navigation + null); // screen size + + addFolder(mResources, config2, validMemberList); + + config3 = getConfiguration( + null, // country code + null, // network code + "en", // language + null, // region + ScreenOrientation.LANDSCAPE.getValue(), // screen orientation + null, // dpi + null, // touch mode + null, // keyboard state + null, // text input + null, // navigation + null); // screen size + + addFolder(mResources, config3, validMemberList); + + config4 = getConfiguration( + "mcc310", // country code + "mnc435", // network code + "en", // language + "rUS", // region + ScreenOrientation.LANDSCAPE.getValue(), // screen orientation + "160dpi", // dpi + TouchScreenType.FINGER.getValue(), // touch mode + KeyboardState.EXPOSED.getValue(), // keyboard state + TextInputMethod.QWERTY.getValue(), // text input + NavigationMethod.DPAD.getValue(), // navigation + "480x320"); // screen size + + addFolder(mResources, config4, invalidMemberList); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mResources = null; + } + + public void test1() { + FolderConfiguration testConfig = getConfiguration( + "mcc310", // country code + "mnc435", // network code + "en", // language + "rUS", // region + ScreenOrientation.LANDSCAPE.getValue(), // screen orientation + "160dpi", // dpi + TouchScreenType.FINGER.getValue(), // touch mode + KeyboardState.EXPOSED.getValue(), // keyboard state + TextInputMethod.QWERTY.getValue(), // text input + NavigationMethod.DPAD.getValue(), // navigation + "480x320"); // screen size + + ResourceFile result = mResources.getMatchingFile(SEARCHED_FILENAME, + ResourceFolderType.LAYOUT, testConfig); + + boolean bresult = result.getFolder().getConfiguration().equals(config3); + assertEquals(bresult, true); + } + + /** + * Creates a {@link FolderConfiguration}. + * @param qualifierValues The list of qualifier values. The length must equals the total number + * of Qualifiers. <code>null</code> is permitted and will make the FolderConfiguration not use + * this particular qualifier. + */ + private FolderConfiguration getConfiguration(String... qualifierValues) { + FolderConfiguration config = new FolderConfiguration(); + + // those must be of the same length + assertEquals(qualifierValues.length, mQualifierList.length); + + int index = 0; + + for (ResourceQualifier qualifier : mQualifierList) { + String value = qualifierValues[index++]; + if (value != null) { + assertTrue(qualifier.checkAndSet(value, config)); + } + } + + return config; + } + + /** + * Adds a folder to the given {@link ProjectResources} with the given + * {@link FolderConfiguration}. The folder is filled with files from the provided list. + * @param resources the {@link ProjectResources} in which to add the folder. + * @param config the {@link FolderConfiguration} for the created folder. + * @param memberList the list of files for the folder. + */ + private void addFolder(ProjectResources resources, FolderConfiguration config, + FileMock[] memberList) throws Exception { + + // figure out the folder name based on the configuration + String folderName = "layout"; + if (config.isDefault() == false) { + folderName += "-" + config.toString(); + } + + // create the folder mock + FolderMock folder = new FolderMock(folderName, memberList); + + // add it to the resource, and get back a ResourceFolder object. + ResourceFolder resFolder = _addProjectResourceFolder(resources, config, folder); + + // and fill it with files from the list. + for (FileMock file : memberList) { + resFolder.addFile(new SingleResourceFile(new IFileWrapper(file), resFolder)); + } + } + + /** Calls ProjectResource.add method via reflection to circumvent access + * restrictions that are enforced when running in the plug-in environment + * ie cannot access package or protected members in a different plug-in, even + * if they are in the same declared package as the accessor + */ + private ResourceFolder _addProjectResourceFolder(ProjectResources resources, + FolderConfiguration config, FolderMock folder) throws Exception { + + Method addMethod = ProjectResources.class.getDeclaredMethod("add", + ResourceFolderType.class, FolderConfiguration.class, + IAbstractFolder.class); + addMethod.setAccessible(true); + ResourceFolder resFolder = (ResourceFolder)addMethod.invoke(resources, + ResourceFolderType.LAYOUT, config, new IFolderWrapper(folder)); + return resFolder; + } + + + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java new file mode 100644 index 0000000..6a555a4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.editors.resources.manager; + +import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration; +import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +import junit.framework.TestCase; + +public class QualifierListTest extends TestCase { + + private ResourceManager mManager; + + @Override + public void setUp() throws Exception { + super.setUp(); + + mManager = ResourceManager.getInstance(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mManager = null; + } + + @SuppressWarnings("unchecked") + public void testQualifierList() { + try { + // get the list of qualifier in the resource manager + Field qualifierListField = ResourceManager.class.getDeclaredField("mQualifiers"); + assertNotNull(qualifierListField); + qualifierListField.setAccessible(true); + + // get the actual list. + ResourceQualifier[] qualifierList = + (ResourceQualifier[])qualifierListField.get(mManager); + + // now get the number of qualifier in the FolderConfiguration + Field qualCountField = FolderConfiguration.class.getDeclaredField("INDEX_COUNT"); + assertNotNull(qualCountField); + qualCountField.setAccessible(true); + + // get the constant value + Integer count = (Integer)qualCountField.get(null); + + // now compare + assertEquals(count.intValue(), qualifierList.length); + } catch (SecurityException e) { + assertTrue(false); + } catch (NoSuchFieldException e) { + assertTrue(false); + } catch (IllegalArgumentException e) { + assertTrue(false); + } catch (IllegalAccessException e) { + assertTrue(false); + } + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java new file mode 100644 index 0000000..010aadf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.mock; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IAccessRule; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +public class ClasspathEntryMock implements IClasspathEntry { + + private int mKind; + private IPath mPath; + + public ClasspathEntryMock(IPath path, int kind) { + mPath = path; + mKind = kind; + } + + public int getEntryKind() { + return mKind; + } + + public IPath getPath() { + return mPath; + } + + // -------- UNIMPLEMENTED METHODS ---------------- + + public boolean combineAccessRules() { + throw new NotImplementedException(); + } + + public IAccessRule[] getAccessRules() { + throw new NotImplementedException(); + } + + public int getContentKind() { + throw new NotImplementedException(); + } + + public IPath[] getExclusionPatterns() { + throw new NotImplementedException(); + } + + public IClasspathAttribute[] getExtraAttributes() { + throw new NotImplementedException(); + } + + public IPath[] getInclusionPatterns() { + throw new NotImplementedException(); + } + + public IPath getOutputLocation() { + throw new NotImplementedException(); + } + + public IClasspathEntry getResolvedEntry() { + throw new NotImplementedException(); + } + + public IPath getSourceAttachmentPath() { + throw new NotImplementedException(); + } + + public IPath getSourceAttachmentRootPath() { + throw new NotImplementedException(); + } + + public boolean isExported() { + throw new NotImplementedException(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java new file mode 100644 index 0000000..a95286c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.mock; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFileState; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResourceProxy; +import org.eclipse.core.resources.IResourceProxyVisitor; +import org.eclipse.core.resources.IResourceVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourceAttributes; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.content.IContentDescription; +import org.eclipse.core.runtime.jobs.ISchedulingRule; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; +import java.util.Map; + +/** + * Mock implementation of {@link IFile}. + * <p/>Supported methods: + * <ul> + * </ul> + */ +public class FileMock implements IFile { + + private String mName; + + public FileMock(String name) { + mName = name; + } + + // -------- MOCKED METHODS ---------------- + + public String getName() { + return mName; + } + + // -------- UNIMPLEMENTED METHODS ---------------- + + public void appendContents(InputStream source, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void appendContents(InputStream source, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void create(InputStream source, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void create(InputStream source, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void createLink(IPath localLocation, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void createLink(URI location, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void delete(boolean force, boolean keepHistory, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public String getCharset() throws CoreException { + throw new NotImplementedException(); + } + + public String getCharset(boolean checkImplicit) throws CoreException { + throw new NotImplementedException(); + } + + public String getCharsetFor(Reader reader) throws CoreException { + throw new NotImplementedException(); + } + + public IContentDescription getContentDescription() throws CoreException { + throw new NotImplementedException(); + } + + public InputStream getContents() throws CoreException { + throw new NotImplementedException(); + } + + public InputStream getContents(boolean force) throws CoreException { + throw new NotImplementedException(); + } + + public int getEncoding() throws CoreException { + throw new NotImplementedException(); + } + + public IPath getFullPath() { + throw new NotImplementedException(); + } + + public IFileState[] getHistory(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public boolean isReadOnly() { + throw new NotImplementedException(); + } + + public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void setCharset(String newCharset) throws CoreException { + throw new NotImplementedException(); + } + + public void setCharset(String newCharset, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void setContents(InputStream source, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void setContents(IFileState source, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void setContents(InputStream source, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void setContents(IFileState source, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceProxyVisitor visitor, int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor, int depth, boolean includePhantoms) + throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor, int depth, int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void clearHistory(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IPath destination, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IPath destination, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IProjectDescription description, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IProjectDescription description, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IMarker createMarker(String type) throws CoreException { + throw new NotImplementedException(); + } + + public IResourceProxy createProxy() { + throw new NotImplementedException(); + } + + public void delete(boolean force, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void delete(int updateFlags, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException { + throw new NotImplementedException(); + } + + public boolean exists() { + throw new NotImplementedException(); + } + + public IMarker findMarker(long id) throws CoreException { + throw new NotImplementedException(); + } + + public IMarker[] findMarkers(String type, boolean includeSubtypes, int depth) + throws CoreException { + throw new NotImplementedException(); + } + + public int findMaxProblemSeverity(String type, boolean includeSubtypes, int depth) + throws CoreException { + throw new NotImplementedException(); + } + + public String getFileExtension() { + throw new NotImplementedException(); + } + + public long getLocalTimeStamp() { + throw new NotImplementedException(); + } + + public IPath getLocation() { + return new Path(mName); + } + + public URI getLocationURI() { + throw new NotImplementedException(); + } + + public IMarker getMarker(long id) { + throw new NotImplementedException(); + } + + public long getModificationStamp() { + throw new NotImplementedException(); + } + + public IContainer getParent() { + throw new NotImplementedException(); + } + + public String getPersistentProperty(QualifiedName key) throws CoreException { + throw new NotImplementedException(); + } + + public IProject getProject() { + throw new NotImplementedException(); + } + + public IPath getProjectRelativePath() { + throw new NotImplementedException(); + } + + public IPath getRawLocation() { + throw new NotImplementedException(); + } + + public URI getRawLocationURI() { + throw new NotImplementedException(); + } + + public ResourceAttributes getResourceAttributes() { + throw new NotImplementedException(); + } + + public Object getSessionProperty(QualifiedName key) throws CoreException { + throw new NotImplementedException(); + } + + public int getType() { + throw new NotImplementedException(); + } + + public IWorkspace getWorkspace() { + throw new NotImplementedException(); + } + + public boolean isAccessible() { + throw new NotImplementedException(); + } + + public boolean isDerived() { + throw new NotImplementedException(); + } + + public boolean isLinked() { + throw new NotImplementedException(); + } + + public boolean isLinked(int options) { + throw new NotImplementedException(); + } + + public boolean isLocal(int depth) { + throw new NotImplementedException(); + } + + public boolean isPhantom() { + throw new NotImplementedException(); + } + + public boolean isSynchronized(int depth) { + throw new NotImplementedException(); + } + + public boolean isTeamPrivateMember() { + throw new NotImplementedException(); + } + + public void move(IPath destination, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IPath destination, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void refreshLocal(int depth, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void revertModificationStamp(long value) throws CoreException { + throw new NotImplementedException(); + } + + public void setDerived(boolean isDerived) throws CoreException { + throw new NotImplementedException(); + } + + public void setLocal(boolean flag, int depth, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public long setLocalTimeStamp(long value) throws CoreException { + throw new NotImplementedException(); + } + + public void setPersistentProperty(QualifiedName key, String value) throws CoreException { + throw new NotImplementedException(); + } + + public void setReadOnly(boolean readOnly) { + throw new NotImplementedException(); + } + + public void setResourceAttributes(ResourceAttributes attributes) throws CoreException { + throw new NotImplementedException(); + } + + public void setSessionProperty(QualifiedName key, Object value) throws CoreException { + throw new NotImplementedException(); + } + + public void setTeamPrivateMember(boolean isTeamPrivate) throws CoreException { + throw new NotImplementedException(); + } + + public void touch(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + @SuppressWarnings("unchecked") + public Object getAdapter(Class adapter) { + throw new NotImplementedException(); + } + + public boolean contains(ISchedulingRule rule) { + throw new NotImplementedException(); + } + + public boolean isConflicting(ISchedulingRule rule) { + throw new NotImplementedException(); + } + + public Map getPersistentProperties() throws CoreException { + throw new NotImplementedException(); + } + + public Map getSessionProperties() throws CoreException { + throw new NotImplementedException(); + } + + public boolean isDerived(int options) { + throw new NotImplementedException(); + } + + public boolean isHidden() { + throw new NotImplementedException(); + } + + public void setHidden(boolean isHidden) throws CoreException { + throw new NotImplementedException(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java new file mode 100644 index 0000000..223deb0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.mock; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceProxy; +import org.eclipse.core.resources.IResourceProxyVisitor; +import org.eclipse.core.resources.IResourceVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourceAttributes; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.jobs.ISchedulingRule; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.net.URI; +import java.util.Map; + +/** + * Mock implementation of {@link IFolder}. + * <p/>Supported methods: + * <ul> + * <li>{@link #getName()}</li> + * <li>{@link #members()}</li> + * </ul> + */ +public final class FolderMock implements IFolder { + + private String mName; + private IResource[] mMembers; + + public FolderMock(String name) { + mName = name; + mMembers = new IResource[0]; + } + + public FolderMock(String name, IResource[] members) { + mName = name; + mMembers = members; + } + + // -------- MOCKED METHODS ---------------- + + public String getName() { + return mName; + } + + public IResource[] members() throws CoreException { + return mMembers; + } + + // -------- UNIMPLEMENTED METHODS ---------------- + + public void create(boolean force, boolean local, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void create(int updateFlags, boolean local, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void createLink(IPath localLocation, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void createLink(URI location, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void delete(boolean force, boolean keepHistory, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IFile getFile(String name) { + throw new NotImplementedException(); + } + + public IFolder getFolder(String name) { + throw new NotImplementedException(); + } + + public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public boolean exists(IPath path) { + throw new NotImplementedException(); + } + + public IFile[] findDeletedMembersWithHistory(int depth, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IResource findMember(String name) { + throw new NotImplementedException(); + } + + public IResource findMember(IPath path) { + throw new NotImplementedException(); + } + + public IResource findMember(String name, boolean includePhantoms) { + throw new NotImplementedException(); + } + + public IResource findMember(IPath path, boolean includePhantoms) { + throw new NotImplementedException(); + } + + public String getDefaultCharset() throws CoreException { + throw new NotImplementedException(); + } + + public String getDefaultCharset(boolean checkImplicit) throws CoreException { + throw new NotImplementedException(); + } + + public IFile getFile(IPath path) { + throw new NotImplementedException(); + } + + public IFolder getFolder(IPath path) { + throw new NotImplementedException(); + } + + public IResource[] members(boolean includePhantoms) throws CoreException { + throw new NotImplementedException(); + } + + public IResource[] members(int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void setDefaultCharset(String charset) throws CoreException { + throw new NotImplementedException(); + } + + public void setDefaultCharset(String charset, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceProxyVisitor visitor, int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor, int depth, boolean includePhantoms) + throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor, int depth, int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void clearHistory(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IPath destination, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IPath destination, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IProjectDescription description, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IProjectDescription description, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IMarker createMarker(String type) throws CoreException { + throw new NotImplementedException(); + } + + public IResourceProxy createProxy() { + throw new NotImplementedException(); + } + + public void delete(boolean force, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void delete(int updateFlags, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException { + throw new NotImplementedException(); + } + + public boolean exists() { + throw new NotImplementedException(); + } + + public IMarker findMarker(long id) throws CoreException { + throw new NotImplementedException(); + } + + public IMarker[] findMarkers(String type, boolean includeSubtypes, int depth) + throws CoreException { + throw new NotImplementedException(); + } + + public int findMaxProblemSeverity(String type, boolean includeSubtypes, int depth) + throws CoreException { + throw new NotImplementedException(); + } + + public String getFileExtension() { + throw new NotImplementedException(); + } + + public IPath getFullPath() { + throw new NotImplementedException(); + } + + public long getLocalTimeStamp() { + throw new NotImplementedException(); + } + + public IPath getLocation() { + throw new NotImplementedException(); + } + + public URI getLocationURI() { + throw new NotImplementedException(); + } + + public IMarker getMarker(long id) { + throw new NotImplementedException(); + } + + public long getModificationStamp() { + throw new NotImplementedException(); + } + + public IContainer getParent() { + throw new NotImplementedException(); + } + + public String getPersistentProperty(QualifiedName key) throws CoreException { + throw new NotImplementedException(); + } + + public IProject getProject() { + throw new NotImplementedException(); + } + + public IPath getProjectRelativePath() { + throw new NotImplementedException(); + } + + public IPath getRawLocation() { + throw new NotImplementedException(); + } + + public URI getRawLocationURI() { + throw new NotImplementedException(); + } + + public ResourceAttributes getResourceAttributes() { + throw new NotImplementedException(); + } + + public Object getSessionProperty(QualifiedName key) throws CoreException { + throw new NotImplementedException(); + } + + public int getType() { + throw new NotImplementedException(); + } + + public IWorkspace getWorkspace() { + throw new NotImplementedException(); + } + + public boolean isAccessible() { + throw new NotImplementedException(); + } + + public boolean isDerived() { + throw new NotImplementedException(); + } + + public boolean isLinked() { + throw new NotImplementedException(); + } + + public boolean isLinked(int options) { + throw new NotImplementedException(); + } + + public boolean isLocal(int depth) { + throw new NotImplementedException(); + } + + public boolean isPhantom() { + throw new NotImplementedException(); + } + + public boolean isReadOnly() { + throw new NotImplementedException(); + } + + public boolean isSynchronized(int depth) { + throw new NotImplementedException(); + } + + public boolean isTeamPrivateMember() { + throw new NotImplementedException(); + } + + public void move(IPath destination, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IPath destination, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void refreshLocal(int depth, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void revertModificationStamp(long value) throws CoreException { + throw new NotImplementedException(); + } + + public void setDerived(boolean isDerived) throws CoreException { + throw new NotImplementedException(); + } + + public void setLocal(boolean flag, int depth, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public long setLocalTimeStamp(long value) throws CoreException { + throw new NotImplementedException(); + } + + public void setPersistentProperty(QualifiedName key, String value) throws CoreException { + throw new NotImplementedException(); + } + + public void setReadOnly(boolean readOnly) { + throw new NotImplementedException(); + } + + public void setResourceAttributes(ResourceAttributes attributes) throws CoreException { + throw new NotImplementedException(); + } + + public void setSessionProperty(QualifiedName key, Object value) throws CoreException { + throw new NotImplementedException(); + } + + public void setTeamPrivateMember(boolean isTeamPrivate) throws CoreException { + throw new NotImplementedException(); + } + + public void touch(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + @SuppressWarnings("unchecked") + public Object getAdapter(Class adapter) { + throw new NotImplementedException(); + } + + public boolean contains(ISchedulingRule rule) { + throw new NotImplementedException(); + } + + public boolean isConflicting(ISchedulingRule rule) { + throw new NotImplementedException(); + } + + public Map getPersistentProperties() throws CoreException { + throw new NotImplementedException(); + } + + public Map getSessionProperties() throws CoreException { + throw new NotImplementedException(); + } + + public boolean isDerived(int options) { + throw new NotImplementedException(); + } + + public boolean isHidden() { + throw new NotImplementedException(); + } + + public void setHidden(boolean isHidden) throws CoreException { + throw new NotImplementedException(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java new file mode 100644 index 0000000..f23d2c1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.mock; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IOpenable; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IRegion; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.WorkingCopyOwner; +import org.eclipse.jdt.core.eval.IEvaluationContext; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.util.Map; + +public class JavaProjectMock implements IJavaProject { + + private IProject mProject; + + private IClasspathEntry[] mEntries; + private IPath mOutputLocation; + private String mCompilerCompliance = "1.4"; //$NON-NLS-1 + private String mCompilerSource = "1.4"; //$NON-NLS-1 + private String mCompilerTarget = "1.4"; //$NON-NLS-1 + + public JavaProjectMock(IClasspathEntry[] entries, IPath outputLocation) { + mEntries = entries; + mOutputLocation = outputLocation; + } + + public IProject getProject() { + if (mProject == null) { + mProject = new ProjectMock(); + } + + return mProject; + } + + + public void setRawClasspath(IClasspathEntry[] entries, IProgressMonitor monitor) + throws JavaModelException { + mEntries = entries; + } + + public void setRawClasspath(IClasspathEntry[] entries, IPath outputLocation, + IProgressMonitor monitor) throws JavaModelException { + mEntries = entries; + mOutputLocation = outputLocation; + } + + public IClasspathEntry[] getRawClasspath() throws JavaModelException { + return mEntries; + } + + public IPath getOutputLocation() throws JavaModelException { + return mOutputLocation; + } + + public String getOption(String optionName, boolean inheritJavaCoreOptions) { + if (optionName.equals(JavaCore.COMPILER_COMPLIANCE)) { + return mCompilerCompliance; + } else if (optionName.equals(JavaCore.COMPILER_SOURCE)) { + return mCompilerSource; + } else if (optionName.equals(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM)) { + return mCompilerTarget; + } + + return null; + } + + public void setOption(String optionName, String optionValue) { + if (optionName.equals(JavaCore.COMPILER_COMPLIANCE)) { + mCompilerCompliance = optionValue; + } else if (optionName.equals(JavaCore.COMPILER_SOURCE)) { + mCompilerSource = optionValue; + } else if (optionName.equals(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM)) { + mCompilerTarget = optionValue; + } else { + throw new NotImplementedException(); + } + } + + + // -------- UNIMPLEMENTED METHODS ---------------- + + public IClasspathEntry decodeClasspathEntry(String encodedEntry) { + throw new NotImplementedException(); + } + + public String encodeClasspathEntry(IClasspathEntry classpathEntry) { + throw new NotImplementedException(); + } + + public IJavaElement findElement(IPath path) throws JavaModelException { + throw new NotImplementedException(); + } + + public IJavaElement findElement(IPath path, WorkingCopyOwner owner) throws JavaModelException { + throw new NotImplementedException(); + } + + public IPackageFragment findPackageFragment(IPath path) throws JavaModelException { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot findPackageFragmentRoot(IPath path) throws JavaModelException { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot[] findPackageFragmentRoots(IClasspathEntry entry) { + throw new NotImplementedException(); + } + + public IType findType(String fullyQualifiedName) throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String fullyQualifiedName, IProgressMonitor progressMonitor) + throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String fullyQualifiedName, WorkingCopyOwner owner) + throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String packageName, String typeQualifiedName) throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String fullyQualifiedName, WorkingCopyOwner owner, + IProgressMonitor progressMonitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String packageName, String typeQualifiedName, + IProgressMonitor progressMonitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String packageName, String typeQualifiedName, WorkingCopyOwner owner) + throws JavaModelException { + throw new NotImplementedException(); + } + + public IType findType(String packageName, String typeQualifiedName, WorkingCopyOwner owner, + IProgressMonitor progressMonitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot[] getAllPackageFragmentRoots() throws JavaModelException { + throw new NotImplementedException(); + } + + public Object[] getNonJavaResources() throws JavaModelException { + throw new NotImplementedException(); + } + + @SuppressWarnings("unchecked") + public Map getOptions(boolean inheritJavaCoreOptions) { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot getPackageFragmentRoot(String jarPath) { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot getPackageFragmentRoot(IResource resource) { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot[] getPackageFragmentRoots() throws JavaModelException { + throw new NotImplementedException(); + } + + public IPackageFragmentRoot[] getPackageFragmentRoots(IClasspathEntry entry) { + throw new NotImplementedException(); + } + + public IPackageFragment[] getPackageFragments() throws JavaModelException { + throw new NotImplementedException(); + } + + public String[] getRequiredProjectNames() throws JavaModelException { + throw new NotImplementedException(); + } + + public IClasspathEntry[] getResolvedClasspath(boolean ignoreUnresolvedEntry) + throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean hasBuildState() { + throw new NotImplementedException(); + } + + public boolean hasClasspathCycle(IClasspathEntry[] entries) { + throw new NotImplementedException(); + } + + public boolean isOnClasspath(IJavaElement element) { + throw new NotImplementedException(); + } + + public boolean isOnClasspath(IResource resource) { + throw new NotImplementedException(); + } + + public IEvaluationContext newEvaluationContext() { + throw new NotImplementedException(); + } + + public ITypeHierarchy newTypeHierarchy(IRegion region, IProgressMonitor monitor) + throws JavaModelException { + throw new NotImplementedException(); + } + + public ITypeHierarchy newTypeHierarchy(IRegion region, WorkingCopyOwner owner, + IProgressMonitor monitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public ITypeHierarchy newTypeHierarchy(IType type, IRegion region, IProgressMonitor monitor) + throws JavaModelException { + throw new NotImplementedException(); + } + + public ITypeHierarchy newTypeHierarchy(IType type, IRegion region, WorkingCopyOwner owner, + IProgressMonitor monitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public IPath readOutputLocation() { + throw new NotImplementedException(); + } + + public IClasspathEntry[] readRawClasspath() { + throw new NotImplementedException(); + } + + @SuppressWarnings("unchecked") + public void setOptions(Map newOptions) { + throw new NotImplementedException(); + } + + public void setOutputLocation(IPath path, IProgressMonitor monitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public void setRawClasspath(IClasspathEntry[] entries, boolean canModifyResources, + IProgressMonitor monitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public void setRawClasspath(IClasspathEntry[] entries, IPath outputLocation, + boolean canModifyResources, IProgressMonitor monitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public IJavaElement[] getChildren() throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean hasChildren() throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean exists() { + throw new NotImplementedException(); + } + + public IJavaElement getAncestor(int ancestorType) { + throw new NotImplementedException(); + } + + public String getAttachedJavadoc(IProgressMonitor monitor) throws JavaModelException { + throw new NotImplementedException(); + } + + public IResource getCorrespondingResource() throws JavaModelException { + throw new NotImplementedException(); + } + + public String getElementName() { + throw new NotImplementedException(); + } + + public int getElementType() { + throw new NotImplementedException(); + } + + public String getHandleIdentifier() { + throw new NotImplementedException(); + } + + public IJavaModel getJavaModel() { + throw new NotImplementedException(); + } + + public IJavaProject getJavaProject() { + throw new NotImplementedException(); + } + + public IOpenable getOpenable() { + throw new NotImplementedException(); + } + + public IJavaElement getParent() { + throw new NotImplementedException(); + } + + public IPath getPath() { + throw new NotImplementedException(); + } + + public IJavaElement getPrimaryElement() { + throw new NotImplementedException(); + } + + public IResource getResource() { + throw new NotImplementedException(); + } + + public ISchedulingRule getSchedulingRule() { + throw new NotImplementedException(); + } + + public IResource getUnderlyingResource() throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean isReadOnly() { + throw new NotImplementedException(); + } + + public boolean isStructureKnown() throws JavaModelException { + throw new NotImplementedException(); + } + + @SuppressWarnings("unchecked") + public Object getAdapter(Class adapter) { + throw new NotImplementedException(); + } + + public void close() throws JavaModelException { + throw new NotImplementedException(); + } + + public String findRecommendedLineSeparator() throws JavaModelException { + throw new NotImplementedException(); + } + + public IBuffer getBuffer() throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean hasUnsavedChanges() throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean isConsistent() throws JavaModelException { + throw new NotImplementedException(); + } + + public boolean isOpen() { + throw new NotImplementedException(); + } + + public void makeConsistent(IProgressMonitor progress) throws JavaModelException { + throw new NotImplementedException(); + } + + public void open(IProgressMonitor progress) throws JavaModelException { + throw new NotImplementedException(); + } + + public void save(IProgressMonitor progress, boolean force) throws JavaModelException { + throw new NotImplementedException(); + } + + public IJavaElement findElement(String bindingKey, WorkingCopyOwner owner) + throws JavaModelException { + throw new NotImplementedException(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java new file mode 100644 index 0000000..4c409dc --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.mock; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IProjectNature; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceProxy; +import org.eclipse.core.resources.IResourceProxyVisitor; +import org.eclipse.core.resources.IResourceVisitor; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourceAttributes; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IPluginDescriptor; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.content.IContentTypeMatcher; +import org.eclipse.core.runtime.jobs.ISchedulingRule; + +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.net.URI; +import java.util.Map; + +public class ProjectMock implements IProject { + + public void build(int kind, IProgressMonitor monitor) throws CoreException { + // pass + } + + @SuppressWarnings("unchecked") + public void build(int kind, String builderName, Map args, IProgressMonitor monitor) + throws CoreException { + // pass + } + + // -------- UNIMPLEMENTED METHODS ---------------- + + + public void close(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void create(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void create(IProjectDescription description, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void delete(boolean deleteContent, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IContentTypeMatcher getContentTypeMatcher() throws CoreException { + throw new NotImplementedException(); + } + + public IProjectDescription getDescription() throws CoreException { + throw new NotImplementedException(); + } + + public IFile getFile(String name) { + throw new NotImplementedException(); + } + + public IFolder getFolder(String name) { + throw new NotImplementedException(); + } + + public IProjectNature getNature(String natureId) throws CoreException { + throw new NotImplementedException(); + } + + @SuppressWarnings("deprecation") + public IPath getPluginWorkingLocation(IPluginDescriptor plugin) { + throw new NotImplementedException(); + } + + public IProject[] getReferencedProjects() throws CoreException { + throw new NotImplementedException(); + } + + public IProject[] getReferencingProjects() { + throw new NotImplementedException(); + } + + public IPath getWorkingLocation(String id) { + throw new NotImplementedException(); + } + + public boolean hasNature(String natureId) throws CoreException { + throw new NotImplementedException(); + } + + public boolean isNatureEnabled(String natureId) throws CoreException { + throw new NotImplementedException(); + } + + public boolean isOpen() { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void open(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void open(int updateFlags, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void setDescription(IProjectDescription description, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void setDescription(IProjectDescription description, int updateFlags, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public boolean exists(IPath path) { + throw new NotImplementedException(); + } + + public IFile[] findDeletedMembersWithHistory(int depth, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IResource findMember(String name) { + throw new NotImplementedException(); + } + + public IResource findMember(IPath path) { + throw new NotImplementedException(); + } + + public IResource findMember(String name, boolean includePhantoms) { + throw new NotImplementedException(); + } + + public IResource findMember(IPath path, boolean includePhantoms) { + throw new NotImplementedException(); + } + + public String getDefaultCharset() throws CoreException { + throw new NotImplementedException(); + } + + public String getDefaultCharset(boolean checkImplicit) throws CoreException { + throw new NotImplementedException(); + } + + public IFile getFile(IPath path) { + throw new NotImplementedException(); + } + + public IFolder getFolder(IPath path) { + throw new NotImplementedException(); + } + + public IResource[] members() throws CoreException { + throw new NotImplementedException(); + } + + public IResource[] members(boolean includePhantoms) throws CoreException { + throw new NotImplementedException(); + } + + public IResource[] members(int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void setDefaultCharset(String charset) throws CoreException { + throw new NotImplementedException(); + } + + public void setDefaultCharset(String charset, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceProxyVisitor visitor, int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor, int depth, boolean includePhantoms) + throws CoreException { + throw new NotImplementedException(); + } + + public void accept(IResourceVisitor visitor, int depth, int memberFlags) throws CoreException { + throw new NotImplementedException(); + } + + public void clearHistory(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IPath destination, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IPath destination, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IProjectDescription description, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void copy(IProjectDescription description, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public IMarker createMarker(String type) throws CoreException { + throw new NotImplementedException(); + } + + public IResourceProxy createProxy() { + throw new NotImplementedException(); + } + + public void delete(boolean force, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void delete(int updateFlags, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException { + throw new NotImplementedException(); + } + + public boolean exists() { + throw new NotImplementedException(); + } + + public IMarker findMarker(long id) throws CoreException { + throw new NotImplementedException(); + } + + public IMarker[] findMarkers(String type, boolean includeSubtypes, int depth) + throws CoreException { + throw new NotImplementedException(); + } + + public int findMaxProblemSeverity(String type, boolean includeSubtypes, int depth) + throws CoreException { + throw new NotImplementedException(); + } + + public String getFileExtension() { + throw new NotImplementedException(); + } + + public IPath getFullPath() { + throw new NotImplementedException(); + } + + public long getLocalTimeStamp() { + throw new NotImplementedException(); + } + + public IPath getLocation() { + throw new NotImplementedException(); + } + + public URI getLocationURI() { + throw new NotImplementedException(); + } + + public IMarker getMarker(long id) { + throw new NotImplementedException(); + } + + public long getModificationStamp() { + throw new NotImplementedException(); + } + + public String getName() { + throw new NotImplementedException(); + } + + public IContainer getParent() { + throw new NotImplementedException(); + } + + public String getPersistentProperty(QualifiedName key) throws CoreException { + throw new NotImplementedException(); + } + + public IProject getProject() { + throw new NotImplementedException(); + } + + public IPath getProjectRelativePath() { + throw new NotImplementedException(); + } + + public IPath getRawLocation() { + throw new NotImplementedException(); + } + + public URI getRawLocationURI() { + throw new NotImplementedException(); + } + + public ResourceAttributes getResourceAttributes() { + throw new NotImplementedException(); + } + + public Object getSessionProperty(QualifiedName key) throws CoreException { + throw new NotImplementedException(); + } + + public int getType() { + throw new NotImplementedException(); + } + + public IWorkspace getWorkspace() { + throw new NotImplementedException(); + } + + public boolean isAccessible() { + throw new NotImplementedException(); + } + + public boolean isDerived() { + throw new NotImplementedException(); + } + + public boolean isLinked() { + throw new NotImplementedException(); + } + + public boolean isLinked(int options) { + throw new NotImplementedException(); + } + + public boolean isLocal(int depth) { + throw new NotImplementedException(); + } + + public boolean isPhantom() { + throw new NotImplementedException(); + } + + public boolean isReadOnly() { + throw new NotImplementedException(); + } + + public boolean isSynchronized(int depth) { + throw new NotImplementedException(); + } + + public boolean isTeamPrivateMember() { + throw new NotImplementedException(); + } + + public void move(IPath destination, boolean force, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IPath destination, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, int updateFlags, IProgressMonitor monitor) + throws CoreException { + throw new NotImplementedException(); + } + + public void move(IProjectDescription description, boolean force, boolean keepHistory, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void refreshLocal(int depth, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public void revertModificationStamp(long value) throws CoreException { + throw new NotImplementedException(); + } + + public void setDerived(boolean isDerived) throws CoreException { + throw new NotImplementedException(); + } + + public void setLocal(boolean flag, int depth, IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public long setLocalTimeStamp(long value) throws CoreException { + throw new NotImplementedException(); + } + + public void setPersistentProperty(QualifiedName key, String value) throws CoreException { + throw new NotImplementedException(); + } + + public void setReadOnly(boolean readOnly) { + throw new NotImplementedException(); + } + + public void setResourceAttributes(ResourceAttributes attributes) throws CoreException { + throw new NotImplementedException(); + } + + public void setSessionProperty(QualifiedName key, Object value) throws CoreException { + throw new NotImplementedException(); + } + + public void setTeamPrivateMember(boolean isTeamPrivate) throws CoreException { + throw new NotImplementedException(); + } + + public void touch(IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public Object getAdapter(Class adapter) { + throw new NotImplementedException(); + } + + public boolean contains(ISchedulingRule rule) { + throw new NotImplementedException(); + } + + public boolean isConflicting(ISchedulingRule rule) { + throw new NotImplementedException(); + } + + public void create(IProjectDescription description, int updateFlags, + IProgressMonitor monitor) throws CoreException { + throw new NotImplementedException(); + } + + public Map getPersistentProperties() throws CoreException { + throw new NotImplementedException(); + } + + public Map getSessionProperties() throws CoreException { + throw new NotImplementedException(); + } + + public boolean isDerived(int options) { + throw new NotImplementedException(); + } + + public boolean isHidden() { + throw new NotImplementedException(); + } + + public void setHidden(boolean isHidden) throws CoreException { + throw new NotImplementedException(); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jar b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jar Binary files differnew file mode 100644 index 0000000..f95b595 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jar diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc new file mode 100644 index 0000000..14cd44f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> +<jardesc> + <jar path="jar_example.jar"/> + <options buildIfNeeded="true" compress="true" descriptionLocation="/common/tests/data/jar_example.jardesc" exportErrors="false" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/> + <storedRefactorings deprecationInfo="true" structuralOnly="false"/> + <selectedProjects/> + <manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true"> + <sealing sealJar="false"> + <packagesToSeal/> + <packagesToUnSeal/> + </sealing> + </manifest> + <selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false"> + <javaElement handleIdentifier="=common/tests<jar.example"/> + </selectedElements> +</jardesc> diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml new file mode 100644 index 0000000..aa9a1f7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml @@ -0,0 +1,340 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- WARNING !!! THIS IS A MOCK FILE. DO NOT USE FOR DOCUMENTATION PURPOSES. + This file has been trimmed down to only extract a number of interesting cases + for unit tests. + + What this contains: + - View + - ViewGroup + - some attributes which format are defined in Theme + - orientation, gravity and layout_gravity defined before they are used + - ViewGroup_Layout + - ViewGroup_MarginLayout + - LinearLayout + - LinearLayout_Layout + - TableLayout + + Note that TableLayout does not have a TableLayout_Layout definition here + where these is a class TableLayout.LayoutData. + --> + + <!-- These are the standard attributes that make up a complete theme. --> + <declare-styleable name="Theme"> + + <!-- Defines the scrollbars size. --> + <attr name="scrollbarSize" format="dimension" /> + + </declare-styleable> + + + <!-- Standard orientation constant. --> + <attr name="orientation"> + <!-- Defines an horizontal widget. --> + <enum name="horizontal" value="0" /> + <!-- Defines a vertical widget. --> + <enum name="vertical" value="1" /> + </attr> + + <!-- Specifies how to place an object, both + its x and y axis, within a larger containing object. --> + <attr name="gravity"> + <!-- Push object to the top of its container, not changing its size. --> + <flag name="top" value="0x30" /> + <!-- Push object to the bottom of its container, not changing its size. --> + <flag name="bottom" value="0x50" /> + <!-- Push object to the left of its container, not changing its size. --> + <flag name="left" value="0x03" /> + <!-- Push object to the right of its container, not changing its size. --> + <flag name="right" value="0x05" /> + <!-- Place object in the vertical center of its container, not changing its size. --> + <flag name="center_vertical" value="0x10" /> + <!-- Grow the vertical size of the object if needed so it completely fills its container. --> + <flag name="fill_vertical" value="0x70" /> + <!-- Place object in the horizontal center of its container, not changing its size. --> + <flag name="center_horizontal" value="0x01" /> + <!-- Grow the horizontal size of the object if needed so it completely fills its container. --> + <flag name="fill_horizontal" value="0x07" /> + <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. --> + <flag name="center" value="0x11" /> + <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. --> + <flag name="fill" value="0x77" /> + </attr> + + <!-- Standard gravity constant that a child can supply to its parent. + Defines how to place an object, both + its x and y axis, within a larger containing object. --> + <attr name="layout_gravity"> + <!-- Push object to the top of its container, not changing its size. --> + <flag name="top" value="0x30" /> + <!-- Push object to the bottom of its container, not changing its size. --> + <flag name="bottom" value="0x50" /> + <!-- Push object to the left of its container, not changing its size. --> + <flag name="left" value="0x03" /> + <!-- Push object to the right of its container, not changing its size. --> + <flag name="right" value="0x05" /> + <!-- Place object in the vertical center of its container, not changing its size. --> + <flag name="center_vertical" value="0x10" /> + <!-- Grow the vertical size of the object if needed so it completely fills its container. --> + <flag name="fill_vertical" value="0x70" /> + <!-- Place object in the horizontal center of its container, not changing its size. --> + <flag name="center_horizontal" value="0x01" /> + <!-- Grow the horizontal size of the object if needed so it completely fills its container. --> + <flag name="fill_horizontal" value="0x07" /> + <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. --> + <flag name="center" value="0x11" /> + <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. --> + <flag name="fill" value="0x77" /> + </attr> + + <declare-styleable name="View"> + <!-- NOTE: View does not have a javadoc. Do not place a comment BEFORE View to make sure it + is NOT interpreted as Javadoc --> + + <!-- Supply an identifier name for this view, to later retrieve it + with {@link android.view.View#findViewById View.findViewById()} or + {@link android.app.Activity#findViewById Activity.findViewById()}. + This must be a + resource reference; typically you set this using the + <code>@+</code> syntax to create a new ID resources. + For example: <code>android:id="@+id/my_id"</code> which + allows you to later retrieve the view + with <code>findViewById(R.id.my_id)</code>. --> + <attr name="id" format="reference" /> + + <!-- Supply a tag for this view containing a String, to be retrieved + later with {@link android.view.View#getTag View.getTag()} or + searched for with {@link android.view.View#findViewWithTag + View.findViewWithTag()}. It is generally preferable to use + IDs (through the android:id attribute) instead of tags because + they are faster and allow for compile-time type checking. --> + <attr name="tag" format="string" /> + + <!-- The initial horizontal scroll offset, in pixels.--> + <attr name="scrollX" format="dimension" /> + + <!-- The initial vertical scroll offset, in pixels. --> + <attr name="scrollY" format="dimension" /> + + <!-- A drawable to use as the background. This can be either a reference + to a full drawable resource (such as a PNG image, 9-patch, + XML state list description, etc), or a solid color such as "#ff000000" + (black). --> + <attr name="background" format="reference|color" /> + + <!-- Boolean that controls whether a view can take focus. By default the user can not + move focus to a view; by setting this attribute to true the view is + allowed to take focus. This value does not impact the behavior of + directly calling {@link android.view.View#requestFocus}, which will + always request focus regardless of this view. It only impacts where + focus navigation will try to move focus. --> + <attr name="focusable" format="boolean" /> + + <!-- Sets the circumstances under which this view will take focus. There are + two choices: "weak" or "normal". The default value is "normal" for + any focusable views. The focus type only applies if the view + has been set to be focusable. --> + <attr name="focusType"> + <!-- This view is focusable, but only if none of its descendants are already focused. --> + <enum name="normal" value="0" /> + <!-- This view will always claim to be focusable. --> + <enum name="weak" value="1" /> + </attr> + + <!-- Controls the initial visibility of the view. --> + <attr name="visibility"> + <!-- Visible on screen; the default value. --> + <enum name="visible" value="0" /> + <!-- Not displayed, but taken into account during layout (space is left for it). --> + <enum name="invisible" value="1" /> + <!-- Completely hidden, as if the view had not been added. --> + <enum name="gone" value="2" /> + </attr> + + <!-- Defines which scrollbars should be displayed on scrolling or not. --> + <attr name="scrollbars"> + <!-- No scrollbar is displayed. --> + <flag name="none" value="0x00000000" /> + <!-- Displays horizontal scrollbar only. --> + <flag name="horizontal" value="0x00000100" /> + <!-- Displays vertical scrollbar only. --> + <flag name="vertical" value="0x00000200" /> + </attr> + + <!-- Sets the width of vertical scrollbars and height of horizontal scrollbars. --> + <attr name="scrollbarSize" /> + + <!-- Text to display. (copied from TextView for the extra localization) --> + <attr name="text" format="string" localization="suggested" /> + + </declare-styleable> + + <!-- Attributes that can be used with a {@link android.view.ViewGroup} or any + of its subclasses. Also see {@link #ViewGroup_Layout} for + attributes that this class processes in its children. --> + <declare-styleable name="ViewGroup"> + <!-- Defines whether a child is limited to draw inside of its bounds or not. + This is useful with animations that scale the size of the children to more + than 100% for instance. In such a case, this property should be set to false + to allow the children to draw outside of their bounds. The default value of + this property is true. --> + <attr name="clipChildren" format="boolean" /> + <!-- Defines the layout animation to use the first time the ViewGroup is laid out. + Layout animations can also be started manually after the first layout. --> + <attr name="layoutAnimation" format="reference" /> + <!-- Defines whether a child's animation should be kept when it is over. Keeping + the animations is useful with animation whose final state is different from + the initial state of the View. This is particularly useful with animation + whose fillAfter property is enabled. This property is set to false by default. --> + <attr name="persistentDrawingCache"> + <!-- The drawing cache is not persisted after use. --> + <flag name="none" value="0x0" /> + <!-- The drawing cache is persisted after a layout animation. --> + <flag name="animation" value="0x1" /> + <!-- The drawing cache is persisted after a scroll. --> + <flag name="scrolling" value="0x2" /> + <!-- The drawing cache is always persisted. --> + <flag name="all" value="0x3" /> + </attr> + </declare-styleable> + + <!-- This is the basic set of layout attributes that are common to all + layout managers. These attributes are specified with the rest of + a view's normal attributes (such as {@link android.R.attr#background}, + but will be parsed by the view's parent and ignored by the child. + <p>The values defined here correspond to the base layout attribute + class {@link android.view.ViewGroup.LayoutParams}. --> + <declare-styleable name="ViewGroup_Layout"> + <!-- Specifies the basic width of the view. This is a required attribute + for any view inside of a containing layout manager. Its value may + be a dimension (such as "12dip") for a constant width or one of + the special constants. --> + <attr name="layout_width" format="dimension"> + <!-- The view should be as big as its parent (minus padding). --> + <enum name="fill_parent" value="-1" /> + <!-- The view should be only big enough to enclose its content (plus padding). --> + <enum name="wrap_content" value="-2" /> + </attr> + + <!-- Specifies the basic height of the view. This is a required attribute + for any view inside of a containing layout manager. Its value may + be a dimension (such as "12dip") for a constant height or one of + the special constants. --> + <attr name="layout_height" format="dimension"> + <!-- The view should be as big as its parent (minus padding). --> + <enum name="fill_parent" value="-1" /> + <!-- The view should be only big enough to enclose its content (plus padding). --> + <enum name="wrap_content" value="-2" /> + </attr> + </declare-styleable> + + <!-- This is the basic set of layout attributes for layout managers that + wish to place margins around their child views. + These attributes are specified with the rest of + a view's normal attributes (such as {@link android.R.attr#background}, + but will be parsed by the view's parent and ignored by the child. + <p>The values defined here correspond to the base layout attribute + class {@link android.view.ViewGroup.MarginLayoutParams}. --> + <declare-styleable name="ViewGroup_MarginLayout"> + <attr name="layout_width" /> + <attr name="layout_height" /> + <!-- Specifies extra space on the left side of this view. + This space is outside this view's bounds. --> + <attr name="layout_marginLeft" format="dimension" /> + <!-- Specifies extra space on the top side of this view. + This space is outside this view's bounds. --> + <attr name="layout_marginTop" format="dimension" /> + <!-- Specifies extra space on the right side of this view. + This space is outside this view's bounds. --> + <attr name="layout_marginRight" format="dimension" /> + <!-- Specifies extra space on the bottom side of this view. + This space is outside this view's bounds. --> + <attr name="layout_marginBottom" format="dimension" /> + </declare-styleable> + + <!-- This is a linear layout. --> + <declare-styleable name="LinearLayout"> + <!-- Should the layout be a column or a row? Use "horizontal" + for a row, "vertical" for a column. The default is + horizontal. --> + <attr name="orientation" /> + <attr name="baselineAligned" format="boolean|reference" /> + <!-- When a linear layout is part of another layout that is baseline + aligned, it can specify which of its children to baseline align to + (i.e which child TextView).--> + <attr name="baselineAlignedChildIndex" format="integer|color" min="0"/> + <!-- Defines the maximum weight sum. If unspecified, the sum is computed + by adding the layout_weight of all of the children. This can be + used for instance to give a single child 50% of the total available + space by giving it a layout_weight of 0.5 and setting the weightSum + to 1.0. --> + <attr name="weightSum" format="float" /> + </declare-styleable> + + <declare-styleable name="LinearLayout_Layout"> + <attr name="layout_width" /> + <attr name="layout_height" /> + <attr name="layout_weight" format="float" /> + <attr name="layout_gravity" /> + </declare-styleable> + + <declare-styleable name="TableLayout"> + <!-- The 0 based index of the columns to stretch. The column indices + must be separated by a comma: 1, 2, 5. Illegal and duplicate + indices are ignored. You can stretch all columns by using the + value "*" instead. Note that a column can be marked stretchable + and shrinkable at the same time. --> + <attr name="stretchColumns" format="string" /> + <!-- The 0 based index of the columns to shrink. The column indices + must be separated by a comma: 1, 2, 5. Illegal and duplicate + indices are ignored. You can shrink all columns by using the + value "*" instead. Note that a column can be marked stretchable + and shrinkable at the same time. --> + <attr name="shrinkColumns" format="string" /> + <!-- The 0 based index of the columns to collapse. The column indices + must be separated by a comma: 1, 2, 5. Illegal and duplicate + indices are ignored. --> + <attr name="collapseColumns" format="string" /> + </declare-styleable> + + <!-- Test for deprecated attributes. --> + <declare-styleable name="DeprecatedTest"> + <!-- Deprecated comments using delimiters. + Ignored. {@deprecated In-line deprecated.} {@ignore Ignored}. + --> + <attr name="deprecated-inline" /> + + <!-- Deprecated comments on their own line. + @deprecated Multi-line version of deprecated + that works till the next tag. + @ignore This tag must be ignored + --> + <attr name="deprecated-multiline" /> + + <!-- This attribute is not deprecated. --> + <attr name="deprecated-not" /> + + <!-- {@deprecated There is no other javadoc here. } --> + <attr name="deprecated-no-javadoc" format="boolean" /> + + </declare-styleable> + +</resources> + diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java new file mode 100644 index 0000000..a80a98d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mock_android.view; + +public class View { + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java new file mode 100644 index 0000000..466470f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mock_android.view; + +public class ViewGroup extends View { + + public class MarginLayoutParams extends LayoutParams { + + } + + public class LayoutParams { + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java new file mode 100644 index 0000000..3870a63 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mock_android.widget; + +import mock_android.view.ViewGroup; + +public class LinearLayout extends ViewGroup { + + public class LayoutParams extends mock_android.view.ViewGroup.LayoutParams { + + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java new file mode 100644 index 0000000..e455e7d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mock_android.widget; + +import mock_android.view.ViewGroup; + +public class TableLayout extends ViewGroup { + + public class LayoutParams extends MarginLayoutParams { + + } + +} diff --git a/eclipse/scripts/_mk_icons.sh b/eclipse/scripts/_mk_icons.sh new file mode 100755 index 0000000..b3ea35b --- /dev/null +++ b/eclipse/scripts/_mk_icons.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +function icon() { + # $1=letter, $2=letter's color (e.g. A red), $3=filename + ./gen_icon.py ${3}.png 16 white black $2 $1 +} + +icon M green manifest + icon S blue sharedUserId + icon S red signature + icon P green package + +icon I green instrumentation + icon F green functionalTest + icon H green handleProfiling + icon I green icon + icon T green targetPackage + +icon U blue uses-permission +icon P red permission + icon N green name + icon L blue label + +icon A blue application + icon P red permission + icon P blue persistent + icon P green process + icon T green taskAffinity + icon T blue theme + icon P red provider + icon A green authorities + icon I green initOrder + icon M green multiprocess + icon R green readPermission + icon W green writePermission + icon S green syncable + icon R green receiver + icon S blue service + icon A green activity + icon C blue clearOnBackground + icon C green configChanges + icon E green excludeFromRecents + icon L green launchMode + icon S green stateNotNeeded + icon F blue intent-filter + icon P green priority + icon A red action + icon C green category + icon D green data + icon M green mimeType + icon S green scheme + icon H green host + icon P green port + icon P blue path + diff --git a/eclipse/scripts/build_plugins.sh b/eclipse/scripts/build_plugins.sh new file mode 100755 index 0000000..5f94ca0 --- /dev/null +++ b/eclipse/scripts/build_plugins.sh @@ -0,0 +1,225 @@ +#!/bin/bash + +# build script for eclipse adt build on linux platform +# +# Usage: development/tools/eclipse/scripts/build_plugins <build_version> +# +# It expects environment variable ECLIPSE_HOME to be defined to point to _your_ +# version of Eclipse RCP (must have the WTP & GEF plugins available too.) +# +# If ECLIPSE_HOME is not provided, this script will _download_ a reference version +# of Eclipse RCP and install it in a specific location. +# +# Other properties, ant scripts that drive the build are defined in ./buildConfig +# Currently, this script will create an update site at ${user.home}/www/no_crawl/android-build +# or at the directory specified using "-d" + +# Known Issues: +# - Build does not properly clean up after itself (build server always executes from +# a clean state.) +# - Script will fail if current absolute path has spaces in it. +# - Only linux is supported for now + + +set -e # abort this script early if any command fails + +# +# -- Utility methods -- +# + +function printUsage() { + echo "Usage: $0 <build_qualifier> [-i] [-d <destination-directory>] [-a <archivePrefix>] " + echo "<build_qualifier>: build qualifier string" + echo "-i = build internal site. Otherwise, external site will be built" + echo "-d = destination directory. Default is $USER/www/no_crawl/. Cannot contain spaces." + echo "-a = archive prefix. Cannot contain spaces." +} + +function die() { + echo $@ + exit 1 +} + +function dieWithUsage() { + echo $@ + echo + printUsage + exit 1 +} + + +# +# -- Setup our custom version of Eclipse -- +# + +# The dependency on the linux platform comes from a series of environment +# variables that the eclipse ant runner expects. These are defined in the +# build.properties file. We can easily support other platforms but would need +# to override those values in this script. +HOST=`uname` +[ "$HOST" == "Linux" ] || die "ERROR: This script is currently only supported on Linux platform" + +# Make sure this runs from the tools/eclipse plugin. +D=`dirname "$0"` +cd "$D/.." +[ `basename "$PWD"` == "eclipse" ] || dieWithUsage "Please run this script from the device/tools/eclipse directory" + +# check for number of parameters +[ $# -lt 1 ] && dieWithUsage "ERROR: Not enough parameters" + +# check if ECLIPSE_HOME set (ECLIPSE_HOME is were the "eclipse" binary and the +# "plugins" sub-directory are located) +if [ -z "$ECLIPSE_HOME" ]; then + BASE_DIR=/buildbot/eclipse-android + + echo "ECLIPSE_HOME not set, using $BASE_DIR as default" + + if [ ! -d "$BASE_DIR" ]; then + mkdir -p "$BASE_DIR" || die "Please create a directory $BASE_DIR where Eclipse will be installed, i.e. execute 'mkdir -p $BASE_DIR && chown $USER $BASE_DIR'." + fi + + # download the version if not available + VERSION="3.4.0" + BASE_DIR="$BASE_DIR/$VERSION" + scripts/setup_eclipse.sh -p "$BASE_DIR" + + ECLIPSE_HOME="$BASE_DIR/eclipse" # path to installed directory + PID_FILE="$BASE_DIR/eclipse.pid" + [ -f "$PID_FILE" ] && ECLIPSE_PID=`cat "$PID_FILE"` +fi + +echo "PWD=`pwd`" +echo "ECLIPSE_HOME=$ECLIPSE_HOME" + +# +# -- Site parameters and Build version -- +# + +BUILD_VERSION="$1" ; shift + +# parse for build internal site flag. If set, pass in internalSite property to ant scripts +if [ "-i" == "$1" ]; then + shift + echo "Setting for internal site build" + SITE_PARAM="-DinternalSite=1 -DupdateSiteSource=$PWD/sites/internal" +else + SITE_PARAM="-DupdateSiteSource=$PWD/sites/external" +fi + +if [ "-d" == $1 ]; then + shift + echo "Setting destination directory to $1" + SITE_PARAM="$SITE_PARAM -DupdateSiteRoot=$1" + shift +fi + +if [ "-a" == "$1" ]; then + shift + echo "Setting archivePrefix to $1" + SITE_PARAM="$SITE_PARAM -DarchivePrefix=$1" + shift +fi + + +# +# -- Configuration directory -- +# + +# The "configuration directory" will hold the workspace for this build. +# If it contains old data the build may fail so we need to clean it first +# and create it if it doesn't exist. +CONFIG_DIR="../../../out/eclipse-configuration-$BUILD_VERSION" +[ -d "$CONFIG_DIR" ] && rm -rfv "$CONFIG_DIR" +mkdir -p "$CONFIG_DIR" + +# The "buildConfig" directory contains our customized ant rules +BUILDCONFIG="$PWD/buildConfig" + + +# +# -- Find Eclipse Launcher -- +# + +# Get the Eclipse launcher and build script to use +function findFirst() { + for i in "$@"; do + if [ -f "$i" ]; then + echo "$i" + return + fi + done +} + +LAUNCHER=`findFirst "$ECLIPSE_HOME"/plugins/org.eclipse.equinox.launcher_*.jar` +BUILDFILE=`findFirst "$ECLIPSE_HOME"/plugins/org.eclipse.pde.build_*/scripts/build.xml` + +# make sure we found valid files +if [ ! -f "$LAUNCHER" ]; then + echo "Installation Error: Eclipse plugin org.eclipse.equinox.launcher...jar not detected. " \ + "Found '$LAUNCHER'. Aborting." + exit 1 +fi +if [ ! -f "$BUILDFILE" ]; then + echo "Installation Error: Eclipse build file org.eclipse.pde.build_.../scripts/build.xml " \ + "not detected. Found '$BUILDFILE'. Aborting." + exit 1 +fi + + +# +# -- Print configuration used and actually execute the build -- +# + +echo "Eclipse configuration found:" +echo " Eclipse Home: $ECLIPSE_HOME" +echo " Launcher: $LAUNCHER" +echo " Build File: $BUILDFILE" +echo " Build Config: $BUILDCONFIG" +echo " Config Dir: $CONFIG_DIR" + +# clean input directories to make sure there's nothing left from previous run + +rm -fv *.properties *.xml +find . -name "@*" | xargs rm -rfv + +# Now execute the ant runner + +set +e # don't stop on errors anymore, we want to catch there here + +java \ + -jar $LAUNCHER \ + -data "$CONFIG_DIR" \ + -configuration "$CONFIG_DIR" \ + -application org.eclipse.ant.core.antRunner \ + -buildfile $BUILDFILE \ + -Dbuilder=$BUILDCONFIG \ + -DbuildDirectory=$PWD \ + -DforceContextQualifier=$BUILD_VERSION \ + -DECLIPSE_HOME=$ECLIPSE_HOME \ + $SITE_PARAM +RESULT=$? + +if [ "0" != "$RESULT" ]; then + echo "JAVA died with error code $RESULT" + echo "Dump of build config logs:" + for i in "$CONFIG_DIR"/*.log; do + if [ -f "$i" ]; then + echo "----------------------" + echo "--- $i" + echo "----------------------" + cat "$i" + echo + fi + done +fi + +# +# -- Cleanup +# + +if [ -n "$ECLIPSE_PID" ] && [ -f "$PID_FILE" ]; then + rm -fv "$PID_FILE" + kill -9 "$ECLIPSE_PID" +fi + +# we're done! diff --git a/eclipse/scripts/build_server.sh b/eclipse/scripts/build_server.sh new file mode 100755 index 0000000..39c8dcd --- /dev/null +++ b/eclipse/scripts/build_server.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# Entry point to build the Eclipse plugins for the build server. +# +# Input parameters: +# $1: *Mandatory* destination directory. Must already exist. Cannot contain spaces. +# $2: Optional build number. If present, will be appended to the date qualifier. +# The build number cannot contain spaces *nor* periods (dashes are ok.) +# -z: Optional, prevents the final zip and leaves the udate-site directory intact. +# -i: Optional, if present, the Google internal update site will be built. Otherwise, +# the external site will be built +# Workflow: +# - make dx, ddms, ping +# - create symlinks (for eclipse code reorg, for ddms, ping) +# - call the actual builder script from Brett +# - zip resulting stuff and move to $DEST +# Note: currently wrap around existing shell script, reuse most of it, +# eventually both might merge as needed. + +set -e # Fail this script as soon as a command fails -- fail early, fail fast + +DEST_DIR="" +BUILD_NUMBER="" +CREATE_ZIP="1" +INTERNAL_BUILD="" + +function get_params() { + # parse input parameters + while [ $# -gt 0 ]; do + if [ "$1" == "-z" ]; then + CREATE_ZIP="" + elif [ "$1" == "-i" ]; then + INTERNAL_BUILD="-i" + elif [ "$1" != "" ] && [ -z "$DEST_DIR" ]; then + DEST_DIR="$1" + elif [ "$1" != "" ] && [ -z "$BUILD_NUMBER" ]; then + BUILD_NUMBER="$1" + fi + shift + done +} + +function die() { + echo "Error:" $* + echo "Aborting" + exit 1 +} + +function check_params() { + # This needs to run from the top android directory + # Automatically CD to the top android directory, whatever its name + D=`dirname "$0"` + cd "$D/../../../../" && echo "Switched to directory $PWD" + + # The current Eclipse build has some Linux dependency in its config files + [ `uname` == "Linux" ] || die "This must run from a Linux box." + + # Check dest dir exists + [ -n "$DEST_DIR" ] || die "Usage: $0 <destination-directory> [build-number]" + [ -d "$DEST_DIR" ] || die "Destination directory $DEST_DIR must exist." +} + +function build_libs() { + MAKE_OPT="-j8" + echo "*** Building: make $MAKE_OPT dx ping ddms jarutils androidprefs layoutlib_api ninepatch sdklib sdkuilib" + make $MAKE_OPT dx ping ddms jarutils androidprefs layoutlib_api layoutlib_utils ninepatch sdklib sdkuilib +} + +function build_plugin { + development/tools/eclipse/scripts/create_all_symlinks.sh + + # Qualifier is "v" followed by date/time in YYYYMMDDHHSS format and the optional + # build number. + DATE=`date +v%Y%m%d%H%M` + QUALIFIER="$DATE" + [ -n "$BUILD_NUMBER" ] && QUALIFIER="${QUALIFIER}-${BUILD_NUMBER}" + + # Compute the final directory name and remove any leftovers from previous + # runs if any. + BUILD_PREFIX="android-eclipse" + if [ "$INTERNAL_BUILD" ]; then + # append 'eng' signifier to end of archive name to denote internal build + BUILD_PREFIX="${BUILD_PREFIX}-eng" + fi + + # exclude date from build-zip name so it can be auto-calculated by continuous + # test process unless there's no build number, in which case the date is + # still used (useful for testing) + ZIP_NAME="${BUILD_PREFIX}-${BUILD_NUMBER:-$DATE}.zip" + [ -d "$DEST_DIR/$BUILD_PREFIX" ] || rm -rfv "$DEST_DIR/$BUILD_PREFIX" + + # Perform the Eclipse build and move the result in $DEST_DIR/android-build + development/tools/eclipse/scripts/build_plugins.sh $QUALIFIER $INTERNAL_BUILD -d "$DEST_DIR" -a "$BUILD_PREFIX" + + # Cleanup + [ -d "$QUALIFIER" ] && rm -rfv "$QUALIFIER" + + if [ "$CREATE_ZIP" ]; then + # The result is a full update-site under $DEST_DIR/BUILD_PREFIX + # Zip it and remove the directory. + echo "**** Package in $DEST_DIR" + [ -d "$DEST_DIR/$BUILD_PREFIX" ] || \ + die "Build failed to produce $DEST_DIR/$BUILD_PREFIX" + cd "$DEST_DIR" + [ -f "$ZIP_NAME" ] && rm -rfv "$ZIP_NAME" + cd "$BUILD_PREFIX" + zip -9r "../$ZIP_NAME" * + cd .. # back to $DEST_DIR + rm -rfv "$BUILD_PREFIX" # removes the directory, not the zip + echo "ZIP of Update site available at $DEST_DIR/${ZIP_NAME}" + else + echo "Update site available in $DEST_DIR/$BUILD_PREFIX" + fi +} + +get_params "$@" +check_params +build_libs +build_plugin diff --git a/eclipse/scripts/build_update_site.sh b/eclipse/scripts/build_update_site.sh new file mode 100755 index 0000000..5998756 --- /dev/null +++ b/eclipse/scripts/build_update_site.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Entry point to build the Eclipse plugins for local deployment. +# +# Input parameters: +# $1: Optional build number. If present, will be appended to the date qualifier. +# The build number cannot contain spaces *nor* periods (dashes are ok.) +# -i: Optional, if present, the Google internal update site will be built. Otherwise, +# the external site will be built +# +# Workflow: +# - calls buildserver with /home/$USER/www/no_crawl and -z +# to build and create the update size but do not zip it in the destination directory. + +set -e # Fail this script as soon as a command fails -- fail early, fail fast + +D=`dirname $0` +BUILD_NUMBER="" +INTERNAL_BUILD="" +# parse input parameters +while [ $# -gt 0 ]; do + if [ "$1" == "-i" ]; then + INTERNAL_BUILD="-i" + elif [ "$1" != "" ]; then + BUILD_NUMBER="$1" + fi + shift +done + +DEST_DIR="$HOME" +[ -z "$DEST_DIR" ] && [ -n "$USER" ] && DEST_DIR="/home/$USER" +[ -z "$DEST_DIR" ] && DEST_DIR="~" +DEST_DIR="$DEST_DIR/www/no_crawl" + +"$D/build_server.sh" "$DEST_DIR" "$BUILD_NUMBER" -z "$INTERNAL_BUILD" diff --git a/eclipse/scripts/collect_sources_for_sdk.sh b/eclipse/scripts/collect_sources_for_sdk.sh new file mode 100644 index 0000000..4824da7 --- /dev/null +++ b/eclipse/scripts/collect_sources_for_sdk.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +function usage() { + cat <<EOF + Description: + This script collects all framework Java sources from the current android + source code and places them in a source folder suitable for the eclipse ADT + plugin. + + Usage: + $0 [-n] <android-git-repo root> <sdk/platforms/xyz/sources> + + The source and destination directories must already exist. + Use -n for a dry-run. + +EOF +} + +DRY="" +if [ "-n" == "$1" ]; then + DRY="echo" + shift +fi + +DIR="frameworks" +if [ "-s" == "$1" ]; then + shift + DIR="$1" + shift +fi + +SRC="$1" +DST="$2" + +if [ -z "$SRC" ] || [ -z "$DST" ] || [ ! -d "$SRC" ] || [ ! -d "$DST" ]; then + usage + exit 1 +fi + +function process() { + echo "Examine" $1 +} + +N=0 +E=0 +for i in `find -L "${SRC}/${DIR}" -name "*.java"`; do + if [ -f "$i" ]; then + # look for ^package (android.view.blah);$ + PACKAGE=`sed -n '/^package [^ ;]\+; */{s/[^ ]* *\([^ ;]*\).*/\1/p;q}' "$i"` + if [ -n "$PACKAGE" ]; then + PACKAGE=${PACKAGE//./\/} # e.g. android.view => android/view + JAVA=`basename "$i"` # e.g. View.java + [ -z $DRY ] && [ ! -d "$DST/$PACKAGE" ] && mkdir -p -v "$DST/$PACKAGE" + $DRY cp -v "$i" "$DST/$PACKAGE/$JAVA" + N=$((N+1)) + else + echo "Warning: $i does not have a Java package." + E=$((E+1)) + fi + fi +done + +echo "$N java files copied" +[ $E -gt 0 ] && echo "$E warnings" + diff --git a/eclipse/scripts/create_adt_symlinks.sh b/eclipse/scripts/create_adt_symlinks.sh new file mode 100755 index 0000000..557c4d9 --- /dev/null +++ b/eclipse/scripts/create_adt_symlinks.sh @@ -0,0 +1,50 @@ +#!/bin/bash +function die() { + echo "Error: $*" + exit 1 +} + +set -e # fail early + +# CD to the top android directory +D=`dirname "$0"` +cd "$D/../../../../" + +DEST="development/tools/eclipse/plugins/com.android.ide.eclipse.adt" +# computes "../.." from DEST to here (in /android) +BACK=`echo $DEST | sed 's@[^/]*@..@g'` + +LIBS="sdkstats jarutils androidprefs layoutlib_api layoutlib_utils ninepatch sdklib sdkuilib" + +echo "make java libs ..." +make -j3 showcommands $LIBS || die "ADT: Fail to build one of $LIBS." + +echo "Copying java libs to $DEST" + +HOST=`uname` +if [ "$HOST" == "Linux" ]; then + for LIB in $LIBS; do + ln -svf $BACK/out/host/linux-x86/framework/$LIB.jar "$DEST/" + done + ln -svf $BACK/out/host/linux-x86/framework/kxml2-2.3.0.jar "$DEST/" + +elif [ "$HOST" == "Darwin" ]; then + for LIB in $LIBS; do + ln -svf $BACK/out/host/darwin-x86/framework/$LIB.jar "$DEST/" + done + ln -svf $BACK/out/host/darwin-x86/framework/kxml2-2.3.0.jar "$DEST/" + +elif [ "${HOST:0:6}" == "CYGWIN" ]; then + for LIB in $LIBS; do + cp -vf out/host/windows-x86/framework/$LIB.jar "$DEST/" + done + + if [ ! -f "$DEST/kxml2-2.3.0.jar" ]; then + cp -v "prebuilt/common/kxml2/kxml2-2.3.0.jar" "$DEST/" + fi + + chmod -v a+rx "$DEST"/*.jar +else + echo "Unsupported platform ($HOST). Nothing done." +fi + diff --git a/eclipse/scripts/create_all_symlinks.sh b/eclipse/scripts/create_all_symlinks.sh new file mode 100755 index 0000000..8508343 --- /dev/null +++ b/eclipse/scripts/create_all_symlinks.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "### $0 executing" + +function die() { + echo "Error: $*" + exit 1 +} + +# CD to the top android directory +D=`dirname "$0"` +cd "$D/../../../../" + +DEST="development/tools/eclipse/scripts" + +set -e # fail early + +echo ; echo "### ADT ###" ; echo +$DEST/create_adt_symlinks.sh "$*" +echo ; echo "### DDMS ###" ; echo +$DEST/create_ddms_symlinks.sh "$*" +echo ; echo "### TEST ###" ; echo +$DEST/create_test_symlinks.sh "$*" +echo ; echo "### BRIDGE ###" ; echo +$DEST/create_bridge_symlinks.sh "$*" + +echo "### $0 done" diff --git a/eclipse/scripts/create_bridge_symlinks.sh b/eclipse/scripts/create_bridge_symlinks.sh new file mode 100755 index 0000000..605ef63 --- /dev/null +++ b/eclipse/scripts/create_bridge_symlinks.sh @@ -0,0 +1,38 @@ +#!/bin/bash +function die() { + echo "Error: $*" + exit 1 +} + +set -e # fail early + +# CD to the top android directory +D=`dirname "$0"` +cd "$D/../../../../" + +HOST=`uname` +if [ "$HOST" == "Linux" ]; then + echo # nothing to do + +elif [ "$HOST" == "Darwin" ]; then + echo # nothing to do + +elif [ "${HOST:0:6}" == "CYGWIN" ]; then + if [ "x$1" == "x" ] || [ `basename "$1"` != "layoutlib.jar" ]; then + echo "Usage: $0 sdk/platforms/xxx/data/layoutlib.jar" + echo "Argument 1 should be the path to the layoutlib.jar that should be updated." + exit 1 + fi + + LIBS="layoutlib ninepatch" + echo "Make java libs: $LIBS" + make -j3 showcommands $LIBS || die "Bridge: Failed to build one of $LIBS." + + echo "Updating your SDK in $1" + cp -vf "out/host/windows-x86/framework/layoutlib.jar" "$1" + chmod -v a+rx "$1" + +else + echo "Unsupported platform ($HOST). Nothing done." +fi + diff --git a/eclipse/scripts/create_ddms_symlinks.sh b/eclipse/scripts/create_ddms_symlinks.sh new file mode 100755 index 0000000..276cf9b --- /dev/null +++ b/eclipse/scripts/create_ddms_symlinks.sh @@ -0,0 +1,79 @@ +#!/bin/bash +#----------------------------------------------------------------------------| +# Creates the links to use ddm{ui}lib in the eclipse-ide plugin. +# Run this from device/tools/eclipse/scripts +#----------------------------------------------------------------------------| + +set -e + +HOST=`uname` +if [ "${HOST:0:6}" == "CYGWIN" ]; then + # We can't use symlinks under Cygwin + + function cpfile { # $1=dest $2=source + cp -fv $2 $1/ + } + + function cpdir() { # $1=dest $2=source + rsync -avW --delete-after $2 $1 + } + +else + # For all other systems which support symlinks + + # computes the "reverse" path, e.g. "a/b/c" => "../../.." + function back() { + echo $1 | sed 's@[^/]*@..@g' + } + + function cpfile { # $1=dest $2=source + ln -svf `back $1`/$2 $1/ + } + + function cpdir() { # $1=dest $2=source + ln -svf `back $1`/$2 $1 + } +fi + +# CD to the top android directory +D=`dirname "$0"` +cd "$D/../../../../" + + +BASE="development/tools/eclipse/plugins/com.android.ide.eclipse.ddms" + +DEST=$BASE/libs +mkdir -p $DEST +for i in prebuilt/common/jfreechart/*.jar; do + cpfile $DEST $i +done + +DEST=$BASE/src/com/android +mkdir -p $DEST +for i in development/tools/ddms/libs/ddmlib/src/com/android/ddmlib \ + development/tools/ddms/libs/ddmuilib/src/com/android/ddmuilib ; do + cpdir $DEST $i +done + +DEST=$BASE/icons +mkdir -p $DEST +for i in \ + add.png \ + backward.png \ + clear.png \ + d.png debug-attach.png debug-error.png debug-wait.png delete.png device.png down.png \ + e.png edit.png empty.png emulator.png \ + forward.png \ + gc.png \ + heap.png halt.png \ + i.png importBug.png \ + load.png \ + pause.png play.png pull.png push.png \ + save.png \ + thread.png \ + up.png \ + v.png \ + w.png warning.png ; do + cpfile $DEST development/tools/ddms/libs/ddmuilib/src/resources/images/$i +done + diff --git a/eclipse/scripts/create_test_symlinks.sh b/eclipse/scripts/create_test_symlinks.sh new file mode 100755 index 0000000..931dce8 --- /dev/null +++ b/eclipse/scripts/create_test_symlinks.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e + +# CD to the top android directory +D=`dirname "$0"` +cd "$D/../../../../" + +# computes relative ".." paths from $1 to here (in /android) +function back() { + echo $1 | sed 's@[^/]*@..@g' +} + +HOST=`uname` +if [ "${HOST:0:6}" == "CYGWIN" ]; then + # We can't use symlinks under Cygwin + function cpdir() { # $1=dest $2=source + rsync -avW --delete-after $2 $1 + } + +else + # For all other systems which support symlinks + function cpdir() { # $1=dest $2=source + ln -svf `back $1`/$2 $1 + } +fi + +BASE="development/tools/eclipse/plugins/com.android.ide.eclipse.tests" +DEST=$BASE +BACK=`back $DEST` + +HOST=`uname` +if [ "$HOST" == "Linux" ]; then + ln -svf $BACK/out/host/linux-x86/framework/kxml2-2.3.0.jar "$DEST/" + +elif [ "$HOST" == "Darwin" ]; then + ln -svf $BACK/out/host/darwin-x86/framework/kxml2-2.3.0.jar "$DEST/" + +elif [ "${HOST:0:6}" == "CYGWIN" ]; then + + if [ ! -f "$DEST/kxml2-2.3.0.jar" ]; then + cp -v "prebuilt/common/kxml2/kxml2-2.3.0.jar" "$DEST/" + chmod -v a+rx "$DEST"/*.jar + fi + +else + echo "Unsupported platform ($HOST). Nothing done." +fi + +# create link to ddmlib tests +DEST=$BASE/unittests/com/android +BACK=`back $DEST` +cpdir $DEST development/tools/ddms/libs/ddmlib/tests/src/com/android/ddmlib + diff --git a/eclipse/scripts/gen_icon.py b/eclipse/scripts/gen_icon.py new file mode 100755 index 0000000..f6274e1 --- /dev/null +++ b/eclipse/scripts/gen_icon.py @@ -0,0 +1,71 @@ +#!/usr/bin/python + +import sys +import StringIO +try: + import Image, ImageDraw, ImageFont +except ImportError, e: + print str(e) + print "Are you missing the Python Imaging Library? (apt-get install python-imaging)" + sys.exit(1) + +FONT_PATH = "/usr/share/fonts/truetype/msttcorefonts/arial.ttf" + +class Args(object): + def __init__(self, dest_name, size, circle_color, border_color, + letter_color, letter): + self.dest_name = dest_name + self.size = size + self.circle_color = circle_color + self.border_color = border_color + self.letter_color = letter_color + self.letter = letter + +def main(args): + data = process_args(args) + if data: + createImage(data) + +def process_args(args): + if not args or len(args) != 6: + usage() + return Args(*args) + +def usage(): + print """Usage: %s <file_name> <size> <circle-color> <border-color> <letter-color> <letter>""" % sys.argv[0] + sys.exit(1) + +def createImage(data): + zoom = 4 + rmin = -zoom/2 + rmax = zoom/2 + if zoom > 1: + r = range(-zoom/2, zoom/2+1) + else: + r = [ 0 ] + sz = int(data.size) + sz4 = sz * zoom + + img = Image.new("RGBA", (sz4, sz4), (255,255,255,0)) + draw = ImageDraw.Draw(img) + + draw.ellipse((0, 0, sz4-zoom, sz4-zoom), + fill=data.circle_color, outline=None) + for i in r: + draw.ellipse((i, i, sz4-i-zoom, sz4-i-zoom), + fill=None, outline=data.border_color) + + font = ImageFont.truetype(FONT_PATH, int(sz4 * .75)) + tsx, tsy = draw.textsize(data.letter, font=font) + + ptx = (sz4 - tsx) / 2 + pty = (sz4 - tsy) / 2 + for i in r: + draw.text((ptx + i, pty), data.letter, font=font, fill=data.letter_color) + + img = img.resize((sz, sz), Image.BICUBIC) + img.save(data.dest_name, "PNG") + print "Saved", data.dest_name + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/eclipse/scripts/setup_eclipse.sh b/eclipse/scripts/setup_eclipse.sh new file mode 100755 index 0000000..5143e23 --- /dev/null +++ b/eclipse/scripts/setup_eclipse.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Quick script used to setup Eclipse for the ADT plugin build. +# +# usage: +# setup_eclipse.sh <dest_dir> +# +# Workflow: +# - downloads & unpack Eclipse if necessary +# - *runs* it once + + +#----------------- +# +# Note: right now this is invoked by //device/tools/eclipse/doBuild.sh +# and it *MUST* be invoked with the following destination directory: +# +# $ setup_eclipse.sh /buildbot/eclipse-android/3.4.0/ +# +#----------------- + + +set -e # abort this script early if any command fails + +function die() { + echo $@ + exit 1 +} + +if [ "-p" == "$1" ]; then + GET_PID="-p" + shift +fi + +BASE_DIR="$1" + +[ -n "$1" ] || die "Usage: $0 <dest-dir>" + +# URL for 3.4.0 RCP Linux 32 Bits. Includes GEF, WTP as needed. +DOWNLOAD_URL="http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/ganymede/R/eclipse-rcp-ganymede-linux-gtk.tar.gz&url=http://eclipse.unixheads.org/technology/epp/downloads/release/ganymede/R/eclipse-rcp-ganymede-linux-gtk.tar.gz&mirror_id=480" + +BIN="$BASE_DIR/eclipse/eclipse" # path to installed binary +TARGZ="$BASE_DIR/eclipse-rcp-ganymede-linux-gtk.tar.gz" + +if [ ! -f "$BIN" ]; then + echo "Downloading and installing Eclipse in $BASE_DIR." + mkdir -p "$BASE_DIR" + wget --continue --no-verbose --output-document="$TARGZ" "$DOWNLOAD_URL" + echo "Unpacking $TARGZ" + (cd "$BASE_DIR" && tar xzf "$TARGZ") + + echo + echo "*** WARNING: To setup Eclipse correctly, it must be ran at least once manually" + echo "*** Eclipse will now start." + echo + if [ -n "$GET_PID" ]; then + # if started from the automatic eclipse build, run Eclipse in the background + "$BIN" & + ECLIPSE_PID=$! + echo "*** Eclipse started in background with PID $ECLIPSE_PID" + echo "$ECLIPSE_PID" > "$BASE_DIR"/eclipse.pid + sleep 5 # give some time for Eclipse to start and setup its environment + else + # if started manually, run Eclipse in the foreground + "$BIN" + fi +fi diff --git a/eclipse/scripts/update_version.sh b/eclipse/scripts/update_version.sh new file mode 100644 index 0000000..a288965 --- /dev/null +++ b/eclipse/scripts/update_version.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +OLD="$1" +NEW="$2" + +# sanity check in input args +if [ -z "$OLD" ] || [ -z "$NEW" ]; then + cat <<EOF +Usage: $0 <old> <new> +Changes the ADT plugin revision number. +Example: + cd tools/eclipse + scripts/update_version.sh 0.1.2 0.2.3 +EOF + exit 1 +fi + +# sanity check on current dir +if [ `basename "$PWD"` != "eclipse" ]; then + echo "Please run this from tools/eclipse." + exit 1 +fi + +# quote dots for regexps +OLD="${OLD//./\.}" +NEW="${NEW//./\.}" + +# Find all the files with the old pattern, except changes.txt and +# p4 edit them. Skip that if there's no p4 in path. +if which g4 1>/dev/null 2>/dev/null ; then + grep -rl "$OLD" * | grep -E "\.xml$|\.MF$" | xargs -n 5 g4 edit +fi + +# Now find the same files but this time use sed to replace in-place with +# the new pattern. Old files get backuped with the .old extension. +grep -rl "$OLD" * | grep -E "\.xml$|\.MF$" | xargs -n 1 sed -i.old "s/$OLD/$NEW/g" + diff --git a/eclipse/sites/external/.project b/eclipse/sites/external/.project new file mode 100644 index 0000000..9916269 --- /dev/null +++ b/eclipse/sites/external/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>external-site</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.pde.UpdateSiteBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.UpdateSiteNature</nature> + </natures> +</projectDescription> diff --git a/eclipse/sites/external/index.html b/eclipse/sites/external/index.html new file mode 100644 index 0000000..784be4b --- /dev/null +++ b/eclipse/sites/external/index.html @@ -0,0 +1,60 @@ +<html> +<head> +<title>Android Development Toolkit update site.</title> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<style>@import url("web/site.css");</style> +<script type="text/javascript"> + var returnval = 0; + var stylesheet, xmlFile, cache, doc; + function init(){ + // NSCP 7.1+ / Mozilla 1.4.1+ / Safari + // Use the standard DOM Level 2 technique, if it is supported + if (document.implementation && document.implementation.createDocument) { + xmlFile = document.implementation.createDocument("", "", null); + stylesheet = document.implementation.createDocument("", "", null); + if (xmlFile.load){ + xmlFile.load("site.xml"); + stylesheet.load("web/site.xsl"); + } else { + alert("Document could not be loaded by browser."); + } + xmlFile.addEventListener("load", transform, false); + stylesheet.addEventListener("load", transform, false); + } + //IE 6.0+ solution + else if (window.ActiveXObject) { + xmlFile = new ActiveXObject("msxml2.DOMDocument.3.0"); + xmlFile.async = false; + xmlFile.load("site.xml"); + stylesheet = new ActiveXObject("msxml2.FreeThreadedDOMDocument.3.0"); + stylesheet.async = false; + stylesheet.load("web/site.xsl"); + cache = new ActiveXObject("msxml2.XSLTemplate.3.0"); + cache.stylesheet = stylesheet; + transformData(); + } + } + // separate transformation function for IE 6.0+ + function transformData(){ + var processor = cache.createProcessor(); + processor.input = xmlFile; + processor.transform(); + data.innerHTML = processor.output; + } + // separate transformation function for NSCP 7.1+ and Mozilla 1.4.1+ + function transform(){ + returnval+=1; + if (returnval==2){ + var processor = new XSLTProcessor(); + processor.importStylesheet(stylesheet); + doc = processor.transformToDocument(xmlFile); + document.getElementById("data").innerHTML = doc.documentElement.innerHTML; + } + } +</script> +</head> +<body onload="init();"> +<!--[insert static HTML here]--> +<div id="data"><!-- this is where the transformed data goes --></div> +</body> +</html> diff --git a/eclipse/sites/external/site.xml b/eclipse/sites/external/site.xml new file mode 100644 index 0000000..9fca8d2 --- /dev/null +++ b/eclipse/sites/external/site.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<site> + <description url="https://dl-ssl.google.com/android/eclipse/"> + Update Site for Android Development Toolkit + </description> + <feature url="features/com.android.ide.eclipse.adt_0.9.0.qualifier.jar" id="com.android.ide.eclipse.adt" version="0.9.0.qualifier"> + <category name="developer"/> + </feature> + <feature url="features/com.android.ide.eclipse.ddms_0.9.0.qualifier.jar" id="com.android.ide.eclipse.ddms" version="0.9.0.qualifier"> + <category name="developer"/> + </feature> + <category-def name="developer" label="Developer Tools"> + <description> + Features that add Android support to Eclipse for application developers. + </description> + </category-def> +</site> diff --git a/eclipse/sites/external/web/site.css b/eclipse/sites/external/web/site.css new file mode 100644 index 0000000..62c6f9f --- /dev/null +++ b/eclipse/sites/external/web/site.css @@ -0,0 +1,12 @@ +<STYLE type="text/css"> +td.spacer {padding-bottom: 10px; padding-top: 10px;} +.title { font-family: sans-serif; color: #99AACC;} +.bodyText { font-family: sans-serif; font-size: 9pt; color:#000000; } +.sub-header { font-family: sans-serif; font-style: normal; font-weight: bold; font-size: 9pt; color: white;} +.log-text {font-family: sans-serif; font-style: normal; font-weight: lighter; font-size: 8pt; color:black;} +.big-header { font-family: sans-serif; font-style: normal; font-weight: bold; font-size: 9pt; color: white; border-top:10px solid white;} +.light-row {background:#FFFFFF} +.dark-row {background:#EEEEFF} +.header {background:#99AADD} +#indent {word-wrap : break-word;width :300px;text-indent:10px;} +</STYLE> diff --git a/eclipse/sites/external/web/site.xsl b/eclipse/sites/external/web/site.xsl new file mode 100644 index 0000000..a94157d --- /dev/null +++ b/eclipse/sites/external/web/site.xsl @@ -0,0 +1,214 @@ +<xsl:stylesheet version = '1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl="urn:schemas-microsoft-com:xslt"> +<xsl:output method="html" encoding="UTF-8"/> +<xsl:key name="cat" match="category" use="@name"/> +<xsl:template match="/"> +<xsl:for-each select="site"> + <html> + <head> + <title>update-site</title> + <style>@import url("web/site.css");</style> + </head> + <body> + <h1 class="title">update-site</h1> + <p class="bodyText"><xsl:value-of select="description"/></p> + <table width="100%" border="0" cellspacing="1" cellpadding="2"> + <xsl:for-each select="category-def"> + <xsl:sort select="@label" order="ascending" case-order="upper-first"/> + <xsl:sort select="@name" order="ascending" case-order="upper-first"/> + <xsl:if test="count(key('cat',@name)) != 0"> + <tr class="header"> + <td class="sub-header" width="30%"> + <xsl:value-of select="@name"/> + </td> + <td class="sub-header" width="70%"> + <xsl:value-of select="@label"/> + </td> + </tr> + <xsl:for-each select="key('cat',@name)"> + <xsl:sort select="ancestor::feature//@version" order="ascending"/> + <xsl:sort select="ancestor::feature//@id" order="ascending" case-order="upper-first"/> + <tr> + <xsl:choose> + <xsl:when test="(position() mod 2 = 1)"> + <xsl:attribute name="class">dark-row</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="class">light-row</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <td class="log-text" id="indent"> + <xsl:choose> + <xsl:when test="ancestor::feature//@label"> + <a href="{ancestor::feature//@url}"><xsl:value-of select="ancestor::feature//@label"/></a> + <br/> + <div id="indent"> + (<xsl:value-of select="ancestor::feature//@id"/> - <xsl:value-of select="ancestor::feature//@version"/>) + </div> + </xsl:when> + <xsl:otherwise> + <a href="{ancestor::feature//@url}"><xsl:value-of select="ancestor::feature//@id"/> - <xsl:value-of select="ancestor::feature//@version"/></a> + </xsl:otherwise> + </xsl:choose> + <br /> + </td> + <td> + <table> + <xsl:if test="ancestor::feature//@os"> + <tr><td class="log-text" id="indent">Operating Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@os"/></td> + </tr> + </xsl:if> + <xsl:if test="ancestor::feature//@ws"> + <tr><td class="log-text" id="indent">Windows Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@ws"/></td> + </tr> + </xsl:if> + <xsl:if test="ancestor::feature//@nl"> + <tr><td class="log-text" id="indent">Languages:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@nl"/></td> + </tr> + </xsl:if> + <xsl:if test="ancestor::feature//@arch"> + <tr><td class="log-text" id="indent">Architecture:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@arch"/></td> + </tr> + </xsl:if> + </table> + </td> + </tr> + </xsl:for-each> + <tr><td class="spacer"><br/></td><td class="spacer"><br/></td></tr> + </xsl:if> + </xsl:for-each> + <xsl:if test="count(feature) > count(feature/category)"> + <tr class="header"> + <td class="sub-header" colspan="2"> + Uncategorized + </td> + </tr> + </xsl:if> + <xsl:choose> + <xsl:when test="function-available('msxsl:node-set')"> + <xsl:variable name="rtf-nodes"> + <xsl:for-each select="feature[not(category)]"> + <xsl:sort select="@id" order="ascending" case-order="upper-first"/> + <xsl:sort select="@version" order="ascending" /> + <xsl:value-of select="."/> + <xsl:copy-of select="." /> + </xsl:for-each> + </xsl:variable> + <xsl:variable name="myNodeSet" select="msxsl:node-set($rtf-nodes)/*"/> + <xsl:for-each select="$myNodeSet"> + <tr> + <xsl:choose> + <xsl:when test="position() mod 2 = 1"> + <xsl:attribute name="class">dark-row</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="class">light-row</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <td class="log-text" id="indent"> + <xsl:choose> + <xsl:when test="@label"> + <a href="{@url}"><xsl:value-of select="@label"/></a> + <br /> + <div id="indent"> + (<xsl:value-of select="@id"/> - <xsl:value-of select="@version"/>) + </div> + </xsl:when> + <xsl:otherwise> + <a href="{@url}"><xsl:value-of select="@id"/> - <xsl:value-of select="@version"/></a> + </xsl:otherwise> + </xsl:choose> + <br /><br /> + </td> + <td> + <table> + <xsl:if test="@os"> + <tr><td class="log-text" id="indent">Operating Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@os"/></td> + </tr> + </xsl:if> + <xsl:if test="@ws"> + <tr><td class="log-text" id="indent">Windows Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@ws"/></td> + </tr> + </xsl:if> + <xsl:if test="@nl"> + <tr><td class="log-text" id="indent">Languages:</td> + <td class="log-text" id="indent"><xsl:value-of select="@nl"/></td> + </tr> + </xsl:if> + <xsl:if test="@arch"> + <tr><td class="log-text" id="indent">Architecture:</td> + <td class="log-text" id="indent"><xsl:value-of select="@arch"/></td> + </tr> + </xsl:if> + </table> + </td> + </tr> + </xsl:for-each> + </xsl:when> + <xsl:otherwise> + <xsl:for-each select="feature[not(category)]"> + <xsl:sort select="@id" order="ascending" case-order="upper-first"/> + <xsl:sort select="@version" order="ascending" /> + <tr> + <xsl:choose> + <xsl:when test="count(preceding-sibling::feature[not(category)]) mod 2 = 1"> + <xsl:attribute name="class">dark-row</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="class">light-row</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <td class="log-text" id="indent"> + <xsl:choose> + <xsl:when test="@label"> + <a href="{@url}"><xsl:value-of select="@label"/></a> + <br /> + <div id="indent"> + (<xsl:value-of select="@id"/> - <xsl:value-of select="@version"/>) + </div> + </xsl:when> + <xsl:otherwise> + <a href="{@url}"><xsl:value-of select="@id"/> - <xsl:value-of select="@version"/></a> + </xsl:otherwise> + </xsl:choose> + <br /><br /> + </td> + <td> + <table> + <xsl:if test="@os"> + <tr><td class="log-text" id="indent">Operating Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@os"/></td> + </tr> + </xsl:if> + <xsl:if test="@ws"> + <tr><td class="log-text" id="indent">Windows Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@ws"/></td> + </tr> + </xsl:if> + <xsl:if test="@nl"> + <tr><td class="log-text" id="indent">Languages:</td> + <td class="log-text" id="indent"><xsl:value-of select="@nl"/></td> + </tr> + </xsl:if> + <xsl:if test="@arch"> + <tr><td class="log-text" id="indent">Architecture:</td> + <td class="log-text" id="indent"><xsl:value-of select="@arch"/></td> + </tr> + </xsl:if> + </table> + </td> + </tr> + </xsl:for-each> + </xsl:otherwise> + </xsl:choose> + </table> + </body> + </html> +</xsl:for-each> +</xsl:template> +</xsl:stylesheet> diff --git a/eclipse/sites/internal/.project b/eclipse/sites/internal/.project new file mode 100644 index 0000000..0bd658d --- /dev/null +++ b/eclipse/sites/internal/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>internal-site</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.pde.UpdateSiteBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.UpdateSiteNature</nature> + </natures> +</projectDescription> diff --git a/eclipse/sites/internal/index.html b/eclipse/sites/internal/index.html new file mode 100644 index 0000000..784be4b --- /dev/null +++ b/eclipse/sites/internal/index.html @@ -0,0 +1,60 @@ +<html> +<head> +<title>Android Development Toolkit update site.</title> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<style>@import url("web/site.css");</style> +<script type="text/javascript"> + var returnval = 0; + var stylesheet, xmlFile, cache, doc; + function init(){ + // NSCP 7.1+ / Mozilla 1.4.1+ / Safari + // Use the standard DOM Level 2 technique, if it is supported + if (document.implementation && document.implementation.createDocument) { + xmlFile = document.implementation.createDocument("", "", null); + stylesheet = document.implementation.createDocument("", "", null); + if (xmlFile.load){ + xmlFile.load("site.xml"); + stylesheet.load("web/site.xsl"); + } else { + alert("Document could not be loaded by browser."); + } + xmlFile.addEventListener("load", transform, false); + stylesheet.addEventListener("load", transform, false); + } + //IE 6.0+ solution + else if (window.ActiveXObject) { + xmlFile = new ActiveXObject("msxml2.DOMDocument.3.0"); + xmlFile.async = false; + xmlFile.load("site.xml"); + stylesheet = new ActiveXObject("msxml2.FreeThreadedDOMDocument.3.0"); + stylesheet.async = false; + stylesheet.load("web/site.xsl"); + cache = new ActiveXObject("msxml2.XSLTemplate.3.0"); + cache.stylesheet = stylesheet; + transformData(); + } + } + // separate transformation function for IE 6.0+ + function transformData(){ + var processor = cache.createProcessor(); + processor.input = xmlFile; + processor.transform(); + data.innerHTML = processor.output; + } + // separate transformation function for NSCP 7.1+ and Mozilla 1.4.1+ + function transform(){ + returnval+=1; + if (returnval==2){ + var processor = new XSLTProcessor(); + processor.importStylesheet(stylesheet); + doc = processor.transformToDocument(xmlFile); + document.getElementById("data").innerHTML = doc.documentElement.innerHTML; + } + } +</script> +</head> +<body onload="init();"> +<!--[insert static HTML here]--> +<div id="data"><!-- this is where the transformed data goes --></div> +</body> +</html> diff --git a/eclipse/sites/internal/site.xml b/eclipse/sites/internal/site.xml new file mode 100644 index 0000000..9f2642f --- /dev/null +++ b/eclipse/sites/internal/site.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<site> + <description url="https://android.corp.google.com/adt/"> + Update Site for Android Development Toolkit + </description> + <feature url="features/com.android.ide.eclipse.adt_0.9.0.qualifier.jar" id="com.android.ide.eclipse.adt" version="0.9.0.qualifier"> + <category name="developer"/> + </feature> + <feature url="features/com.android.ide.eclipse.ddms_0.9.0.qualifier.jar" id="com.android.ide.eclipse.ddms" version="0.9.0.qualifier"> + <category name="developer"/> + </feature> + <feature url="features/com.android.ide.eclipse.tests_0.9.0.qualifier.jar" id="com.android.ide.eclipse.tests" version="0.9.0.qualifier"> + <category name="test"/> + </feature> + <category-def name="developer" label="Application Developer Tools"> + <description> + Features that add Android support to Eclipse for application developers. + </description> + </category-def> + <category-def name="test" label="Plugin Developer Tests"> + <description> + Tests for the other Android plugins + </description> + </category-def> +</site> diff --git a/eclipse/sites/internal/web/site.css b/eclipse/sites/internal/web/site.css new file mode 100644 index 0000000..62c6f9f --- /dev/null +++ b/eclipse/sites/internal/web/site.css @@ -0,0 +1,12 @@ +<STYLE type="text/css"> +td.spacer {padding-bottom: 10px; padding-top: 10px;} +.title { font-family: sans-serif; color: #99AACC;} +.bodyText { font-family: sans-serif; font-size: 9pt; color:#000000; } +.sub-header { font-family: sans-serif; font-style: normal; font-weight: bold; font-size: 9pt; color: white;} +.log-text {font-family: sans-serif; font-style: normal; font-weight: lighter; font-size: 8pt; color:black;} +.big-header { font-family: sans-serif; font-style: normal; font-weight: bold; font-size: 9pt; color: white; border-top:10px solid white;} +.light-row {background:#FFFFFF} +.dark-row {background:#EEEEFF} +.header {background:#99AADD} +#indent {word-wrap : break-word;width :300px;text-indent:10px;} +</STYLE> diff --git a/eclipse/sites/internal/web/site.xsl b/eclipse/sites/internal/web/site.xsl new file mode 100644 index 0000000..a94157d --- /dev/null +++ b/eclipse/sites/internal/web/site.xsl @@ -0,0 +1,214 @@ +<xsl:stylesheet version = '1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:msxsl="urn:schemas-microsoft-com:xslt"> +<xsl:output method="html" encoding="UTF-8"/> +<xsl:key name="cat" match="category" use="@name"/> +<xsl:template match="/"> +<xsl:for-each select="site"> + <html> + <head> + <title>update-site</title> + <style>@import url("web/site.css");</style> + </head> + <body> + <h1 class="title">update-site</h1> + <p class="bodyText"><xsl:value-of select="description"/></p> + <table width="100%" border="0" cellspacing="1" cellpadding="2"> + <xsl:for-each select="category-def"> + <xsl:sort select="@label" order="ascending" case-order="upper-first"/> + <xsl:sort select="@name" order="ascending" case-order="upper-first"/> + <xsl:if test="count(key('cat',@name)) != 0"> + <tr class="header"> + <td class="sub-header" width="30%"> + <xsl:value-of select="@name"/> + </td> + <td class="sub-header" width="70%"> + <xsl:value-of select="@label"/> + </td> + </tr> + <xsl:for-each select="key('cat',@name)"> + <xsl:sort select="ancestor::feature//@version" order="ascending"/> + <xsl:sort select="ancestor::feature//@id" order="ascending" case-order="upper-first"/> + <tr> + <xsl:choose> + <xsl:when test="(position() mod 2 = 1)"> + <xsl:attribute name="class">dark-row</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="class">light-row</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <td class="log-text" id="indent"> + <xsl:choose> + <xsl:when test="ancestor::feature//@label"> + <a href="{ancestor::feature//@url}"><xsl:value-of select="ancestor::feature//@label"/></a> + <br/> + <div id="indent"> + (<xsl:value-of select="ancestor::feature//@id"/> - <xsl:value-of select="ancestor::feature//@version"/>) + </div> + </xsl:when> + <xsl:otherwise> + <a href="{ancestor::feature//@url}"><xsl:value-of select="ancestor::feature//@id"/> - <xsl:value-of select="ancestor::feature//@version"/></a> + </xsl:otherwise> + </xsl:choose> + <br /> + </td> + <td> + <table> + <xsl:if test="ancestor::feature//@os"> + <tr><td class="log-text" id="indent">Operating Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@os"/></td> + </tr> + </xsl:if> + <xsl:if test="ancestor::feature//@ws"> + <tr><td class="log-text" id="indent">Windows Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@ws"/></td> + </tr> + </xsl:if> + <xsl:if test="ancestor::feature//@nl"> + <tr><td class="log-text" id="indent">Languages:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@nl"/></td> + </tr> + </xsl:if> + <xsl:if test="ancestor::feature//@arch"> + <tr><td class="log-text" id="indent">Architecture:</td> + <td class="log-text" id="indent"><xsl:value-of select="ancestor::feature//@arch"/></td> + </tr> + </xsl:if> + </table> + </td> + </tr> + </xsl:for-each> + <tr><td class="spacer"><br/></td><td class="spacer"><br/></td></tr> + </xsl:if> + </xsl:for-each> + <xsl:if test="count(feature) > count(feature/category)"> + <tr class="header"> + <td class="sub-header" colspan="2"> + Uncategorized + </td> + </tr> + </xsl:if> + <xsl:choose> + <xsl:when test="function-available('msxsl:node-set')"> + <xsl:variable name="rtf-nodes"> + <xsl:for-each select="feature[not(category)]"> + <xsl:sort select="@id" order="ascending" case-order="upper-first"/> + <xsl:sort select="@version" order="ascending" /> + <xsl:value-of select="."/> + <xsl:copy-of select="." /> + </xsl:for-each> + </xsl:variable> + <xsl:variable name="myNodeSet" select="msxsl:node-set($rtf-nodes)/*"/> + <xsl:for-each select="$myNodeSet"> + <tr> + <xsl:choose> + <xsl:when test="position() mod 2 = 1"> + <xsl:attribute name="class">dark-row</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="class">light-row</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <td class="log-text" id="indent"> + <xsl:choose> + <xsl:when test="@label"> + <a href="{@url}"><xsl:value-of select="@label"/></a> + <br /> + <div id="indent"> + (<xsl:value-of select="@id"/> - <xsl:value-of select="@version"/>) + </div> + </xsl:when> + <xsl:otherwise> + <a href="{@url}"><xsl:value-of select="@id"/> - <xsl:value-of select="@version"/></a> + </xsl:otherwise> + </xsl:choose> + <br /><br /> + </td> + <td> + <table> + <xsl:if test="@os"> + <tr><td class="log-text" id="indent">Operating Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@os"/></td> + </tr> + </xsl:if> + <xsl:if test="@ws"> + <tr><td class="log-text" id="indent">Windows Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@ws"/></td> + </tr> + </xsl:if> + <xsl:if test="@nl"> + <tr><td class="log-text" id="indent">Languages:</td> + <td class="log-text" id="indent"><xsl:value-of select="@nl"/></td> + </tr> + </xsl:if> + <xsl:if test="@arch"> + <tr><td class="log-text" id="indent">Architecture:</td> + <td class="log-text" id="indent"><xsl:value-of select="@arch"/></td> + </tr> + </xsl:if> + </table> + </td> + </tr> + </xsl:for-each> + </xsl:when> + <xsl:otherwise> + <xsl:for-each select="feature[not(category)]"> + <xsl:sort select="@id" order="ascending" case-order="upper-first"/> + <xsl:sort select="@version" order="ascending" /> + <tr> + <xsl:choose> + <xsl:when test="count(preceding-sibling::feature[not(category)]) mod 2 = 1"> + <xsl:attribute name="class">dark-row</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="class">light-row</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <td class="log-text" id="indent"> + <xsl:choose> + <xsl:when test="@label"> + <a href="{@url}"><xsl:value-of select="@label"/></a> + <br /> + <div id="indent"> + (<xsl:value-of select="@id"/> - <xsl:value-of select="@version"/>) + </div> + </xsl:when> + <xsl:otherwise> + <a href="{@url}"><xsl:value-of select="@id"/> - <xsl:value-of select="@version"/></a> + </xsl:otherwise> + </xsl:choose> + <br /><br /> + </td> + <td> + <table> + <xsl:if test="@os"> + <tr><td class="log-text" id="indent">Operating Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@os"/></td> + </tr> + </xsl:if> + <xsl:if test="@ws"> + <tr><td class="log-text" id="indent">Windows Systems:</td> + <td class="log-text" id="indent"><xsl:value-of select="@ws"/></td> + </tr> + </xsl:if> + <xsl:if test="@nl"> + <tr><td class="log-text" id="indent">Languages:</td> + <td class="log-text" id="indent"><xsl:value-of select="@nl"/></td> + </tr> + </xsl:if> + <xsl:if test="@arch"> + <tr><td class="log-text" id="indent">Architecture:</td> + <td class="log-text" id="indent"><xsl:value-of select="@arch"/></td> + </tr> + </xsl:if> + </table> + </td> + </tr> + </xsl:for-each> + </xsl:otherwise> + </xsl:choose> + </table> + </body> + </html> +</xsl:for-each> +</xsl:template> +</xsl:stylesheet> diff --git a/eclipse/source_package_readme.txt b/eclipse/source_package_readme.txt new file mode 100644 index 0000000..456bae7 --- /dev/null +++ b/eclipse/source_package_readme.txt @@ -0,0 +1,49 @@ +HOW TO PACKAGE THE SOURCE OF THE PLUGINS FOR RELEASE. + +Note: this is only useful before we move to the public source repository, after which this will +be obsolete. + +The source archive must contains: +1/ Source of the EPL plugins that are released. +2/ Any closed source dependencies that were created by Google. +3/ The readme file explaining how to build the plugins. + + +1/ PLUGIN SOURCE + +The Plugins that are currently released and that are EPL are: +- Android Developer Tools => com.android.ide.eclipse.adt +- Common => com.android.ide.eclipse.common +- Android Editors => com.android.ide.eclipse.editors + +All three plugins are located in + device/tools/eclipse/plugins/ + +Before packing them up, it is important to: +- remove the bin directory if it exists +- remove any symlinks to jar files from the top level folder of each plugin + +2/ PLUGIN DEPENDENCIES + +The plugin dependencies are jar files embedded in some of the plugins. Some of those jar files +are android libraries for which the source code is not yet being released (They will be released +under the APL). + +Those libraries are not part of the SDK, and need to be taken from a engineering build. +They will be located in + device/out/host/<platform>/framework/ + +The libraries to copy are: + - layoutlib_api.jar + - layoutlib_utils.jar + - ninepatch.jar + +They should be placed in a "libs" folder in the source archive. + +3/ README + +In the source archive, at the top level, needs to be present a file explaining how to compile +the plugins. + +This file is located at: + device/tools/eclipse/plugins/README.txt
\ No newline at end of file |