From 39f5c5cb289a945ea1b2b4785932b87bc44df224 Mon Sep 17 00:00:00 2001 From: Ingo Maier Date: Fri, 11 Jul 2008 15:25:57 +0000 Subject: large swing update, added ComboBox --- src/swing/doc/README | 4 + src/swing/images/banana.jpg | Bin 0 -> 5999 bytes src/swing/images/margarita1.jpg | Bin 0 -> 14769 bytes src/swing/images/margarita2.jpg | Bin 0 -> 17309 bytes src/swing/images/rose.jpg | Bin 0 -> 13807 bytes src/swing/scala/swing/AbstractButton.scala | 8 +- src/swing/scala/swing/Action.scala | 12 ++ src/swing/scala/swing/Border.scala | 2 + src/swing/scala/swing/BorderPanel.scala | 2 +- src/swing/scala/swing/BoxPanel.scala | 2 +- src/swing/scala/swing/Button.scala | 2 +- src/swing/scala/swing/CheckBox.scala | 2 +- src/swing/scala/swing/ComboBox.scala | 198 +++++++++++++++++++++++ src/swing/scala/swing/Component.scala | 30 +++- src/swing/scala/swing/Label.scala | 4 +- src/swing/scala/swing/ListView.scala | 76 ++++++++- src/swing/scala/swing/Publisher.scala | 130 ++++++++++++++- src/swing/scala/swing/Reactions.scala | 53 +++--- src/swing/scala/swing/Reactor.scala | 2 +- src/swing/scala/swing/SimpleGUIApplication.scala | 6 + src/swing/scala/swing/Slider.scala | 6 +- src/swing/scala/swing/Swing.scala | 4 + src/swing/scala/swing/Table.scala | 81 +++++++++- src/swing/scala/swing/TextComponent.scala | 4 +- src/swing/scala/swing/TextField.scala | 6 +- src/swing/scala/swing/event/ActionEvent.scala | 7 + src/swing/scala/swing/event/ButtonClicked.scala | 2 +- src/swing/scala/swing/event/LiveEvent.scala | 11 +- src/swing/scala/swing/test/ComboBoxes.scala | 79 +++++++++ src/swing/scala/swing/test/TableSelection.scala | 12 +- src/swing/scala/swing/test/UIDemo.scala | 3 + 31 files changed, 685 insertions(+), 63 deletions(-) create mode 100644 src/swing/images/banana.jpg create mode 100644 src/swing/images/margarita1.jpg create mode 100644 src/swing/images/margarita2.jpg create mode 100644 src/swing/images/rose.jpg create mode 100644 src/swing/scala/swing/ComboBox.scala create mode 100644 src/swing/scala/swing/event/ActionEvent.scala create mode 100644 src/swing/scala/swing/test/ComboBoxes.scala (limited to 'src') diff --git a/src/swing/doc/README b/src/swing/doc/README index 00aebe7b19..3f6d1b1c2f 100644 --- a/src/swing/doc/README +++ b/src/swing/doc/README @@ -30,3 +30,7 @@ The library comprises three main packages: scala.swing.test A set of demos. + +Notes: + +Visual appearance of combo boxes using the GTK LaF is broken on JDKs < 1.7b30. \ No newline at end of file diff --git a/src/swing/images/banana.jpg b/src/swing/images/banana.jpg new file mode 100644 index 0000000000..81fc4ab387 Binary files /dev/null and b/src/swing/images/banana.jpg differ diff --git a/src/swing/images/margarita1.jpg b/src/swing/images/margarita1.jpg new file mode 100644 index 0000000000..485723334a Binary files /dev/null and b/src/swing/images/margarita1.jpg differ diff --git a/src/swing/images/margarita2.jpg b/src/swing/images/margarita2.jpg new file mode 100644 index 0000000000..c5fefb0bd5 Binary files /dev/null and b/src/swing/images/margarita2.jpg differ diff --git a/src/swing/images/rose.jpg b/src/swing/images/rose.jpg new file mode 100644 index 0000000000..5c2e75637e Binary files /dev/null and b/src/swing/images/rose.jpg differ diff --git a/src/swing/scala/swing/AbstractButton.scala b/src/swing/scala/swing/AbstractButton.scala index d21f71aaaa..e496d0566a 100644 --- a/src/swing/scala/swing/AbstractButton.scala +++ b/src/swing/scala/swing/AbstractButton.scala @@ -11,7 +11,7 @@ import event._ * @see javax.swing.AbstractButton */ abstract class AbstractButton extends Component with Action.Trigger with Publisher { - override lazy val peer: JAbstractButton = new JAbstractButton {} + override lazy val peer: JAbstractButton = new JAbstractButton with SuperMixin {} def text: String = peer.getText def text_=(s: String) = peer.setText(s) @@ -38,10 +38,8 @@ abstract class AbstractButton extends Component with Action.Trigger with Publish //1.6: def hideActionText: Boolean = peer.getHideActionText //def hideActionText_=(b: Boolean) = peer.setHideActionText(b) - peer.addActionListener(new java.awt.event.ActionListener { - def actionPerformed(e: java.awt.event.ActionEvent) { - publish(ButtonClicked(AbstractButton.this)) - } + peer.addActionListener(Swing.ActionListener { e => + publish(ButtonClicked(AbstractButton.this)) }) def selected: Boolean = peer.isSelected diff --git a/src/swing/scala/swing/Action.scala b/src/swing/scala/swing/Action.scala index e259ba748a..46dc470558 100644 --- a/src/swing/scala/swing/Action.scala +++ b/src/swing/scala/swing/Action.scala @@ -1,10 +1,22 @@ package scala.swing import javax.swing.{KeyStroke, Icon} +import java.awt.event.ActionListener object Action { case object NoAction extends Action("") { def apply() {} } + object Trigger { + abstract trait Wrapper extends Component with Action.Trigger { + self: Component { + def peer: javax.swing.JComponent { + def addActionListener(a: ActionListener) + def removeActionListener(a: ActionListener) + } + } => + } + } + /** * Anything that triggers an action. */ diff --git a/src/swing/scala/swing/Border.scala b/src/swing/scala/swing/Border.scala index 13d5d7ae1e..bdc347763e 100644 --- a/src/swing/scala/swing/Border.scala +++ b/src/swing/scala/swing/Border.scala @@ -11,6 +11,8 @@ import javax.swing.border._ */ object Border { def Empty = BorderFactory.createEmptyBorder() + def Empty(weight: Int) = + BorderFactory.createEmptyBorder(weight, weight, weight, weight) def Empty(top: Int, left: Int, bottom: Int, right: Int) = BorderFactory.createEmptyBorder(top, left, bottom, right) diff --git a/src/swing/scala/swing/BorderPanel.scala b/src/swing/scala/swing/BorderPanel.scala index ad3c4ab11c..93ee4eec71 100644 --- a/src/swing/scala/swing/BorderPanel.scala +++ b/src/swing/scala/swing/BorderPanel.scala @@ -25,7 +25,7 @@ object BorderPanel { class BorderPanel extends Panel with LayoutContainer { import BorderPanel._ def layoutManager = peer.getLayout.asInstanceOf[BorderLayout] - override lazy val peer = new javax.swing.JPanel(new BorderLayout) + override lazy val peer = new javax.swing.JPanel(new BorderLayout) with SuperMixin type Constraints = Position.Value diff --git a/src/swing/scala/swing/BoxPanel.scala b/src/swing/scala/swing/BoxPanel.scala index 101298bf6e..285f245167 100644 --- a/src/swing/scala/swing/BoxPanel.scala +++ b/src/swing/scala/swing/BoxPanel.scala @@ -8,7 +8,7 @@ package scala.swing */ class BoxPanel(orientation: Orientation.Value) extends Panel with SequentialContainer.Wrapper { override lazy val peer = { - val p = new javax.swing.JPanel + val p = new javax.swing.JPanel with SuperMixin val l = new javax.swing.BoxLayout(p, orientation.id) p.setLayout(l) p diff --git a/src/swing/scala/swing/Button.scala b/src/swing/scala/swing/Button.scala index c28773a9c4..c146e66cb0 100644 --- a/src/swing/scala/swing/Button.scala +++ b/src/swing/scala/swing/Button.scala @@ -9,7 +9,7 @@ import event._ * @see javax.swing.JButton */ class Button(text0: String) extends AbstractButton with Publisher { - override lazy val peer: JButton = new JButton(text0) + override lazy val peer: JButton = new JButton(text0) with SuperMixin def this() = this("") def this(a: Action) = { this("") diff --git a/src/swing/scala/swing/CheckBox.scala b/src/swing/scala/swing/CheckBox.scala index ceea4483a1..ed266cca27 100644 --- a/src/swing/scala/swing/CheckBox.scala +++ b/src/swing/scala/swing/CheckBox.scala @@ -8,6 +8,6 @@ import javax.swing._ * @see javax.swing.JCheckBox */ class CheckBox(text: String) extends ToggleButton { - override lazy val peer: JCheckBox = new JCheckBox(text) + override lazy val peer: JCheckBox = new JCheckBox(text) with SuperMixin def this() = this("") } \ No newline at end of file diff --git a/src/swing/scala/swing/ComboBox.scala b/src/swing/scala/swing/ComboBox.scala new file mode 100644 index 0000000000..6c2dd4a487 --- /dev/null +++ b/src/swing/scala/swing/ComboBox.scala @@ -0,0 +1,198 @@ +package scala.swing + +import event._ +import javax.swing.{JList, JComponent, JComboBox, JTextField, ComboBoxModel, AbstractListModel, ListCellRenderer} +import java.awt.event.ActionListener + + +object ComboBox { + /** + * An editor for a combo box. Let's you edit the currently selected item. + * It is highly recommended to use the BuiltInEditor class. For anything + * else, one cannot guarantee that it integrated nicely into the current + * LookAndFeel. + * + * Publishes action events. + */ + trait Editor[A] extends Publisher { + lazy val comboBoxPeer: javax.swing.ComboBoxEditor = new javax.swing.ComboBoxEditor with Publisher { + def addActionListener(l: ActionListener) { + this match { + // TODO case w: Action.Trigger.Wrapper => + // w.peer.addActionListener(l) + case _ => + this.subscribe(new Reactions.Wrapper(l) ({ + case ActionEvent(c) => l.actionPerformed(new java.awt.event.ActionEvent(c.peer, 0, "")) + })) + } + } + def removeActionListener(l: ActionListener) { + this match { + // TODO case w: Action.Trigger.Wrapper => + // w.peer.removeActionListener(l) + case _ => + this.unsubscribe(new Reactions.Wrapper(l)({ case _ => })) + } + } + def getEditorComponent: JComponent = Editor.this.component.peer + def getItem(): AnyRef = item.asInstanceOf[AnyRef] + def selectAll() { startEditing() } + def setItem(a: Any) { item = a.asInstanceOf[A] } + } + def component: Component + def item: A + def item_=(a: A) + def startEditing() + } + + /** + * Use this editor, if you want to reuse the builtin editor supplied by the current + * Look and Feel. This is restricted to a text field as the editor widget. The + * conversion from and to a string is done by the supplied functions. + * + * It's okay if string2A throws exceptions. They are caught by an input verifier. + */ + class BuiltInEditor[A](comboBox: ComboBox[A])(string2A: String => A, + a2String: A => String) extends ComboBox.Editor[A] { + protected[swing] class DelegatedEditor(editor: javax.swing.ComboBoxEditor) extends javax.swing.ComboBoxEditor { + var value: A = { + val v = comboBox.peer.getSelectedItem + try { + v match { + case s: String => string2A(s) + case _ => v.asInstanceOf[A] + } + } catch { + case _: Exception => + throw new IllegalArgumentException("ComboBox not initialized with a proper value, was '" + v + "'.") + } + } + def addActionListener(l: ActionListener) { + editor.addActionListener(l) + } + def removeActionListener(l: ActionListener) { + editor.removeActionListener(l) + } + + def getEditorComponent: JComponent = editor.getEditorComponent.asInstanceOf[JComponent] + def selectAll() { editor.selectAll() } + def getItem(): AnyRef = { verifier.verify(getEditorComponent); value.asInstanceOf[AnyRef] } + def setItem(a: Any) { editor.setItem(a) } + + val verifier = new javax.swing.InputVerifier { + // TODO: should chain with potentially existing verifier in editor + def verify(c: JComponent) = try { + println(c) + value = string2A(c.asInstanceOf[JTextField].getText) + true + } + catch { + case e: Exception => false + } + } + + def textEditor = getEditorComponent.asInstanceOf[JTextField] + textEditor.setInputVerifier(verifier) + textEditor.addActionListener(Swing.ActionListener{ a => + getItem() // make sure our value is updated + textEditor.setText(a2String(value)) + }) + } + + + override lazy val comboBoxPeer: javax.swing.ComboBoxEditor = new DelegatedEditor(comboBox.peer.getEditor) + + def component = Component.wrap(comboBoxPeer.getEditorComponent.asInstanceOf[JComponent]) + def item: A = { comboBoxPeer.asInstanceOf[DelegatedEditor].value } + def item_=(a: A) { comboBoxPeer.setItem(a2String(a)) } + def startEditing() { comboBoxPeer.selectAll() } + } + + implicit def stringEditor(c: ComboBox[String]): Editor[String] = new BuiltInEditor(c)(s => s, s => s) + implicit def intEditor(c: ComboBox[Int]): Editor[Int] = new BuiltInEditor(c)(s => s.toInt, s => s.toString) + implicit def floatEditor(c: ComboBox[Float]): Editor[Float] = new BuiltInEditor(c)(s => s.toFloat, s => s.toString) + implicit def doubleEditor(c: ComboBox[Double]): Editor[Double] = new BuiltInEditor(c)(s => s.toDouble, s => s.toString) + + def newConstantModel[A](items: Seq[A]): ComboBoxModel = { + new AbstractListModel with ComboBoxModel { + private var selected = items(0) + def getSelectedItem: AnyRef = selected.asInstanceOf[AnyRef] + def setSelectedItem(a: Any) { selected = a.asInstanceOf[A] } + def getElementAt(n: Int) = items(n).asInstanceOf[AnyRef] + def getSize = items.size + } + } + + def newMutableModel[A, Self](items: Seq[A] with scala.collection.mutable.Publisher[scala.collection.mutable.Message[A], Self]): ComboBoxModel = { + new AbstractListModel with ComboBoxModel { + private var selected = items(0) + def getSelectedItem: AnyRef = selected.asInstanceOf[AnyRef] + def setSelectedItem(a: Any) { selected = a.asInstanceOf[A] } + def getElementAt(n: Int) = items(n).asInstanceOf[AnyRef] + def getSize = items.size + } + } + + /*def newConstantModel[A](items: Seq[A]): ComboBoxModel = items match { + case items: Seq[A] with scala.collection.mutable.Publisher[scala.collection.mutable.Message[A], Self] => newMutableModel + case _ => newConstantModel(items) + }*/ +} + +/** + * Has built-in default editor and renderer that cannot be exposed. + * They are set by the look and feel (LaF). Unfortunately, this design in + * inherently broken, since custom editors will almost always look + * differently. The border of the built-in text field editor, e.g., is drawn + * by the LaF. In a custom text field editor we have no way to mirror that. + * + * This combo box has to be initialized with a valid selected value. + * Otherwise it will fail. + * + * @see javax.swing.JComboBox + */ +class ComboBox[A](items: Seq[A]) extends Component with Publisher { + override lazy val peer: JComboBox = new JComboBox(ComboBox.newConstantModel(items)) with SuperMixin + + object selection extends Publisher { + def index: Int = peer.getSelectedIndex + def item: A = peer.getSelectedItem.asInstanceOf[A] + + println("created") + peer.addActionListener(Swing.ActionListener { e => + println("action") + publish(event.SelectionChanged(ComboBox.this)) + }) + } + + /** + * Sets the renderer for this combo box's items. Index -1 is + * passed to the renderer for the selected item (not in the popup menu). + * + * The underlying combo box renders all items in a ListView (both, in + * the pulldown menu as well as in the box itself), hence the + * ListView.Renderer. + * + * Note that the UI peer of a combo box usually changes the colors + * of the component to its own defaults _after_ the renderer has configured it. + * That's Swing's principle of most suprise. + */ + def renderer: ListView.Renderer[A] = ListView.Renderer.wrap(peer.getRenderer) + def renderer_=(r: ListView.Renderer[A]) { peer.setRenderer(r.peer) } + + /* XXX: currently not safe to expose: + def editor: ComboBox.Editor[A] = + def editor_=(r: ComboBox.Editor[A]) { peer.setEditor(r.comboBoxPeer) } + */ + def editable: Boolean = peer.isEditable + + def makeEditable()(implicit editor: ComboBox[A] => ComboBox.Editor[A]) { + peer.setEditable(true) + peer.setEditor(editor(this).comboBoxPeer) + } + + def prototypeDisplayValue: Option[A] = Swing.toOption(peer.getPrototypeDisplayValue) + def prototypeDisplayValue_=(v: Option[A]) { + peer.setPrototypeDisplayValue(Swing.toNull(v.map(_.asInstanceOf[AnyRef]))) + } +} \ No newline at end of file diff --git a/src/swing/scala/swing/Component.scala b/src/swing/scala/swing/Component.scala index 2039a09d36..3a715e91aa 100644 --- a/src/swing/scala/swing/Component.scala +++ b/src/swing/scala/swing/Component.scala @@ -12,9 +12,17 @@ object Component { /** * Returns the wrapper for a given peer. + * Fails if there is no wrapper for the given component. */ protected[swing] def wrapperFor[C<:Component](c: javax.swing.JComponent): C = c.getClientProperty(ClientKey).asInstanceOf[C] + + /** + * Wraps a given Java Swing Component into a new wrapper. + */ + def wrap[A](c: JComponent) = new Component { + override lazy val peer = c + } } /** @@ -23,9 +31,20 @@ object Component { * @see javax.swing.JComponent */ abstract class Component extends UIElement with Publisher { - override lazy val peer: javax.swing.JComponent = new javax.swing.JComponent{} + override lazy val peer: javax.swing.JComponent = new javax.swing.JComponent with SuperMixin {} + var initP: JComponent = null peer.putClientProperty(Component.ClientKey, this) + trait SuperMixin extends JComponent { + override def paintComponent(g: java.awt.Graphics) { + Component.this.paintComponent(g) + } + def __super__paintComponent(g: java.awt.Graphics) { + super.paintComponent(g) + } + } + + /** * Used by certain layout managers, e.g., BoxLayout or OverlayLayout to * align components relative to each other. @@ -182,5 +201,14 @@ abstract class Component extends UIElement with Publisher { def revalidate() { peer.revalidate() } + def requestFocus() { peer.requestFocus() } + + protected def paintComponent(g: java.awt.Graphics) { + peer match { + case peer: SuperMixin => peer.__super__paintComponent(g) + case _ => // it's a wrapper created on the fly + } + } + override def toString = "scala.swing wrapper " + peer.toString } diff --git a/src/swing/scala/swing/Label.scala b/src/swing/scala/swing/Label.scala index 20692628d8..bcb421e344 100644 --- a/src/swing/scala/swing/Label.scala +++ b/src/swing/scala/swing/Label.scala @@ -10,8 +10,10 @@ import javax.swing._ class Label(text0: String) extends Component { override lazy val peer: JLabel = new JLabel(text0) def this() = this("") - def text: String = peer.getText() + def text: String = peer.getText def text_=(s: String) = peer.setText(s) + def icon: Icon = peer.getIcon + def icon_=(i: Icon) = peer.setIcon(i) /** * The alignment of the label's contents relative to its bounding box. */ diff --git a/src/swing/scala/swing/ListView.scala b/src/swing/scala/swing/ListView.scala index c0a8b2fc4c..25af541802 100644 --- a/src/swing/scala/swing/ListView.scala +++ b/src/swing/scala/swing/ListView.scala @@ -3,6 +3,7 @@ package scala.swing import javax.swing._ import javax.swing.event._ import event._ +import java.awt.Color object ListView { object IntervalMode extends Enumeration { @@ -10,6 +11,68 @@ object ListView { val SingleInterval = Value(ListSelectionModel.SINGLE_INTERVAL_SELECTION) val MultiInterval = Value(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) } + + def wrap[A](c: JList) = new ListView[A] { + override lazy val peer = c + } + + object Renderer { + def wrap[A](r: ListCellRenderer): Renderer[A] = new Wrapped[A](r) + + class Wrapped[A](override val peer: ListCellRenderer) extends Renderer[A] { + def componentFor(list: ListView[_<:A], isSelected: Boolean, hasFocus: Boolean, a: A, index: Int) = { + Component.wrap(peer.getListCellRendererComponent(list.peer, a, index, isSelected, hasFocus).asInstanceOf[JComponent]) + } + } + } + + abstract class Renderer[-A] { + def peer: ListCellRenderer = new ListCellRenderer { + def getListCellRendererComponent(list: JList, a: Any, index: Int, isSelected: Boolean, hasFocus: Boolean) = { + componentFor(ListView.wrap[A](list), isSelected, hasFocus, a.asInstanceOf[A], index).peer + } + } + def componentFor(list: ListView[_<:A], isSelected: Boolean, hasFocus: Boolean, a: A, index: Int): Component + } + + /** + * A default renderer that maintains a single component for item rendering + * and preconfigures it to sensible defaults. It is polymorphic on the + * components type so clients can easily use component specific attributes + * during configuration. + */ + abstract class DefaultRenderer[-A, C<:Component](protected val component: C) extends Renderer[A] { + // The renderer component is responsible for painting selection + // backgrounds. Hence, make sure it is opaque to let it draw + // the background. + component.opaque = true + + /** + * Standard preconfiguration that is commonly done for any component. + */ + def preConfigure(list: ListView[_<:A], isSelected: Boolean, hasFocus: Boolean, a: A, index: Int) { + if (isSelected) { + component.background = list.selectionBackground + component.foreground = list.selectionForeground + } else { + component.background = list.background + component.foreground = list.foreground + } + } + /** + * Configuration that is specific to the component and this renderer. + */ + def configure(list: ListView[_<:A], isSelected: Boolean, hasFocus: Boolean, a: A, index: Int) + + /** + * Configures the component before returning it. + */ + def componentFor(list: ListView[_<:A], isSelected: Boolean, hasFocus: Boolean, a: A, index: Int): Component = { + preConfigure(list, isSelected, hasFocus, a, index) + configure(list, isSelected, hasFocus, a, index) + component + } + } } /** @@ -23,11 +86,11 @@ class ListView[A] extends Component { import ListView._ override lazy val peer: JList = new JList - def this(elems: Seq[A]) = { + def this(items: Seq[A]) = { this() peer.setModel(new AbstractListModel { - def getElementAt(n: Int) = elems(n).asInstanceOf[AnyRef] - def getSize = elems.size + def getElementAt(n: Int) = items(n).asInstanceOf[AnyRef] + def getSize = items.size }) } @@ -48,7 +111,7 @@ class ListView[A] extends Component { def anchorIndex: Int = peer.getSelectionModel.getAnchorSelectionIndex } - object elements extends SeqProxy[A] { + object items extends SeqProxy[A] { def self = peer.getSelectedValues.projection.map(_.asInstanceOf[A]) def leadIndex: Int = peer.getSelectionModel.getLeadSelectionIndex def anchorIndex: Int = peer.getSelectionModel.getAnchorSelectionIndex @@ -70,6 +133,11 @@ class ListView[A] extends Component { def fixedCellHeight = peer.getFixedCellHeight def fixedCellHeight_=(x: Int) = peer.setFixedCellHeight(x) + def selectionForeground: Color = peer.getSelectionForeground + def selectionForeground_=(c: Color) = peer.setSelectionForeground(c) + def selectionBackground: Color = peer.getSelectionBackground + def selectionBackground_=(c: Color) = peer.setSelectionBackground(c) + peer.getModel.addListDataListener(new ListDataListener { def contentsChanged(e: ListDataEvent) { publish(ListChanged(ListView.this)) } def intervalRemoved(e: ListDataEvent) { publish(ListElementsRemoved(ListView.this, e.getIndex0 to e.getIndex1)) } diff --git a/src/swing/scala/swing/Publisher.scala b/src/swing/scala/swing/Publisher.scala index ad1b3b5da0..bcefc6c42e 100644 --- a/src/swing/scala/swing/Publisher.scala +++ b/src/swing/scala/swing/Publisher.scala @@ -1,17 +1,137 @@ package scala.swing -import scala.collection.mutable.HashSet +import scala.collection.mutable._ import event.Event /** * Notifies subscribed observers when a event is published. */ trait Publisher extends Reactor { - protected var listeners = new HashSet[Reactions] + import Reactions._ + // TODO: optionally weak references + protected var listeners = new RefSet[Reaction] { + import scala.ref._ + val underlying = new HashSet[Reference[Reaction]] + protected def Ref(a: Reaction) = a match { + case a: StronglyReferenced => new StrongReference[Reaction](a) with super.Ref[Reaction] { + type _$1 = Reaction // FIXME: what's going on here? + } + case _ => new WeakReference[Reaction](a, referenceQueue) with super.Ref[Reaction] { + type _$1 = Reaction // FIXME: what's going on here? + } + } + } - def subscribe(listener: Reactions) { listeners += listener } - def unsubscribe(listener: Reactions) { listeners -= listener } - def publish(e: Event) { for (val l <- listeners) l.send(e) } + def subscribe(listener: Reaction) { listeners += listener } + def unsubscribe(listener: Reaction) { listeners -= listener } + def publish(e: Event) { for (val l <- listeners) l(e) } listenTo(this) } + +import scala.ref._ + +private[swing] trait SingleRefCollection[+A <: AnyRef] extends Collection[A] { self => + + trait Ref[+A<:AnyRef] extends Reference[A] { + override def hashCode() = { + val v = get + if (v == None) 0 else v.get.hashCode + } + override def equals(that: Any) = that match { + case that: ReferenceWrapper[_] => + val v1 = this.get + val v2 = that.get + v1 == v2 + case _ => false + } + } + + //type Ref <: Reference[A] // TODO: could use higher kinded types, but currently crashes + protected[this] def Ref(a: A): Ref[A] + protected[this] val referenceQueue = new ReferenceQueue[A] + + protected val underlying: Collection[Reference[A]] + + def purgeReferences() { + var ref = referenceQueue.poll + while(ref != None) { + removeReference(ref.get.asInstanceOf[Reference[A]]) + ref = referenceQueue.poll + } + } + + protected[this] def removeReference(ref: Reference[A]) + + def elements = new Iterator[A] { + private val elems = self.underlying.elements + private var hd: A = _ + private var ahead: Boolean = false + private def skip: Unit = + while (!ahead && elems.hasNext) { + // make sure we have a reference to the next element, + // otherwise it might be garbage collected + val next = elems.next.get + ahead = next != None + if (ahead) hd = next.get + } + def hasNext: Boolean = { skip; ahead } + def next(): A = + if (hasNext) { ahead = false; hd } + else throw new NoSuchElementException("next on empty iterator") + } +} + +private[swing] class StrongReference[+T <: AnyRef](value: T) extends Reference[T] { + private[this] var ref: Option[T] = Some(value) + @deprecated def isValid: Boolean = ref != None + def apply(): T = ref.get + def get : Option[T] = ref + override def toString = get.map(_.toString).getOrElse("") + def clear() { ref = None } + def enqueue(): Boolean = false + def isEnqueued(): Boolean = false + } + + abstract class RefBuffer[A <: AnyRef] extends Buffer[A] with SingleRefCollection[A] { self => + protected val underlying: Buffer[Reference[A]] + + def +=(el: A) { purgeReferences(); underlying += Ref(el) } + def +:(el: A) = { purgeReferences(); Ref(el) +: underlying; this } + def remove(el: A) { underlying -= Ref(el); purgeReferences(); } + def remove(n: Int) = { val el = apply(n); remove(el); el } + def insertAll(n: Int, iter: Iterable[A]) { + purgeReferences() + underlying.insertAll(n, iter.projection.map(Ref(_))) + } + def update(n: Int, el: A) { purgeReferences(); underlying(n) = Ref(el) } + def readOnly : Seq[A] = new Seq[A] { + def length = self.length + def elements = self.elements + def apply(n: Int) = self(n) + } + def apply(n: Int) = { + purgeReferences() + var el = underlying(n).get + while (el == None) { + purgeReferences(); el = underlying(n).get + } + el.get + } + + def length = { purgeReferences(); underlying.length } + def clear() { underlying.clear(); purgeReferences() } + + protected[this] def removeReference(ref: Reference[A]) { underlying -= ref } +} + +private[swing] abstract class RefSet[A <: AnyRef] extends Set[A] with SingleRefCollection[A] { self => + protected val underlying: Set[Reference[A]] + + def -=(el: A) { underlying -= Ref(el); purgeReferences() } + def +=(el: A) { purgeReferences(); underlying += Ref(el) } + def contains(el: A) = { purgeReferences(); underlying.contains(Ref(el)) } + def size = { purgeReferences(); underlying.size } + + protected[this] def removeReference(ref: Reference[A]) { underlying -= ref } +} diff --git a/src/swing/scala/swing/Reactions.scala b/src/swing/scala/swing/Reactions.scala index cdb6d198a9..fce1fe3956 100644 --- a/src/swing/scala/swing/Reactions.scala +++ b/src/swing/scala/swing/Reactions.scala @@ -1,41 +1,44 @@ package scala.swing import event.Event +import scala.collection.mutable.{Buffer, ListBuffer} + +object Reactions { + import scala.ref._ + + class Impl extends Reactions { + private val parts: Buffer[Reaction] = new ListBuffer[Reaction] + def isDefinedAt(e: Event) = parts.exists(_ isDefinedAt e) + def += (r: Reaction) = { parts += r } + def -= (r: Reaction) { parts -= r } + def apply(e: Event) { + for (p <- parts) if (p isDefinedAt e) p(e) + } + } + + type Reaction = PartialFunction[Event, Unit] -class Reactions { /** - * Convenience type alias. + * A Reaction implementing this trait is strongly referenced in the reaction list */ - type Reaction = PartialFunction[Event, unit] + trait StronglyReferenced // TODO: implement in Publisher + + class Wrapper(listener: Any)(r: Reaction) extends Reaction with StronglyReferenced with Proxy { + def self = listener + def isDefinedAt(e: Event) = r.isDefinedAt(e) + def apply(e: Event) { r(e) } + } +} - private var parts: List[Reaction] = List() +abstract class Reactions extends Reactions.Reaction { /** * Add a reaction. */ - def += (r: Reaction) = { parts = r :: parts } + def += (r: Reactions.Reaction) /** * Remove the given reaction. */ - def -= (r: Reaction) = { - def withoutR(xs: List[Reaction]): List[Reaction] = - if (xs.isEmpty) xs - else if (xs.head == r) xs.tail - else xs.head :: withoutR(xs.tail) - parts = withoutR(parts) - } - - /** - * Pass the given event to registered reactions. - */ - def send(e: Event) = { - def sendTo(ps: List[Reaction]): Unit = ps match { - case Nil => - case p :: ps => - if (p isDefinedAt e) p(e) - /*else*/ sendTo(ps) // no overwrite semantics - } - sendTo(parts) - } + def -= (r: Reactions.Reaction) } diff --git a/src/swing/scala/swing/Reactor.scala b/src/swing/scala/swing/Reactor.scala index 919ae15209..002002d3f9 100644 --- a/src/swing/scala/swing/Reactor.scala +++ b/src/swing/scala/swing/Reactor.scala @@ -1,7 +1,7 @@ package scala.swing trait Reactor { - val reactions = new Reactions + val reactions = new Reactions.Impl /** * Listen to the given publisher as long as deafTo isn't called for * them. diff --git a/src/swing/scala/swing/SimpleGUIApplication.scala b/src/swing/scala/swing/SimpleGUIApplication.scala index b341a16e0e..436ba1e10b 100644 --- a/src/swing/scala/swing/SimpleGUIApplication.scala +++ b/src/swing/scala/swing/SimpleGUIApplication.scala @@ -10,4 +10,10 @@ abstract class SimpleGUIApplication extends GUIApplication { new Runnable { def run() { init(); top.pack(); top.visible = true } } } } + + def resourceFromClassloader(path: String): java.net.URL = + this.getClass.getResource(path) + + def resourceFromUserDirectory(path: String): java.io.File = + new java.io.File(System.getProperty("user.dir"), path) } diff --git a/src/swing/scala/swing/Slider.scala b/src/swing/scala/swing/Slider.scala index fe18965ea3..e33d079c13 100644 --- a/src/swing/scala/swing/Slider.scala +++ b/src/swing/scala/swing/Slider.scala @@ -33,9 +33,9 @@ class Slider extends Component with Orientable with Publisher { def majorTickSpacing: Int = peer.getMajorTickSpacing def majorTickSpacing_=(v: Int) { peer.setMajorTickSpacing(v) } - def labels: collection.Map[Int, Label] = - new collection.jcl.MapWrapper[Int, Label] { def underlying = peer.getLabelTable.asInstanceOf[java.util.Hashtable[Int, Label]] } - def labels_=(l: collection.Map[Int, Label]) { + def labels: scala.collection.Map[Int, Label] = + new scala.collection.jcl.MapWrapper[Int, Label] { def underlying = peer.getLabelTable.asInstanceOf[java.util.Hashtable[Int, Label]] } + def labels_=(l: scala.collection.Map[Int, Label]) { val table = new java.util.Hashtable[Any, Any] for ((k,v) <- l) table.put(k, v) peer.setLabelTable(table) diff --git a/src/swing/scala/swing/Swing.scala b/src/swing/scala/swing/Swing.scala index e34ab1052b..80d22d7a10 100644 --- a/src/swing/scala/swing/Swing.scala +++ b/src/swing/scala/swing/Swing.scala @@ -1,6 +1,7 @@ package scala.swing import java.awt.Dimension +import java.awt.event._ import javax.swing._ import javax.swing.event._ @@ -20,6 +21,9 @@ object Swing { def ChangeListener(f: ChangeEvent => Unit) = new ChangeListener { def stateChanged(e: ChangeEvent) { f(e) } } + def ActionListener(f: ActionEvent => Unit) = new ActionListener { + def actionPerformed(e: ActionEvent) { f(e) } + } def Box(min: Dimension, pref: Dimension, max: Dimension) = new Component { override lazy val peer = new javax.swing.Box.Filler(min, pref, max) diff --git a/src/swing/scala/swing/Table.scala b/src/swing/scala/swing/Table.scala index e4c957e2a5..22012b0b45 100644 --- a/src/swing/scala/swing/Table.scala +++ b/src/swing/scala/swing/Table.scala @@ -25,19 +25,75 @@ object Table { object ElementMode extends Enumeration { val Row, Column, Cell, None = Value } + + abstract class Renderer[-A] { + def peer: TableCellRenderer = new TableCellRenderer { + def getTableCellRendererComponent(table: JTable, value: AnyRef, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int) = { + componentFor(table match { + case t: JTableMixin => t.tableWrapper + case _ => assert(false); null + }, isSelected, hasFocus, value.asInstanceOf[A], row, column).peer + } + } + def componentFor(table: Table, isSelected: Boolean, hasFocus: Boolean, a: A, row: Int, column: Int): Component + } + + abstract class DefaultRenderer[-A, C<:Component](val component: C) extends Renderer[A] { + // The renderer component is responsible for painting selection + // backgrounds. Hence, make sure it is opaque to let it draw + // the background. + component.opaque = true + + /** + * Standard preconfiguration that is commonly done for any component. + */ + def preConfigure(table: Table, isSelected: Boolean, hasFocus: Boolean, a: A, row: Int, column: Int) { + if (isSelected) { + component.background = table.selectionBackground + component.foreground = table.selectionForeground + } else { + component.background = table.background + component.foreground = table.foreground + } + } + /** + * Configuration that is specific to the component and this renderer. + */ + def configure(table: Table, isSelected: Boolean, hasFocus: Boolean, a: A, row: Int, column: Int) + + /** + * Configures the component before returning it. + */ + def componentFor(table: Table, isSelected: Boolean, hasFocus: Boolean, a: A, row: Int, column: Int): Component = { + preConfigure(table, isSelected, hasFocus, a, row, column) + configure(table, isSelected, hasFocus, a, row, column) + component + } + } + + class LabelRenderer[A](convert: A => (Icon, String)) extends DefaultRenderer[A, Label](new Label) { + def configure(table: Table, isSelected: Boolean, hasFocus: Boolean, a: A, row: Int, column: Int) { + val (icon, text) = convert(a) + component.icon = icon + component.text = text + } + } + + private[swing] trait JTableMixin { def tableWrapper: Table } } /** * @see javax.swing.JTable */ class Table extends Component with Scrollable with Publisher { - override lazy val peer: JTable = new JTable { + override lazy val peer: JTable = new JTable with Table.JTableMixin { + def tableWrapper = Table.this override def getCellRenderer(r: Int, c: Int) = new TableCellRenderer { def getTableCellRendererComponent(table: JTable, value: AnyRef, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int) = Table.this.renderer(isSelected, hasFocus, row, column).peer } override def getCellEditor(r: Int, c: Int) = editor(r, c) - override def getValueAt(r: Int, c: Int) = Table.this.apply(r,c) + override def getValueAt(r: Int, c: Int) = Table.this.apply(r,c).asInstanceOf[AnyRef] } import Table._ @@ -188,10 +244,25 @@ class Table extends Component with Scrollable with Publisher { Table.this.peer.getDefaultEditor(classOf[Object]) } - def apply(row: Int, column: Int) = model.getValueAt(row, column) - def update(row: Int, column: Int, value: Any) = model.setValueAt(value, row, column) + /** + * Get the current value of the given cell. + */ + def apply(row: Int, column: Int): Any = model.getValueAt(row, column) + + /** + * Change the value of the given cell. + */ + def update(row: Int, column: Int, value: Any) { model.setValueAt(value, row, column) } + + /** + * Visually update the given cell. + */ + def updateCell(row: Int, column: Int) = update(row, column, apply(row, column)) - def markUpdated(row: Int, column: Int) = update(row, column, apply(row, column)) + def selectionForeground: Color = peer.getSelectionForeground + def selectionForeground_=(c: Color) = peer.setSelectionForeground(c) + def selectionBackground: Color = peer.getSelectionBackground + def selectionBackground_=(c: Color) = peer.setSelectionBackground(c) protected val modelListener = new TableModelListener { def tableChanged(e: TableModelEvent) = publish( diff --git a/src/swing/scala/swing/TextComponent.scala b/src/swing/scala/swing/TextComponent.scala index b1e53b98c3..e24940680e 100644 --- a/src/swing/scala/swing/TextComponent.scala +++ b/src/swing/scala/swing/TextComponent.scala @@ -22,7 +22,7 @@ object TextComponent { * @see javax.swing.JTextComponent */ class TextComponent extends Component with Publisher { - override lazy val peer: JTextComponent = new JTextComponent {} + override lazy val peer: JTextComponent = new JTextComponent with SuperMixin {} def text: String = peer.getText def text_=(t: String) = peer.setText(t) @@ -53,6 +53,8 @@ class TextComponent extends Component with Publisher { def copy() { peer.copy() } def selected: String = peer.getSelectedText + def selectAll() { peer.selectAll() } + peer.getDocument.addDocumentListener(new DocumentListener { def changedUpdate(e:DocumentEvent) { publish(ValueChanged(TextComponent.this, true)) } def insertUpdate(e:DocumentEvent) { publish(ValueChanged(TextComponent.this, true)) } diff --git a/src/swing/scala/swing/TextField.scala b/src/swing/scala/swing/TextField.scala index bd23457e04..3844c8c515 100644 --- a/src/swing/scala/swing/TextField.scala +++ b/src/swing/scala/swing/TextField.scala @@ -10,7 +10,7 @@ import event._ * @see javax.swing.JTextField */ class TextField(text0: String, columns0: Int) extends TextComponent with TextComponent.HasColumns { - override lazy val peer: JTextField = new JTextField(text0, columns0) + override lazy val peer: JTextField = new JTextField(text0, columns0) with SuperMixin def this(text: String) = this(text, 0) def this(columns: Int) = this("", columns) def this() = this("") @@ -18,7 +18,7 @@ class TextField(text0: String, columns0: Int) extends TextComponent with TextCom def columns: Int = peer.getColumns def columns_=(n: Int) = peer.setColumns(n) - peer.addActionListener(new ActionListener { - def actionPerformed(e: ActionEvent) { publish(ValueChanged(TextField.this, false)) } + peer.addActionListener(Swing.ActionListener { e => + publish(ValueChanged(TextField.this, false)) }) } diff --git a/src/swing/scala/swing/event/ActionEvent.scala b/src/swing/scala/swing/event/ActionEvent.scala new file mode 100644 index 0000000000..e1020fe82c --- /dev/null +++ b/src/swing/scala/swing/event/ActionEvent.scala @@ -0,0 +1,7 @@ +package scala.swing.event + +object ActionEvent { + def unapply(e: ActionEvent): Option[Component] = Some(e.source) +} + +trait ActionEvent extends ComponentEvent diff --git a/src/swing/scala/swing/event/ButtonClicked.scala b/src/swing/scala/swing/event/ButtonClicked.scala index c0029f27fe..ce3fcf3c18 100644 --- a/src/swing/scala/swing/event/ButtonClicked.scala +++ b/src/swing/scala/swing/event/ButtonClicked.scala @@ -1,4 +1,4 @@ package scala.swing.event -case class ButtonClicked(override val source: AbstractButton) extends ComponentEvent(source) +case class ButtonClicked(override val source: AbstractButton) extends ComponentEvent(source) with ActionEvent diff --git a/src/swing/scala/swing/event/LiveEvent.scala b/src/swing/scala/swing/event/LiveEvent.scala index a68a616f90..868a2d0164 100644 --- a/src/swing/scala/swing/event/LiveEvent.scala +++ b/src/swing/scala/swing/event/LiveEvent.scala @@ -1,5 +1,14 @@ package scala.swing.event -trait LiveEvent { +object LiveEvent { + def unapply(e: LiveEvent): Option[(Component, Boolean)] = Some(e.source, e.live) +} + +/** + * An event that indicates some editing operation that can be explicitly + * finished by some final action. Example: entering text in a text field + * (not a text area) is finalized not before the user hits enter. + */ +trait LiveEvent extends ComponentEvent { def live: Boolean } diff --git a/src/swing/scala/swing/test/ComboBoxes.scala b/src/swing/scala/swing/test/ComboBoxes.scala new file mode 100644 index 0000000000..46441dbeef --- /dev/null +++ b/src/swing/scala/swing/test/ComboBoxes.scala @@ -0,0 +1,79 @@ +package scala.swing.test + +import swing._ +import event._ +import java.util.Date +import java.awt.Color +import java.text.SimpleDateFormat +import javax.swing.{Icon, ImageIcon} + +/** + * Demonstrates how to use combo boxes and custom item renderers. + * + * TODO: clean up layout + */ +object ComboBoxes extends SimpleGUIApplication { + import ComboBox._ + val ui = new FlowPanel { + contents += new ComboBox(List(1,2,3,4)) + + val patterns = List("dd MMMMM yyyy", + "dd.MM.yy", + "MM/dd/yy", + "yyyy.MM.dd G 'at' hh:mm:ss z", + "EEE, MMM d, ''yy", + "h:mm a", + "H:mm:ss:SSS", + "K:mm a,z", + "yyyy.MMMMM.dd GGG hh:mm aaa") + val dateBox = new ComboBox(patterns) { makeEditable() } + contents += dateBox + val field = new TextField(20) { editable = false } + contents += field + + reactions += { + case SelectionChanged(`dateBox`) => reformat() + } + listenTo(dateBox.selection) + + def reformat() { + try { + val today = new Date + val formatter = new SimpleDateFormat(dateBox.selection.item) + val dateString = formatter.format(today) + field.foreground = Color.black + field.text = dateString + } catch { + case e: IllegalArgumentException => + field.foreground = Color.red + field.text = "Error: " + e.getMessage + } + } + + val icons = List(new ImageIcon(resourceFromUserDirectory("swing/images/margarita1.jpg").toURL), + new ImageIcon(resourceFromUserDirectory("swing/images/margarita2.jpg").toURL), + new ImageIcon(resourceFromUserDirectory("swing/images/rose.jpg").toURL), + new ImageIcon(resourceFromUserDirectory("swing/images/banana.jpg").toURL)) + + val iconBox = new ComboBox(icons) { + renderer = new ListView.DefaultRenderer[Icon, Label](new Label) { + def configure(list: ListView[_<:Icon], isSelected: Boolean, hasFocus: Boolean, icon: Icon, index: Int) { + component.icon = icon + component.xAlignment = Alignment.Center + if(isSelected) { + component.border = Border.Line(list.selectionBackground, 3) + } else { + component.border = Border.Empty(3) + } + } + } + } + contents += iconBox + } + + def top = new MainFrame { + title = "ComboBoxes Demo" + contents = ui + } +} + diff --git a/src/swing/scala/swing/test/TableSelection.scala b/src/swing/scala/swing/test/TableSelection.scala index f9b4a8c20f..7cb7c45755 100644 --- a/src/swing/scala/swing/test/TableSelection.scala +++ b/src/swing/scala/swing/test/TableSelection.scala @@ -12,9 +12,15 @@ object TableSelection extends SimpleGUIApplication { List("Philip", "Milne", "Pool", 5, false).toArray) val ui = new BoxPanel(Orientation.Vertical) { - val table = new Table(model, Array("First Name", "Last Name", "Sport", "# of Years", "Vegetarian")) - listenTo() - table.preferredViewportSize = new Dimension(500, 70) + val table = new Table(model, Array("First Name", "Last Name", "Sport", "# of Years", "Vegetarian")) { + preferredViewportSize = new Dimension(500, 70) + val l = new Table.LabelRenderer[String](a => (null,a)) + override def renderer(isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component = + TableSelection.model(row)(column) match { + case s: String => l.componentFor(this, isSelected, hasFocus, s, row, column) + case _ => super.renderer(isSelected, hasFocus, row, column) + } + } //1.6:table.fillsViewportHeight = true listenTo(table.selection) diff --git a/src/swing/scala/swing/test/UIDemo.scala b/src/swing/scala/swing/test/UIDemo.scala index 830fbff28a..a55913d47d 100644 --- a/src/swing/scala/swing/test/UIDemo.scala +++ b/src/swing/scala/swing/test/UIDemo.scala @@ -70,6 +70,7 @@ object UIDemo extends SimpleGUIApplication { pages += new Page("Converter", CelsiusConverter2.ui) pages += new Page("Tables", TableSelection.ui) pages += new Page("Dialogs", Dialogs.ui) + pages += new Page("Combo Boxes", ComboBoxes.ui) val password = new FlowPanel { contents += new Label("Enter your secret password here ") @@ -105,5 +106,7 @@ object UIDemo extends SimpleGUIApplication { } } } + //val keys = javax.swing.UIManager.getDefaults().keys + //while(keys.hasMoreElements) println(keys.nextElement) } -- cgit v1.2.3