xBrowse GoTo

Post Reply
User avatar
Detlef
Posts: 218
Joined: Mon Feb 07, 2022 9:54 pm

xBrowse GoTo

Post by Detlef »

Hi all,
with great help from this forum I created an xBrowse with a tree to visualize a one to many relationship.
Everything is working fine. I can add tasks and detail records and the tree is building up correctly.

My only problem is to position the selected line of the browse.
I made a screenhot for clarification:

The actual line of oBrw is record # 3 of details.dbf
When pressing the button | + Detail | a new record is appended and BuildTree() is called.
To show the new line I have to call oBrw:GoBottom() and oBrw:Refresh().
But than the hilighted row is of course at the bottom.
And the selected row of oBrw line # 6.

I tried several ways to keep the selected line 3 in view. but I couldn't succeed.
I read about xBrowse bookmarks. Perhaps this could help.nBut I didn't understand how to handle with boomarks.

Could anyone give me a clue about this?
Thanks, Detlef
Image

User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: xBrowse GoTo

Post by Antonio Linares »

Dear Detlef,

A tree has opened branches and not opened ones. So we need to save those states before we rebuild it
so later we can restore the same states.

In order to save them, we need to steep through all the tree branches:

Code: Select all | Expand

FUNCTION SaveTreeState( oTree )  
LOCAL aState := {}
LOCAL oItem
oItem := oTree:oFirst
WHILE oItem != NIL
AADD( aState, { oItem:lOpened, oItem:Cargo } ) oItem := oItem:Skip( 1 )
END
RETURN aState
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: xBrowse GoTo

Post by Antonio Linares »

Now, in order to restore such state we need to use this function:

Code: Select all | Expand

FUNCTION RestoreTreeState( oTree, aState )
   LOCAL oItem := oTree:oFirst
   LOCAL nAt

   WHILE oItem != NIL
      // We check if this item Cargo exists in the save states array
      // We compare type and ID (elements 1 and 2 from the Cargo array)
      nAt := AScan( aState, { | a | ValType( a[ 2 ] ) == "A" .and. ;
                                    ValType( oItem:Cargo ) == "A" .and. ;
                                    a[ 2 ][ 1 ] == oItem:Cargo[ 1 ] .and. ;
                                    a[ 2 ][ 2 ] == oItem:Cargo[ 2 ] } )

  // If found and opened (.T.), we open it
  IF nAt > 0 .and. aState[ nAt ][ 1 ] 
     oItem:Open()
  ENDIF

  // We go to the next. As we opened above, Skip(1) will step through its childs
  oItem := oItem:Skip( 1 )
   END
RETURN NIL
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: xBrowse GoTo

Post by Antonio Linares »

In order to get a cleaner code we reorganize the code like this:

Code: Select all | Expand

function BuildBar( oDlg, oBrw )

   local oBar

   DEFINE BUTTONBAR oBar OF oDlg SIZE 50, 50 2007

   DEFINE BUTTON OF oBar PROMPT "Task" RESOURCE "add" ;
      ACTION AddTask( oBrw )

   DEFINE BUTTON OF oBar PROMPT "Detail" RESOURCE "add" ;
      ACTION AddDetail( oBrw )

   DEFINE BUTTON OF oBar PROMPT "Edit" RESOURCE "edit" ACTION oBrw:Edit()

   DEFINE BUTTON OF oBar PROMPT "Del" RESOURCE "del"
   
oDlg:Resize() return .T.

Code: Select all | Expand

// Function to save the current status (Open/Closed) of the tree items
FUNCTION SaveTreeState( oTree )
   LOCAL aState := {}
   LOCAL oItem
     
