Há um bug na API java.util.prefs (bug nº 4772228 no Bug Database da Sun) rodando em Windows: como os métodos Preferences.userNodeForPackage(java.lang.Class) e Preferences.systemNodeForPackage(java.lang.class) utilizam o registro do Windows para guardar e recuperar as informações de preferências, programas rodando em contas de usuários sem privilégios de administrador não conseguem ler as preferências.
Como precisamos implementar preferências de usuário persistentes em um programa, implementamos uma classe XMLPreferences que faz exatamente o que a AbstractPreferences determina, utilizando a DTD definida pela Sun para preferências; usamos DOM para armazenar e recuperar preferências em arquivos XML, e escapar assim do registro.
Se alguém precisar da Preferences funcionando, manda um e-mail que eu repasso, ok? A implementação está meio vagabunda e precisa de melhoras, mas acho que é um bom começo.
Nãããããooooo!!! Não comecem mais uma feira-livre aqui no fórum. escordeiro, por favor, se for possível, disponibilize o código aqui no fórum. Ou, melhor ainda, faça um mini-tutorialzinho descrevendo sua solução.
E
escordeiro
hahahahaha
Tranquilo, hoje mais tarde eu posto o código aqui, não tenho ele comigo.
[]'s
E
escordeiro
Daniel, um mini-tutorial não vai dar, mas acho que pelos comentários dá pra entender. A implementação está meio ruim e tem um método que nós não implementamos, mas para o básico está funcionando. :oops:
Temos duas classes para fazer o negócio funcionar: XMLPreferences, que é a implementação de Preferences propriamente dita, e PreferencesController, que é um Singleton que mantém o documento XML e permite o uso da XMLPreferences.
/* XMLPreferences.java */importjava.util.Vector;importjava.util.prefs.AbstractPreferences;importjava.util.prefs.BackingStoreException;importorg.w3c.dom.*;/** * This implementation of AbstractPreferences contains the SPI methods * as defined in java.util.prefs.AbstractPreferences. These SPI iplementations * make the preferences backing store an XML file. To avoid disk read-write * issues, the Document tree is only saved to a file when the 'flush' method * is called. * * This class assumes that there is a PreferencesController singleton object, * which will take care of creating or loading an XML file to a DOM Document. * * Though the DTD provided for the preferences API has been followed in this * implementation, the current DOM API doesn't provide a method to set a * Document's DOCTYPE, so no DOCTYPE will be seen on XML files generated * by this implementation. * * @author Eduardo S. Cordeiro and Tays C. A. P. Soares */publicclassXMLPreferencesextendsAbstractPreferences{/** * Creates an instance of a preferences node, and sets the 'newNode' * flag to null. * @param parent the parent of this node * @param name the name of this node */publicXMLPreferences(AbstractPreferencesparent,Stringname){super(parent,name);newNode=true;}/** * Requests the PreferencesController to save the preferences tree * to the backing store. Please note that, though the AbstractPreferences * method signature defines that this method should throw a BackingStoreException, * this implementation doesn't. */protectedvoidflushSpi()throwsBackingStoreException{PreferencesController.getInstance().savePreferences();}/** * Removes this node from the backing store. Please note that, though the * AbstractPreferences method signature defines that this method should * throw a BackingStoreException, this implementation doesn't. */protectedvoidremoveNodeSpi()throwsBackingStoreException{org.w3c.dom.Noderoot=PreferencesController.getInstance().getXMLDocument().getDocumentElement();Elementmy_node=myXMLNode();Elementparent_node=(Element)my_node.getParentNode();if(root.getNodeType()==org.w3c.dom.Node.ELEMENT_NODE){Elementdoc_root=(Element)root;parent_node.removeChild(my_node);}}/** * Not implemented, but a future TO-DO. */protectedvoidsyncSpi()throwsBackingStoreException{}/** * Retrieves a String array with the names of this node's children from * the backing store. Please note that, though the AbstractPreferences * method signature defines that this method should throw a BackingStoreException, * this implementation doesn't. * @return a String array with the names of this node's children */protectedString[]childrenNamesSpi()throwsBackingStoreException{Elementnode=myXMLNode();Vectornode_names=null;if(node!=null){NodeListchildren=node.getChildNodes();node_names=newVector();for(inti=0;i<children.getLength();i++){Elementchild=(Element)children.item(i);if(child.getTagName().equals("node"))node_names.addElement(child.getAttribute("name"));}}String[]names=newString[node_names.size()];for(inti=0;i<names.length;i++)names[i]=(String)node_names.elementAt(i);returnnames;}/** * Retrieves a String array with the keys assigned to this node from the * backing store. Please note that, though the AbstractPreferences * method signature defines that this method should throw a BackingStoreException, * this implementation doesn't. * @return a String array with the keys assigned to this node */protectedString[]keysSpi()throwsBackingStoreException{Elementnode=myXMLNode();String[]keys=null;if(node!=null){NodeListentries=myEntries();keys=newString[entries.getLength()];for(inti=0;i<entries.getLength();i++){Elemententry=(Element)entries.item(i);keys[i]=entry.getAttribute("key");}}returnkeys;}/** * Removes a given key from this node on the backing store. * @param key the key to be removed */protectedvoidremoveSpi(Stringkey){Elementnode=myXMLNode();if(node!=null){Elementmap=(Element)node.getElementsByTagName("map").item(0);NodeListentries=myEntries();for(inti=0;i<entries.getLength();i++){Elemententry=(Element)entries.item(i);Stringnode_key=entry.getAttribute("key");if(node_key.equals(key)){map.removeChild(entry);}}}}/** * Retrieves the value associated to a given key from this node from the * backing store. * @param key the key whose value will be retrieved * @return the value associated to the given key, if this node contains such * a pair, or null otherwise */protectedStringgetSpi(Stringkey){Elementnode=myXMLNode();if(node!=null){NodeListentries=myEntries();booleankey_exists=false;// test whether or not the node already contains the given keyfor(intj=0;j<entries.getLength();j++){Elemententry=(Element)entries.item(j);Stringnode_key=entry.getAttribute("key");if(node_key.equals(key)){returnentry.getAttribute("value");}}}returnnull;}/** * Sets a key-value pair on this node, on the backing store. If this node * doesn't contain the given key, this pair will be created on the backing * store. * @param key the key to be set * @param the value to be set on the given key */protectedvoidputSpi(Stringkey,Stringvalue){Elementnode=myXMLNode();if(node!=null){Elementmap=(Element)node.getElementsByTagName("map").item(0);NodeListentries=myEntries();booleankey_exists=false;// test whether or not the node already contains the given keyfor(intj=0;j<entries.getLength();j++){Elemententry=(Element)entries.item(j);Stringnode_key=entry.getAttribute("key");if(node_key.equals(key)){entry.setAttribute("value",value);key_exists=true;}}// if the entry doesn't already exist, create it and append it to this nodeif(!key_exists){Elemententry=PreferencesController.getInstance().getXMLDocument().createElement("entry");entry.setAttribute("key",key);entry.setAttribute("value",value);map.appendChild(entry);}}}/** * Retrieves this node's child with the given name from the backing store. If * such a child doesn't exist, one will be created. * @param name the name of the desired child * @return the desired node, which will have the 'newNode' flag set if it didn't * exist before this method */protectedAbstractPreferenceschildSpi(Stringname){Elementnode=myXMLNode();if(node!=null){NodeListchildren=node.getElementsByTagName("node");for(inti=0;i<children.getLength();i++){Elementchild=(Element)children.item(i);Stringchild_name=child.getAttribute("name");if(child_name.equals(name))returncreatePreferencesFromElement(child,child_name);}// the child doesn't exist, so one will be createdAbstractPreferencesprefs=newXMLPreferences(this,name);Elementnew_node=PreferencesController.getInstance().getXMLDocument().createElement("node");Elementmap=PreferencesController.getInstance().getXMLDocument().createElement("map");new_node.appendChild(map);new_node.setAttribute("name",name);node.appendChild(new_node);returnprefs;}returnnull;}/** * Creates a preferences node from an XML element. * @param child the XML element whose node will be created * @return the newly created preferences node */privateAbstractPreferencescreatePreferencesFromElement(Elementchild,Stringname){AbstractPreferencesprefs=newXMLPreferences(this,name);NodeListmap=child.getElementsByTagName("map");NodeListentries=((Element)map.item(0)).getElementsByTagName("entry");for(inti=0;i<entries.getLength();i++){Elemententry=(Element)entries.item(i);prefs.put(entry.getAttribute("key"),entry.getAttribute("value"));}returnprefs;}/** * Retrieves a DOM/XML Element that represents this node on the backing store. * @return */privateElementmyXMLNode(){org.w3c.dom.Noderoot=PreferencesController.getInstance().getXMLDocument().getDocumentElement();if(root.getNodeType()==org.w3c.dom.Node.ELEMENT_NODE){Elementdoc_root=(Element)root;Elementprefs_root=(Element)doc_root.getElementsByTagName("root").item(0);// checks whether this node is the rootif(this.name().equals(""))returnprefs_root;// fetch the node with the name of this preferences nodeElementmy_node=findNode(prefs_root,this.name());returnmy_node;}returnnull;}/** * Recursively finds a node on the DOM/XML tree. * @param parent the parent of nodes to be searched in a recursive * call of this method * @param name the name of the desired Element * @return the desired Element if it is found, or null otherwise */privateElementfindNode(Elementparent,Stringname){NodeListchildren=parent.getElementsByTagName("node");for(inti=0;i<children.getLength();i++){Elementchild=(Element)children.item(i);Stringchild_name=child.getAttribute("name");if(child_name.equals(name))returnchild;else{Elementsearched=findNode(child,name);if(searched!=null)returnsearched;}}returnnull;}/** * Retrieves the DOM nodes that represent this preference's entries from * the backing store. * @return a NodeList with the entries in this preferences node */privateNodeListmyEntries(){Elementnode=myXMLNode();if(node!=null){NodeListmap=node.getElementsByTagName("map");return((Element)map.item(0)).getElementsByTagName("entry");}returnnull;}/** * Retrieves the String value associated with the given key from this preferences * node, from the backing store. * @param key the key of the desired entry * @param def the default value to be set if the desired pair doesn't yet exist * on the backing store * @return the desired value */publicStringget(Stringkey,Stringdef){Stringvalue=getSpi(key);if(value==null)this.put(key,def);elsereturnvalue;returngetSpi(key);}/** * Retrieves the boolean value associated with the given key from this preferences * node, from the backing store. * @param key the key of the desired entry * @param def the default value to be set if the desired pair doesn't yet exist * on the backing store * @return the desired value */publicbooleangetBoolean(Stringkey,booleandef){Stringvalue=getSpi(key);if(value==null){this.putBoolean(key,def);returnBoolean.getBoolean(getSpi(key));}elseif(value.equals("true"))returntrue;elsereturnfalse;}/** * Retrieves the double value associated with the given key from this preferences * node, from the backing store. * @param key the key of the desired entry * @param def the default value to be set if the desired pair doesn't yet exist * on the backing store * @return the desired value */publicdoublegetDouble(Stringkey,doubledef){Stringvalue=getSpi(key);if(value==null){this.putDouble(key,def);returnDouble.parseDouble(getSpi(key));}elsereturnDouble.parseDouble(value);}/** * Retrieves the float value associated with the given key from this preferences * node, from the backing store. * @param key the key of the desired entry * @param def the default value to be set if the desired pair doesn't yet exist * on the backing store * @return the desired value */publicfloatgetFloat(Stringkey,floatdef){Stringvalue=getSpi(key);if(value==null){this.putFloat(key,def);returnFloat.parseFloat(getSpi(key));}elsereturnFloat.parseFloat(value);}/** * Retrieves the String value associated with the given key from this preferences * node, from the backing store. * @param key the key of the desired entry * @param def the default value to be set if the desired pair doesn't yet exist * on the backing store * @return the desired value */publicintgetInt(Stringkey,intdef){Stringvalue=getSpi(key);if(value==null){this.putInt(key,def);returnInteger.parseInt(getSpi(key));}elsereturnInteger.parseInt(value);}/** * Retrieves the long value associated with the given key from this preferences * node, from the backing store. * @param key the key of the desired entry * @param def the default value to be set if the desired pair doesn't yet exist * on the backing store * @return the desired value */publiclonggetLong(Stringkey,longdef){Stringvalue=getSpi(key);if(value==null){this.putLong(key,def);returnLong.parseLong(getSpi(key));}elsereturnLong.parseLong(value);}/** * Retrieves the 'newNode' flag for this node, which indicates whether or not * it has been recently created. * @return true if this node is new, false otherwise */publicbooleannewNode(){returnnewNode;}/** * Sets this node's 'newNode' flag to false, to indicate that it has already * been recognized as a new node and this value is no longer needed, or * that this node should no longer be used as a new node */publicvoidsetNotNewNode(){newNode=false;}}
A PreferencesController faz um pequeno “desvio” do preferences original: ela é implementada de forma que a árvore de preferências tenha o nodo raiz, seus filhos sejam categorias de preferências, e os filhos das categorias sejam os valores de preferências propriamente ditos; só que esses valores são guardados na forma de duas keys - uma indica o tipo do valor, e a outra o valor do valor :roll: . Fizemos esse “desvio” para podermos criar uma interface dinâmica de preferências, que cria uma lista com as categorias e, quando clica-se em uma categoria da lista, um painel é aberto para exibir os valores de preferências. Só que, como cada tipo tem uma visão mais eficiente (como checkbox para boolean, caixinha para int e String e JColorChooser para cores), precisamos avaliar o tipo do valor durante a execução…
/* PreferencesController.java */importjava.io.*;importjava.awt.Color;importjavax.xml.parsers.DocumentBuilder;importjavax.xml.parsers.DocumentBuilderFactory;importjavax.xml.parsers.ParserConfigurationException;importjavax.xml.transform.Transformer;importjavax.xml.transform.TransformerConfigurationException;importjavax.xml.transform.TransformerException;importjavax.xml.transform.TransformerFactory;importjavax.xml.transform.TransformerFactoryConfigurationError;importjavax.xml.transform.dom.DOMSource;importjavax.xml.transform.stream.StreamResult;importorg.w3c.dom.*;importorg.xml.sax.SAXException;importgraph_editor.prefs.*;/** * This class is responsible for controlling the user XMLPreferences, which * will be used throughout the program to allow persistence of these * settings. * The tree is composed in the following way: there is a root node that * will cluster several property categories. Each category is a different * node and will contain several children, each of which represents a * preferences value to the program. * Each preference value is a node, child * of a category, and will contain two items: a value and a type. This is * used to indicate to the program the type of the preference, in case it * needs to be known in run-time. */publicclassPreferencesController{/** * The properties category's identifier. */publicfinalstaticStringPROPERTIES_CATEGORY="Properties";/** * The performance category's identifier. */publicfinalstaticStringPERFORMANCE_CATEGORY="Performance";/** * The edge color property's identifier. */publicfinalstaticStringEDGE_COLOR_PROPERTY="Edge color";/** * The node color property's identifier. */publicfinalstaticStringNODE_COLOR_PROPERTY="Node color";/** * The anti-aliasing property's identifier. */publicfinalstaticStringANTIALIASING_PROPERTY="Anti-aliasing";/** * The path in the filesystem to the XMLPreferences file. */privatefinalstaticStringPREFERENCES_PATH=".//user//.preferences";/** * A String that will represent an integer type. */publicfinalstaticStringTYPE_INT="int";/** * A String that will represent a double type. */publicfinalstaticStringTYPE_DOUBLE="double";/** * A String that will represent a boolean type. */publicfinalstaticStringTYPE_BOOLEAN="boolean";/** * A String that will represent a color type. */publicfinalstaticStringTYPE_COLOR="color";/** * A String that will represent a string type. */publicfinalstaticStringTYPE_STRING="string";/** * A String that will represent a type node. */publicfinalstaticStringTYPE="type";/** * A String that will represent a value node. */publicfinalstaticStringVALUE="value";/** * The unique instance to this class. */privatestaticPreferencesControllerinstance;/** * The DOM document that will accesses the xml document */privatestaticDocumentprefs_document=null;/** * The tree's root node for user XMLPreferences. */privatestaticXMLPreferencesroot_prefs;/** * Creates an instance of this class and the root XMLPreferences node. */protectedPreferencesController(){}/** * Retrieves the unique instance to this class. * @return the unique instance to this class */publicstaticPreferencesControllergetInstance(){if(instance==null){instance=newPreferencesController();loadPreferences();}returninstance;}/** * Adds a new XMLPreferences category to the XMLPreferences tree; each * category will be a leaf in the XMLPreferences tree. * @param name the new XMLPreferences category */publicvoidaddCategory(Stringname){XMLPreferencesnew_node=(XMLPreferences)root_prefs.node(name);}/** * Puts an int value into a XMLPreferences category. * @param category_name the name of the category * @param key the key of the new value * @param value the default value */publicvoidputInt(Stringcategory_name,Stringkey,intvalue){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name).node(key);node.put(TYPE,TYPE_INT);node.putInt(VALUE,value);}/** * Puts a double value into a XMLPreferences category. * @param category_name the name of the category * @param key the key of the new value * @param value the default value */publicvoidputDouble(Stringcategory_name,Stringkey,doublevalue){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name).node(key);node.put(TYPE,TYPE_DOUBLE);node.putDouble(VALUE,value);}/** * Puts a boolean value into a XMLPreferences category. * @param category_name the name of the category * @param key the key of the new value * @param value the default value */publicvoidputBoolean(Stringcategory_name,Stringkey,booleanvalue){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name).node(key);node.put(TYPE,TYPE_BOOLEAN);node.putBoolean(VALUE,value);}/** * Puts a String value into a XMLPreferences category. * @param category_name the name of the category * @param key the key of the new value * @param value the default value */publicvoidput(Stringcategory_name,Stringkey,Stringvalue){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name).node(key);node.put(TYPE,TYPE_STRING);node.put(VALUE,value);}/** * Puts a Color value into a XMLPreferences category. * @param category_name the name of the category * @param key the key of the new value * @param value the default value */publicvoidputColor(Stringcategory_name,Stringkey,Colorvalue){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name).node(key);node.put(TYPE,TYPE_COLOR);node.putInt(VALUE,value.getRGB());}/** * Retrieves the int value in the specified category; the default value * will be set if the category isn't found. * @param category_name the name of the category * @param key the key of the value * @param def the default value * @return the desired value, if it is found, or the default otherwise */publicintgetInt(Stringcategory_name,Stringkey,intdef){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name+"/"+key);if(node.newNode()){this.putInt(category_name,key,def);node.setNotNewNode();}returnnode.getInt(VALUE,def);}/** * Retrieves the double value in the specified category; the default value * will be set if the category isn't found. * @param category_name the name of the category * @param key the key of the value * @param def the default value * @return the desired value, if it is found, or the default otherwise */publicdoublegetDouble(Stringcategory_name,Stringkey,doubledef){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name+"/"+key);if(node.newNode()){this.putDouble(category_name,key,def);node.setNotNewNode();}returnnode.getDouble(VALUE,def);}/** * Retrieves the boolean value in the specified category; the default value * will be set if the category isn't found. * @param category_name the name of the category * @param key the key of the value * @param def the default value * @return the desired value, if it is found, or the default otherwise */publicbooleangetBoolean(Stringcategory_name,Stringkey,booleandef){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name+"/"+key);if(node.newNode()){this.putBoolean(category_name,key,def);node.setNotNewNode();}returnnode.getBoolean(VALUE,def);}/** * Retrieves the String value in the specified category; the default value * will be set if the category isn't found. * @param category_name the name of the category * @param key the key of the value * @param def the default value * @return the desired value, if it is found, or the default otherwise */publicStringget(Stringcategory_name,Stringkey,Stringdef){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name+"/"+key);if(node.newNode()){this.put(category_name,key,def);node.setNotNewNode();}returnnode.get(VALUE,def);}/** * Retrieves the Color value in the specified category; the default value * will be set if the category isn't found. * @param category_name the name of the category * @param key the key of the value * @param def the default value * @return the desired value, if it is found, or the default otherwise */publicColorgetColor(Stringcategory_name,Stringkey,Colordef){XMLPreferencesnode=(XMLPreferences)root_prefs.node(category_name+"/"+key);if(node.newNode()){this.putColor(category_name,key,def);node.setNotNewNode();}returnnewColor(node.getInt(VALUE,def.getRGB()));}/** * Flushes the XMLPreferences to the output file for persistence. */publicvoidsavePreferences(){try{Filepref_file=newFile(PREFERENCES_PATH);if(!pref_file.exists())pref_file.createNewFile();// save the XML tree to the XMLPreferences fileDOMSourcesource=newDOMSource(prefs_document);StreamResultsr=newStreamResult(pref_file);Transformertransformer=TransformerFactory.newInstance().newTransformer();transformer.transform(source,sr);}catch(IOExceptionexc){exc.printStackTrace();}catch(TransformerConfigurationExceptionexc){exc.printStackTrace();}catch(TransformerFactoryConfigurationErrorexc){exc.printStackTrace();}catch(TransformerExceptionexc){exc.printStackTrace();}}/** * Loads the existing XMLPreferences file into the program's XMLPreferences * tree; if the file does not exist, an IOException will be caught and * no settings will be imported. * @return the DOM Document to be used by all the preferences nodes */publicstaticDocumentloadPreferences(){Filexml_prefs=newFile(PREFERENCES_PATH);try{DocumentBuilderFactoryfactory=DocumentBuilderFactory.newInstance();factory.setValidating(false);DocumentBuilderbuilder=factory.newDocumentBuilder();if(xml_prefs.exists()){prefs_document=builder.parse(xml_prefs);buildPreferencesFromDocument();}else{prefs_document=builder.newDocument();// TODO set DOCTYPE// create root node and its mapElementprefs=prefs_document.createElement("preferences");prefs.setAttribute("EXTERNAL_XML_VERSION","1.0");prefs_document.appendChild(prefs);Elementroot=prefs_document.createElement("root");root.setAttribute("type","user");Elementroot_map=prefs_document.createElement("map");root.appendChild(root_map);prefs.appendChild(root);root_prefs=newXMLPreferences(null,"");}}catch(ParserConfigurationExceptione){e.printStackTrace();}catch(SAXExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}returnprefs_document;}/** * Starts the creation of the preferences tree from the DOM/XML Document. * This method will retrieve the 'root' Element from the DOM Document and * call the recursive 'buildPreferences' method upon it to start the * creation of the preferences tree. */privatestaticvoidbuildPreferencesFromDocument(){Elementdoc_root=(Element)(prefs_document.getElementsByTagName("root").item(0));buildPreferences(doc_root,null);}/** * Recursively builds the preferences tree from the DOM Element nodes. This method * should be called initially for the 'root' Element, since it will build * an XMLPreferences instance for the given node and call itself upon its * children. * @param node the DOM Element node for which an XMLPreferences instance * will be created * @param parent the parent of the node */privatestaticvoidbuildPreferences(Elementnode,XMLPreferencesparent){NodeListchildren=node.getChildNodes();XMLPreferencesprefs=null;if(parent!=null)prefs=(XMLPreferences)parent.node(node.getAttribute("name"));elseprefs=newXMLPreferences(null,"");if(children.getLength()!=0){Elementmap=(Element)children.item(0);NodeListentries=map.getElementsByTagName("entry");for(inti=0;i<entries.getLength();i++){Elemententry=(Element)entries.item(0);prefs.put(entry.getAttribute("key"),entry.getAttribute("value"));}for(inti=1;i<children.getLength();i++)buildPreferences((Element)children.item(i),prefs);}prefs.setNotNewNode();if(parent==null)root_prefs=prefs;}/** * Retrieves the root node in the XMLPreferences tree. * @return the root node in the XMLPreferences tree */publicXMLPreferencesgetRoot(){returnroot_prefs;}/** * Retrieves the DOM document for the Preferences XML tree. * @return the DOM document for the Preferences XML tree */publicDocumentgetXMLDocument(){returnprefs_document;}/** * * @param doc */publicvoidsetXMLDocument(Documentdoc){prefs_document=doc;}}
:!: Disclaimer:
Aqueles nomes propriedades e categorias estáticas da classe PreferencesController fazem parte do nosso programa, mas estão aí apenas para exemplificar categorias e valores que utilizamos. Além disso, nem todos os tipos suportados por Preferences estão no PreferencesController…digamos que isso é pura preguiça :shock: .
Por favor, critiquem e comentem, acho que dá pra melhorar bastante esse código.