An "Expand/Collapse"-Canvas Java-Bean
In one of the posts on the OTN forms forum last week there was a question of if there is something like an expandable/collapsable Component like its known from outlook. It seems there is no component like this at the time, so i had the idea to write a java-bean to implement such a feature.
So what should such a component be capable of
- the panel should integrate in the standard-lookandfeeld
- the panle should be capable of containing anything possible in forms
- everything on such a panel should be a "standard" forms-object
- you should be able to configure as many single "Panel"
And thats my approach.
- Several panels can be grouped in one panelgroup, from each panelgroup always one panel is active.
- For each panel you define one stacked canvas, and you can put on that canvas everything you like (items, tree, whole blocks)
- On each of these stacked canvases there has to be one button which has my javabean as implementation class
- The stacked canvases have to be arranged on a content canvas that way, that the viewport form one stacked canvas is fully visible, the others are shrinked to show just the button
- There is one initialization procedure called in the WHEN-NEW-FORM-INSTANCE-trigger
- You can react on a WHEN-BUTTON-PRESSED-trigger for the created buttons to do some initialization for the shown "Panel"
Thats it. Everything else is handled at the java-side, there is no more interaction between the client and application-server.
I've made up a first version of my bean, heres the java-code
package forms; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Image; import java.awt.Toolkit; import java.net.URL; import java.util.StringTokenizer; import javax.swing.ImageIcon; import oracle.forms.handler.IHandler; import oracle.forms.ui.DrawnPanel; import oracle.forms.ui.VBean; import oracle.forms.properties.ID; import oracle.forms.ui.VButton; import java.awt.event.MouseEvent; import java.awt.Event; import java.awt.event.ActionEvent; import java.awt.Graphics; /** This is just sample code, its free to use. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. The code relies on the internal structure of forms when rendering stacked canvases It is tested against Forms 10.1.2.0.2, but may stop working with any patch or future version of forms Sample code for a Javabean to implement an expandabled/collapseable canvas. For usage-notes see the the package PK_ACCORDION, which is the counterpart for this code on the forms-side */ public class AccordionButton extends VButton implements Runnable { /** Ids for Forms-events */ public static final ID INIT_ACCORDION =ID.registerProperty("INIT_ACCORDION"); public static final ID SCALE_ACCORDION =ID.registerProperty("SCALE_ACCORDION"); public static final ID ACTIVATE =ID.registerProperty("ACTIVATE"); /** default-images */ private final ImageIcon IMG_COLLAPSED=new ImageIcon(this.getClass().getResource("collapsed.png")); private final ImageIcon IMG_EXPANDED=new ImageIcon(this.getClass().getResource("expanded.png")); /** constants */ private static final int MODE_SHRINK=0; private static final int MODE_EXPAND=1; /** class variables */ private static boolean c_processing=false; /** currently used images */ private ImageIcon m_collapsed=null; private ImageIcon m_expanded=null; /** codebase */ private URL m_codeBase=null; /** other vars */ private int m_mode=-1; private String m_buttonName=null; private Dimension m_dimension=null; private DrawnPanel m_masterCanvas=null; private String m_previousItemName=null; private String m_nextItemName=null; private boolean m_active=false; private AccordionButton m_previousItem=null; private AccordionButton m_nextItem=null; private AccordionButton m_toMakeActive=null; private int m_minHeight=-1; private int m_maxHeight=-1; /** * Initialize the codebase, used for image-loading * * @param handler */ public void init(IHandler handler) { // Remember Codebase m_codeBase = handler.getCodeBase(); super.init(handler); } /** * * @return Searches for an AccordionButton with the given name, * starting the search at the given Container * @param name Name of the accordion button to search * @param c Container to start at */ private AccordionButton rekuFindChild(Container c, String name) { AccordionButton result=null; for (int i=0;i<c.getComponentCount();i++) { Component comp=c.getComponent(i); if (comp instanceof AccordionButton) { if (name.equals(((AccordionButton)comp).getButtonName())) { result=(AccordionButton)comp; break; } } } if (result==null) { for (int i=0;i<c.getComponentCount();i++) { if (c.getComponent(i) instanceof Container) { result=rekuFindChild((Container)c.getComponent(i), name); if (result!=null) { break; } } } } return result; } /** * finds the "Neighbor"-Accordion-Button by takeing the names given by * the initialization property */ private void findNeighbors() { if (!".".equals(m_previousItemName)) { m_previousItem=rekuFindChild(m_masterCanvas, m_previousItemName); } if (!".".equals(m_nextItemName)) { m_nextItem=rekuFindChild(m_masterCanvas, m_nextItemName); } } /** * Initializationof the AccordionButton. The data is given as a string, the * values are concatenated by |. * @param data */ private void init(String data) { StringTokenizer st=new StringTokenizer(data, "|"); if (st.hasMoreTokens()) { m_buttonName=st.nextToken(); //System.out.println("My name is " + m_buttonName); } String active="N"; if (st.hasMoreTokens()) { active=st.nextToken(); } m_active="J".equals(active); if (st.hasMoreTokens()) { m_previousItemName=st.nextToken(); } if (st.hasMoreTokens()) { m_nextItemName=st.nextToken(); } if (st.hasMoreTokens()) { String image=st.nextToken(); m_expanded=loadImage(image); } if (st.hasMoreTokens()) { String image=st.nextToken(); m_collapsed=loadImage(image); } int dp=0; Container c=this; while (c.getParent()!=null && dp<2) { c=c.getParent(); if (c instanceof DrawnPanel) { dp++; } } if (c instanceof DrawnPanel) { m_masterCanvas=(DrawnPanel)c; } m_dimension=this.getParent().getParent().getParent().getSize(); } /** * Delegates the current maximum height to the next AccordionButon in the group * @param maxHeight */ protected void scaleNext(int maxHeight) { // init Neighbours findNeighbors(); m_minHeight=this.getX()+this.getHeight(); m_maxHeight=maxHeight; if (m_active) { m_maxHeight=(int)m_dimension.getHeight()-m_minHeight; } if (m_nextItem!=null) { m_nextItem.scaleNext(m_maxHeight); } else if (m_previousItem!=null) { m_previousItem.scalePrev(m_maxHeight); } } /** * Delegates the current maximum height to the previous AccordionButon in the group * @param maxHeight */ protected void scalePrev(int maxHeight) { m_maxHeight=maxHeight; if (m_previousItem!=null) { m_previousItem.scalePrev(m_maxHeight); } } /** * Searches the previous buttons if one of them is the active one, * if found, it will start the logic to deactivate (shrink) it * * @return true if one of the previous buttons was the active one * @param toMakeActive the button which should be activated */ protected boolean closePrevActive(AccordionButton toMakeActive) { if (m_active) { m_mode=MODE_SHRINK; m_toMakeActive=toMakeActive; m_active=false; Thread t=new Thread(this); t.start(); return true; } if (m_previousItem!=null) { return m_previousItem.closePrevActive(toMakeActive); } return false; } /** * Searches the following buttons if one of them is the active one, * if found, it will start the logic to deactivate (shrink) it * * @return true if one of the following buttons was the active one * @param toMakeActive the button which should be activated */ protected boolean closeNextActive(AccordionButton toMakeActive) { if (m_active) { m_mode=MODE_SHRINK; m_toMakeActive=toMakeActive; m_active=false; Thread t=new Thread(this); t.start(); return true; } if (m_nextItem!=null) { return m_nextItem.closeNextActive(toMakeActive); } return false; } /** * closes the active Accordion, either a previos or a following one */ private void closeActive() { // Try to close to prev-direction if (!closePrevActive(this)) { // Not found, close next active closeNextActive(this); } } /** * Standard Method, overwritten to make the bean-specific properties from forms * @return true * @param value * @param id */ public boolean setProperty(ID id, Object value) { if (id==INIT_ACCORDION) { //System.out.println(INIT_ACCORDION); init((String)value); return true; } else if (id==SCALE_ACCORDION) { //System.out.println(SCALE_ACCORDION); // Scale scaleNext(-1); // Adjust Initial Layout if (m_nextItem!=null) { m_nextItem.adjustPosition(this.getParent().getParent().getParent().getY()+this.getParent().getParent().getParent().getHeight()); } return true; } else if (id==ACTIVATE) { processActionEvent(null); return true; } else { return super.setProperty(id, value); } } /** * Run-Method of the Runnable-interface, does the animation of * slowly shrinking or growing the canvas */ public void run() { boolean done=false; int height=this.getParent().getParent().getHeight(); //System.out.println("Start mit Höhe" + height); int offset=(m_mode==MODE_SHRINK ? -10 : 10); while (!done) { try { height=height+offset; if (m_mode==MODE_SHRINK && height<=m_minHeight) { height=m_minHeight; done=true; } else if (m_mode==MODE_EXPAND && height>=m_maxHeight+m_minHeight) { height=m_maxHeight+m_minHeight; done=true; } this.getParent().getParent().setSize((int)m_dimension.getWidth(), height); this.getParent().getParent().getParent().setSize((int)m_dimension.getWidth(), height); if (m_nextItem!=null) { m_nextItem.adjustPosition(this.getParent().getParent().getParent().getY()+this.getParent().getParent().getParent().getHeight()); } Thread.sleep(10); } catch (Exception e) { } } if (m_mode==MODE_SHRINK && m_toMakeActive!=null) { // Now make the other one active m_toMakeActive.makeActive(); } c_processing=false; ActionEvent e=new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null); super.processActionEvent(e); } /** * Intercepty the standard WHEN-BUTTON-PRESSED and activates the * Accordion instead. The WHEN-BUTTON-PRESSED-event is deferred until * the current accordion has become active. * @param p0 */ protected void processActionEvent(ActionEvent p0) { // Nothing happens whn already active if (!m_active && !c_processing) { c_processing=true; // close the active item // This will in turn start the thread to open this accordion closeActive(); } } /** * Name assigned to the button * @return Name of the button */ public String getButtonName() { return m_buttonName; } /** * Set flags and start expanding thread */ public void makeActive() { m_active=true; m_mode=MODE_EXPAND; m_toMakeActive=null; Thread t=new Thread(this); t.start(); } /** * adjust the position of the stacked canvas and delegate to the following Accordion. * @param yPosition */ protected void adjustPosition(int yPosition) { this.getParent().getParent().getParent().setLocation(this.getParent().getParent().getParent().getX(), yPosition); if (m_nextItem!=null) { m_nextItem.adjustPosition(this.getParent().getParent().getParent().getY()+this.getParent().getParent().getParent().getHeight()); } } /** * Paint the button * @param g */ public void paint(Graphics g) { g.setColor(getBackground()); g.fillRect(getX()+1, getY()+1, getWidth()-2, getHeight()-2); getBorderPainter().paint(getPaintContext(),g, getX(), getY(), getWidth(), getHeight()); int textpos=4; if (m_collapsed!=null || m_expanded!=null) { if (m_active && m_expanded!=null) { int yPos=((m_expanded.getIconHeight()<getHeight()) ? ((getHeight()-m_expanded.getIconHeight())/2) : 2); int height=((m_expanded.getIconHeight()<getHeight()) ? m_expanded.getIconHeight() : getHeight()-4); g.drawImage(m_expanded.getImage(), 4, yPos, m_expanded.getIconWidth(), height, null); } else if (!m_active && m_collapsed!=null) { int yPos=((m_collapsed.getIconHeight()<getHeight()) ? ((getHeight()-m_collapsed.getIconHeight())/2) : 2); int height=((m_collapsed.getIconHeight()<getHeight()) ? m_collapsed.getIconHeight() : getHeight()-4); g.drawImage(m_collapsed.getImage(), 4, yPos, m_collapsed.getIconWidth(), height, m_collapsed.getImageObserver()); } textpos=textpos+Math.max(m_collapsed.getIconWidth(), m_expanded.getIconWidth()); } g.setFont(getFont()); g.setColor(getForeground()); g.drawString(getLabel(), textpos, (getHeight()/2)+(g.getFontMetrics().getHeight()-g.getFontMetrics().getDescent())/2); } /** * Load the image given by name. Code is taken from oracle-demo RolloverButton * @return loaded Icon or null * @param imageName */ private ImageIcon loadImage(String imageName) { // LoadImage, taken from oracle-demo RolloverButton URL imageURL = null; ImageIcon result=null; boolean loadSuccess=false; if ("DEFAULT_EXPANDED".equals(imageName)) { result=IMG_EXPANDED; } else if ("DEFAULT_COLLAPSED".equals(imageName)) { result=IMG_COLLAPSED; } else if (!".".equals(imageName)) { //JAR imageURL = getClass().getResource("/"+imageName); if (imageURL != null) { try { result = new ImageIcon(Toolkit.getDefaultToolkit().getImage(imageURL)); loadSuccess = true; } catch (Exception ilex) { } //DOCBASE if (!loadSuccess) { try { if (imageName.toLowerCase().startsWith("http://")||imageName.toLowerCase().startsWith("https://")) { imageURL = new URL(imageName); } else { imageURL = new URL(m_codeBase.getProtocol() + "://" + m_codeBase.getHost() + ":" + m_codeBase.getPort() + imageName); } try { result= new ImageIcon(createImage((java.awt.image.ImageProducer) imageURL.getContent())); loadSuccess = true; } catch (Exception ilex) { } } catch (java.net.MalformedURLException urlex) { } } //CODEBASE if (!loadSuccess) { try { imageURL = new URL(m_codeBase, imageName); try { result= new ImageIcon(createImage((java.awt.image.ImageProducer) imageURL.getContent())); loadSuccess = true; } catch (Exception ilex) { } } catch (java.net.MalformedURLException urlex) { } } } } return result; } }
And here's the forms-code
PACKAGE PK_ACCORDION IS
/**
This is just sample code, its free to use.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
The code relies on the internal structure of forms when rendering stacked canvases
It is tested against Forms 10.1.2.0.2, but may stop working with any patch or future version of forms
Sample code for a forms-Side Package for the Accordion Java-Bean
To build up an accordion, follow these steps
-Create as many stacked canvases as you want to have accordion-canvases
-Create a Button on each canvas at position 0,0 with the full width of the canvas and
set the Implementation class for that buttons to forms.AccordionButton
-Arrange these canvases on one content canvas, so that all canvases are placed beneath each other
+The viewport for one canvas should be so that the canvas is fully shown
+The viewport for all other canvases should be so that just the button is visible.
-Initialize the Accordion in the WHEN-NEW-FORM-INSTANCE-trigger with code lik
DECLARE
lAccordionList PK_ACCORDION.tAccordionList;
rAccordion PK_ACCORDION.tAccordion;
BEGIN
rAccordion.vcCanvas:='CANVAS1';
rAccordion.vcButton:='BLOCK.BUTTON1';
rAccordion.vcExpandedImage:=PK_ACCORDION.VCC_DEFAULT_EXPANDED_IMAGE;
rAccordion.vcCollapsedImage:=PK_ACCORDION.VCC_DEFAULT_COLLAPSED_IMAGE;
rAccordion.bOpened:=TRUE;
lAccordionList(1):=rAccordion;
rAccordion.vcCanvas:='CANVAS2';
rAccordion.vcButton:='BLOCK.BUTTON2';
rAccordion.vcExpandedImage:=PK_ACCORDION.VCC_DEFAULT_EXPANDED_IMAGE;
rAccordion.vcCollapsedImage:=PK_ACCORDION.VCC_DEFAULT_COLLAPSED_IMAGE;
rAccordion.bOpened:=FALSE;
lAccordionList(2):=rAccordion;
PK_ACCORDION.PR_INIT_ACCORDION('MAIN', lAccordionList);
END;
-make sure the jar is on the archive or achive_jini-tag
*/
-- constants used for Default-Images conatined in the jar-file
VCC_DEFAULT_EXPANDED_IMAGE CONSTANT VARCHAR2(30):='DEFAULT_EXPANDED';
VCC_DEFAULT_COLLAPSED_IMAGE CONSTANT VARCHAR2(30):='DEFAULT_COLLAPSED';
-- Type which describes one Accordion
TYPE tAccordion IS RECORD (
vcCanvas VARCHAR2(30), -- the Canvas which represents the Accordion-area
vcButton VARCHAR2(61), -- the "Activation"-button on that canvas which is used to activate the Accordion
vcExpandedImage VARCHAR2(255),-- image-name for Expanded-state, image must be accessible by forms
vcCollapsedImage VARCHAR2(255),-- image-name for Collapsed-state, image must be accessible by forms
bOpened BOOLEAN -- Flag, if this accordion is the one that is displayed at startup
-- Must match the canvas. The canvases area beneath the button is taken
-- as the area which is given to other accordions when opened
);
-- Table-Type of Accordion-records
TYPE tAccordionList IS TABLE OF tAccordion INDEX BY BINARY_INTEGER;
/** Initialization-method for an accordion group
i_vcAccordionGroup indicates a logical name. It gives the capability of having several Accordion-groups inside one form.
i_lAccordionList is a list of the Accordion-entries which belong to the accordion-group
*/
PROCEDURE PR_INIT_ACCORDION(i_vcAccordionGroup IN VARCHAR2, i_lAccordionList IN tAccordionList);
/** method to activate a specific accordion programmatically
i_vcAccordionGroup indicates a logical name. Must be a name which has been initialized via PR_INIT_ACCORDION before
i_vcCanvas is the name of the canvas in one of the accordion-entries in the group
*/
PROCEDURE PR_ACTIVATE(i_vcAccordionGroup IN VARCHAR2, i_vcCanvas IN VARCHAR2);
/** method to go to an item placed on an accordion-canvas.
i_vcItem Name of the item which should get the focus
Thanks to Francois Degrelle for supplying the code
*/
PROCEDURE PR_GO_ITEM(i_vcItem IN VARCHAR2);
/** method to go to a block the first item of which is placed on an accordion-canvas.
i_vcBlock Name of the block which should get the focus
Thanks to Francois Degrelle for supplying the code
*/
PROCEDURE PR_GO_BLOCK(i_vcBlock IN VARCHAR2);
END;
PACKAGE BODY PK_ACCORDION IS
TYPE tList IS TABLE OF tAccordionList INDEX BY VARCHAR2(30);
lList tList;
-- --------------------------------------------------------------------------------------
PROCEDURE PR_INIT_ACCORDION(i_vcAccordionGroup IN VARCHAR2, i_lAccordionList IN tAccordionList) IS
vcPrior VARCHAR2(4000);
vcNext VARCHAR2(4000);
BEGIN
lList(i_vcAccordionGroup):=i_lAccordionList;
FOR i IN 1..i_lAccordionList.COUNT LOOP
-- prior Accordion-entry
IF i>1 THEN
vcPrior:=i_lAccordionList(i-1).vcButton;
ELSE
vcPrior:='.';
END IF;
-- following Accordion-entries
IF i<i_lAccordionList.COUNT THEN
vcNext:=i_lAccordionList(i+1).vcButton;
ELSE
vcNext:='.';
END IF;
SET_CUSTOM_ITEM_PROPERTY(i_lAccordionList(i).vcButton, 'INIT_ACCORDION', i_lAccordionList(i).vcButton || '|' ||
CASE WHEN i_lAccordionList(i).bOpened THEN
'J'
ELSE
'N'
END || '|' ||
vcPrior || '|' ||
vcNext || '|' ||
NVL(i_lAccordionList(i).vcExpandedImage, '.') || '|' ||
NVL(i_lAccordionList(i).vcCollapsedImage, '.') || '|'
);
END LOOP;
SYNCHRONIZE;
-- Make first item scale, it will delegate to others
SET_CUSTOM_ITEM_PROPERTY(i_lAccordionList(1).vcButton, 'SCALE_ACCORDION', ' ');
END;
-- --------------------------------------------------------------------------------------
PROCEDURE PR_ACTIVATE(i_vcAccordionGroup IN VARCHAR2, i_vcCanvas IN VARCHAR2) IS
lAccordionList tAccordionList;
BEGIN
IF lList.EXISTS(i_vcAccordionGroup) THEN
lAccordionList:=lList(i_vcAccordionGroup);
FOR i IN 1..lAccordionList.COUNT LOOP
IF lAccordionList(i).vcCanvas=i_vcCanvas THEN
SET_CUSTOM_ITEM_PROPERTY(lAccordionList(i).vcButton, 'ACTIVATE', ' ');
EXIT;
END IF;
END LOOP;
END IF;
END;
-- --------------------------------------------------------------------------------------
PROCEDURE PR_GO_ITEM(i_vcItem IN VARCHAR2) IS
lAccordionList tAccordionList;
vcCanvas VARCHAR2(100);
vcGroup VARCHAR2(30);
itId ITEM;
BEGIN
-- find the item canvas --
itId:=FIND_ITEM(i_vcItem);
IF NOT ID_NULL(itId) THEN
vcCanvas:=GET_ITEM_PROPERTY(itId, ITEM_CANVAS ) ;
-- find the accordion group --
vcGroup:=lList.FIRST ;
LOOP
EXIT WHEN vcGroup IS NULL;
lAccordionList:=lList(vcGroup);
FOR i IN 1..lAccordionList.COUNT LOOP
IF UPPER(lAccordionList(i).vcCanvas) = vcCanvas THEN
PR_ACTIVATE(vcGroup, vcCanvas);
GO_ITEM(i_vcItem);
RETURN;
END IF;
END LOOP;
vcGroup:=lList.NEXT(vcGroup) ;
END LOOP;
END IF;
END;
-- --------------------------------------------------------------------------------------
PROCEDURE PR_GO_BLOCK(i_vcBlock IN VARCHAR2) IS
lAccordionList tAccordionList;
vcCanvas VARCHAR2(100);
vcGroup VARCHAR2(30);
vcItem VARCHAR2(61);
blId BLOCK;
bOk BOOLEAN:= FALSE ;
BEGIN
-- find the canvas --
blId := FIND_BLOCK(i_vcBlock);
IF NOT ID_NULL(blId) THEN
vcItem:= GET_BLOCK_PROPERTY(blId, FIRST_ITEM);
LOOP
vcCanvas:= GET_ITEM_PROPERTY(vcItem, ITEM_CANVAS ) ;
IF vcCanvas IS NOT NULL THEN
bOk:=TRUE;
EXIT;
END IF ;
vcItem:= GET_ITEM_PROPERTY(vcItem, NEXTITEM ) ;
EXIT WHEN vcItem IS NULL;
END LOOP;
IF bOk THEN
-- find the accordion group --
vcGroup:=lList.FIRST;
LOOP
EXIT WHEN vcGroup IS NULL;
lAccordionList:=lList(vcGroup);
FOR i IN 1..lAccordionList.COUNT LOOP
If UPPER(lAccordionList(i).vcCanvas)=vcCanvas THEN
PR_ACTIVATE(vcGroup, vcCanvas);
GO_BLOCK(i_vcBlock);
RETURN;
END IF;
END LOOP;
vcGroup:=lList.NEXT(vcGroup);
END LOOP;
END IF;
END IF ;
END PR_GO_BLOCK;
END;
This video shows the usage of the bean with the Oracle demo-tables EMPLOYEES and DEPARTMENTS
A compiled version with a demo-fmb can be found at Francois Degrelle's PJC-site here