oItem := oTree:oFirst WHILE oItem != NIL // Save { IsOpen, CargoData } AADD( aState, { oItem:lOpened, oItem:Cargo } ) oItem := oItem:Skip( 1 ) END RETURN aState // Function to restore the status (Open/Closed) to a new tree FUNCTION RestoreTreeState( oTree, aState ) LOCAL oItem := oTree:oFirst LOCAL nAt WHILE oItem != NIL // Search if this item's 'Cargo' exists in our saved state array // Note: We compare Type and ID (elements 1 and 2 of Cargo array) nAt := AScan( aState, { | a | ValType( a[ 2 ] ) == "A" .and. ; ValType( oItem:Cargo ) == "A" .and. ; a[ 2 ][ 1 ] == oItem:Cargo[ 1 ] .and. ; a[ 2 ][ 2 ] == oItem:Cargo[ 2 ] } ) // If found and it was marked as opened (.T.), we open it in the new tree IF nAt > 0 .and. aState[ nAt ][ 1 ] oItem:Open() ENDIF // Move to next. Calling Open() above makes Skip(1) enter the children automatically oItem := oItem:Skip( 1 ) END RETURN NIL // Helper to update tree without collapsing everything FUNCTION UpdateTree( oBrw, nBrwPos ) LOCAL aState // 1. Save which branches are open before destroying the tree aState := SaveTreeState( oBrw:oTree ) // 2. Rebuild the tree (this creates a new, fully collapsed tree) oBrw:oTree := BuildTree() // 3. Restore the previously open branches RestoreTreeState( oBrw:oTree, aState ) // 4. Restore cursor position and refresh oBrw:nRowSel := nBrwPos oBrw:Refresh()
RETURN NIL

Code: Select all | Expand

// Action to add a new Task
STATIC FUNCTION AddTask( oBrw )
   LOCAL aState

   // 1. Save current state
   aState := SaveTreeState( oBrw:oTree )

   // 2. Database logic
   Tasks->( DbAppend() )
   Details->( DbAppend() )
   Details->Task_id := Tasks->Id
   Details->Date    := Date()
   Details->From    := "09:00"
   Details->To      := "11:00"

   // 3. Rebuild the tree
   oBrw:oTree := BuildTree()

   // 4. Restore what was previously open
   RestoreTreeState( oBrw:oTree, aState )
   
oBrw:Refresh() RETURN NIL // Action to add a new Detail STATIC FUNCTION AddDetail( oBrw ) LOCAL aState LOCAL nTaskRec // Check if there are items before trying to read Cargo IF oBrw:oTreeItem == NIL; RETURN NIL; ENDIF // Get Parent Task RecNo (regardless of current position - whether on task or detail) nTaskRec := If( Len( oBrw:oTreeItem:Cargo ) == 2,; oBrw:oTreeItem:Cargo[ 2 ],; oBrw:oTreeItem:Cargo[ 3 ] ) // 1. Save current state aState := SaveTreeState( oBrw:oTree ) // --- TRICK --- // If we add a detail, we WANT the parent task branch to be visible/open. // Modify 'aState' to force this task as 'Open' (.T.) before restoring. ForceParentOpen( aState, nTaskRec ) // ------------- // 2. Database logic Tasks->( DbGoTo( nTaskRec ) ) Details->( DbAppend() ) Details->task_id := Tasks->id Details->Date := Date() Details->From := "09:00" Details->To := "11:00" // 3. Rebuild tree oBrw:oTree := BuildTree() // 4. Restore (now includes our forced open task) RestoreTreeState( oBrw:oTree, aState ) oBrw:Refresh() RETURN NIL // Helper to ensure the task where we are adding a detail is open STATIC FUNCTION ForceParentOpen( aState, nTaskRec ) LOCAL nAt // Search in state array if that specific Task exists nAt := AScan( aState, { | a | a[ 2 ][ 1 ] == "task" .and. a[ 2 ][ 2 ] == nTaskRec } ) IF nAt > 0 // If exists, force it to be Open (.T.) aState[ nAt ][ 1 ] := .T. ELSE // If for some reason it wasn't there (rare), add it forced as open AAdd( aState, { .T., { "task", nTaskRec } } ) ENDIF RETURN NIL

