Thursday, February 3, 2011

Pharo GUI with Polymorph

Develop a contact manager desktop application.

You will learn:
  • how Pharo helps the "Program by intention" style of programming
  • how to create basic persistency of created Contact objects
  • how to compose a GUI, basic widgets, events and layout

Download screencast (1280x768): .mov 75.2 MB

View mobile version.



More Polymorph in Picasa screencast.

Get the code:
Gofer it
 squeaksource: 'Pharocasts';
 package: 'ContactManager';
 load.
 
(Smalltalk at:#ContactListEditor) open.


Start by looking how the contact list is built:
ContactListEditor>>open
  |builder content|
  builder := UITheme builder.
  content := builder newColumn: {   
        builder 
                newListFor: self   
                list: #contacts
                selected: #contactSelectedIndex
                changeSelected: #contactSelectedIndex:
                help: 'contacts'.
        builder newRow: {
                builder newButtonFor: self 
                        action: #addButtonClick 
                        label: 'Add' 
                        help: 'Create a new contact'.
                builder newButtonFor: self 
                        action: #removeButtonClick 
                        getEnabled: #hasSelectedContact 
                        label: 'Remove' 
                        help: 'Remove selected contact'.
                builder newButtonFor: self 
                        action: #editButtonClick 
                        getEnabled: #hasSelectedContact 
                        label: 'Edit' 
                        help: 'Edit selected contact'  }}.
   
  (content openInWindowLabeled: 'Contacts') extent: 400@500.
#newRow: and #newColumn: are an easy way to align elements on the window.

When the Add button is clicked, message #addButtonClick is sent on the ContactListEditor object:
ContactListEditor>>addButtonClick
  |newContact|
  newContact := Contact new.
 
  ContactEditor new
        contact: newContact;
        onOK: [ Contact database add: newContact.  
                selectedContactIndex := Contact database size.
                self 
                        changed: #contacts;
                        changed: #hasSelectedContact];
        openModal.
The closure given to #onOK: adds the new Contact and tells the view to refresh components which depends on #contacts and #hasSelectedContact selectors - that means the contact list and the Remove and Edit buttons.

ContactEditor defines a modal dialog to edit the firstName and lastName of a Contact:
ContactEditor>>openModal
  |builder dialog content firstName|
  
  builder := UITheme builder.
  content := (builder newLabelGroup: {
                'First name' -> (
                       firstName := (builder
                         newTextEntryFor: contact
                         getText: #firstName
                         setText: #firstName: 
                         help: 'Enter the first name of the contact')
                       acceptOnCR: false;
                       minWidth: 200).
                'Last name' -> (
                       (builder
                          newTextEntryFor: contact 
                          getText: #lastName 
                          setText: #lastName: 
                          help: 'Enter the last name of the contact')
                        acceptOnCR: false;
                        minWidth: 200) }).
 
  dialog := builder 
              newPluggableDialogWindow:'Edit contact' 
              for: content.
  dialog rememberKeyboardFocus: firstName.
  builder openModal: dialog.
 
  dialog cancelled ifFalse: [self doOnOK].
From Gary Chambers (and thanks !):

Disabling the acceptOnCR for each text frield allows the default dialog handling for the return key (defaults to OK).

Normally the initial keyboard focus for a dialog is the default button, if specified. Remembering the first name field prior to opening will give that field focus.


Now it should be easier to understand Polymorph examples found in
UITheme class>>exampleBasicControls and friends (in examples protocol).

21 comments:

  1. Thanks Laurent! I really appreciate your videos. They are very helpful and give enough information to help me to make progress in my Smalltalk code. Keep up the great work please!

    ReplyDelete
  2. Great work! Please continue with videos like this.

    I am the type of programmer who wants to make GUI mainly for database records and this is what I was seeking for. Lack of documentation (at least telling where to start and showing the basics ... the rest we can read from the code) is a huge break from starting developing in Smalltalk for many of developers.

    ReplyDelete
  3. If I inspect "ContactListEditor allInstances" it shows an empty array.

    If I then doit "ContactListEditor open" twice (creating 2 instances), and inspect "ContactListEditor allInstances" again, it shows an array with two entries, as expected.

    But if I close one (or both) of the "Contacts" windows, then inspect "ContactListEditor allInstances" again, it still shows two instances.

    Are the instances still somewhere and accessible? If not, is there any way to make "ContactListEditor allInstances" report correctly?

    ReplyDelete
  4. I could not reproduce it. Which version of Pharo ? VM ? OS ?

    ReplyDelete
  5. My "ContactListEditor allInstances" question appears to be related to garbage collection. The instances are in the array until the system cleans up.

    ReplyDelete
  6. Button questions:
    1. How do you add a button to the Add/Edit/Remove row that will close the ContactListEditor window? I tried adding

    builder newButtonFor: self
    action: #closeButtonClick
    label: 'Close'
    help: 'Close this window'.

    with a new method
    closeButtonClick
    self delete.

    but that generates a MNU error.

    ---
    2. How do you code a button that takes one or more parameters when the button is clicked?

    ReplyDelete
  7. 1. ContactListEditor is not a Window but an Object subclass.

    The window is created by #openInWindowLabeled:. So in ContactListEditor>>open
    ....
      mainWindow := (content openInWindowLabeled: 'Contacts') extent: 400@500.

    and in
    ContactListEditor>>closeButtonClick
      mainWindow delete

    2. What do you want to do ? You can define a new button class which do it but may be overkill.

    ReplyDelete
  8. Can you explain how changes (via Edit button) are saved? The addButtonClick method uses onOK:aBlock, but editButtonClick doesn't.

    ReplyDelete
  9. In ContactEditor>>openModal, Contact objects are associated to TextEntries. So on OK the data in the fields are passed to #setText: selectors (whether it's an add or edit - ContactEditor doesn't know the workflow).

    The onOK: block is just used to add the Contact object to the database. In case of an edit, the edited Contact is already in the database so there's nothing to do.

    ReplyDelete
  10. I would like to see this type of sample application updated with a connection to a Magma instance to demonstrate how to start and connect to Magma

    ReplyDelete
  11. Nelson, have you seen http://www.pharocasts.com/2010/02/magma-object-oriented-database.html ?

    ReplyDelete
  12. Watching now! thank you very much

    ReplyDelete
  13. Good night, I´m novice in Pharo, anyone know where I can download the UIBuilder for Pharo?

    ReplyDelete
  14. Thanks for this great Pharocast! Helping a lot.

    ReplyDelete
  15. What screen-capturing software do you use for the screencast?

    ReplyDelete
    Replies
    1. Thanks Sebastian. I use http://www.telestream.net/screenflow/overview.htm

      Delete
  16. On this example you don't forbid the user to close the contact list if the ContacEditor is open, Am I Right? How should I do that?

    ReplyDelete
    Replies
    1. I've replaced

      builder openModal: dialog

      with:

      World openModal: dialog.

      Delete