开发丰富的桌面 Java 应用程序历来是非常辛苦的过程;但是现在 NetBeans 平台中有了新的高级 Swing 组件和完整的应用程序框架。了解这个平台如何让开发复杂的桌面应用程序变得轻松,以及在构建这个平台方面的一些教训。
和很早就开始使用 Java 的大多数人一样,我最初接触这项技术是使用(小型)桌面应用程序的时候:它们是我读博士期间的一些研究素材和一个卫生保健呼叫中心的简单控制面板。那时是 AWT 时代,能做的事有限。因此我很快就转到了服务器这边,这边的系统似乎更强健、更有发展。事实确实如此。我在这次领域工作了很长时间,并且成为一名 J2EE 架构师。
几年之后,由于对数字摄影热情的不断高涨,我又重新被吸引到桌面。我仍然遇到了许多问题,但是就在我即将放弃的时候,Sun 和开发人员社区推出的 SwingLabs、java.net 和新版本的 NetBeans 把我从困难中解救出来。现在,我正在热衷于使用一个(可能是)富有前途的开放源码应用程序 blueMarine,这个程序基于 NetBeans 平台。
在本文中,我将告诉您有关 blueMarine 的更多故事,并且回顾一些主要的 NetBeans 扩展 API。我将介绍如何使用和自定义这些 API,同时指出我曾经面临的问题以及解决这些问题的方法。如果您对 NetBeans 知之甚少,而您又需要接触各种各样的客户端应用程序,我认为您应该好好阅读这篇文章。
开始
2001 年前后,由于厌倦了使用 OpenOffice 电子表格,我第一次编写了一些 Java 代码,用于管理我的照片。我将所有内容导出到 XML 并且利用 XSLT 转换,确定了我自己的数据库格式,通过基于 Swing 的一个非常简单的图形用户界面进行管理。
在 2003 年夏天,我在数码相机世界有了较大的飞跃,购买了 Nikon D100(专业的 SLR)。那个夏天是意大利本世纪最炎热的夏天,因此我不得不最大程度地减少拍照旅行的数量:外出散步也是受罪。我不得不待在家里,尽管托斯卡纳乡村的环境能令人放松,因此,大部分假期我都在学习 NEF 格式。
当时,NEF 是一种“原始数据文件格式”,并且几乎没有公开。原始数据文件格式包含的数据直接来自于相机 CCD 传感器,未经过处理。如果要将它转换为质量比较好的图片,还需要对数据进行处理。这通常认为这一过程是旧式潮湿暗室相片成像的数字过程。我从来没有拥有过自己的潮湿暗室,我为“数字冲洗”照片而入迷,开始为此编写了一些 Java 代码。
夏天快结束的时候,我创建了一个简单的能够显示照片的微型导航程序—— blueMarine 就此诞生了。一年之后,这个项目便能够用编录设备标记照片,并且能够在网上发布图片库。
但是,让我非常苦恼的是,我需要多个软件来执行编辑、打印、编录、归档以及 Web 发布任务。因此我着手开始研究在一个应用程序中实现所有这些工作流程。同时,我认为是时候公开发布 blueMarine 了,因此第一个 alpha 版本发布到了 GPL License(后来更改为 Apache 2.0)支持下的 SourceForge 。
另一个推动力是在台式计算机上挑战使用 Java 对数字图像进行处理。对于我来说,Java 在科学图像处理方面的优势已经非常明显;例如, NASA 的工程师们已经成功使用了 JAI,它是一种高级成像的 API。但是,对于普通摄影师来说,桌面处理包含哪些内容呢?自从成为 Java 顾问 10 多年来,证明 Java 在各种各样的应用程序方面的优势一直是我追求的目标。
挫折
虽然开始时热情高涨,但是在 2005 年底,我在这个项目上受到了很大的打击。性能不是大问题,但是,在使用 plain Swing 开发丰富的 GUI 应用程序时,我遇到了困难。Swing 是一个非常出色的 API,但是当您使用它构建复杂的应用程序时,您会发现仍然需要增加很多功能。
实现这些缺少的部分并不是前言科学,但是这项工作浪费了很多宝贵的时间。再次举例来说明这个问题,例如构建菜单、以上下文相关的方式启用操作、为内部窗口定义灵活的、可在工作时停靠的机制等等,并且您将发现自己的大部分时间都花费在编写通用的 GUI 组件上,而没有花费在应用程序的核心内容上。
直到最近,才有了几个处理此类问题的开放源码库,但是大部分库都不太令人满意,而且也很难集成。还有几个较早版本的 NetBeans,但是我不满意它们的性能。也可以选择 Eclipse 和 SWT,但是我觉得我并没有真正地仔细研究完全的替代方法和非标准的 API,我采用的学习投资回报低,继承 Swing的方法比较麻烦。
总的来讲,我认真地考虑过放弃 blueMarine – 或许 Java 尚未准备用于桌面开发。
新生
但是,同时出现了两个事件使这个项目得以保留下来,这两个事件是,我在 2005 年底参与了 JavaPolis 以及 2006 年初发布了 NetBeans 5.0。
在 JavaPolis,我呼吸到了我几乎都快忘记的社区空气(自从我上次参加 JavaOne 已经过去了三年)。这重新点燃了我的热情,Romain Guy 的演示说明了使用 Swing 构建 GUI 的效果如何,,这使我兴趣倍增。我开始查看 Romain 的博客,并按照下面的链接访问了其他人的博客,如 Joshua Marinacci 的博客,然后再从那里访问所有 java.net 和 JavaDesktop 的站点。我发现 Swing 里面有很多新鲜有趣的事;像 SwingLabs 里质量优良的 Swing组件和出众的演示程序 – 很多材料我都能够使用。但是,我仍然需要一个平台。
几周之后,推出了NetBeans 5.0 。这个新的版本看起来似乎最终解决了传统的平台问题,因此我决定试一试。我开始分解 blueMarine,只提取成像代码并对该代码重新设计以便使用 NetBeans 平台。几个月之后,便可以发布了第一个 Early Access 版本,我开始使用这个工具管理我自己的照片。同时,从前一个 PPC Apple iBook 切换到新的 Intel MacBook Pro 没有出现任何问题则强有力地表明了我的选择是正确的。
目前,我正致力于使新的 blueMarine 更稳定、更便于使用。获得了新的 early access 版本,并且我正在进行所需的质量测试(整个新设计明显破坏了以前版本的一些稳定性;这就是付出的代价)。
NetBeans 平台的功能
由于您已经了解了 blueMarine 的由来,因此我将概述一些 NetBeans 和 Swing 带来的开发优势、我曾经面临的问题以及解决这些问题的方法。
第一点:是 Swing!
对于我来说,与很多竞争对手(如 Eclipse RCP)相比,NetBeans 平台基于常规的Swing 是一个巨大的优势。搜索一下,您会发现 Swing 组件(包括实现动画和效果比较酷的组件)有更广阔的选择余地。
去年 6 月份我非常具体地认识到了这个优势,当时 Joshua Marinacci 发布了能够显示地图的 Aerith Swing 组件的源代码,命名为 JXMapViewer(Aerith 是 2006 年 JavaOne 上最热门的演示程序)。几周以来,我一直都在等待这个时刻,blueMarine 的其中一个功能是地理标记(将地理位置与每个照片相关联,以便这些照片可以显示在地图上)。将 JXMapViewer 集成到 blueMarine 中只需要几个小时;
模块系统
当然,NetBeans 平台应用程序自然分为几个模块,实际上,它是一组绑定在一起的模块。每个模块都有一个名称、一组版本标记及其自己的类路径以及一个声明的依赖关系的列表。开发人员可以控制哪些公共类的子集向其他模块公开,其他模块可以看到哪些公共类的子集,平台可以在模块之间施加依赖关系(例如,如果所需的模块都不存在或者太旧,则阻止模块安装)。
此外,通过发布新的模块(放在 nbm 文件中)可以在以后扩展应用程序,用户可以建立他们自己的“更新中心”,以便从 Internet 下载更新。可以对各个模块进行数字签名,系统会自动弹出他们的许可证以便进行批准(如果需要的话)。
BueMarine 项目充分利用这个组织。该应用程序的核心 API 由实现工作空间管理器、照片、缩略图管理、简单缩略图和照片查看器的相对比较小的模块集进行定义。更多的高级功能(如编录、图库管理器和地理标记功能,包括地图查看器)可以在单独并且几乎不相关的模块中实现,这些模块就作为核心 API 的“客户端”。
DataObject、Node 和 ExplorerManager
ExplorerManager、Node 和 DataObject 可能是 NetBeans 中最有用的 API。使用 DataObject,您可以实现特定于应用程序的实体,这些实体是映射到磁盘上的文件中。例如,blueMarine 的基本实体为 PhotoDataObject,它代表数据库中的照片。
而 DataObject 包含实体的所有状态和行为,为了进行可视化可以将 Node 与DataObject绑定。还可以采用很多种不同的方式(如集合或图形)将实体聚合在一起。NetBeans 平台提供 GUI 组件,如表和列表,这些组件可以将一组 Node 对象用作其模型;最常见的组件是 BeanTreeView、ContextTreeView 和 ListView。最后,ExplorerManager 控制选择和树导航。
是的,这只不过是一个复杂的 MVC 实现,而且这个实现中已经为您编写了很多样板文件代码。例如,平台 API 考虑类似于拖放支持(以及拖放操作过程中类似于可视提示等详细的细节信息)、剪切粘贴操作和上下文菜单的事情。
Lookup API
NetBeans 平台组件的生命周期(更像是容器中的 EJB)由平台控制,因此未直接实例化这些组件。若要检索现有模块的引用,可以使用 Lookup API。这个 API 与其他查询机制非常类似。获得对象的引用要以对象“名称”开头,这并不是一个字符串,而是相应的 Class 对象。
例如,假设我们有一个名为 it.tidalwave.catalog.CatalogImpl(实现一个接口 it.tidalwave.catalog.Catalog)的模块。首先,通过将特殊文件放置在 META-INF/services 目录下的类路径中“注册”该模块。该文件必须按实现的接口命名,并且包含该实现类的名称。只要加载模块,NetBeans 都会扫描这些特殊文件、实例化对象并将它们放置到“默认的”Lookup 对象中,以后其他任何代码片段都可以从这里检索到该对象。
我通常使用定位器模式将查询代码包起来,如清单 1 中所示,然后执行类似于下面内容的查询:
Catalog catalog = CatalogLocator.findCatalog();
Listing 1. A Locator that uses the Lookup class. |
|
该机制不仅仅支持解耦,而且还创建可插拔的行为。例如,让我们看一看 blueMarine 的地图显示功能。正如您所知,有很多地图提供程序,如 Google Maps、Microsoft Visual Earth、NASA 以及其他提供程序。我想让人们能够通过向其中插入用于处理其他地图提供程序的新代码来扩展 blueMarine。这个解决方案非常简单:首先定义一个接口 MapProvider,该接口声明所有所需的功能,然后编写备用实现,每个实现都位于其自己的模块中,例如 GoogleMapProvider、MicrosoftVisualEarthMapProvider 等。
使用相同的“名称”MapProvider(允许注册多个具有此相同名称的对象)在默认的 Lookup 实例中注册每个实现。现在,检索对象变成一个非常简单的任务。如清单 2 中的示例所示。您可以添加具有新地图提供程序的模块,检索代码会在运行时查找这些模块。
Listing 2. Retrieving registered objects. |
private void searchMapProviders() { for (Object provider : result.allInstances()) { |
Lookup API 还促进解耦
默认的 Lookup 实例还包含当前选择的 Node 对象组。这样便有可能设计有效且耦合松散的机制用于内部模块通信。它基于“观察者”模式:某些模块将它们的节点选择发布到默认的 Lookup,而其他模块侦听更改。通过对与更改节点关联的信息种类进行某些相关的筛选,我们便可以达到“发布/订阅”设计模式。
在 blueMarine 中,有很多方法用于导航照片数据库和显示缩略图组,例如,浏览文件夹、日历、共享标记的照片、相同图片库中的照片等等。“浏览器”模块只是将绑定到 PhotoDataObject 的 Node 选择发布到默认的 Lookup 中;缩略图查看器接收通知并相应地更新自身
浏览器组件并不依赖于缩略图查看器。实际上,浏览器组件与缩略图查看器已完全解耦(此处我们正在应用控制反转)。通过这个设计,我可以添加任意多个浏览器,甚至在可以作为附加项安装的独立模块中也可以添加。我还可以非常容易地添加新的查看器。例如,我能够将 Filmstrip Viewer 作为完全解耦的组件,可以单独使用,也可以与原始的缩略图查看器一起使用
由于很多对象类型(包括 Node 自身)都有自己的本地 Lookup 实例,Lookup API 也有很多其他用处。这里我所说的还只是冰山的一角。
文件系统 API
Java 只是通过 java.io.File 类提供了一种文件管理的基本方法,该类包含文件名并提供属性访问、基本操作和目录清单。由于没有文件系统的概念,这种方法还不甚成熟,此外,File 对象还绑定到本地的物理文件/目录。如果您需要表示一个虚拟或远程的目录树该怎么办呢?几年前我就曾经面临这个问题,最后我通过大量继承 File 解决了这个问题,尽管可行但不简洁。
NetBeans 的 FileSystem API 填补了这个空隙。有一整套的 FileSystem 类可以用于表示不同类型的文件系统:本地的、远程的、甚至虚拟的。(NetBeans 的内部配置设置,包括您自定义代码的配置设置,都存储在这样一个虚拟的文件系统中。)唯一的告诫是您必须使用 FileObject 而不是 File 的特定于 NetBeans 的实例,但是将其中一个转换为另外一个非常容易:
FileObject fileObject = ...;
File file = ...;
file = FileUtil.toFile(fileObject);
fileObject = FileUtil.toFileObject(file);
BlueMarine 对管理文件有非常严格的要求。每个文件都必须与本地数据库中存储的唯一 ID 相关联。之后可以使用该 ID 建立每个照片和其他实体(如缩路图、元数据、图片库、编辑设置等等)之间的关系。对于这种应用程序来说,这是相当普通的设计,该设计允许您之后无需在数据库中进行太多更改即可移动或重命名照片。还允许您使用远程卷,如外部磁盘和 DVD。换句话说,即使系统中该文件不可用时,您也可以查看该文件的缩略图和元数据。
调节文件系统管理的良好开端就是 LocalFileSystem 类,该类表示具有一个根的文件树(对于具有多个根层次的系统,如 Windows,您只需要将几个 LocalFileSystem 对象放到一起)。
LocalFileSystem 类包括 AbstractFileSystem.Attr 和 AbstractFileSystem.List。Attr 允许操纵每个 FileObject 的一组属性,并且 List 允许自定义目录清单。(属性只是绑定到 FileObject 的简单属性,并且可以通过 getter 和 setter 进行操纵。)
开始时,我编写了 LocalFileSystem 的一个简单子类,用于安装 Attr 和 List 的装饰器,如清单 3 中所示。AttrDecorator 类检索每个文件路径的惟一 ID(FileIndexer 只是该数据的一种 DAO 排序),并使其可以作为 FileObject (ID_ATTRIBUTE)的一种特殊属性。代码如清单 4 中所示。
Listing 3. Plugging decorators into the LocalFileSystem class. |
class LocalIndexedFileSystem extends LocalFileSystem { public LocalIndexedFileSystem() { attr = new AttrDecorator(attr, this); list = new ListDecorator(list, this); } } |
Listing 4. Retrieving registered objects. |
class AttrDecorator implements AbstractFileSystem.Attr { private static final FileIndexer fileIndexer = FileIndexerLocator.findFileIndexer(); private AbstractFileSystem.Attr peer; private LocalIndexedFileSystem fileSystem;
public AttrDecorator(AbstractFileSystem.Attr peer, LocalIndexedFileSystem fileSystem) { this.peer = peer; this.fileSystem = fileSystem; } public Object readAttribute (String path, String name) { if (IndexedFileSystem.ID_ATTRIBUTE.equals(name)) { String path2 = fileSystem.findCompletePath(path); Serializable id = fileIndexer.findIdByPath(path2); if (id == null) { fileIndexer.createIndexing(path2, false); id = fileIndexer.findIdByPath(path2); } return id; } else { return peer.readAttribute(path, name); } }
... } |
尽管 AttrDecorator 足够满足功能规范,但还是存在批量加载的问题。将会非常随机地调用 readAttribute() 方法,因此防碍了所有有效的批处理策略(FileIndexer 能够通过延迟加载进行批处理,但是若要使其有效,必须具有大量要进行批处理的条目!)。
这里 ListDecorator 帮助了我们,它在子文件从父目录中列出之后拦截了子文件(请参见清单 5)。在列出的一组文件上立即调用 createIndexing() 允许 FileIndexer 对其 ID 的检索进行批处理。
Listing 5. Decorating directory scanning. |
public String[] children (String path) { |
操作和菜单
操作和菜单(以及辅助组件,如工具栏)是与用户交互的主要工具。Swing 基本支持这些工具,但是您很快就会发现这远远不够用,尤其当您设计一个模块化的应用程序时更是如此。
菜单是按照层次进行组织,并且根据直观标准进行分组的。因此可插拔模块需要在适当的位置(例如,在菜单栏中的全局“编辑”或“视图”项下)放置自己的菜单项,可能遵照一些有意义的顺序(例如,“模块 C 的菜单项应该出现在模块 A 和模块 B 的菜单项之间”)。您还可能希望引入菜单分隔器以便将某些菜单项组合在一起。
通过简单的属性更改可以启用或禁用 Swing 操作;但到底是启用还是禁用操作由您决定。通常,通过一种特殊的方法来执行该操作,这个方法就是计算一组动作的状态并在用户进行更改之后进行调用,如下所示:
private void setEnablementStatus() {
myAction1.setEnabled(/* condition 1 */);
myAction2.setEnabled(/* condition 2 */);
...
}
可以使用该方法,但是它既不是模块也不容易维护。大多数情况必须考虑布尔条件(以前的代码中的条件 1、2 等等)只是用户当前选择的一组对象的一个功能,例如,只在选中照片的情况下才可以运行“编辑”或“打印”。
采用比较明智的方法使用 plain Swing 管理菜单和操作并不是需要尖端科学,但是对于一个复杂的、模块化的应用程序来说,需要从零开始做,这让人很头疼。幸运的是,NetBeans 平台也可以在这个方面帮助您。
首先,该平台提供了比 Swing 的 Action 更丰富的类。一些最常用的类为:
· NodeAction – 选中一组新的 Node 时更改状态的常规操作。程序员必须继承它并覆盖 enable(Node[]) 方法,该方法评估正确的布尔表达式以便激活操作。
· CookieAction – 这是一个操作,其状态取决于当前选中的 Node,以及它是否绑定到给定的对象(通常是特定的 DataObject)。它还涉及不同的选择模式,如“只一个”、“至少一个”等等。
使用正确的类实现您的操作之后,您在模块的 layer.xml 中声明该类,layer.xml 充当常规配置文件(它将“虚拟文件系统”结构模型化为 包含 XML DOM )。
注意:通常,您不需要手动执行该操作:NetBeans IDE 提供了一个“新建操作”向导,该向导要求输入所需的信息,生成 Java 代码框架并更新 layer.xml 的相关部分。实际上,以下示例中的大部分 XML 都可以通过 IDE 生成或操纵。
该方法适用于上下文菜单上出现的操作以及需要附加到“常规”菜单的操作。在 layer.xml 中,您还可以声明工具栏(其中很多按钮已经按逻辑顺序绑定到操作),定义键盘快捷方式。
窗口 API
NetBeans 平台提供了特定的窗口组件,名为 TopComponent。该组件模拟矩形形状的主窗口,可以调整该部分的大小以及将其停靠在屏幕的不同区域(这些区域称为“模式”)。例如,“浏览器”模式是左侧的一个垂直列;“属性”模式是右侧的一个垂直列;“编辑器”模式是中央的剩余空间
此外,还可以激活或取消激活 TopComponent,它具有自己的机制来保持永久状态(重新启动时自动恢复该状态),并且可以通过使其选项卡闪烁引起注意。
可以使用鼠标或通过编程的方式调整停靠区域的大小。通过用鼠标拖动或通过编程,可以将 TopComponent 指定给不同的区域。也可以定义您自己的停靠区域。例如,我需要一个应该放置在窗口底部的名为“Film Strip”的组件。因此我定义了一个名为“strip”的停靠区域,并且将 Film Strip 与之绑定在一起。
对于某些类型的应用程序(例如 IDE 或 CAD 系统)来说,这种灵活性非常好,因此大量控制可以转移到某些级别的用户。对于 blueMarine,我不喜欢有这样灵活的停靠:使用固定模式,只是通过菜单命令(允许您在“浏览器”模式和“属性”模式中交换组件)提供某些控制。
在 blueMarine 中,通过指定特殊的 TabDisplayerUI(用于每个模式的可视组件)已经删除了允许用鼠标停靠的选项卡和控制代码。我通过清单 6 中的代码用程序进行控制。
Listing 6. Programmatic component docking. |
TopComponent topComponent = ...; String newMode = “explorer”; Mode targetMode = WindowManager.getDefault().findMode(newMode); if (targetMode != null) { component.close(); targetMode.dockInto(component); component.open(); component.requestVisible(); } |
注意:实际上,都是由于我在 Geertjan Wielenga 的博客上发现了这个解决方案,因此实现 TabDisplayerUI 技术并不太难;如果没有它的帮助,我将需要更长的时间。我发现程序员可以获得 NetBeans 社区中邮件列表和传播者博客的大力支持,我建议您对这些内容进行标记!
外观
随着 JDK 版本的不断发布,预定义的 Java 外观变得越来越出众,但是有时您需要特殊的 LAF。例如,要处理照片,您需要一个外观朴素、主题灰暗的整洁GUI (所有使用的颜色都有严格的灰度梯度),以便不会干扰颜色的正确分辨。
自从 JDK 1.4 开始,UIManager 类就允许在尽量少影响或不影响现有代码的情况下插入不同的外观。由于该类是标准 Swing API 的一部分,因此可以将很多兼容的 LAF 轻松插入到应用程序中。
如果您发现了喜欢的外观,您可以通过简单的命令行切换将它安装到 NetBeans 中(当然是安装到您的 NetBeans 平台应用程序中):
--look-and-feel <name of the L&F class>
经过一些测试之后,我决定保留除了主窗口之外每个 GUI 部分的原始外观,在主窗口中我只更改了组件颜色(Mac OS X 就是一个特例;请参见下面内容)。
正如您所知,更改 Swing 组件的颜色通常是 c.setForeground() 和 c.setBackground() 的事。由于 NetBeans 平台是基于 Swing 的,因此并没有很多不同。但是也有几个例外。例如,这些标准方法不适用 ListView(Node 对象最常用的视图组件之一)。在 blueMarine 中,通过 清单 7 中的代码已经解决了这个问题,该代码首先检索内部的 Jlist,然后根据需要更改其属性。类似的代码也适用于基于树的组件(这些组件也有相同的问题)。
Listing 7. An enhanced ListView. |
public class EnhancedListView extends ListView { protected JList jList;
@Override protected JList createList() { jList = super.createList(); jList.setOpaque(isOpaque()); jList.setBackground(getBackground()); jList.setForeground(getForeground()); return jList; } @Override public void setBackground (Color color) { super.setBackground(color); if (jList != null) { jList.setBackground(color); } } @Override public void setForeground (Color color){ super.setForeground(color); if (jList != null) { jList.setForeground(color); } } @Override public void setOpaque (boolean opaque){ super.setOpaque(opaque); if (jList != null) { jList.setOpaque(opaque); } } }
|
我发现了基于树的组件存在的另一个问题:即使使用前面列出的代码,树单元仍以黑白颜色显示。而且,通过检查源代码很容易找到原因:通常,NetBeans 的树具有一个特殊的单元格渲染器,它执行很多操作,如支持 HTML 显示(以便您可以使用多个文本样式);该单元格渲染器还选择在前景和背景之间对比明确的颜色主题。在大多数情况下,这是一个比较明智的方法,但是当您想要微调颜色时,这个方法就不太合适。通过清单 8 中所示的几行代码解决了这个问题。下面是安装修补后的渲染器的方法:
PatchedNodeRenderer nodeRenderer = new PatchedNodeRenderer(tree.getCellRenderer());
tree.setCellRenderer(nodeRenderer);
Listing 8. A patched cell renderer for controlling colors in JTree’s. |
class PatchedNodeRenderer extends DefaultTreeCellRenderer { private TreeCellRenderer peer; public PatchedNodeRenderer (final TreeCellRenderer peer) { this.peer = peer; } @Override public Component getTreeCellRendererComponent (final JTree jTree, final Object object, final boolean selected, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { final Component component = peer.getTreeCellRendererComponent( jTree, object, selected, expanded, leaf, row, hasFocus); component.setBackground( selected ? getBackgroundSelectionColor() : getBackgroundNonSelectionColor()); component.setForeground( selected ? getTextSelectionColor() : getTextNonSelectionColor()); return component; } } |