Code: Select all | Expand

@ 10, 14 BUTTON "OK" OF oDlg SIZE 80, 25 ;
      ACTION ( Details->( DbCommit() ), oDlg:End(),;
               UpdateTree( oBrw, nBrwPos ) ) // <--- Using the helper
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: xBrowse GoTo

Post by Antonio Linares »

Full source code: (waiting for your feedback)

detlef.prg

Code: Select all | Expand

#include "FiveWin.ch"

REQUEST DbfCdx

FUNCTION Main()

   LOCAL oDlg, oBrw

   // 1. Database Creation Checks
   IF !File( "tasks.dbf" )
      DbCreate( "tasks.dbf", { ;
         { "ID",   "+",  10, 0 }, ;
         { "TASK", "C",  50, 0 } } )
   ENDIF   

   IF !File( "details.dbf" )
      DbCreate( "details.dbf", { ;
         { "ID",      "N", 10, 0 }, ;
         { "TASK_ID", "N", 10, 0 },;
         { "DATE",    "D",  8, 0 },;
         { "FROM",    "C", 50, 0 },;
         { "TO",      "C", 50, 0 } } )
   ENDIF   

   // 2. Open Databases and Indexes
   USE tasks VIA "DBFCDX"

   IF Tasks->( RecCount() ) == 0
      Tasks->( DbAppend() )
      Tasks->Task := "Project Alpha"
   ENDIF   

   INDEX ON field->ID TO "ID"
   SET ORDER TO TAG ID

   USE details VIA "DBFCDX" NEW

   IF Details->( RecCount() ) == 0
      Details->( DbAppend() )
      Details->Task_id   := Tasks->Id
      Details->Date      := Date()
      Details->From      := "09:00"
      Details->To        := "11:00"
   ENDIF   

   INDEX ON field->Task_id TO "TASK_ID"
   SET ORDER TO TAG TASK_ID

   SELECT "tasks"
   SET RELATION TO details->Task_id INTO details

   // 3. UI Setup
   DEFINE DIALOG oDlg SIZE 1200, 400 PIXEL TRUEPIXEL

   @ 2.5, 2 XBROWSE oBrw SIZE 1170, 350 OF oDlg ;
      DATASOURCE "Tasks" ;
      CELL LINES NOBORDER
   
oBrw:CreateFromCode() oBrw:SetTree( BuildTree() ) oBrw:aCols[ 1 ]:nWidth := 500
// Edit action oBrw:bEdit = { | o | EditItem( o:oBrw:oTreeItem, o:oBrw ) }
// Navigation action (Sync DB cursors) oBrw:bChange = { | o | If( Len( o:oTreeItem:Cargo ) == 2,; Tasks->( DbGoTo( o:oTreeItem:Cargo[ 2 ] ) ),; Details->( DbGoTo( o:oTreeItem:Cargo[ 2 ] ) ) ) }
oDlg:oClient = oBrw ACTIVATE DIALOG oDlg CENTERED ; ON INIT BuildBar( oDlg, oBrw ) RETURN NIL //----------------------------------------------------------------------------// FUNCTION BuildBar( oDlg, oBrw ) LOCAL oBar DEFINE BUTTONBAR oBar OF oDlg SIZE 50, 50 2007 // Modified: Call static functions instead of inline code blocks DEFINE BUTTON OF oBar PROMPT "Task" RESOURCE "add" ; ACTION AddTask( oBrw ) DEFINE BUTTON OF oBar PROMPT "Detail" RESOURCE "add" ; ACTION AddDetail( oBrw ) DEFINE BUTTON OF oBar PROMPT "Edit" RESOURCE "edit" ACTION oBrw:Edit() DEFINE BUTTON OF oBar PROMPT "Del" RESOURCE "del"
oDlg:Resize() RETURN .T. //----------------------------------------------------------------------------// FUNCTION BuildTree() LOCAL oTree, nRecNo := Tasks->( RecNo() ) IF Tasks->( RecCount() ) > 0 TREE oTree
Details->( DbGoTop() ) Tasks->( DbGoTop() )
WHILE !Tasks->( Eof() ) // Cargo format: { "task", TaskRecNo } TREEITEM tasks->Task CARGO { "task", Tasks->( RecNo() ) } IF Details->( RecCount() ) > 0 TREE WHILE Details->Task_id == tasks->id .and. !Details->( Eof() ) // Cargo format: { "detail", DetailRecNo, TaskRecNo } TREEITEM "Date: " + DToC( Details->Date ) + ; " - From: " + RTRim( Details->From ) + ; " - To: " + RTrim( Details->To ) ; CARGO { "detail", Details->( RecNo() ), Tasks->( RecNo() ) } Details->( DbSkip() ) END ENDTREE ENDIF Tasks->( DbSkip() ) END ENDTREE Tasks->( DbGoTo( nRecNo ) ) ELSE TREE oTree ENDTREE ENDIF RETURN oTree //----------------------------------------------------------------------------// FUNCTION EditItem( oTreeItem, oBrw ) LOCAL nRec := oTreeItem:Cargo[ 2 ], oDlg LOCAL nBrwPos := oBrw:nRowSel IF oTreeItem:Cargo[ 1 ] == "detail" DEFINE DIALOG oDlg SIZE 400, 220 PIXEL TRUEPIXEL TITLE "Edit detail" Details->( DbGoTo( nRec ) ) @ 1, 2 SAY "Date:" OF oDlg @ 1, 6 GET Details->Date PICTURE "9999-99-99" OF oDlg @ 3.2, 2 SAY "From:" OF oDlg @ 3.5, 6 GET Details->From PICTURE "99:99" OF oDlg @ 5.3, 2 SAY "To:" OF oDlg @ 5.9, 6 GET Details->To PICTURE "99:99" OF oDlg @ 10, 14 BUTTON "OK" OF oDlg SIZE 80, 25 ; ACTION ( Details->( DbCommit() ), oDlg:End(),; UpdateTree( oBrw, nBrwPos ) ) // Modified: Use UpdateTree helper @ 10, 34 BUTTON "Cancel" OF oDlg SIZE 80, 25 ACTION oDlg:End() ACTIVATE DIALOG oDlg CENTERED ELSE
DEFINE DIALOG oDlg SIZE 400, 220 PIXEL TRUEPIXEL TITLE "Edit task" Tasks->( DbGoTo( nRec ) ) @ 1, 2 SAY "Task:" OF oDlg @ 1, 6 GET Tasks->Task SIZE 340, 20 OF oDlg @ 10, 14 BUTTON "OK" OF oDlg SIZE 80, 25 ; ACTION ( Tasks->( DbCommit() ), oDlg:End(),; UpdateTree( oBrw, nBrwPos ) ) // Modified: Use UpdateTree helper @ 10, 34 BUTTON "Cancel" OF oDlg SIZE 80, 25 ACTION oDlg:End() ACTIVATE DIALOG oDlg CENTERED ENDIF RETURN NIL //----------------------------------------------------------------------------// // NEW HELPER FUNCTIONS FOR TREE STATE MANAGEMENT //----------------------------------------------------------------------------// // Action to add a new Task STATIC FUNCTION AddTask( oBrw ) LOCAL aState // 1. Save current state aState := SaveTreeState( oBrw:oTree ) // 2. Database logic Tasks->( DbAppend() ) Details->( DbAppend() ) Details->Task_id := Tasks->Id Details->Date := Date() Details->From := "09:00" Details->To := "11:00" // 3. Rebuild the tree oBrw:oTree := BuildTree() // 4. Restore what was previously open RestoreTreeState( oBrw:oTree, aState )
oBrw:Refresh() RETURN NIL // Action to add a new Detail STATIC FUNCTION AddDetail( oBrw ) LOCAL aState LOCAL nTaskRec // Check if there are items before trying to read Cargo IF oBrw:oTreeItem == NIL; RETURN NIL; ENDIF // Get Parent Task RecNo (regardless of current position) nTaskRec := If( Len( oBrw:oTreeItem:Cargo ) == 2,; oBrw:oTreeItem:Cargo[ 2 ],; oBrw:oTreeItem:Cargo[ 3 ] ) // 1. Save current state aState := SaveTreeState( oBrw:oTree ) // --- TRICK --- // Force the parent task to be Open (.T.) in the state array ForceParentOpen( aState, nTaskRec ) // ------------- // 2. Database logic Tasks->( DbGoTo( nTaskRec ) ) Details->( DbAppend() ) Details->task_id := Tasks->id Details->Date := Date() Details->From := "09:00" Details->To := "11:00" // 3. Rebuild tree oBrw:oTree := BuildTree() // 4. Restore (now includes our forced open task) RestoreTreeState( oBrw:oTree, aState ) oBrw:Refresh() RETURN NIL // Helper to update tree without collapsing everything (Used in EditItem) FUNCTION UpdateTree( oBrw, nBrwPos ) LOCAL aState // 1. Save state aState := SaveTreeState( oBrw:oTree ) // 2. Rebuild oBrw:oTree := BuildTree() // 3. Restore RestoreTreeState( oBrw:oTree, aState ) // 4. Restore cursor position and refresh oBrw:nRowSel := nBrwPos oBrw:Refresh()
RETURN NIL // Helper to ensure the task where we are adding a detail is open STATIC FUNCTION ForceParentOpen( aState, nTaskRec ) LOCAL nAt // Search in state array if that specific Task exists nAt := AScan( aState, { | a | a[ 2 ][ 1 ] == "task" .and. a[ 2 ][ 2 ] == nTaskRec } ) IF nAt > 0 // If exists, force it to be Open (.T.) aState[ nAt ][ 1 ] := .T. ELSE // If not found, add it forced as open AAdd( aState, { .T., { "task", nTaskRec } } ) ENDIF RETURN NIL // Function to save the current status (Open/Closed) of the tree items FUNCTION SaveTreeState( oTree ) LOCAL aState := {} LOCAL oItem
oItem := oTree:oFirst WHILE oItem != NIL // Save { IsOpen, CargoData } AADD( aState, { oItem:lOpened, oItem:Cargo } ) oItem := oItem:Skip( 1 ) END RETURN aState // Function to restore the status (Open/Closed) to a new tree FUNCTION RestoreTreeState( oTree, aState ) LOCAL oItem := oTree:oFirst LOCAL nAt WHILE oItem != NIL // Search if this item's 'Cargo' exists in our saved state array // Cargo structure: [1] = Type ("task"/"detail"), [2] = ID nAt := AScan( aState, { | a | ValType( a[ 2 ] ) == "A" .and. ; ValType( oItem:Cargo ) == "A" .and. ; a[ 2 ][ 1 ] == oItem:Cargo[ 1 ] .and. ; a[ 2 ][ 2 ] == oItem:Cargo[ 2 ] } ) // If found and it was marked as opened (.T.), we open it in the new tree IF nAt > 0 .and. aState[ nAt ][ 1 ] oItem:Open() ENDIF // Move to next. Calling Open() above makes Skip(1) enter the children automatically oItem := oItem:Skip( 1 ) END RETURN NIL
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Detlef
Posts: 218
Joined: Mon Feb 07, 2022 9:54 pm

Re: xBrowse GoTo

Post by Detlef »

Many thanks for your help, Antonio.

I appreciate your explanations how to manage trees because until now I had poor knowledge about this.
In my case I need a tree whose branches are always open. No need to close them.

I tried your source code.
Unfortunately I get an error 9009, hbx_realloc can't reallocate memory.

Regards, Detlef

Post Reply