www.it-ebooks.info Eclipse 4 Plug-in Development by Example Beginner's Guide How to develop, build, test, package, and release Eclipse plug-ins with features for Eclipse 3.x and Eclipse 4.x Dr Alex Blewitt BIRMINGHAM - MUMBAI www.it-ebooks.info Eclipse 4 Plug-in Development by Example Beginner's Guide Copyright © 2013 Packt Publishing All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information. First published: June 2013 Production Reference: 1140613 Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK. ISBN 978-1-78216-032-8 www.packtpub.com Cover Image by Asher Wishkerman ([email protected]) www.it-ebooks.info Credits Author Dr Alex Blewitt Reviewers Project Coordinator Arshad Sopariwala Proofreaders Ann Ford Linda Morris Thomas Fletcher Lindsey Thomas Jeff MAURY Indexers Acquisition Editor Kartikey Pandey Lead Technical Editor Dayan Hyames Technical Editors Prasad Dalvi Hemangini Bari Tejal R. Soni Production Coordinator Arvindkumar Gupta Cover Work Arvindkumar Gupta Mausam Kothari Worrell Lewis Pushpak Poddar Amit Ramadas www.it-ebooks.info About the Author Dr Alex Blewitt has been developing Java applications since Version 1.0 was released in 1996, and has been using the Eclipse platform since its first release as part of the IBM WebSphere Studio product suite. He even migrated some plugins from Visual Age for Java to WebSphere Studio/Eclipse as part of his PhD on Automated Verification of Design Patterns. He got involved in the open source community as a tester when Eclipse 2.1 was being released for Mac OS X, and then subsequently as an editor for EclipseZone, including being a finalist for Eclipse Ambassador in 2007. More recently, Alex has been writing for InfoQ, covering generic Java and specifically, Eclipse and OSGi subjects. He keynoted the 2011 OSGi Community Event on the past, present, and future of OSGi. The coverage of both new releases of the Eclipse platform and its projects, as well as video interviews with some of the Eclipse project leads can be found via the InfoQ home page, for which he was nominated and won the Eclipse Top Contributor 2012 award. Alex currently works for an investment bank in London. He also has a number of apps on the Apple AppStore through Bandlem Limited. When he's not working on technology, and if the weather is nice, he likes to go flying from the nearby Cranfield airport. Alex writes regularly at his blog, http://alblue.bandlem.com, as well as tweets regularly from Twitter and App.Net as @alblue. www.it-ebooks.info Acknowledgement I'd like to thank my wife Amy for supporting me during the development of this book, (particularly the late nights and weekends that were spent completing it), and indeed throughout our decade plus marriage. I'd also like to thank my parents, Derek and Ann, for installing a sense of independence and self-belief which has taken me many places in my lifetime. I hope that I can encourage a similar level of confidence and self-belief in my children, Sam and Holly. Special thanks are due to Ann Ford, who provided detailed feedback about every chapter and the exercises therein. Without her diligence and attention, this book would contain many more errors than I would like. Any remaining errors are my own. My thanks also go to the other reviewers of earlier draft chapters: Thomas Fletcher and Jeff Maury, for their comments and suggestions. During the later stages of the book, I was also fortunate enough to receive some good feedback and advice from Paul Webster and Lars Vogel, both of whom are heavily involved in the Eclipse 4 platform. Their comments on the chapter on Eclipse 4 have measurably improved the content. Finally, I'd like to thank OD, DJ, and JC for their support in making this book possible. www.it-ebooks.info About the Reviewers Ann Ford is an experienced Eclipse plugin developer who has contributed significant portions of the Eclipse Technology Accessibility Tools Framework incubator project as a former committer. Having over 30 years of programming experience with IBM, she has worked on tools and components of OS/2, DB2, and the IBM JDK, with extensive experience in issues of usability, accessibility, and translation. Currently, she specializes in the design and development of GUIs for desktop applications and tools using Java Swing, Eclipse SWT, and JFace, with an eye towards mobile applications in the future. Thomas Fletcher has worked in the field of real-time and embedded software development for more than 10 years and is a frequent presenter at industry conferences. He is a Technical Subject Matter Expert and Thought Leader on Embedded System Architecture and Design, Real-time Performance Analysis, Power Management, and High Availability. Prior to Crank Software, Thomas directed QNX Software Systems' Tools Development Team. He was the Lead Architect for Multimedia, Team Leader of Core OS, and regularly engaged with sales and marketing as a result of his ability to bridge technology and customer needs. Thomas is an active participant within the Eclipse Community. He was a committer with the C/C++ Development Tools (CDT) project and represented QNX on the Eclipse Architecture and the Multicore Association review boards. Thomas holds a degree in Master of Computer Engineering from Carleton University, focusing on instrumentation and performance analysis of embedded systems, and a degree in Bachelor of Electrical Engineering from the University of Victoria. www.it-ebooks.info Jeff MAURY is currently working as the technical lead for the Java team at SYSPERTEC, a French ISV offering mainframe integration tools. Prior to SYSPERTEC, he co-founded in 1996 a French ISV called SCORT, precursor of the application server concept and offering J2EE-based integration tools. He started his career in 1988 at MARBEN, a French integration company specialized in telecommunication protocols. At MARBEN, he started as a software developer and finished as X.400 team technical lead and Internet division strategist. I would like to dedicate my work to Jean-Pierre ANSART, my mentor, and thank my wife Julia for her patience and my three sons Robinson, Paul, and Ugo. www.it-ebooks.info www.PacktPub.com Support files, eBooks, discount offers and more You might want to visit www.PacktPub.com for support files and downloads related to your book. Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.PacktPub.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at [email protected] for more details. At www.PacktPub.com, you can also read a collection of free technical articles, sign up for a range of free newsletters and receive exclusive discounts and offers on Packt books and eBooks. http://PacktLib.PacktPub.com Do you need instant solutions to your IT questions? PacktLib is Packt's online digital book library. Here, you can access, read and search across Packt's entire library of books. Why Subscribe? Fully searchable across every book published by Packt Copy and paste, print and bookmark content On demand and accessible via web browser Free Access for Packt account holders If you have an account with Packt at www.PacktPub.com, you can use this to access PacktLib today and view nine entirely free books. Simply use your login credentials for immediate access. www.it-ebooks.info Table of Contents Preface1 Chapter 1: Creating Your First Plug-in 7 Getting started Time for action – setting up the Eclipse SDK environment Creating your first plug-in Time for action – creating a plug-in Running plug-ins Time for action – launching Eclipse from within Eclipse Debugging a plug-in Time for action – debugging a plug-in Time for action – updating code in debugger Debugging with step filters Time for action – setting up step filtering Using different breakpoint types Time for action – breaking at method entry and exit Using conditional breakpoints Time for action – setting a conditional breakpoint Using exceptional breakpoints Time for action – catching exceptions Time for action – using watch variables and expressions Summary Chapter 2: Creating Views with SWT Creating views and widgets Time for action – creating a view Time for action – drawing a custom view Time for action – drawing a second hand www.it-ebooks.info 7 8 11 11 15 15 18 18 22 23 23 25 25 26 26 28 28 31 34 35 35 36 38 41 Table of Contents Time for action – animating the second hand Time for action – running on the UI thread Time for action – creating a reusable widget Time for action – using layouts Managing resources Time for action – getting colorful Time for action – finding the leak Time for action – plugging the leak Interacting with the user Time for action – getting in focus Time for action – responding to input Using other SWT widgets Time for action – adding items to the tray Time for action – responding to the user Time for action – modal and other effects Time for action – groups and tab folders Summary Chapter 3: Creating JFace Viewers Why JFace? Creating TreeViewers Time for action – creating a TreeViewer Time for action – using Images in JFace Time for action – styling label providers Sorting and filtering Time for action – sorting items in a viewer Time for action – filtering items in a viewer Interaction and properties Time for action – adding a double-click listener Time for action – showing properties Tabular data Time for action – viewing time zones in tables Time for action – syncing selection Summary Chapter 4: Interacting with the User Creating actions, commands, and handlers Time for action – adding context menus Time for action – creating commands and handlers Time for action – binding commands to keys [ ii ] www.it-ebooks.info 42 43 45 47 50 51 52 54 56 57 58 60 60 62 64 66 73 75 75 76 76 81 84 86 87 89 91 92 95 100 100 104 107 109 109 110 111 114 Table of Contents Time for action – changing contexts Time for action – enabling and disabling the menu's items Time for action – reusing expressions Time for action – contributing commands to pop-up menus Jobs and progress Time for action – running operations in the background Time for action – reporting progress Time for action – dealing with cancellation Time for action – using subtasks and subprogress monitors Time for action – using null progress monitors and submonitors Time for action – setting job properties Reporting errors Time for action – showing errors Summary 115 118 120 121 124 125 127 128 129 131 133 137 137 141 Chapter 5: Storing Preferences and Settings 143 Chapter 6: Working with Resources 163 Storing preferences Time for action – persisting a value Time for action – creating a preference page Time for action – creating warning and error messages Time for action – choosing from a list Time for action – using a grid Time for action – placing the preferences page Time for action – using other field editors Time for action – adding keywords Time for action: using IEclipsePreferences Using IMemento and DialogSettings Time for action – adding a memento for the Time Zone View Time for action – using DialogSettings Summary Using the workspace and resources Time for action – creating an editor Time for action – writing the markup parser Time for action – building the builder Time for action – iterating through resources Time for action – creating resources Time for action – implementing incremental builds [ iii ] www.it-ebooks.info 143 144 145 147 148 150 151 153 154 156 157 158 159 161 163 164 166 168 170 173 174 Table of Contents Time for action – handling deletion Using natures Time for action – creating a nature Using markers Time for action – error markers if the file is empty Time for action – registering a marker type Summary 175 178 178 182 182 184 186 Chapter 7: Understanding the Eclipse 4 Model 187 Chapter 8: Creating Features, Update Sites, Applications, and Products 237 Working with the Eclipse 4 model Time for action – installing E4 tooling Time for action – creating an E4 application Time for action – creating a part Time for action – styling the UI with CSS Using services and contexts Time for action – adding logging Time for action – getting the window Time for action – obtaining the selection Time for action – dealing with events Time for action – calculating values on demand Time for action – using preferences Time for action – interacting with the UI Using Commands, Handlers, and MenuItems Time for action – wiring a menu to a command with a handler Time for action – passing command parameters Time for action – creating a direct menu and keybindings Time for action – creating a pop-up menu and a view menu Creating custom injectable classes Time for action – creating a simple service Time for action – injecting subtypes Summary Grouping plug-ins with features Time for action – creating a feature Time for action – exporting a feature Time for action – installing a feature Time for action – categorizing the update site Time for action – depending on other features Time for action – branding features [ iv ] www.it-ebooks.info 188 188 190 195 200 206 206 208 209 212 215 217 219 221 221 224 226 229 232 232 233 235 237 238 240 242 244 249 250 Table of Contents Building applications and products Time for action – creating a headless application Time for action – creating a product Summary 254 254 259 263 Chapter 9: Automated Testing of Plug-ins 265 Chapter 10: Automated Builds with Tycho 283 Using JUnit for automated testing Time for action – writing a simple JUnit test case Time for action – writing a plug-in test Using SWTBot for user interface testing Time for action – writing an SWTBot test Time for action – working with menus Working with SWTBot Time for action – hiding the welcome screen Time for action – avoiding SWTBot runtime errors Working with views Time for action – showing views Time for action – interrogating views Interacting with the UI Time for action – getting values from the UI Time for action – waiting for a condition Summary Using Maven to build Eclipse plug-ins with Tycho Time for action – installing Maven Time for action – building with Tycho Building features and update sites with Tycho Time for action – creating a parent project Time for action – building a feature Time for action – building an update site Time for action – building a product Testing and releasing Time for action – running automated tests Time for action – changing the version numbers Signing update sites Time for action – creating a self-signed certificate Time for action – signing the plug-ins Time for action – serving an update site Summary [v] www.it-ebooks.info 265 266 267 268 268 271 273 273 274 274 275 276 277 277 278 281 283 284 286 289 289 292 293 295 300 300 303 305 305 307 309 310 Table of Contents Appendix: Pop Quiz Answers 311 Index 323 Chapter 1, Creating Your First Plug-in Chapter 2, Creating Views with SWT Chapter 3, Creating JFace Viewers Chapter 4, Interacting with the User Chapter 5, Storing Preferences and Settings Chapter 6, Working with Resources Chapter 7, Understanding the Eclipse 4 Model Chapter 8, Creating Features, Update Sites, Applications, and Products Chapter 9, Automated Testing of Plug-ins Chapter 10, Automated Builds with Tycho [ vi ] www.it-ebooks.info 311 312 314 315 317 317 318 320 320 321 Preface This book provides a general introduction to developing plug-ins for the Eclipse platform. No prior experience, other than Java, is necessary to be able to follow the examples presented in this book. By the end of the book, you should be able to create an Eclipse plug-in from scratch, as well as be able to create an automated build of those plug-ins. What this book covers Chapter 1, Creating Your First Plug-in, provides an overview of how to download Eclipse, set it up for plug-in development, create a sample plug-in, launch, and debug it. Chapter 2, Creating Views with SWT, provides an overview of how to build views with SWT, along with other custom SWT components such as system trays and resource management. Chapter 3, Creating JFace Viewers, discusses creating views with JFace using TableViewers and TreeViewers, along with integration with the properties view and user interaction. Chapter 4, Interacting with the User, discusses using commands, handlers, and menus to interact with the user, as well as the Jobs and Progress APIs. Chapter 5, Storing Preferences and Settings, tells how to store preference information persistently, as well as displaying it via the preferences pages. Chapter 6, Working with Resources, teaches how to load and create Resources in the workbench, as well as how to create a builder and nature for automated processing. Chapter 7, Understanding the Eclipse 4 Model, discusses the key differences between the Eclipse 3.x and Eclipse 4.x models, as well as how to migrate existing content to the new model. www.it-ebooks.info Preface Chapter 8, Creating Features, Update Sites, Applications, and Products, tells how to take the plug-ins created so far in this book, aggregate them into features, publish to update sites, and how applications and products are used to create standalone entities. Chapter 9, Automated Testing of Plug-ins, teaches how to write automated tests that exercise Eclipse plug-ins, including both UI and non-UI components. Chapter 10, Automated builds with Tycho, details how to build Eclipse plug-ins, features, update sites, applications, and products automatically with Maven Tycho. What you need for this book To run the exercises for this book, you will need a computer with an up-to-date operating system (running Windows, Linux, or Mac OS X). Java also needs to be installed; JDK 1.7 is the current released version although the instructions should work for a newer version of Java as well. This book has been tested with the Eclipse SDK (Classic/Standard) for Juno (4.2) and Kepler (4.3). Newer versions of Eclipse may also work. Care should be taken while installing Eclipse for RCP and RAP developers, as this will cause the applications created in Chapter 7, Understanding the Eclipse 4 Model and Chapter 8, Creating Features, Update Sites, Applications, and Products. The first chapter explains how to get started with Eclipse, including how to obtain and install both Eclipse and Java. Who this book is for This book is aimed at Java developers who are interested in learning how to create plug-ins, products, and applications for the Eclipse platform. The book starts with how to install and use Eclipse to build and debug plug-ins, covers different types of user interfaces, and finishes with how to create update sites and build and test plug-ins automatically. This book will also be useful to those who already have some experience in building Eclipse plug-ins and want to know how to create automated builds using Maven Tycho, which has become the de-facto standard for building Eclipse plug-ins. Finally, those Eclipse developers who are familiar with the Eclipse 3.x model but are interested in learning about the changes that the Eclipse 4.x model brings will find the information presented in Chapter 7, Understanding the Eclipse 4 Model a useful summary of what opportunities the new model provides. [2] www.it-ebooks.info Preface E4: In this book, both the Eclipse 3.x and Eclipse 4.x models are covered. The Eclipse 4 platform contains a backwards-compatible runtime for the Eclipse 3.x APIs. Where the Eclipse 3.x APIs differ from the Eclipse 4.x APIs, the icon E4 will be used to call out a difference. A full explanation of the Eclipse 4 concepts will be covered in Chapter 7, Understanding the Eclipse 4 Model; so the E4 notes can be skipped on the first reading if necessary. If you are developing an Eclipse IDE-based plug-in, you should consider using the Eclipse 3.x APIs, as these will work in both older Eclipse instances, as well as newer ones. If you are developing an Eclipse RCP-based application and do not need to support older versions, consider building an Eclipse 4-based application. Future versions of the Eclipse platform (4.4/Luna and afterwards) will make it possible to use some of the Eclipse 4 APIs in the IDE. Conventions In this book, you will find several headings appear frequently. To give clear instructions of how to complete a procedure or task, we use: Time for action – heading 1. 2. 3. Action 1 Action 2 Action 3 Instructions often need some extra explanation so that they make sense, so they are followed with: What just happened? This heading explains the working of tasks or instructions that you have just completed. You will also find some other learning aids in the book, including: Pop quiz – heading These are short multiple-choice questions intended to help you test your own understanding. [3] www.it-ebooks.info Preface Have a go hero – heading These practical challenges give you ideas for experimenting with what you have learned. You will also find a number of styles of text that distinguish between different kinds of information. Here are some examples of these styles, and an explanation of their meaning. Code words in text are shown as follows: "You may notice that we used the Unix command rm to remove the Drush directory rather than the DOS del command." A block of code is set as follows: # * Fine Tuning # key_buffer = 16M key_buffer_size = 32M max_allowed_packet = 16M thread_stack = 512K thread_cache_size = 8 max_connections = 300 When we wish to draw your attention to a particular part of a code block, the relevant lines or items are set in bold: # * Fine Tuning # key_buffer = 16M key_buffer_size = 32M max_allowed_packet = 16M thread_stack = 512K thread_cache_size = 8 max_connections = 300 Any command-line input or output is written as follows: cd /ProgramData/Propeople rm -r Drush git clone --branch master http://git.drupal.org/project/drush.git New terms and important words are shown in bold. Words that you see on the screen, in menus or dialog boxes for example, appear in the text like this: "On the Select Destination Location screen, click on Next to accept the default destination". [4] www.it-ebooks.info Preface Warnings or important notes appear in a box like this. Tips and tricks appear like this. Reader feedback Feedback from our readers is always welcome. Let us know what you think about this book—what you liked or may have disliked. Reader feedback is important for us to develop titles that you really get the most out of. To send us general feedback, simply send an e-mail to [email protected], and mention the book title through the subject of your message. If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, see our author guide on www.packtpub.com/authors. Customer support Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase. Downloading the example code You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you. The code samples from this book are also available from the GitHub repository at http://github.com/alblue/com.packtpub.e4. There are ten branches, each corresponding to the state of the book's examples at the end of each chapter. [5] www.it-ebooks.info Preface Errata Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you find a mistake in one of our books—maybe a mistake in the text or the code—we would be grateful if you would report this to us. By doing so, you can save other readers from frustration and help us improve subsequent versions of this book. If you find any errata, please report them by visiting http://www.packtpub.com/submit-errata, selecting your book, clicking on the errata submission form link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata will be uploaded to our website, or added to any list of existing errata, under the Errata section of that title. See also the book's GitHub repository at http://github.com/alblue/com.packtpub. e4. If any code samples need to be updated, they will be updated there. Piracy Piracy of copyright material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works, in any form, on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy. Please contact us at [email protected] with a link to the suspected pirated material. We appreciate your help in protecting our authors, and our ability to bring you valuable content. Questions You can contact us at [email protected] if you are having a problem with any aspect of the book, and we will do our best to address it. [6] www.it-ebooks.info 1 Creating Your First Plug-in Eclipse is a highly modular application, consisting of hundreds of plug-ins, and can be extended by installing additional plug-ins. Plug-ins are developed and debugged with the Plug-in Development Environment (PDE). In this chapter, we shall: Set up an Eclipse environment for performing plug-in development Create a plug-in with the new plug-in wizard Launch a new Eclipse instance with the plug-in enabled Debug the Eclipse plug-in Getting started Developing plug-ins requires an Eclipse development environment. This book has been developed and tested on Juno (Eclipse 4.2) Kepler (4.3). Use the most recent version available. Eclipse plug-ins are generally written in Java. Although it's possible to use other JVM-based languages (such as Groovy or Scala), this book will use the Java language. There are several different packages of Eclipse available from the downloads page, each of which contains a different combination of plug-ins. This book has been tested with: Eclipse SDK from http://download.eclipse.org/eclipse/downloads/ Eclipse Classic and Eclipse Standard from http://www.eclipse.org/downloads/ www.it-ebooks.info Creating Your First Plug-in These contain the necessary Plug-in Development Environment (PDE) feature as well as the source code, the help documentation, and other useful features. (The RCP and RAP package should not be used, as it will cause problems with exercises in the Chapter 7, Understanding the Eclipse 4 Model.) It is also possible to install the Eclipse PDE feature into an existing Eclipse instance. To do this, go to the Help menu and select Install New Software, followed by choosing the General Purpose Tools category from the update site. The Eclipse Plug-in Development Environment feature contains everything needed to create a new plug-in. Time for action – setting up the Eclipse SDK environment Eclipse is a Java-based application, which needs Java installed. Eclipse is distributed as a compressed archive and doesn't require an explicit installation step. 1. To obtain Java, go to http://java.com and follow the instructions to download and install Java. Note that Java comes in two flavors; a 32-bit install and a 64-bit install. If the running OS is a 32-bit, install the 32-bit JDK; alternatively, if the running OS is 64-bit, install the 64-bit JDK. 2. Running java -version should give output like so: java version "1.7.0_09" Java(TM) SE Runtime Environment (build 1.7.0_09-b05) Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode) 3. Go to http://www.eclipse.org/downloads/ and select the Eclipse Classic or Eclipse Standard distribution. 4. Download the one which matches the installed JDK. Running java -version should report either: If it's a 32-bit JDK: Java HotSpot(TM) Client VM If it's a 64-bit JDK: Java HotSpot(TM) 64-Bit Server VM On Linux, Eclipse requires GTK2 to be installed. Most Linux distributions have a window manager based on GNOME that provides GTK2.x. 5. To install Eclipse, download and extract the contents to a suitable location. Eclipse is shipped as an archive, and needs no administrator privileges. Do not run it from a networked drive as this will cause performance problems. [8] www.it-ebooks.info Chapter 1 Note that Eclipse needs to write to the folder from which it is extracted, so it's normal that the contents are writable afterwards. Generally, installing into /Applications or C:\Program Files, while being logged in with administrator account, is not recommended. 6. Run Eclipse by double-clicking on the Eclipse icon, or by running eclipse.exe (Windows), eclipse (Linux), or Eclipse.app (OS X). 7. Upon startup, the splash screen should be shown: 8. Choose a workspace, which is the location in which projects are be stored, and click on OK: [9] www.it-ebooks.info Creating Your First Plug-in 9. Close the welcome screen by clicking on the cross icon in the tab next to the Welcome text. The welcome screen can be re-opened by navigating to Help | Welcome: What just happened? Eclipse needs Java to run, and so the first step involved in installing Eclipse is ensuring that an up-to-date Java installation is available. By default, Eclipse will find a copy of Java installed on the path or from one of the standard locations. It is also possible to specify a different Java version by using the -vm command-line argument. [ 10 ] www.it-ebooks.info Chapter 1 If the splash screen doesn't show up, the Eclipse version may be incompatible with the JDK (for example, a 64-bit JDK with a 32-bit Eclipse, or vice-versa). Common error messages shown at the launcher may include Unable to find companion launcher or a cryptic message about being unable to find an SWT library. On Windows, there is an additional eclipsec.exe launcher, which allows log messages printed to the console to be seen. This is sometimes useful if Eclipse fails to load and no other message is displayed. Other operating systems can use the eclipse command. Both eclipse.exe and eclipse support the -consolelog argument, which can display more diagnostic information about problems while launching Eclipse. The Eclipse workspace is a directory used for two purposes: as the default project location, and to hold the .metadata directory containing Eclipse settings, preferences, and other runtime information. The Eclipse runtime log is stored in the .metadata/.log file. The workspace chooser dialog has an option to set the chosen value as the default workspace. It can be changed within Eclipse by navigating to File | Switch Workspace. It can also be overridden by specifying a different workspace location with the -data command-line argument. Finally, the welcome screen is useful for first-time users, but is worth closing (rather than minimizing) once Eclipse has started. Creating your first plug-in In this task, Eclipse's plug-in wizard will be used to create a plug-in. Time for action – creating a plug-in In the Plug-in Development Environment (PDE), every plug-in has its own individual project. A plug-in project is typically created with the New Project wizard, although it is possible to upgrade an existing Java project to a plug-in project by adding the PDE nature and the required files (by navigating to Configure | Convert to plug-in project). [ 11 ] www.it-ebooks.info Creating Your First Plug-in 1. To create a Hello World plugin, navigate to File | New | Project: 2. The project types shown may be different from this list, but should include Plug-in Project with Eclipse Classic. If nothing is shown under the File | New menu, navigate to Window | Open Perspective | Other | Plug-in Development first; the entries should then be seen under the File | New menu. 3. Choose Plug-in Project and click on Next. Fill in the dialog as follows: 4. Project name: com.packtpub.e4.hello.ui Select the checkbox for Use default location Select the checkbox for Create a Java project Target Eclipse Version: 3.5 or greater Click on Next again, and fill in the plug-in properties: ID: com.packtpub.e4.hello.ui Version: 1.0.0.qualifier Name: Hello Vendor: PacktPub [ 12 ] www.it-ebooks.info Chapter 1 5. 6. 7. 8. Execution Environment: Use the default (for example: JavaSE-1.6 or JavaSE-1.7) Select the checkbox for Generate an Activator Activator: com.packtpub.e4.hello.ui.Activator Select the checkbox for This plug-in will make contributions to the UI Rich client application: No Click on Next and a set of templates will be provided: Select the checkbox for Create a plug-in using one of the templates Choose the Hello World Command template Click on Next to customize the sample, including: The Java package name, which defaults to the project's name The handler class name, which is the code that gets invoked for the action The message box text, which is the message supplied Finally, click on Finish and the project will be generated. If a dialog asks, click on Yes to show the plug-in development perspective. What just happened? Creating a plug-in project is the first step towards creating a plug-in for Eclipse. The New Plug-in Project wizard was used with one of the sample templates to create a project. Plug-ins are typically named in reverse domain name format, so these examples will be prefixed with com.packtpub.e4. This helps to distinguish between many plug-ins; the stock Eclipse SDK comes with more than 440 individual plug-ins, for example, the Eclipse-developed ones start with org.eclipse. Conventionally, plug-ins which create additions to (or require) the use of the UI have .ui. in the name. This helps to distinguish from those that don't, which can often be used headlessly. Of the 440+ plug-ins that make up the Eclipse SDK, 120 of those are UI related and the rest are headless. The project contains a number of files which are automatically generated, based on the content filled in the wizard. The key files in an Eclipse plug-in are: META-INF/MANIFEST.MF: The OSGi manifest describes the plug-in's dependencies, version, and name. Double-clicking it will open a custom editor, which shows the information entered in the wizards; or it can be opened in a standard text editor. [ 13 ] www.it-ebooks.info Creating Your First Plug-in The Manifest follows standard Java conventions; continuations are represented by a new line followed by a single space character, and the file must end with a new line. (For example, the maximum line length is 72 characters, although many ignore this.) plugin.xml: The plugin.xml file declares what extensions this plug-in provides to the Eclipse runtime. Not all plug-ins need a plugin.xml file; headless (non-UI) plug-ins often don't need to have one. Extension points will be covered in more detail later, but the sample project creates an extension for the commands, handlers, bindings, and menus extension points. (If the older Hello World template was chosen, present on 3.7 and older, only the actionSets extension will be used.) Text labels for the commands, actions, or menus are represented declaratively in the plugin.xml file, rather than programmatically; this allows Eclipse to show the menu before needing to load or execute any code. This is one of the reasons Eclipse starts so quickly; by not needing to load or execute classes, it can scale by showing what's needed at the time, and then load the class on demand when the user invokes the action. Java Swing's Actions provides labels and tool tips programmatically, which can result in a slower initialization of the user interface. build.properties: This file is used by PDE at development time and at build time. Generally it can be ignored, but if resources are added that need to be made available to the plug-in (such as images, properties files, HTML content, and so on), an entry must be added here as otherwise it won't be found. Generally, the easiest way to do this is by going to the Build tab of the build.properties file, which will give a tree-like view of the project's contents. This file is an archaic hangover from the days of Ant builds, and is generally useless when using more up-to-date builds such as Maven Tycho, which will be covered in Chapter 10, Automated Builds with Tycho. Pop quiz – Eclipse workspaces and plug-ins Q1. What is an Eclipse workspace? Q2. What is the naming convention for Eclipse plug-in projects? Q3. What are the names of the three key files in an Eclipse plug-in? [ 14 ] www.it-ebooks.info Chapter 1 Running plug-ins To test an Eclipse plug-in, Eclipse is used to run or debug a new Eclipse instance with the plug-in installed. Time for action – launching Eclipse from within Eclipse Eclipse can launch a new Eclipse application by clicking on the Run button, or via the Run menu. 1. 2. Select the plug-in project in the workspace. 3. Choose the Eclipse Application type and click on OK, and a new Eclipse instance will be launched. 4. 5. Close the welcome page in the launched application, if shown. Click on the Run button to launch the project. The first time this happens, a dialog will be shown; subsequent launches will remember the previous type: Click on the Hello World icon in the menu bar, or navigate to Sample Menu | Sample Command, and the dialog box created via the wizard will be shown: [ 15 ] www.it-ebooks.info Creating Your First Plug-in 6. Quit the test Eclipse instance by closing the window, or via the usual keyboard shortcuts or menus (Cmd + Q on OS X, Alt + F4 on Windows). What just happened? When clicking Run in the toolbar (or via the Run | Run As | Eclipse Application menu) a launch configuration is created, which includes any plug-ins open in the workspace. A second copy of Eclipse—with its own temporary workspace—will enable the plug-in to be tested and verified so that it works as expected. The Run operation is intelligent, in that it launches an application based on what is selected in the workspace. If a plug-in is selected, it will offer the opportunity to run as an Eclipse application; if a Java project with a class with a main method, it will run it as a standard Java application; and if it has tests then it will offer to run the test launcher instead. However, the Run operation can also be counter-intuitive; if clicked a second time, and in a different project context, something other than the expected launch might be run. A list of the available launch configurations can be seen by going to the Run menu, or by going to the drop-down list to the right of the Run icon. The Run | Run Configurations menu shows all the available types, including any previously run: [ 16 ] www.it-ebooks.info Chapter 1 By default, the runtime workspace is kept between runs. The launch configuration for an Eclipse application has options that can be customized; in the previous screenshot, the Workspace Data section in the Main tab shows where the runtime workspace is stored, and an option is shown that allows the workspace to be cleared (with or without confirmation) between runs. Launch configurations can be deleted by clicking on the red Delete icon on the top-left, and new launch configurations can be created by clicking on the New icon . Each launch configuration has a type: Eclipse Application Java Applet Java Application JUnit JUnit Plug-in Test OSGi Framework The launch configuration can be thought of as a precanned script, which can launch different types of programs. Additional tabs are used to customize the launch, such as the environment variables, system properties, or command-line arguments. The type of the launch configuration specifies what parameters are required, and how the launch is executed. When a program is launched with the Run icon, changes to the project's source code do not take effect. However, as we'll see in the next section, if it's launched with the Debug icon, changes can take effect. If the test Eclipse is hanging or otherwise unresponsive, in the host Eclipse instance the Console view (shown with the Window | View | Show View | Other | General | Console menu) can be used to stop ( ) the test Eclipse instance. Pop quiz – launching Eclipse Q1. What are the two ways of terminating a launched Eclipse instance? Q2. What are launch configurations? Q3. How do you create and delete launch configurations? [ 17 ] www.it-ebooks.info Creating Your First Plug-in Have a go hero – modifying the plug-in Now that you've got the Eclipse plug-in running, try the following: Change the message of the label and title of the dialog box to something else Invoke the action by using the keyboard shortcut (defined in plugin.xml) Change the tool tip of the action to a different message Switch the action icon to a different graphic (note that if you use a different filename, remember to update build.properties). Debugging a plug-in Since it's rare that everything works first time, it's often necessary to develop iteratively, adding progressively more functionality each time. Secondly, sometimes it's necessary to find out what's going on under the covers when trying to fix a bug, particularly if it's hard to track down exceptions such as NullPointerException. Fortunately, Eclipse comes with excellent debugging support, which can be used for debugging both standalone Java applications as well as Eclipse plug-ins. Time for action – debugging a plug-in Debugging an Eclipse plug-in is almost the same as running an Eclipse plug-in, except that breakpoints can be used, and the state of the program can be updated, variables, and minor changes to the code can be done. Rather than debugging plug-ins individually, the entire Eclipse launch configuration is started in debug mode. That way, all the plug-ins can be debugged at the same time. Although run mode is slightly faster, the added flexibility of being able to make changes makes debug mode much more attractive to use as a default. Start the test Eclipse, by navigating to the Debug | Debug As | Eclipse Application menu, or by clicking on Debug ( ) in the toolbar. 1. Click on the Hello World icon and click on OK to dismiss it. in the test Eclipse to display the dialog, as before, [ 18 ] www.it-ebooks.info Chapter 1 2. 3. In the host Eclipse, open the SampleHandler class and go to the execute() method. Add a breakpoint by double-clicking in the vertical ruler (the gray/blue bar on the left of the editor), or by pressing Ctrl + Shift + B (or Cmd + Shift + B on OS X). A blue dot representing the breakpoint will appear in the ruler: [ 19 ] www.it-ebooks.info Creating Your First Plug-in 4. Click on the Hello World icon in the test Eclipse to display the dialog, and the debugger will pause the thread at the breakpoint in the host Eclipse: The debugger perspective will open whenever a breakpoint is triggered and the program will be paused. While it is paused, the test Eclipse is unresponsive. Any clicks on the test Eclipse application will be ignored, and it will show a busy cursor. 5. On the top-right, variables that are active in the line of code are shown. In this case, it's just the implicit variables (via this), any local variables (none, yet) as well as the parameter (in this case, event). [ 20 ] www.it-ebooks.info Chapter 1 6. Click on Step Over available variables: 7. When ready to continue, click on Resume or press F6, and window will be added to the list of or press F8 to keep running. What just happened? The built-in Eclipse debugger was used to launch Eclipse in debug mode. By triggering an action which led to a breakpoint, the debugger was revealed allowing the local variables to be introspected. When in the debugger, there are several options available for stepping through the code: Step Over Step Into – allows stepping over line-by-line in the method – follow the method calls recursively as execution unfolds [ 21 ] www.it-ebooks.info Creating Your First Plug-in There is also a Run | Step into Selection menu item that does not have a toolbar icon. It can be invoked with Ctrl + F5 (Alt + F5 on OS X), and is used to step into a specific expression. Step Return Drop to Frame – jump to the end of a method – return to a stack frame in the thread to re-run an operation Time for action – updating code in debugger When an Eclipse instance is launched in run mode, changes made to the source code aren't reflected in the running instance. However, debug mode allows changes made to the source to be reflected in the running test Eclipse instance. 1. 2. Launch the test Eclipse in debug mode by clicking on the Debug 3. In the host Eclipse, open the SampleHandler class and go to the execute() method. 4. 5. icon. Click on the Hello World icon in the test Eclipse to display the dialog, as before, and click on OK to dismiss it. It may be necessary to remove or resume the breakpoint in the host Eclipse instance to allow execution to continue. Change the title of the dialog to Hello again, Eclipse world and save the file. Provided that Project | Build Automatically is enabled, the change will be recompiled. Click on the Hello World icon in the test Eclipse instance again. The new message should be shown. What just happened? By default, Eclipse ships with Project | Build Automatically enabled. Whenever changes are made to Java files, they are recompiled along with their dependencies if necessary. When a Java program is launched in run mode, it will load classes in on-demand and then keep using that definition until the JVM shuts down. Even if the classes are changed, the JVM won't notice that they have been updated, and so no differences will be seen in the running application. However, when a Java program is launched in debug mode, whenever changes to classes are made, it will update the running JVM with the new code if possible. The limits to what can be replaced are controlled by the JVM through the JVMTI and whether, for example, the virtual machine's canUnrestrictedlyRedefineClasses() call returns true. Generally, updating an existing method and adding a new method or field will work, but changes to interfaces and super classes may not be. (Refer to http://en.wikipedia.org/wiki/ Java_Virtual_Machine_Tools_Interface for more information.) [ 22 ] www.it-ebooks.info Chapter 1 The ex-Sun Hotspot JVM cannot replace classes if methods are added or interfaces are updated. Some JVMs have additional capabilities which can substitute more code on demand. With the merging of JRockit and Hotspot over time, more may be replaceable at runtime than before; for everything else, there's JRebel. Other JVMs, such as IBM's, can deal with a wider range of replacements. Note that there are some changes which won't be picked up; for example, new extensions added to the plugin.xml file. In order to see these changes, it is possible to start and stop the plug-in through the command-line OSGi console, or restart Eclipse inside or outside the host Eclipse to see the change. Debugging with step filters When debugging using Step Into, the code will frequently go into Java internals, such as the implementation of Java collections classes or other internal JVM classes. These don't usually add value, but fortunately Eclipse has a way of ignoring uninteresting classes. Time for action – setting up step filtering Step filtering allows for uninteresting packages and classes to be ignored during step debugging. 1. 2. Run the test Eclipse application in debug mode. 3. Click on the Hello World icon, and the debugger should open at the first line as before. 4. Click on Step Into five or six times. At each point, the code will jump into the next method in the expression; first through various methods in HandlerUtil and then into ExecutionEvent. 5. 6. 7. Click on Resume Ensure a breakpoint is set at the start of the SampleHandler class's execute() method. to continue. Open Preferences, and then navigate to Java | Debug | Step Filtering. Check the Use Step Filters option. [ 23 ] www.it-ebooks.info Creating Your First Plug-in 8. Click on Add Package and enter org.eclipse.ui, followed by clicking on OK. 9. Click on the Hello World icon again. 10. Click on Step Into as before. This time, the debugger goes straight to getApplicationContext() in the Execution Event class. 11. Click on the Resume icon to continue. 12. To make debugging more efficient by skipping accessors, go back into the Step Filters preference and select the Filter Simple Getters from the Step Filters preference's page. 13. Click on the Hello World icon again. 14. Click on Step Into as before. 15. Instead of going into the getApplicationContext() method, execution will drop through to the ExpressionContext class's getVariable() method instead. What just happened? The Step Filters preferences allows uninteresting packages to be skipped, at least from the point of debugging. Typically, JVM internal classes (such as those beginning with sun or sunw) are not helpful when debugging and can easily be ignored. This also avoids debugging through the class loader, as it loads classes on demand. [ 24 ] www.it-ebooks.info Chapter 1 Typically, it makes sense to enable all the default packages in the Step Filters dialog, as it's pretty rare to need to debug any of the JVM libraries (internal or public interfaces). This means when stepping through the code, if a common method such as List.toString() is called, debugging won't step through the internal implementation. It also makes sense to filter out simple setters and getters (those that just set a variable, or those that just return a variable). If the method is more complex (like the getVariable() method discussed previously), it will still stop in the debugger. Constructors and static initializers can also be specifically filtered. Using different breakpoint types Although it's possible to place a breakpoint anywhere in a method, a special breakpoint type exists, which can fire on method entry, exit, or both. Breakpoints can also be customized to only fire in certain situations or when certain conditions are met. Time for action – breaking at method entry and exit Method breakpoints allow the user to see when a method is entered or exited. 1. 2. Open the SampleHandler class, and go to the execute() method. Double-click in the vertical ruler at the method signature, or select Toggle Method Breakpoint from the method in one of the Outline, Package Explorer, or Members views. 3. The breakpoint should be shown on the line public Object execute(...) throws ExecutionException {. 4. Open the breakpoint properties by right-clicking on the breakpoint or via the Breakpoints view, which is shown in the debug perspective. Set the breakpoint to trigger at the method entry and method exit. 5. 6. 7. Click the Hello World icon again. When the debugger stops at the method entry, click on the Resume icon. When the debugger stops at the method exit, click on the Resume icon. What just happened? The breakpoint triggers at the time the method enters and subsequently when the method's return statement is reached. [ 25 ] www.it-ebooks.info Creating Your First Plug-in Note that the exit is only triggered if the method returns normally; if an exception is raised which causes the method to return, this is not treated as a normal method exit, and so the breakpoint won't fire. Other than the breakpoint type, there's not a significant difference between creating a breakpoint on method entry and creating one on the first statement of the method. Both give the ability to introspect the parameters and do further debugging prior to any statements in the method itself are called. The method exit breakpoint, on the other hand, will only trigger once the return statement is about to leave the method. Thus, any expression in the method's return value will have been evaluated prior to the exit breakpoint firing. Compare and contrast this to the line breakpoint, which will wait to evaluate the argument of the return statement. Note that Eclipse's Step Return icon has the same effect; this will run until the method's return statement is about to be executed. However, to find when a method returns, using a method exit breakpoint is far faster than stopping at a specific line and then clicking on Step Return. Using conditional breakpoints Breakpoints are useful, since they can be invoked on every occasion that a line of code is triggered. However, sometimes they need to break for specific actions only, such as when a particular option is set, or when a value has been incorrectly initialized. Fortunately, this can be done with conditional breakpoints. Time for action – setting a conditional breakpoint Normally, breakpoints fire on each invocation. It is possible to configure breakpoints such that they fire when certain conditions are met. 1. 2. Go to the execute() method of the SampleHandler class. 3. 4. Add a breakpoint to the first line of the execute() method body. Clear any existing breakpoints by double-clicking them or using Delete all breakpoints from the Breakpoints view. Right-click on the breakpoint and select the Breakpoint Properties menu. (It can also be shown by Ctrl + double-clicking, or Cmd + double-clicking on OS X on the breakpoint icon itself.) [ 26 ] www.it-ebooks.info Chapter 1 5. 6. Set the Hit Count field to 3, and click on OK. 7. Open the breakpoint properties, deselect Hit Count and select the Enabled and Conditional options. Put the following code into the conditional trigger field: Click on the Hello World icon button three times. On the third click, the debugger will open up at that line of code. ((org.eclipse.swt.widgets.Event)event.trigger).stateMask == 65536 8. 9. Click on the Hello World icon and the breakpoint will not fire. Hold down Alt, click on the Hello World icon, and the debugger will open. (65536 is the value of SWT.MOD3, which is the Alt key.) [ 27 ] www.it-ebooks.info Creating Your First Plug-in What just happened? When a breakpoint is created, it is enabled by default. A breakpoint can be temporarily disabled, which has the effect of removing it from the flow of execution. Disabled breakpoints can be trivially re-enabled on a per-breakpoint basis, or from the Breakpoints view. Quite often it's useful to have a set of breakpoints defined in the codebase, but not necessarily have them all enabled at once. It is also possible to temporarily disable all breakpoints using the Skip All Breakpoints setting, which can be changed from the corresponding item in the Run menu (when the in the Breakpoints view. When debug perspective is enabled), or the corresponding icon this is toggled, no breakpoints will be fired. Conditional breakpoints must use a Boolean expression, rather than a statement or a set of statements. Sometimes this is constraining; if that's the case, having a utility class with a static method allows more complex code paths (with the caveat that all interesting data must be passed in as method arguments). Using exceptional breakpoints Sometimes when debugging a program, an exception occurs. Typically this isn't known about until it happens, when an exception message is printed or displayed to the user via some kind of dialog box. Time for action – catching exceptions Although it's trivial to put a breakpoint in the catch block, this is merely the location where the failure was ultimately caught, not where it was caused. The place where it was caught can often be in a completely different plug-in from where it was raised, and depending on the amount of information encoded within the exception (particularly if it has been transliterated into a different exception type), it may hide the original source of the problem. Fortunately, Eclipse can handle such cases with a Java Exception breakpoint. 1. Introduce a bug into the SampleHandler class's execute() method, by adding the following just before the MessageDialog.openInformation() call: window = null; [ 28 ] www.it-ebooks.info Chapter 1 Downloading the example code You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www. packtpub.com/support and register to have the files e-mailed directly to you. 2. 3. Click on the Hello World icon. Nothing will appear to happen in the target Eclipse, but in the Console view of the host Eclipse instance, the following error message should be seen: Caused by: java.lang.NullPointerException at com.packtpub.e4.hello.ui.handlers.SampleHandler. execute(SampleHandler.java:30) at org.eclipse.ui.internal.handlers.HandlerProxy. execute(HandlerProxy.java:293) at org.eclipse.ui.internal.handlers.E4HandlerProxy. execute(E4HandlerProxy.java:76) 4. Create a Java Exception breakpoint in the Breakpoints view of the debug perspective. The exception dialog will be shown as follows: [ 29 ] www.it-ebooks.info Creating Your First Plug-in 5. 6. Enter NullPointerException into the search dialog and click on OK. Click on the Hello World icon and the debugger will stop at the line the exception is thrown, instead of where it is caught: What just happened? The Java Exception breakpoint stops when an exception is thrown, not when it is caught. The dialog asks for a single exception class to catch and by default, the wizard has been prefilled with any class whose name includes *Exception*. However, any name (or filter) can be typed into the search box, including abbreviations such as FNFE for FileNotFoundException. Wildcard patterns can also be used, which allows searching for Nu*Ex or *Unknown*. By default, the exception breakpoint corresponds to instances of that specific class. This is useful (and quick) for exceptions such as NullPointerException, but not so useful for ones with an extensive class hierarchy such as IOException. In this case, there is a checkbox visible in the Breakpoint properties window and the bottom of the Breakpoints view, which allows the selection of all subclasses of that exception, not just of the specific class itself. [ 30 ] www.it-ebooks.info Chapter 1 There are also two other checkboxes, which say whether the debugger should stop when the exception is caught or uncaught. Both of these are selected by default; if both are deselected, the breakpoint effectively becomes disabled. Caught means that the exception is thrown in a corresponding try/catch block, and Uncaught means that the exception is thrown without a try/catch block (thus, bubbles up to the method's caller). Time for action – using watch variables and expressions Finally, it's worth seeing what the Variables view can do. 1. 2. 3. 4. 5. Create a breakpoint at the start of the execute() method. Click on the Hello World icon again. Highlight the openInformation() call and navigate to Run | Step Into Selection. Select the title in the the Variables view. Modify where it says Hello in the bottom half of the Variables view and change it to Goodbye: [ 31 ] www.it-ebooks.info Creating Your First Plug-in 6. 7. 8. 9. Save the value using Ctrl + S (or Cmd + S on OS X). Click on the Resume icon and the newly updated title can be seen in the dialog. Click on the Hello World icon again. With the debugger stopped in the execute() method, highlight event in the Variables view. 10. Right-click on the value and choose Inspect (Ctrl + Shift + I or Cmd + Shift + I on an OS X) and the value is opened in the Expressions view: 11. Click on the Add new expression option in the bottom of the Variables view. 12. Add new java.util.Date() and the right-hand side will show the current time. 13. Right-click on the new java.util.Date() and choose Re-evaluate Watch Expression. The right-hand pane shows the new value. 14. Step through the code line by line, and notice that the watch expression is reevaluated after each step. [ 32 ] www.it-ebooks.info Chapter 1 15. Disable the watch expression by right-clicking on it and choosing Disable. 16. Step through the code line by line and the watch expression will not be updated. What just happened? The Eclipse debugger has many powerful features, and the ability to inspect (and change) the state of the program is one of the more important ones. Watch expressions, when combined with conditional breakpoints, can be used to find out when data becomes corrupted or used to show the state of a particular object's value. Expressions can also be evaluated based on objects in the Variables view, and the code completion is available to select methods, with the result being shown with Display. Pop quiz – debugging Q1. How can an Eclipse plug-in be launched in debug mode? Q2. How can certain packages be avoided when debugging? Q3. What are the different types of breakpoints that can be set? Q4. How can a loop debugged that only exhibits a bug after 256 iterations? Q5. How can a breakpoint be set on a method when its argument is null? Q6. What does inspecting an object do? Q7. How can the value of an expression be calculated? Have a go hero – working with breakpoints Using the conditional breakpoint to stop at a certain method is fine if the data is simple, but sometimes there needs to be more than one expression. To implement additional functionality, the breakpoint can be delegated to a breakpoint() method in a Utility class. The following steps will help you to work with breakpoints: 1. Create a Utility class with a static method breakpoint(), this will return a true value if the breakpoint should stop and false otherwise: public class Utility { public static boolean breakpoint() { System.out.println("Breakpoint"); return false; } } [ 33 ] www.it-ebooks.info Creating Your First Plug-in 2. Create a conditional breakpoint in the execute() method, which calls Utility. breakpoint(). 3. Click on the Hello World icon again and the message will be printed to the host Eclipse's Console view. The breakpoint will not stop. 4. Modify the breakpoint() method to return true instead of false. Run the action again. The debugger will stop. 5. Modify the breakpoint() method to take the message as an argument, along with a Boolean value that is returned to say if the breakpoint should stop. 6. Set up a conditional breakpoint with the following code: Utility.breakpoint( ((org.eclipse.swt.widgets.Event)event.trigger).stateMask != 0,"Breakpoint") 7. Modify the breakpoint() method to take a varargs Object array, and use that in conjunction with the message to use String.format() for the resulting message: Utility.breakpoint( ((org.eclipse.swt.widgets.Event)event. trigger).stateMask != 0,"Breakpoint" %s %h",event, new java.util.Date().getTime())) Summary In this chapter, we covered how to get started with Eclipse plug-in development. From downloading the right Eclipse package (from a bewildering array of choices) to getting started with a wizard-generated plug-in, you should now have the tools to follow through with the remainder of the chapters in this book. Specifically, we covered: The Eclipse SDK (also known as Eclipse Classic) has the necessary Plug-in Development Environment to get you started The plug-in creation wizard can be used to create a plug-in project, optionally using one of the example templates Testing an Eclipse plug-in launches a second copy of Eclipse with the plug-in installed and available for use Launching Eclipse in debug mode allows you to update code and stop execution at breakpoints defined via the editor Now that we've learned about how to get started with Eclipse plug-ins, we're ready to look at creating plug-ins which contribute to the IDE; starting with SWT and Views, which is the topic of the next chapter. [ 34 ] www.it-ebooks.info 2 Creating Views with SWT SWT is the widget toolkit that Eclipse uses, which gives performant access to the platform's native tools in a portable manner. Unlike Swing, which is rendered with Java native drawing operations, SWT delegates the drawing to the underlying operating system. In this chapter, we shall: Create an Eclipse view with SWT widgets Create a custom SWT widget Work with resources and learn how to detect and fix resource leaks Handle focus operations Group components and resize them automatically Create system tray items Display non-rectangular windows Provide scrolling and tabbed navigation Creating views and widgets This section introduces views and widgets and uses as its example clocks. www.it-ebooks.info Creating Views with SWT Time for action – creating a view The Eclipse UI consists of multiple views, which are the rectangular areas that display content such as the Outline, Console, or Package Explorer. In Eclipse 3.x, views are created by adding an extension point to an existing plug-in, or by using a template. A clock.ui plug-in will be created to host the clock widgets and views. 1. 2. 3. 4. 5. Open the plug-in wizard by navigating to File | New | Other | Plug-in Project. Enter the details as follows: Project name: com.packtpub.e4.clock.ui Select the checkbox for Use default location Select the checkbox for Create a Java project Target Eclipse Version: 3.5 or greater Click on Next again, and fill in the plug-in properties: ID: com.packtpub.e4.clock.ui Version: 1.0.0.qualifier Name: Clock Vendor: PacktPub Select the checkbox for Generate an Activator Activator: com.packtpub.e4.clock.ui.Activator Select the checkbox for This plug-in will make contributions to the UI Rich client application: No Click on Next to choose from a set of templates: Select the checkbox for Create a plug-in using one of the templates Choose the Plug-in with a view template Click on Next to customize a few aspects of the sample, including: Java package name: com.packtpub.e4.clock.ui.views View class name: ClockView View name: Clock View View category ID: com.packtpub.e4.clock.ui View category name: Timekeeping Viewer type: Table Viewer Deselect the Add checkboxes as these are not required. [ 36 ] www.it-ebooks.info Chapter 2 6. 7. 8. Click on Finish to create the project. Run the Eclipse application via the Run toolbar icon. Go to Window | Show View | Other | Timekeeping | Clock View to show the Clock View, which has a simple list view with One, Two, Three listed: What just happened? It's not usual to create a plug-in project per view (or per action) but often plug-ins are encapsulated and provide functionality specific to that particular plug-in. In this case, clocks aren't related to the Hello World one created before, so a new plug-in was created to host them. The plug-in wizard created an empty plug-in project, as well as two key files. MANIFEST.MF The manifest contains references to dependent plug-ins and interfaces, and includes the following: Bundle-SymbolicName: com.packtpub.e4.clock.ui; singleton:=true Bundle-Version: 1.0.0.qualifier Bundle-Activator: com.packtpub.e4.clock.ui.Activator Require-Bundle: org.eclipse.ui, org.eclipse.core.runtime Plug-ins that contribute to the user interface need to do two things: Depend on org.eclipse.ui Have ;singleton:=true after the bundle symbolic name The dependency on the org.eclipse.ui bundle gives access to the Standard Widget Toolkit and other key parts of the Eclipse framework. The clause ;singleton:=true is an OSGi directive, which means that only one version of this plug-in can be installed in Eclipse at one time. For plug-ins which add dependencies to the UI, there is a restriction that they must be singletons. (This constraint is one of the main reasons why installing a new plug-requires the IDE to restart.) The Manifest sets up the project's classpath. Any additional plug-in dependencies need to be added to the Manifest. [ 37 ] www.it-ebooks.info Creating Views with SWT plugin.xml The plugin.xml file defines a list of extensions that this plug-in provides. Extension points are how Eclipse advertises the plug-in extensions, much like a USB hub provides a generic connector that allows many other types of devices to be plugged in. The Eclipse extension points are documented in the help system, and each has a point identifier, with optional children that are point-specific. In this case, the extension is defined using the org.eclipse.ui.views point, which expects a combination of category and view elements. In this case, it will look like: <plugin> <extension point="org.eclipse.ui.views"> <category name="Timekeeping" id="com.packtpub.e4.clock.ui"/> <view name="Clock View" icon="icons/sample.gif" category="com.packtpub.e4.clock.ui" class="com.packtpub.e4.clock.ui.views.ClockView" id="com.packtpub.e4.clock.ui.views.ClockView"/> </extension> </plugin> The class in this case extends the ViewPart abstract class, which is used for all views in Eclipse 3.x. E4: The Eclipse 4 model defines views in a different way, which is covered in more detail in Chapter 7, Understanding the Eclipse 4 Model. The Eclipse 4.x SDK includes a 3.x compatibility layer, so these examples will work in Eclipse 4.x SDKs. The viewer component is a default table view, which will be replaced in the next section. Time for action – drawing a custom view An SWT Canvas can be used to provide custom rendering for a view. As a starting point for drawing a clock, the Canvas will use drawArc() to create a circle. 1. Remove the content of ClockView leaving behind an empty implementation of the setFocus() and createPartControl() methods. 2. Run the target Eclipse instance and see that ClockView is now empty. [ 38 ] www.it-ebooks.info Chapter 2 3. In the createPartControl() method, do the following: 1. Create a new Canvas, which is a drawable widget. 2. Add PaintListener to the Canvas. 3. Get gc from PaintEvent and call drawArc() to draw a circle. The code will look like: import org.eclipse.swt.*; import org.eclipse.swt.events.*; import org.eclipse.swt.widgets.*; import org.eclipse.ui.part.ViewPart; public class ClockView extends ViewPart { public void createPartControl(Composite parent) { final Canvas clock = new Canvas(parent,SWT.NONE); clock.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { e.gc.drawArc(e.x,e.y,e.width-1,e.height-1,0,360); } }); } public void setFocus() { } } 4. 5. Run the Eclipse instance and show the Clock View. Resize the view and the clock should change size with it: What just happened? In SWT, the widget used for custom drawing is Canvas. The view is constructed with a call to createPartControl(), which is invoked once when the view is shown for the first time. If the view is minimized, then maximized, this is not invoked again; however, if the view is closed and a new view is opened, then a call will be made to a new instance of ClockView to initialize it. [ 39 ] www.it-ebooks.info Creating Views with SWT Unlike other Java GUI frameworks, a widget is not added or removed to a containing parent once created; the widget's parent is specified at construction time. Thus, instead of creating it with an empty constructor and then adding it, the parent is passed into the widget's constructor. There is also a style flag that is passed in. This is used by widgets in different ways; for example, the Button widget takes various flags to indicate whether it should be rendered as a push button, radio button, checkbox, toggle, or arrow. For consistency, in SWT all widgets have an int style flag, which enables up to 32 bits of different options to be configured. These are defined as constants in the SWT class; for example, the checkbox button style is represented as SWT.CHECKBOX. Options can be combined; to specify a flat button, one would bitwise or the values of the two fields together: new Button(parent,SWT.PUSH|SWT.FLAT) Generally, the value SWT.NONE can be used to represent default options. The code adds an empty Canvas to the view, but how can it be drawn on? SWT does not expose a paint method on any of its widgets. Instead, PaintListener is called whenever the canvas needs to be repainted. All in the name of performance You may wonder why all these little things are different between the way SWT handles its widgets versus how AWT or Swing handle them. The answer is in the name of speed and delegation to native rendering and controls if at all possible. This mattered back in the early days of Java (Eclipse 1.0 was released when Java 1.3 was the most advanced runtime available) when neither the JITs nor the CPUs were as powerful as today. Secondly, the goal of SWT was to offload as much of the processing onto native components (like AWT) and let the OS do the heavy work instead of Java. By doing that, the time spent in the JVM could be minimized while allowing the OS to render the graphics in the most appropriate (and performant) way. The paint listener is one such example of how to avoid performing unnecessary drawing-related calls unless a component actually needs it. The paintControl() method is invoked with a PaintEvent argument, which contains references to all the data needed to draw the component. To minimize method calls, the fields are publicly readable (not considered to be a good style, but certainly performant in this case). It also contains a reference to the graphics context (GC) which can be used to invoke drawing commands. [ 40 ] www.it-ebooks.info Chapter 2 Finally, the event also records the region in which the paint event is to be fired. The x and y fields show the position of the top-left to start from, and the width and height fields of the event shows the drawing bounds. In this case, the graphics context is set up with the necessary foreground color, and drawArc() is called between the bounds specified. Note that the arc is specified in degrees (from 0 with a 360 span) rather than radians or any other measure. Time for action – drawing a second hand A clock with no hands and no numbers is just a circle. Since arcs are drawn anticlockwise from 0 (on the right, or 3 p.m.) through 90 (12 p.m.), then 180 (9 p.m.), then 270 (6 p.m.), and finally back to 360 (3 p.m.), it is possible to calculate the arc's position for the second hand by using (15-s)*6%360. 1. 2. 3. 4. 5. Go to the paintControl() method of the PaintListener inside ClockView. Add a variable seconds that is initialized to new Date().getSeconds(). Get SWT.COLOR_BLUE via the display and store it in a local variable blue. Set the background color of the graphics context to blue. Draw an arc using the previous formula to draw the second hand. The code should look like the following: public void paintControl(PaintEvent e) { int seconds = new Date().getSeconds(); int arc = (15-seconds) * 6 % 360; Color blue = e.display.getSystemColor(SWT.COLOR_BLUE); e.gc.setBackground(blue); e.gc.fillArc(e.x,e.y,e.width-1,e.height-1,arc-1,2); } 6. Start Eclipse and show the Clock View. The second hand will be shown once but won't change. 7. Resize the view then the second hand will be drawn in the new location. What just happened? The code calculates the position on the arc that the second hand will need to be drawn. Since the arc degrees go anticlockwise, the seconds have to be negative. The offset of 15 represents the fact that 0 represents 15 seconds on the clock (3 p.m.). This is then multiplied by 6 (60 seconds = 360 degrees) and finally the result is calculated modulus 360, to ensure that it's up to 360 degrees. (The value can be negative; the arc calculation works in this way as well.) [ 41 ] www.it-ebooks.info Creating Views with SWT Although drawArc() colors in the foreground color, the fillArc() colors in the background color. The GC maintains two colors; a foreground and a background color. Normally, an SWT Color object needs to have dispose() after use, but to simplify this example the Display class' getSystemColor() is used, whose result does not need to be disposed. Finally, the arc is drawn from the second hand position and 2 degrees afterwards. To center it, it starts from pos-1, so the arc is drawn from pos-1 to pos+1. When the view is resized, a redraw() is issued on the Canvas, and so the second hand is drawn in the correct position. However, to be useful as a clock, this should be done automatically; do this while the view is active. Time for action – animating the second hand The second hand is drawn with a redraw() on the Canvas, but this will need to be run periodically. If it is redrawn once per second, it can emulate a clock ticking. Eclipse has a mechanism called jobs which would be just right for this task, but these will be covered in Chapter 4, Interacting with the User. So to begin with, a simple Thread class will be used to issue the redraw. 1. 2. Open the ClockView class. Add the following to the bottom of the createPartControl() method: new Thread("TickTock") { public void run() { while (!clock.isDisposed()) { clock.redraw(); try { Thread.sleep(1000); } catch (InterruptedException e) { return; } } } }.start(); 3. 4. Re-launch the test Eclipse instance and open the Clock View. Open the host Eclipse instance and look in the Console View for the errors. [ 42 ] www.it-ebooks.info Chapter 2 What just happened? When the Clock View is created, a Thread is created and started, which runs once per second. Every second, in the host Eclipse instance's Console View, an exception is generated that looks like this: Exception in thread "TickTock" org.eclipse.swt.SWTException: Invalid thread access at org.eclipse.swt.SWT.error(SWT.java:4361) at org.eclipse.swt.SWT.error(SWT.java:4276) at org.eclipse.swt.SWT.error(SWT.java:4247) at org.eclipse.swt.widgets.Widget.error(Widget.java:775) at org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:570) at org.eclipse.swt.widgets.Control.redraw(Control.java:2748) at com.packtpub.e4.clock.ui.views.ClockView$2.run(ClockView.java:41) This is expected behavior in this case, but it's worth taking a dive into the SWT internals to understand why. Many windowing systems have a UI thread which is responsible for coordinating the user interface updates with the program code. If long running operations execute on the UI thread, the program can appear to hang and become unresponsive. Many windowing systems will have an automated process, which changes the cursor into an hourglass if the UI thread for an application is blocked for more than a short period of time. SWT mirrors this by providing a UI thread for interacting with the user interface, and ensures that updates to SWT components are done on this thread. Redraws occur on the SWT thread, as do calls to methods like createPartControl(). Technically, SWT is capable of supporting multiple OS UI threads. In the clock update example, updates are being fired on a different thread (in this case, the TickTock thread) and it results in the preceding exception. So how are these updates run on the right thread? Time for action – running on the UI thread To execute code on the UI thread, Runnables are posted to the Display class via two methods, syncExec() and asyncExec(). The syncExec() method runs the code synchronously (that is, the caller blocks until the code has been run), while the asyncExec() method runs the code asynchronously (that is, the caller continues while the code is run in the background). [ 43 ] www.it-ebooks.info Creating Views with SWT The Display class is SWT's handle to a monitor (so a runtime may have more than one Display object, and each may have its own resolution). To get hold of an instance, call either Display.getCurrent() or Display.getDefault(). However, it's much better to get a Display class from an associated view or widget. In this case, the Canvas has an associated Display. 1. Go to the TickTock thread inside the createPartControl() method of the ClockView class. 2. Inside the run() method, replace the call to clock.redraw() with: clock.getDisplay().asyncExec(new Runnable() { public void run() { if(clock != null && !clock.isDisposed()) clock.redraw(); } }); 3. Run the test Eclipse instance and show the Clock View. The second hand should now update automatically. What just happened? This time, the event will execute as expected. One thread, TickTock, is running in the background, and every second it posts a Runnable to the UI thread, which then runs asynchronously. This example could have used syncExec(), and the difference would not have been noticeable; but in general, using asyncExec() is to be preferred unless there is a specific reason to need the synchronous blocking behavior. The thread is in a while loop and is guarded with a call to clock.isDisposed(). Each SWT widget is non-disposed when it is initially created, and then subsequently disposed with a call to dispose(). Once a widget is disposed, any native operating system resources are returned and any further operations will throw an exception. In this example, the Canvas is disposed when the view is closed, which in turn disposes the components contained therein. As a result, when the view is closed, the Thread automatically ceases its loop. (The Thread can also be aborted by interrupting it during its 1 second sleep pauses.) Note that the test for whether the widget is not null or disposed is done in the Runnable as well. Although the widget isn't disposed when it is added to the event queue, it might be when the event is processed some time later. [ 44 ] www.it-ebooks.info Chapter 2 Time for action – creating a reusable widget Although the ClockView shows an animated clock, creating an independent widget will allow the clock to be reused in other places. 1. Create a new class in the com.packtpub.e4.clock.ui package, called ClockWidget, that extends Canvas. 2. Create a constructor that takes a Composite parent and an int style bits parameter, and passes them to the superclass: public ClockWidget(Composite parent, int style) { super(parent, style); } 3. Move the implementation of the paintControl() method from the ClockView to the ClockWidget. Remove the PaintListener references from the ClockView class. 4. In the ClockWidget constructor, register an anonymous PaintListener that delegates the call to the paintControl() method: addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { ClockWidget.this.paintControl(e); } }); 5. Move the TickTock thread from ClockView to the ClockWidget constructor; this will allow the ClockWidget to operate independently. Change any references for clock to ClockWidget.this: new Thread("TickTock") { public void run() { while (!ClockWidget.this.isDisposed()) { ClockWidget.this.getDisplay().asyncExec( new Runnable() { public void run() { if (!ClockWidget.this.isDisposed()) ClockWidget.this.redraw(); } }); try { Thread.sleep(1000); } catch (InterruptedException e) { return; } } } }.start(); [ 45 ] www.it-ebooks.info Creating Views with SWT 6. Add a computeSize() method to allow the clock to have a square appearance that is the minimum of the width and height. Note that SWT.DEFAULT may be passed in, which has the value -1, so this needs to be handled explicitly: public Point computeSize(int w,int h,boolean changed) { int size; if(w == SWT.DEFAULT) { size = h; } else if (h == SWT.DEFAULT) { size = w; } else { size = Math.min(w,h); } if(size == SWT.DEFAULT) size = 50; return new Point(size,size); } 7. Finally, change ClockView to instantiate the ClockWidget instead of the Canvas in the createPartControl() method: final ClockWidget clock = new ClockWidget(parent,SWT.NONE); 8. Run the test Eclipse instance and the clock should be shown as before. What just happened? The drawing logic was moved into its own widget, and hooked up PaintListener to a custom method in ClockWidget so that it could render itself. This allows Clock to be used standalone in any Eclipse or SWT application. In a real application, the clocks would not have their own thread; it would either be the case that a single Thread would control updates to all Clock instances, or they would be set up with repeating Jobs using the Eclipse jobs framework. Jobs will be covered in Chapter 4, Interacting with the User. The technique of using an anonymous class to bind a specific listener type to the instance of the class is a common pattern in SWT. The convention is to use the same method name in the enclosing class; this helps to disambiguate the use. (Remember to set the listener at startup, as otherwise it can be confusing why it's not getting called.) It's also why ClockWidget.this is used in the delegation call; directly invoking this. paintControl() or paintControl() would have ended up in an infinite loop. [ 46 ] www.it-ebooks.info Chapter 2 It's also possible for ClockWidget to implement PaintListener directly; in this case, addPaintListener(this) would be called in the constructor. Modern JITs will optimize the calls to equivalent code paths in any case; it comes down to a style decision as to whether ClockWidget should implement the PaintListener interface or not. Finally, compute the size based on the hints. This is called by the layout manager to determine what size the widget should be. For widgets with a fixed size (say, a text string or an image) the size can vary depending on the layout. In this case, it returns a square, based on the minimal size of the supplied width and height hints, or 50, whichever is bigger. The SWT.DEFAULT value is -1 which has to be dealt with specifically. Time for action – using layouts Now that ClockWidget has been created, multiple instances can be added into ClockView. 1. Modify the ClockView class's createPartControl() method to create three ClockWidget instances: final ClockWidget clock1 = new ClockWidget(parent, SWT.NONE); final ClockWidget clock2 = new ClockWidget(parent, SWT.NONE); final ClockWidget clock3 = new ClockWidget(parent, SWT.NONE); 2. Run the test Eclipse instance and show the Clock View. Three clocks will be shown, counting in seconds: 3. In the ClockView constructor, create a new RowLayout with SWT.HORIZONTAL, and then set it as the layout on parent Composite: public void createPartControl(Composite parent) { RowLayout layout = new RowLayout(SWT.HORIZONTAL); parent.setLayout(layout); [ 47 ] www.it-ebooks.info Creating Views with SWT 4. Run the code again now and the clocks will be in a row (horizontal): 5. Resize the view, the clocks will flow into different rows: RowLayout has a number of fields that can affect how the widgets are laid out: center – if components are centered (vertically or horizontally) fill – if the entire size of the parent should be taken up justify – if the components should be spaced so they reach the end pack – if components should get their preferred size or expanded to fill space wrap – if the components should wrap at the end of the line There are also options to control any pixel spacing between elements (spacing) and any margins at the edge (marginHeight and marginWidth, or which can be specified individually as marginTop, marginBottom, marginLeft, and marginRight). 6. Every SWT widget has an optional layout data object, which is specific to the kind of layout being used by its containing parent. In the ClockView class's createPartControl() method, add a RowData object to the first and last clocks: clock1.setLayoutData(new RowData(20,20)); clock3.setLayoutData(new RowData(100,100)); 7. Open the Clock View, and the clocks are shown in increasing order of size: [ 48 ] www.it-ebooks.info Chapter 2 What just happened? A Composite is capable of handling multiple widgets, and the job of deciding where to put these components is done by the associated LayoutManager. The standard layout managers include FillLayout, RowLayout, GridLayout, FormLayout, and CellLayout (note that CellLayout is technically not SWT, but part of the Eclipse UI Workbench). The default for Eclipse Views is to use a FillLayout; though a manually created Composite has no associated layout by default. Both FillLayout and RowLayout create a horizontal or vertical set of widgets with controlled sizes. FillLayout is the default for views and expands the size of the widgets to the space available. RowLayout will set the component's sizes to their default size as calculated by computeSize(0,0). Layout managers have different properties such as SWT.HORIZONTAL and SWT.VERTICAL, which change how elements are wrapped if the row becomes full. The documentation for each layout manager has information as to what it supports. Layout data objects are used to specify different values for objects within Composite. The preceding example looked at RowData options. The corresponding FillData class for FillLayout has no public fields, and therefore is of lesser use. Other layout managers, such as GridLayout, have more extensive customization options in the GridData class. Remember when changing LayoutManager the associated layout data objects will need to be modified accordingly. Pop quiz – understanding views Q1. What is the parent class of any views that you create? Q2. How do you register views with the Eclipse workbench? Q3. What two arguments are passed into every SWT widget and what are they for? Q4. What does it mean for a widget to be disposed? Q5. How do you draw a circle on a Canvas? Q6. What listener do you have to register to execute drawing operations? Q7. What happens if you try and update an SWT object from outside a UI thread? Q8. How do you update SWT components from a different thread? Q9. What value is SWT.DEFAULT used for? Q10. How do you specify a specific size for a widget in a RowLayout? [ 49 ] www.it-ebooks.info Creating Views with SWT Have a go hero – drawing hours and minute hands Now that the Clock View is animating a second hand, do the same calculation for the hour and minute hands. Minutes will be calculated the same way as seconds; for hours, multiply the hours by 5 to map onto the same path. Draw lines for every five minutes using the drawLine() function. Some simple maths will be required to calculate the start and end points of the line. Finally, draw the text lettering for the numbers in the right locations. The drawText() method can be used to place a string at a particular place. Use this to print out the current time in the center of the clock, or print out the date. Managing resources One of the challenges in adopting SWT is that native resources must be freed when they are no longer needed. Unlike AWT or Swing, which perform these operations automatically when an object is garbage collected, SWT needs manual resource management. Why does SWT need manual resource management? A common question asked is why SWT has this rule, when Java has had perfectly acceptable garbage collection for many years. In part, it's because SWT predated acceptable garbage collection, but it's also to try and return native resources as soon as they are no longer needed. From a performance perspective, adding a finalize() method to an object also causes the garbage collector to work harder; much of the speed in today's garbage collectors are because they don't need to call methods, as they are invariably missing. It also hurts in SWT's case because the object must post its dispose request onto the UI thread, which delays its garbage collection as the object becomes reachable again. Not all objects need to be disposed; in fact, there is an abstract class Resource, which is the parent of all resources that need disposal. It is this class that implements the dispose() method, as well as the isDisposed() call. Once a resource is disposed, subsequent calls to its methods will throw an exception with a Widget is disposed or Graphic is disposed message. Further confusing matters, some instances of Resources should not be disposed by the caller. Generally, instances owned by other classes in accessors should not be disposed; for example, the Color instance returned by the Display class's getSystemColor() method is owned by the Display class, so shouldn't be disposed by the caller. Resources that are instantiated by the caller must be disposed of explicitly. [ 50 ] www.it-ebooks.info Chapter 2 Time for action – getting colorful To add an option for ClockWidget to have a different color, an instance must be obtained, instead of the hardcoded BLUE reference. Since Color objects are Resources, they must be disposed correctly when the widget is disposed. To avoid passing in a Color directly, the constructor will be changed to take an RGB value (which is three int values), and use that to instantiate a Color object to store for later. The lifetime of the Color instance can be tied to the lifetime of ClockWidget. 1. Add a private final Color instance called color to ClockWidget: private final Color color; 2. Modify the constructor of ClockWidget to take an RGB instance, and use it to instantiate a Color object. Note that the color is leaked at this point, and will be fixed later: public ClockWidget(Composite parent, int style, RGB rgb) { super(parent, style); // FIXME color is leaked! this.color = new Color(parent.getDisplay(),rgb); ... 3. Modify the paintControl() method to use this custom color: protected void paintControl(PaintEvent e) { ... e.gc.setBackground(color); e.gc.fillArc(e.x, e.y, e.width-1, e.height-1, arc-1, 2); 4. Finally, change ClockView to instantiate the three clocks with different colors: public void createPartControl(Composite parent) { ... final ClockWidget clock = new ClockWidget(parent, SWT.NONE, new RGB(255,0,0)); final ClockWidget clock2 = new ClockWidget(parent, SWT.NONE, new RGB(0,255,0)); final ClockWidget clock3 = new ClockWidget(parent, SWT.NONE, new RGB(0,0,255)); [ 51 ] www.it-ebooks.info Creating Views with SWT 5. Now run the application and see the new colors in use: What just happened? The Color object was created based on the red/green/blue value passed in to the ClockWidget constructor. Since RGB is just a value object, it doesn't need to be disposed afterwards. Once the Color is created, it is assigned to the instance field. When the clocks are drawn, the second hands are the appropriate colors. The one problem with this approach is that the Color instance is leaked. When the view is disposed, the associated Color object is garbage collected, but the resources associated with the native handle are not. Time for action – finding the leak It is necessary to know how many resources are allocated in order to know if the leak has been plugged or not. Fortunately, SWT provides a mechanism to do this via the Display and the DeviceData classes. Normally, this is done by a separate plug-in, but in this example ClockView will be modified to show this behavior. 1. At the start of the ClockView class' createPartControl() method, add a call to obtain the number of allocated Objects, via DeviceData of the Display class: public void createPartControl(Composite parent) { Object[] oo=parent.getDisplay().getDeviceData().objects; 2. Iterate through the allocated objects counting how many are instances of Color: int c = 0; for (int i = 0; i < oo.length; i++) if (oo[i] instanceof Color) c++; [ 52 ] www.it-ebooks.info Chapter 2 3. Print the count to the standard error stream: System.err.println("There are " + c + " Color instances"); 4. Now run the code in debug mode and show the Clock View. The following will be displayed in the host Eclipse Console View: There are 0 Color instances There are 0 Color instances There are 0 Color instances For efficiency, SWT doesn't log all the allocated resources all the time. Instead, it's an option which is enabled at startup through an options file, which is a text properties file with name=value pairs. This can be passed to an Eclipse instance at launch via the -debug flag. Fortunately, it is easy to set within Eclipse from the launch configuration's tracing tab. 5. 6. 7. Close the target Eclipse application, if it is running. Go to the launch configuration via the Debug | Debug Configurations menu. Select the Eclipse Application (if it's not selected already) and go to the Tracing tab. Enable the tracing option, and select the org.eclipse.ui plugin. Select both the debug (at the top) and the trace/graphics options: [ 53 ] www.it-ebooks.info Creating Views with SWT 8. Now launch the application by hitting Debug, and open and close the Clock View a few times: There There There There are are are are 87 92 95 98 Color Color Color Color instances instances instances instances What just happened? Clearly, something is leaking three Color instances each time the Clock View is opened. Not surprisingly, three instances of the Color object are allocated in the three instances of ClockWidget. This suggests that there is a resource leak in ClockView or ClockWidget. When SWT is running in trace mode, it will keep a list of previously allocated resources in a global list, which is accessible through the DeviceData object. When the resource is disposed, it will be removed from the allocated list. This allows the monitoring of the state of resources at play in the Eclipse workbench and discover leaks, typically through repeated actions and noting an increase each time in the resource count. Other object types are also stored in this list (for example, Fonts and Images) so it's important to filter by type when looking for a resource set. It's also important to note that Eclipse has its own runtime resources, which are used and so when tracing these are included in the list as well. By learning how to enable tracing and how to programmatically detect what objects are allocated, it will be possible to discover such leaks, or verify whether they have been fixed afterwards. Time for action – plugging the leak Now that the leak has been discovered, it needs to be fixed. The solution is to dispose() the Color once it is finished with, which will be when the view itself is removed. A quick investigation of ClockWidget suggests that overriding dispose() might work. (Note that this is not the correct solution; see later for why.) 1. Create a dispose() method in ClockWidget with the following code: @Override public void dispose() { if(color != null && !color.isDisposed()) color.dispose(); super.dispose(); } [ 54 ] www.it-ebooks.info Chapter 2 2. Run the Eclipse application in debug mode (with the tracing enabled, as before) and open and close the view. The output will show something like: There There There There 3. are are are are 87 91 94 98 Color Color Color Color instances instances instances instances Remove the dispose() method (since it doesn't work as intended) and modify the constructor of ClockWidget to add an anonymous DisposeListener that disposes of the associated Color object: public ClockWidget(Composite parent, int style, RGB rgb) { super(parent, style); this.color = new Color(parent.getDisplay(),rgb); addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { if(color != null && !color.isDisposed()) color.dispose(); } }); } 4. Now run the code and see what happens when the view is opened and closed a few times: There There There There are are are are 87 88 88 88 Color Color Color Color instances instances instances instances The leak has been plugged. What just happened? Once the source of the leak has been identified, the correct course of action is to dispose() the Color object when no longer needed. However, although it is tempting to think that overriding the dispose() method of ClockWidget would be all that is needed, in fact it doesn't work. The only time dispose() is called is at the top level Shell (or ViewPart), and if there are no registered listeners then the dispose method is not called on any components beneath. Since this can be quite counter-intuitive, it is of value to have stepped through code to verify that that is the behavior so that it can be avoided in the future. [ 55 ] www.it-ebooks.info Creating Views with SWT Detecting, and resolving, resource leaks can be a time-consuming process. There is a plug-in, developed by the SWT team, which can perform a snapshot of resources and verify whether there are any leaks using a similar technique to the preceding one. The plug-in is located at the SWT tools update site (refer to http://www.eclipse.org/swt/tools.php and search for Sleak for more information) and can be installed to avoid having to modify code (as was done in this example) for the purposes of monitoring allocated resources. Don't forget when performing tests that the first one or two runs may give different results by virtue of the fact that other resources may be being initialized at the time. Take a couple of readings first before relying on any data, and bear in mind that other plug-ins (that maybe executing in the background) may be doing resource allocation as well. Finally, when working with any SWT widget, it is good practice to check whether the resource is already disposed or not. The JavaDoc for dispose() says that this is not strictly necessary, and that resources which are already disposed will treat this as a no-op method. Pop quiz – understanding resources Q1. Where do resource leaks come from? Q2. What are the different types of Resources? Q3. How can you enable SWT resource tracking? Q4. Once enabled, how do you find out what objects are tracked? Q5. What's the right way and the wrong way to free resources after use? Have a go hero – extending the clock widget Now that the ClockWidget is running, try the following: Write a sleak-like view, which periodically counts allocated objects by type. Modify any text written by acquiring a Font object, with disposal. Create a generic dispose listener that takes an instance of Resource. Provide a setColor() method which allows you to change the color. Interacting with the user The whole point of a user interface is to interact with the user. Having a view which displays information may be useful, but it is often necessary to ask the user for data or respond to user actions. [ 56 ] www.it-ebooks.info Chapter 2 Time for action – getting in focus To allow the time zone of the clock widgets to be changed, a drop-down box (known as Combo) as well as a Button will be added to the view. The Combo will be created from an array of String representing TimeZone IDs. 1. Create a timezones field in the ClockView class: private Combo timezones; 2. At the end of the createPartControl() method, add this snippet to create the drop-down list: public void createPartControl(Composite parent) { ... String[] ids = TimeZone.getAvailableIDs(); timezones = new Combo(parent, SWT.SIMPLE); timezones.setVisibleItemCount(5); for (int i = 0; i < ids.length; i++) { timezones.add(ids[i]); } } 3. Run the Eclipse and open the Clock View again, and a list of time zones will be shown: 4. It's conventional to set the focus on a particular widget when a view is opened. Implement the appropriate call in the ClockView's setFocus() method: public void setFocus() { timezones.setFocus(); } 5. Run Eclipse and open the Clock View, and the time zone drop-down widget will be focused automatically. [ 57 ] www.it-ebooks.info Creating Views with SWT What just happened? Every SWT Control has a setFocus() method, which is used to switch focus for the application to that particular widget. When the view is focused (which happens both when it's opened, and also when the user switches to it after being in a different view) its setFocus() method is called. E4: As will be discussed in Chapter 7, Understanding the Eclipse 4 Model, in E4 the setFocus() method may be called anything and annotated with the @Focus annotation. Conventionally, and to save sanity, it helps to call this method setFocus(). Time for action – responding to input To show the effect of changing the TimeZone, it is necessary to add an hour hand to the clock. When the time zone is changed in the drop-down box, the hour hand will be updated. 1. Add an offset field to ClockWidget along with a setter: private int offset; public void setOffset(int offset) { this.offset = offset; } 2. Getters and setters can be generated automatically. Once the field is added, the Source | Generate Getters and Setters menu option can be used to generate all missing getters and/or setters; in addition, a single getter/setter can be generated by typing set in the class body, followed by Ctrl + Space (Cmd + Space on OS X). 3. Add an hour hand in the paintControl() method using the following: e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_BLACK)); int hours = new Date().getHours() + offset; arc = (3 - hours) * 30 % 360; e.gc.fillArc(e.x, e.y, e.width-1, e.height-1, arc - 5, 10); 4. To update the clock when the time zone is changed, register a SelectionListener property on the Combo in the createPartControl() method of the ClockView class: timezones.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { String z = timezones.getText(); TimeZone tz = z == null ? null : TimeZone.getTimeZone(z); TimeZone dt = TimeZone.getDefault(); [ 58 ] www.it-ebooks.info Chapter 2 int offset = tz == null ? 0 : ( tz.getOffset(System.currentTimeMillis()) dt.getOffset(System.currentTimeMillis())) / 3600000; clock3.setOffset(offset); clock3.redraw(); } public void widgetDefaultSelected(SelectionEvent e) { clock3.setOffset(0); clock3.redraw(); } }); 5. Run the Eclipse instance, and modify the time zone. The updates should be drawn on the last clock instance: What just happened? The Combo box's addSelectionListener() method notifies any changes in the drop-down list. When a notification is received, the text from the Combo box is used to lookup the corresponding time zone offset from the TimeZone class. The difference with the current time zone's offset is subtracted, and since it's in milliseconds, divide by 1000 and then divide by 60 times 60 to convert it to hours. (This also truncates it to an even number of hours.) If the selection is not found, or the default selection is chosen (in this case, the one with no value) then the offset is reset to zero. The clock's hour hand doesn't quite behave properly; typically, the hour hand is shorter than the second hand, and the hour hand jumps between hours instead of smoothly moving round as the time progresses. Fixing this is left as an exercise for the reader. To render the changes immediately, the clock is asked to redraw itself. This could be done inside the ClockView class's setOffset() method; but it would have to check that it was done from the SWT thread, or arrange for it to be posted on the thread asynchronously. Instead, for convenience clock3.redraw() is done immediately after setting the offset, while still inside the SWT thread. [ 59 ] www.it-ebooks.info Creating Views with SWT Pop quiz – understanding widgets Q1. How do you mark a widget as being the default one for a view? Q2. How do you update a widget after modifying it? Q3. What listener can you register with a Combo? Q4. What's the purpose of the widgetDefaultSelected() method? Have a go hero – updating the clock widget Now that the ClockWidget can handle time zones, do the following: Update the hour hand so that the position is calculated based on fractional hours. Display the time zone underneath the clock face in the ClockWidget. Modify the ClockWidget so that it can take a TimeZone rather than an offset. Show if the time displayed is in summer time or not. Using other SWT widgets SWT contains many other widgets other than Canvas, and this section covers some of them. JFace will be covered in the next chapter, which provides a Model-View-Controller view to designing GUIs, but it's helpful to know the base SWT classes upon which they are built. Time for action – adding items to the tray Most operating systems have the concept of a tray as a set of icons visible from the main window, which can provide quick access components. On OS X, these are represented as icons across the top menu bar; on Windows, as icons on the bottom-right near the clock. Linux systems have various approaches which do similar, and some operating systems have none. Since there is only one Tray, it is necessary to add the item only once. The Activator class can be used to ensure that TrayItem is created at startup and removed at shutdown. 1. Open the Activator class and add two private fields: private TrayItem trayItem; private Image image; 2. Add the following to the start() method: final Display display = Display.getDefault(); display.asyncExec(new Runnable() { public void run() { [ 60 ] www.it-ebooks.info Chapter 2 image = new Image(display, Activator.class .getResourceAsStream("/icons/sample.gif")); Tray tray = display.getSystemTray(); if (tray != null && image != null) { trayItem = new TrayItem(tray, SWT.NONE); trayItem.setToolTipText("Hello World"); trayItem.setVisible(true); trayItem.setText("Hello World"); trayItem.setImage(new Image(trayItem.getDisplay(), Activator.class.getResourceAsStream("/icons/sample.gif"))); } } }); 3. Run the test Eclipse instance, and show the Clock View. The small sample.gif icon should appear in the task area (top right of OS X, bottom-right of Windows). 4. To test the effect of stopping and restarting the bundle, open the Console View in the test Eclipse instance. Click on the drop-down list on the top-right of the View to create a Host OSGi Console. WARNING: This console is connected to the current running instance of Eclipse!osgi> "Framework is launched." 5. Type ss clock at the osgi> prompt and it will show a bundle ID, which can be used to start/stop: osgi> ss clock id 4 6. Bundle com.packtpub.e4.clock.ui_1.0.0.qualifier Start and stop the bundle by typing start and stop into the console with the ID given the preceding output: osgi> osgi> osgi> osgi> 7. State RESOLVED stop 4 start 4 stop 4 start 4 Notice that a new TrayItem appears in the Tray each time it is started. The clean routine needs to be added in the Activator class's stop() method: public void stop(BundleContext context) throws Exception { if (trayItem != null && !trayItem.isDisposed()) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (trayItem != null && !trayItem.isDisposed()) [ 61 ] www.it-ebooks.info Creating Views with SWT trayItem.dispose(); } }); } if (image != null && !image.isDisposed()) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (image != null && !image.isDisposed()) image.dispose(); } }); } 8. Re-run the application and start and stop the bundle (probably the same bundle ID as before, but you should check rather than assume); the SWT tray icon should go and come back each time the bundle is stopped and started. What just happened? An SWT TrayItem is added to the system's Tray when the bundle started, and removed it when the bundle stopped. The icon that came with the sample project was used. To use a different one, don't forget to update the build.properties file. Since the tray is a graphical component, if there's no image then the item isn't shown. The tooltips are optional. Note also that not every system has the concept of a tray, so null is a legitimate return value for display.getSystemTray(). A bundle is started automatically when it is loaded, and the loading is triggered by a menu item. If a View is opened, the bundle that class is loaded from is automatically started. They can also be started and stopped programmatically, or through the host OSGi console, which is useful to test whether the Activator class's start() and stop() methods are working correctly. Time for action – responding to the user When the user clicks on the icon, nothing happens. That's because there is not a registered listener on TrayItem itself. There are two listeners that can be registered: a SelectionListener, called when the icon is clicked, and a MenuDetectListener which can respond to a context-sensitive menu. The former will be used to present a clock in its own window, which in SWT terms is called a Shell. 1. 2. Open the Activator class. Add a field to store a Shell: private Shell shell; [ 62 ] www.it-ebooks.info Chapter 2 3. Go to the run() method inside the Runnable in the Activator class's start() method. 4. After the creation of the TrayItem, call addSelectionListener(), which creates a new Shell with a ClockView: trayItem.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { if (shell == null) { shell = new Shell(trayItem.getDisplay()); shell.setLayout(new FillLayout()); new ClockWidget(shell, SWT.NONE, new RGB(255, 0, 255)); shell.pack(); } shell.open(); } }); 5. Run the Eclipse instance, open the Clock View and click on the tray icon. A windowed clock will be shown: 6. Ensure that the Shell is disposed when the bundle is stopped, so add the following to the end of the Activator class 's stop() method: if (shell != null && !shell.isDisposed()) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (shell != null && !shell.isDisposed()) shell.dispose(); } }); } 7. Run the Eclipse instance, click on the TrayItem, and use the host OSGi console to stop and start the bundle. The window should disappear when the bundle is stopped. [ 63 ] www.it-ebooks.info Creating Views with SWT What just happened? When TrayItem is installed into the system's Tray, event listeners can be registered to respond to user input. The one that gets called when the icon is clicked is the SelectionListener, and this gives the opportunity to display the window (or Shell in SWT's terminology). The Display associated with the TrayItem is used when instantiating the Shell. Although either Display.getDefault() or Display.getCurrent() could be used, neither of these would be the right option. When developers are running in multi-monitor mode, or with a virtual display that spans multiple desktops, it's important to ensure that the Shell is shown on the same display as the corresponding Tray. Without a LayoutManager, the clock won't show up. FillLayout is used here to ensure that the clock is made as large as the window (and resizes accordingly to the window itself). Once the window is created the pack() method is called, which sets the size of the window to the preferred size of its children; in this case, ClockView. Finally, the window is shown with the open() call. Time for action – modal and other effects There are a number of style bits that are applicable to windows, and some useful methods to affect how the window appears. For example, it might be desirable to make the clock appear semi-transparently, which allows the clock to float above other windows. SWT's Shell has a number of these options that can be set. 1. Modify the instantiation of the Shell inside the widgetSelected() method in the Activator class's inner class to add SWT.NO_TRIM (no close/minimize/ maximize widgets) and SWT.ON_TOP (floating on top of other windows): shell = new Shell(trayItem.getDisplay(),SWT.NO_TRIM|SWT.ON_TOP); 2. Set the alpha value as 128, which is semi-transparent: shell.setAlpha(128); 3. Run the Eclipse instance, and click on the tray item to see what kind of window is created. 4. To create a modal window (and thus, prevent interaction on the main window), change the flag to use SWT.APPLICATION_MODAL: shell = new Shell(trayItem.getDisplay(),SWT.APPLICATION_MODAL); [ 64 ] www.it-ebooks.info Chapter 2 5. To make the application full-screen, call either setFullScreen() or setMaximized() depending on the platform: shell.setFullScreen(true); shell.setMaximized(true); 6. Note that without trims, it may be necessary to add controls such as detecting selection events to close the window. 7. 8. 9. Run the Eclipse application and see the effect these flags have on the window. Change the Shell back to use SWT.NO_TRIM and SWT.ON_TOP. To calculate a circular shape for the floating clock window, add the circle() method to the Activator class, which has been taken from Snippet134. java (taken from the SWT snippets page at http://www.eclipse.org/swt/ snippets/): private static int[] circle(int r, int offsetX, int offsetY) { int[] polygon = new int[8 * r + 4]; //x^2 + y^2 = r^2 for (int i = 0; i < 2 * r + 1; i++) { int x = i – r; int y = (int)Math.sqrt(r*r – x*x); polygon[2*i] = offsetX + x; polygon[2*i+1] = offsetY + y; polygon[8*r - 2*i - 2] = offsetX + x; polygon[8*r - 2*i - 1] = offsetY – y; } return polygon; } 10. Finally, change the shape of the window to be circular by setting a Region on the Shell. This will have the effect of making it look like the clock itself is floating. Add the following code after the Shell is created in the widgetSelected() method: final Region region = new Region(); region.add(circle(25, 25, 25)); shell.setRegion(region); 11. When run, the clock will look something like this: [ 65 ] www.it-ebooks.info Creating Views with SWT 12. For completeness, register a dispose listener on the shell to ensure that Region is cleaned up: shell.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { if (region != null && !region.isDisposed()) region.dispose(); } }); What just happened? Varying the flags used to create the shell affects how that window is displayed and interacts with the user. Other calls on the shell can programmatically drive transitions to full-screen and maximized or minimized status, which can be useful in specific circumstances. Some windowing systems differentiate maximized and full-screen; others have distinct characteristics. The SWT.NO_TRIM flag is used to display a window without the normal window furniture. This can be combined with setting a region via setRegion(), which allows creation of a non-rectangular shape. Often, windows without trim are floating, that is, they should stay on top of the application window even when it hasn't got the focus. To achieve this, set the SWT.ON_TOP flag as well, and adjust the alpha (transparency) value with setAlpha(). The alpha value is between 0 (fully transparent) and 255 (fully opaque). A Region can be defined from a set of connected points, or set on a pixel-by-pixel basis. It's important to note that a Region is also a Resource, and thus must be disposed of after use (which is typically when the Shell is closed). The clean-up operation is similar to that of the others discussed earlier, via the addDisposeListener() on the Shell. Time for action – groups and tab folders A new TimezoneView will show a list of clocks in time zones around the world. This time, instead of using the plug-in wizard, the extension will be added manually. E4: The way views are defined for E4 is covered in Chapter 7, Understanding the Eclipse 4 Model. This chapter discusses how to do it in Eclipse 3.x and the Eclipse 3.x compatibility model of Eclipse 4.x. 1. Right-click on the project and navigate to Plug-in Tools | Open Manifest, or find the plugin.xml file in the navigator and double-click on it. [ 66 ] www.it-ebooks.info Chapter 2 2. Go to the manifest editor's Extensions tab. The extensions will list org.eclipse. ui.views. Expand this, and underneath the Timekeeping (category) the Clock View (view) will be displayed, added via the plug-in wizard. 3. 4. Right-click on org.eclipse.ui.views and navigate to New | view from the menu. A placeholder entry name (view) will be added to the list, and the right-hand side lists properties such as the id, name, class, and category. Fill in the following: ID: com.packtpub.e4.clock.ui.views.TimeZoneView Name: Time Zone View Class: com.packtpub.e4.clock.ui.views.TimeZoneView Category: com.packtpub.e4.clock.ui Icon: icons/sample.gif Save the file. The following will be added into the plugin.xml file: <view category="com.packtpub.e4.clock.ui" class="com.packtpub.e4.clock.ui.views.TimeZoneView" icon="icons/sample.gif" id="com.packtpub.e4.clock.ui.views.TimeZoneView" name="Time Zone View" restorable="true"> </view> 5. Create the TimeZoneView class. The easiest way is to go to the plugin.xml file's Extensions tab, select the Time Zone View and click on the hyperlinked class* label next to the class name. Alternatively, use the New Class wizard by navigating to File | New | Class to create TimeZoneView as a subclass of ViewPart, in the com. packtpub.e4.clock.ui.views package. 6. Create a class called TimeZoneComparator, which implements Comparator, in a new package com.packtpub.e4.clock.ui.internal. It is conventional to provide utility classes in an internal package to ensure that the implementation is not visible to others. The compare method should delegate to the TimeZone class's compareTo() method: public class TimeZoneComparator implements Comparator { public int compare(Object o1, Object o2) { if(o1 instanceof TimeZone && o2 instanceof TimeZone) { return ((TimeZone) o1).getID(). compareTo(((TimeZone) o2).getID()); } else { throw new IllegalArgumentException(); } } } [ 67 ] www.it-ebooks.info Creating Views with SWT 7. Add a public static method to the TimeZoneComparator class called getTimeZones(), which will return a Map of Sets of TimeZones. The Map will be indexed by the first half of the TimeZone class's ID. (A TimeZone class's ID is something like Europe/Milton_Keynes or America/New_York.) This will group all TimeZone in Europe together, and all TimeZone in America together: public static Map<String, Set<TimeZone>> getTimeZones(){ String[] ids = TimeZone.getAvailableIDs(); Map<String, Set<TimeZone>> timeZones = new TreeMap<String, Set<TimeZone>>(); for (int i = 0; i < ids.length; i++) { String[] parts = ids[i].split("/"); if (parts.length == 2) { String region = parts[0]; Set<TimeZone> zones = timeZones.get(region); if (zones == null) { zones = new TreeSet<TimeZone>(new TimeZoneComparator()); timeZones.put(region, zones); } TimeZone timeZone = TimeZone.getTimeZone(ids[i]); zones.add(timeZone); } } return timeZones; } 8. In the createPartControl() method in TimeZoneView, create CTabFolder and then iterate through the time zones, creating CTabItem for each one: public void createPartControl(Composite parent) { Map<String, Set<TimeZone>> timeZones = TimeZoneComparator.getTimeZones(); CTabFolder tabs = new CTabFolder(parent, SWT.BOTTOM); Iterator<Entry<String, Set<TimeZone>>> regionIterator = timeZones.entrySet().iterator(); while(regionIterator.hasNext()) { Entry<String, Set<TimeZone>> region = regionIterator.next(); CTabItem item = new CTabItem(tabs, SWT.NONE); item.setText(region.getKey()); } tabs.setSelection(0); } [ 68 ] www.it-ebooks.info Chapter 2 9. Run this example and show the Time Zone View, and there should be a populated list of tabs along the bottom: 10. Inside the while loop, add a Composite instance to hold multiple ClockWidget classes for each TimeZone instance: item.setText(region.getKey()); // from before Composite clocks = new Composite(tabs, SWT.NONE); clocks.setLayout(new RowLayout()); item.setControl(clocks); 11. Now iterate through the TimeZones, adding a ClockWidget for each: RGB rgb = new RGB(128, 128, 128); TimeZone td = TimeZone.getDefault(); Iterator<TimeZone> timezoneIterator = region.getValue(). iterator(); while (timezoneIterator.hasNext()) { TimeZone tz = timezoneIterator.next(); ClockWidget clock = new ClockWidget(clocks, SWT.NONE, rgb); clock.setOffset(( tz.getOffset(System.currentTimeMillis()) td.getOffset(System.currentTimeMillis())) / 3600000); } 12. Run the Eclipse instance and open the Time Zone View to see all of the clocks: [ 69 ] www.it-ebooks.info Creating Views with SWT 13. To make the clocks more identifiable, each will be put into a Group with an associated text label, so that the view hierarchy goes from CTabItem→Composite→ClockWidget to CTabItem→Composite→ Group→ClockWidget. Replace the call to create the the ClockWidget with this: ClockWidget clock = new ClockWidget(clocks, SWT.NONE, rgb); Group group = new Group(clocks,SWT.SHADOW_ETCHED_IN); group.setText(tz.getID().split("/")[1]); ClockWidget clock = new ClockWidget(group, SWT.NONE, rgb); 14. Run it again, and a series of blank elements will be shown: 15. Since the default layout manager for general Composite classes is null, Groups don't have a layout manager and thus, the clocks are not getting sized appropriately. This can be fixed by setting a layout manager explicitly: group.setLayout(new FillLayout()); 16. Run it again, it looks a little bit more sane: 17. The clocks at the bottom are squashed and the view can't be scrolled, even though there are clearly more time zones available. To add scrolling to a widget, the ScrolledComposite can be used. This provides automatic scroll bars and interaction with the user to permit a much larger virtual area to be scrolled. The View hierarchy will change from CtabItem→Composite→Group→ClockWidget to CTa bItem→ScrolledComposite→Composite→Group→ClockWidget instead: Composite clocks = new Composite(tabs, SWT.NONE); item.setControl(clocks); ScrolledComposite scrolled = new [ 70 ] www.it-ebooks.info Chapter 2 ScrolledComposite(tabs,SWT.H_SCROLL | SWT.V_SCROLL); Composite clocks = new Composite(scrolled, SWT.NONE); item.setControl(scrolled); scrolled.setContent(clocks); 18. Run it again, but unfortunately this will be seen: 19. The problem is that ScrolledComposite has no minimum size. This can be calculated from the clocks container. Add this to the bottom of the while loop, after the contents of ScrolledComposite have been created: Point size = clocks.computeSize(SWT.DEFAULT,SWT.DEFAULT); scrolled.setMinSize(size); scrolled.setExpandHorizontal(true); scrolled.setExpandVertical(true); 20. Run it again, and the clocks now show up as expected: 21. The ScrolledComposite has a different background. To change it, add this line after constructing the clock's Composite: clocks.setBackground(Display.getDefault() .getSystemColor(SWT.COLOR_LIST_BACKGROUND)); [ 71 ] www.it-ebooks.info Creating Views with SWT 22. Now the Time Zone View is complete: What just happened? A combination of Composite types created a tabbed environment using CTabFolder and CTabItem instances. Inside each CTabItem, a ScrolledComposite contained a Composite of multiple Group instances, each of which had a single ClockWidget. Adding the ScrolledComposite provided the scrolling for free, and Group allowed us to place text above the ClockWidget to display its time zone. Some of the components used here lie in the org.eclipse.swt. custom package, instead of the org.eclipse.swt.widgets package. Several of these begin with C as a custom designator to distinguish similarly named widgets. The CTabFolder/Item is an SWT-implemented class that provides the tab functionality; the corresponding OS widget TabFolder/ Item uses a native rendered tab switcher. Pop quiz – using SWT Q1. How do you add an icon to the system menu? Q2. What does the SWT.NO_TRIM style do for Shell objects? Q3. How do you make a Shell transparent? Q4. What do you need to set to create a nonrectangular Shell? Q5. What Composite allows you to attach a label to a set of related items? Q6. What is the default layout manager for a Group instance? Q7. How do you add scrolling to an existing widget? [ 72 ] www.it-ebooks.info Chapter 2 Have a go hero – enhancing the time zones A set of times are displayed in different time zones, but there is scope for enhancements: Switch to the tab with the user's default time zone when the view is created Sort the clocks by time zone offset, rather than by name of the region Create a favorites tab and allow it to be populated by drag-and-drop Improve the speed of updates by sharing a single Thread to update all clocks Improve the sizing of the ScrollableComposite so that more than one row is displayed Summary In this chapter, we covered how to create views with SWT widgets. We looked at both standard widget types as well as creating our own, and how those widgets can be assembled into groups with Composite and Layout classes. We also looked at how resources are managed within SWT, including stepping through the debug procedure for detecting and eliminating leaks. In the next chapter, we will look at how to use a higher level of abstraction, JFace. [ 73 ] www.it-ebooks.info www.it-ebooks.info 3 Creating JFace Viewers In the last chapter, we looked at the basic building blocks of SWT, which provide a glue layer between the native operating system's widgets and Java. We'll now look at JFace, which builds upon SWT to provide an MVC architecture as well as many of the common widgets used by Eclipse. In this chapter we will cover: Creating a view for showing hierarchical data Using image, font, and color resources Generating styled text Sorting and filtering entries in viewers Adding double-click actions Selections and property support Creating a view for showing tabular data Why JFace? While SWT provides generic implementations for basic widgets (such as trees, buttons, labels, and so on), these often work at a level that deals with strings and responding to selection by integer index. To make it easier to display structured content, JFace provides several viewers, which provide combinations of SWT widgets and event managers to provide a UI for structured content. www.it-ebooks.info Creating JFace Viewers There are many types of viewers (all subclasses of Viewer), but the most common ones are ContentViewers such as the TreeViewer and TableViewer. There are also text-based viewers (TextViewer has subclasses for SourceViewer) as well as operational views (ConsoleViewer for the Console view, DetailedProgressViewer for the Progress view). In this chapter, we'll create views based on TreeViewer and TableViewer. Since JFace is based on SWT, knowing how SWT works is essential to understand how JFace is used. Creating TreeViewers Many widgets in Eclipse are based upon a tree-like view, from a file navigator to class contents. The JFace framework provides a TreeViewer, which provides all of this functionality. This will be used to provide a TimeZoneTreeView. Time for action – creating a TreeViewer As done in the previous chapter, a new TimeZoneTreeView will be created using the plugin.xml editor. This view will show the time zones, organized hierarchically by region. 1. Right-click on the com.packtpub.e4.clock.ui project and select Plug-in Tools | Open Manifest, if it's not open already. 2. Open the Extensions tab and go to the org.eclipse.ui.views. Right-click on this and choose New | View, and fill in the following fields: 3. ID: com.packtpub.e4.clock.ui.views.TimeZoneTreeView Name: Time Zone Tree View Class: com.packtpub.e4.clock.ui.views.TimeZoneTreeView Category: com.packtpub.e4.clock.ui Icon: icons/sample.gif An entry is created in the plugin.xml file that looks like the following code snippet: <view category="com.packtpub.e4.clock.ui" class="com.packtpub.e4.clock.ui.views.TimeZoneTreeView" icon="icons/sample.gif" id="com.packtpub.e4.clock.ui.views.TimeZoneTreeView" name="Time Zone Tree View" restorable="true"> </view> 4. As before, create the class TimeZoneTreeView, which extends ViewPart. [ 76 ] www.it-ebooks.info Chapter 3 5. In the view's createPartControl() method, create an instance of a TreeViewer, with the V_SCROLL, H_SCROLL, and MULTI flags set and store it in a field. Implement the setFocus() method of the view to that of the tree viewer's control: public class TimeZoneTreeView extends ViewPart { private TreeViewer treeViewer; public void createPartControl(Composite parent) { treeViewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL ); } public void setFocus() { treeViewer.getControl().setFocus(); } } E4: Although E4 will be discussed in detail in Chapter 7, Understanding the Eclipse 4 Model, in E4 the @Inject annotation is needed above the createPartControl() method (to supply the parent Composite) and @Focus above the setFocus() method. This allows the view to be a POJO with no Eclipse UI specific references, other than SWT. 6. Run the Eclipse application, and show the view by navigating to Window | Show View | Timekeeping | Time Zone Tree View: 7. Unlike Swing, which expects data to be presented in a specific interface, the JFace viewers don't expect any specific data class. Instead, they expect an object value to display (the input), an interface which can read that data (the content provider), and an interface for displaying that data (a label provider). [ 77 ] www.it-ebooks.info Creating JFace Viewers 8. Create a new class, called TimeZoneLabelProvider, which extends LabelProvider (from the org.eclipse.jface.viewers package). This has a method called getText(), which is passed an object and translates that into a textual representation. Instead of a toString() call here, return an appropriate value for a Map.Entry or a TimeZone: public class TimeZoneLabelProvider extends LabelProvider { public String getText(Object element) { if (element instanceof Map) { return "Time Zones"; } else if (element instanceof Map.Entry) { return ((Map.Entry) element).getKey().toString(); } else if (element instanceof TimeZone) { return ((TimeZone) element).getID().split("/")[1]; } else { return "Unknown type: " + element.getClass(); } } } Since a TreeViewer can have multiple roots, the instanceof Map test is used to represent the top of the tree, called Time Zones. 9. It's usually a good idea to have a default value—even if it's only an empty string—so that when an unexpected value type is seen in the list, it can be recognized and debugged. 10. Create a new class, TimeZoneContentProvider, which implements the ITreeContentProvider interface. This requires the implementation of three methods: hasChildren(): returns true if the node has children getChildren(): provides the children of a given node getElements(): provides the top-level roots 11. The hasChildren() will return true, if a Map or Collection is passed to it, and it's not empty; otherwise, if a Map.Entry is passed, recurse. For trees based on nested Map or Collection, the hasChildren() method will look identical: public boolean hasChildren(Object element) { if (element instanceof Map) { return !((Map) element).isEmpty(); } else if (element instanceof Map.Entry) { return hasChildren(((Map.Entry)element).getValue()); } else if (element instanceof Collection) { return !((Collection) element).isEmpty(); } else { [ 78 ] www.it-ebooks.info Chapter 3 return false; } } 12. The getChildren() implementation recurses into a Map, Collection, or Map. Entry following the same pattern. Since the return of this function is an Object[], the built-in functionality of Map is used to convert the contents via an entrySet() to an array: public Object[] getChildren(Object parentElement) { if (parentElement instanceof Map) { return ((Map) parentElement).entrySet().toArray(); } else if (parentElement instanceof Map.Entry) { return getChildren(((Map.Entry)parentElement).getValue()); } else if (parentElement instanceof Collection) { return ((Collection) parentElement).toArray(); } else { return new Object[0]; } } 13. The key to implementing a ITreeContentProvider is to remember to keep the implementation of the getChildren() and hasChildren() methods in sync. One way of doing this is to implement the hasChildren() method in terms of getChildren() returning an empty array, but this may be less performant if the getChildren() is an expensive operation. 14. Since a TreeViewer can have multiple roots, there is a method to get the array of roots from the input element object. A bug in the JFace framework prevents the getElements() argument containing its own value. It is therefore, conventional to pass in an array (containing a single element) and return it. This method will be identical for every TreeContentProvider that you're ever likely to write: public Object[] getElements(Object inputElement) { if (inputElement instanceof Object[]) { return (Object[]) inputElement; } else { return new Object[0]; } } Since a TreeViewer can have multiple roots, there is a method to get the array of roots from the input element object. [ 79 ] www.it-ebooks.info Creating JFace Viewers 15. Now that the providers are complete, in the createPartControl() method of TimeZoneTreeViewer, connect the providers to the viewer, and finally set the input data object: treeViewer.setLabelProvider(new TimeZoneLabelProvider()); treeViewer.setContentProvider(new TimeZoneContentProvider()); treeViewer.setInput(new Object[] {TimeZoneComparator.getTimeZones()}); 16. Run the Eclipse instance, open the view with Window | Show View | Timekeeping | Time Zone Tree View and see the results: What just happened? The data of TreeViewer was provided by the setInput() method, which is almost always an array of objects, even if it contains only a single element. To unpack the data structure, the ITreeContentProvider interface provides two key methods: hasChildren() and getChildren(). These allow the data structure to be walked on demand as the user opens and closes nodes in the tree. The rationale for having two separate methods is that the calculation for getChildren() may be expensive; the hasChildren() is used to show the expandable icon in the node, but the getChildren() is deferred until the user opens that node in the tree. For data structures that support it, implement the getParent() method as well; this makes accessing (or revealing) the object possible. With this method implemented, viewer.reveal(Object) will expand the nodes in the hierarchy to reveal that particular object. To render the labels in the tree, a LabelProvider is used. This provides a label (and an optional image) for each element. It is possible to present a different icon for each type of object; this is used by the Package view in the Java perspective to present a class icon for the classes, a package icon for the packages, and so on. The LabelProvider can render the messages in different ways; for example, if could append the timezone offset (or only show the difference between that and GMT). [ 80 ] www.it-ebooks.info Chapter 3 Time for action – using Images in JFace The TimeZoneLabelProvider can return an Image, which is a standard SWT widget. Although the Image can be loaded (as in the previous chapter), in JFace there are resource registries, which can be used to manage a set of resources for the application. These include the ImageRegistry, FontRegistry, and ColorRegistry classes. The purpose of a resource registry is to maintain a list of Resource instances and ensure that they are correctly disposed, but only when they are no longer needed. JFace has a set of these global registries; but there are specific ones, such as the ones used by the IDE to maintain a folder and file type icons, for example. These use descriptors to hold a meaning for the resource, and a means to acquire an instance of the resource based on that descriptor. The returned resource is owned by the registry, and as such, should not be disposed by clients that acquire them. 1. In the TimeZoneLabelProvider, add a method getImage() that uses the Workbench's ImageRegistry to provide the folder icon. Implement it as follows: public Image getImage(Object element) { if(element instanceof Map.Entry) { return PlatformUI.getWorkbench().getSharedImages() .getImage(ISharedImages.IMG_OBJ_FOLDER); } else { return super.getImage(element); } } 2. Run the Eclipse instance, and open the Time Zone Tree View. A folder icon will be shown; regions show a folder shown for each of the time zones in your view. The Image instance doesn't need to be disposed, because it's owned by the PlatformUI plug-in (the images are disposed when the PlatformUI shuts down): [ 81 ] www.it-ebooks.info Creating JFace Viewers E4: If using E4, the ISharedImages instance can be obtained via injection: @Inject private ISharedImages images; images.getImage(ISharedImages.IMG_OBJ_FOLDER); 3. To use a different image, either the global ImageRegistry of JFaceRegistry could be used, or one can be instantiated separately. Using the global one will work, but it means that effectively the Image instance never gets disposed, since the JFaceRegistry will last for the lifetime of your Eclipse instance. Instead, create a LocalResourceManager and ImageRegistry, which are tied to the lifetime of the control. When the parent control is disposed, the images will be disposed automatically. These should be added to the createPartControl() method of TimeZoneTreeView. public void createPartControl(Composite parent) { ResourceManager rm = JFaceResources.getResources(); LocalResourceManager lrm = new LocalResourceManager(rm,parent); 4. Using the LocalResourceManger, create an ImageRegistry, and add an ImageDescriptor from a URL using createFromURL(). ImageRegistry ir = new ImageRegistry(lrm); URL sample = getClass().getResource("/icons/sample.gif"); ir.put("sample", ImageDescriptor.createFromURL(sample)); 5. Now the ImageRegistry is populated, it must be hooked up to the LabelProvider, so that it can show the right Image on demand. Pass the image registry into the constructor of the TimeZoneLabelProvider. treeViewer.setLabelProvider(new TimeZoneLabelProvider()); treeViewer.setLabelProvider(new TimeZoneLabelProvider(ir)); 6. Implement the constructor in the TimeZoneLabelProvider to store the ImageRegistry, and use it to acquire the image in the getImage() call: private final ImageRegistry ir; public TimeZoneLabelProvider(ImageRegistry ir) { this.ir = ir; } public Image getImage(Object element) { if(element instanceof Map.Entry) { return ir.get("sample"); } else if(element instanceof TimeZone) { return ir.get("sample"); } else { return super.getImage(element); } } [ 82 ] www.it-ebooks.info Chapter 3 7. Now, when opening the view, the sample gif is used as a tree icon: What just happened? To start with, standard images from the PlatformUI were used, using predefined descriptors from ISharedImages. The names of the descriptors begin with IMG, and then follow a predefined pattern: etool and dtool: Enabled and disabled toolbar icons elcl and dlcl: enabled and disabled local toolbar icons dec: decorator obj and objs: object(s) (files, folders, and so on) Other plug-ins have a similar set of images; such as JDT UI, which adds icons for packages, classes, methods, and fields. To use custom images, an ImageRegistry was created backed by a LocalResourceManager. When a Control is passed into the constructor, it registers itself as a DisposeListener—so when the control is disposed, so are the associated images. This also makes the code cleaner, because the ImageRegistry can be passed into the TimeZoneContentProvider. Finally, the ImageRegistry is initialized with a set of ImageDescriptors—in this case, the icons/sample.gif that came from the new plug-in project wizard. The same key is used when both initializing and accessing the image. Some Eclipse projects follow a convention of having an ISharedImages interface with a set of constants. [ 83 ] www.it-ebooks.info Creating JFace Viewers Time for action – styling label providers The IStyledLabelProvider is used to style the representation of the tree viewer, as used by the Java outline viewer for displaying the return type of the method, and by the team's decorator when showing when changes have occurred. 1. Add the IStyledLabelProvider interface to the TimeZoneLabelProvider, and create the getStyledText() method. If the selected element is a Map.Entry that contains a TimeZone, add the offset afterwards in brackets. public class TimeZoneLabelProvider extends LabelProvider implements IStyledLabelProvider { public StyledString getStyledText(Object element) { String text = getText(element); StyledString ss = new StyledString(text); if (element instanceof TimeZone) { int offset = -((TimeZone) element).getOffset(0); ss.append(" (" + offset / 3600000 + "h)", StyledString.DECORATIONS_STYLER); } return ss; } } 2. In order to use the styled label provider, it has to be wrapped within a DelegatingStyledCellLabelProvider. Modify the constructor, called from the createPartControl() method of TimeZoneTreeView. treeViewer.setLabelProvider( new DelegatingStyledCellLabelProvider( new TimeZoneLabelProvider(ir))); 3. Run the Eclipse instance, open the view, and the offset is displayed in a different color: [ 84 ] www.it-ebooks.info Chapter 3 4. To change the Font used by the view, the TimeZoneLabelProvider needs to implement the IFontProvider interface. JFace's FontRegistry can be used to return an italicized version of the default font. Add a FontRegistry parameter to the TimeZoneLabelProvider constructor, and implement the getFont() method as follows: public class TimeZoneLabelProvider extends LabelProvider implements IStyledLabelProvider, IFontProvider { private final FontRegistry fr; public TimeZoneLabelProvider(ImageRegistry ir, FontRegistry fr){ this.ir = ir; this.fr = fr; } public Font getFont(Object element) { Font italic = fr.getItalic(JFaceResources.DEFAULT_FONT); return italic; } } 5. Modify the TimeZoneTreeView to instantiate and pass in the global FontRegistry from the JFaceResources class. FontRegistry fr = JFaceResources.getFontRegistry(); treeViewer.setLabelProvider( new DelegatingStyledCellLabelProvider( new TimeZoneLabelProvider(ir))); treeViewer.setLabelProvider( new DelegatingStyledCellLabelProvider( new TimeZoneLabelProvider(ir, fr))); 6. Run the Eclipse instance again, and now the time zones should be shown in an italic font. What just happened? By implementing the IStyledLabelProvider and wrapping it with a DelegatingStyledCellLabelProvider, the style of the individual elements in the tree can be controlled, including any additions or style/colors of the item. The StyledText can render the string in different styles. Although the DecorationsStyler was used here, additional stylers can be defined with StyledString.createColorRegistryStyler("foreground", "background"), where the two strings are keys in the global JFace ColorRegistry. [ 85 ] www.it-ebooks.info Creating JFace Viewers Although Colours can be changed on a character-by-character basis, the Font is global for the string. That's because when the label is calculated, its size is calculated based on the assumption that the string is displayed in a single Font. It's generally good programming practice to have the content or label providers use resource managers that are passed in at construction time. That way, the code can be tested using automated tests or other mock resources. Whether using the Eclipse 3.x or the Eclipse 4.x programming model, decoupling where the resources come from is key to testing. Pop quiz – understanding JFace Q1. What methods are present on LabelProvider? Q2. What is the difference between hasChildren() and getChildren() on the ContentProvider? Q3. What is an ImageRegistry used for? Q4. How do you style entries in a TreeView? Have a go hero – adding images for regions Now that the basics have been covered, try extending the example as follows: Correct the TimeZoneLabelProvider so that it shows hours/minutes offset from GMT Update the plug-in with a number of flag icons, and then create entries for the image registry (the name of the time zone can be used for the key, which will make accessing it easier) Display the name of the region in italics, but the time zones themselves in bold font Sorting and filtering One of the features of JFace is that the ordering of the data can be processed by the view, rather than having to require that the data structure do the processing. This makes it easy to present filtered views, where the user either searches for a particular term, or performs a sort in a different manner. These filters are used heavily in the Eclipse IDE, where options such as Hide libraries from external and Hide closed projects can be found in many of the drop-down actions for the view. [ 86 ] www.it-ebooks.info Chapter 3 Time for action – sorting items in a viewer The TreeViewer is already showing data in a sorted format, but this is not a view-imposed sort. Because the data is stored in a TreeMap, the sort ordering is created by the TreeMap itself, which in turn is sorting on the value of toString(). To use a different ordering (say, based on offset) the choices are either to modify the TreeMap to add a Comparator and sort the data at creation time, or add a sorter to the TreeViewer. The first choice is applicable if the data is only used by a single view, or if the data is coming from a large external data store, which can perform the sorting more efficiently (such as a relational database). For smaller data sets, the sorting can be done in the viewer itself. 1. JFace structured viewers allow view-specific sorting with the ViewerComparator. Create a new subclass, TimeZoneViewerComparator, in the package com. packtpub.e4.clock.ui.internal, and implement the compare() method as follows: public class TimeZoneViewerComparator extends ViewerComparator { public int compare(Viewer viewer, Object o1, Object o2) { int compare; if (o1 instanceof TimeZone && o2 instanceof TimeZone) { compare=((TimeZone)o2).getOffset(System.currentTimeMillis()) - ((TimeZone)o1).getOffset(System.currentTimeMillis()); } else { compare = o1.toString().compareTo(o2.toString()); } return compare; } } 2. Hook it up to the viewer as follows: treeViewer.setComparator(new TimeZoneViewerComparator()); 3. Run the Eclipse instance, open the Time Zone Tree View, and the TimeZones should be sorted first by offset, then alphabetically: [ 87 ] www.it-ebooks.info Creating JFace Viewers 4. To add a viewer-specific sort, modify the compare() method of TimeZoneViewerComparator to get a REVERSE key from the viewer's data. Use that to invert the results of the sort: return compare; boolean reverse = Boolean.parseBoolean( String.valueOf(viewer.getData("REVERSE"))); return reverse ? -compare : compare; 5. To see the effect of this sort, set the REVERSE key just before the setComparator() call at the end of the createPartControl() method of TimeZoneTreeView. treeViewer.setData("REVERSE",Boolean.TRUE); treeViewer.setComparator(new TimeZoneViewerComparator()); 6. Re-launch the Eclipse instance, and the view should be in the reverse order. What just happened? By adding a ViewerComparator to the Viewer, the data can be sorted in an appropriate manner for the viewer in question. Typically, this will be done in conjunction with selecting an option in the view—for example, an option may be present to reverse the ordering, or to sort by name or offset. When implementing a specific Comparator, check that the method can handle multiple object types (including ones that may not be expected). The data in the viewer may change, or be different at runtime than expected. Use instanceof to check that the items are of the expected type. To store properties that are specific to a viewer, use the setData() and getData() calls on the viewer itself. This allows a generic comparator to be used across views while still respecting per view filtration/sorting operations. The preceding example hardcodes the sort data, which requires an Eclipse relaunch to see the effect. Typically, after modifying properties that may affect the view's sorting or filtering, the viewer has a refresh() invoked to bring the display in line with the new settings. [ 88 ] www.it-ebooks.info Chapter 3 Time for action – filtering items in a viewer Another common feature of viewers is filtering. This is used when performing a manual search as well as for filtering-specific aspects from a view. Quite often, the filtering is connected to the view's menu, which is the drop-down triangle on the top right of the view, using a common name such as Filters. The ViewerFilter class provides a filtering method, confusingly called select(). (There are some filter() methods, but these are used to filter the entire array; the select() method is used to determine if a specific element is shown or not.) 1. Create a class, TimeZoneViewerFilter, in the com.packtpub.e4.clock. ui.internal package, which extends ViewerFilter. It should take a String pattern in the constructor, and return true if the element is a TimeZone with that pattern in its display name. public class TimeZoneViewerFilter extends ViewerFilter { private String pattern; public TimeZoneViewerFilter(String pattern) { this.pattern = pattern; } public boolean select(Viewer v, Object parent, Object element) { if(element instanceof TimeZone) { TimeZone zone = (TimeZone)element; return zone.getDisplayName().contains(pattern); } else { return true; } } } 2. The filter is set on the viewer; since views can have multiple filters, it set this as an array on the corresponding viewer. The pattern to filter is passed into the constructor in this case, but would normally be taken from the user. Modify the TimeZoneTreeViewer class at the bottom of the createPartControl() method: treeViewer.setFilters(new ViewerFilter[] { new TimeZoneViewerFilter("GMT")}); [ 89 ] www.it-ebooks.info Creating JFace Viewers 3. Now run the Eclipse instance and open the Time Zone Tree View; only time zones in the etc region are listed: 4. To remove the expandable nodes next to the tree items, the TreeViewer can be configured to expand nodes automatically: treeViewer.setExpandPreCheckFilters(true); 5. Now run the Eclipse instance and open the Time Zone Tree View. The empty groups are still listed, but the expandable markers only appear on those which have children after filtration. What just happened? The TimeZoneViewerFilter class was created as a subclass of ViewerFilter and set on the TreeViewer. When displaying and filtering the data, the filter is called for every element in the tree (including the root node). By default, if the hasChildren() method returns true, the expandable icon is shown. When clicked, it will iterate through the children, applying the filter to them. If all the elements are filtered, the expandable marker will be removed. By calling setExpandPreCheckFilters(true) on the viewer, it will verify that at least one child is left after filtration. This has no negative effect when there aren't any filters set. If there are filters set and there are large data sets, it may take some time to perform the calculation of whether they should be filtered or not. To show all the tree's elements by default, or collapse it down to a single tree, use expandAll() and collapseAll() on the viewer. This is typically bound to a local view command with a [+] and [-] icon (for example, the Synchronize view or the Package Explorer). [ 90 ] www.it-ebooks.info Chapter 3 If the data is a tree structure, which only needs to show some levels by default, there is an expandToLevel() and collapseToLevel(), which take an integer and an object (use the getRoot() of the tree if not specified) and mark everything as expanded or collapsed to that level. The expandAll() method is a short-hand for expandToLevel(getRoot(), ALL_LEVELS). When responding to a selection event, which contains a hidden object, it is conventional to perform a reveal() operation on the object to make it visible in the tree. Note that reveal() only works when the getParent() is correctly implemented, which isn't the case with this example. Pop quiz – understanding sorting and filters Q1. How can elements of a tree be sorted in an order which isn't its default? Q2. What method is used to filter elements? Q3. How can multiple filters be combined? Have a go hero – expanding and filtering Now that views can be sorted and filtered, try the following: Add a second filter which removes all time zones, with a negative offset When the view is opened, perform an expandAll() operation of the elements Provide a sort that sorts the regions in reverse order, but the time zones in ascending order Provide a dialog that can be used to update the filter and use the empty string, which can be used to reset the filter Interaction and properties Being able to display data is one thing, but invariably views need to be interactive. Whether that's hooking up the sort or filter functionality from the previous chapter, or seeing information about the selected item, views must be interactive not only for exploring data, but also for working with data. [ 91 ] www.it-ebooks.info Creating JFace Viewers Time for action – adding a double-click listener Typically, a tree view is used to show content in a hierarchical manner. However, a tree on its own is not enough to be able to show all the details associated with an object. When the user double-clicks on an element, more details can be shown. 1. At the end of the createPartControl() method of TimeZoneTreeView, register an anonymous inner class that implements the IDoubleClickListener interface with the addDoubleClickListener() method on the treeViewer. As with the example in Chapter 1, Creating Your First Plug-in, this will open a message dialog to verify that it works as expected. treeViewer.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(DoubleClickEvent event) { Viewer viewer = event.getViewer(); Shell shell = viewer.getControl().getShell(); MessageDialog.openInformation(shell, "Double click", "Double click detected"); } }); 2. Run the Eclipse instance, and open the view. Double-click on the tree, and a shell will be displayed with the message Double click detected. The dialog is modal and prevents other components in the user interface from being selected until it is dismissed. 3. To find the selected object(s), Eclipse provides an ISelection interface (which only provides an isEmpty() method) and an IStructuredSelection (which provides an iterator and other accessor methods). There's also a couple of specialized subtypes, such as ITreeSelection, which can be interrogated for the path that led to the selection in the tree. In the createPartControl() method of TimeZoneTreeView, where the doubleClick() method of the DoubleClickListener inner classis present, replace the MessageDialog as follows: MessageDialog.openInformation(shell, "Double click", "Double click detected"); ISelection sel = viewer.getSelection(); Object selectedValue; if (!(sel instanceof IStructuredSelection) || sel.isEmpty()) { selectedValue = null; } else { selectedValue = ((IStructuredSelection)sel).getFirstElement(); if (selectedValue instanceof TimeZone) { TimeZone timeZone = (TimeZone)selectedValue; MessageDialog.openInformation(shell, timeZone.getID(), timeZone.toString()); } [ 92 ] www.it-ebooks.info Chapter 3 4. Run the Eclipse instance, and open the view. Double-click on the tree, and a shell will be displayed with the string representation of the TimeZone. 5. To display more information about the TimeZone in the displayed window, create a subclass of MessageDialog called TimeZoneDialog in the com.packtpub. e4.clock.ui.internal package. Implement it as follows: public class TimeZoneDialog extends MessageDialog { private TimeZone timeZone; public TimeZoneDialog(Shell parentShell, TimeZone timeZone) { super(parentShell, timeZone.getID(), null, "Time Zone " + timeZone.getID(), INFORMATION, new String[] { IDialogConstants.OK_LABEL }, 0); this.timeZone = timeZone; } } 6. The contents of the Dialog are provided by the customArea() method, which can be used to build up the view. Add the following createCustomArea() method to the TimeZoneDialog: protected Control createCustomArea(Composite parent) { ClockWidget clock = new ClockWidget(parent,SWT.NONE, new RGB(128,255,0)); clock.setOffset( (TimeZone.getDefault().getOffset(System.currentTimeMillis()) - timeZone.getOffset(System.currentTimeMillis())) /3600000); return parent; } 7. Finally, modify the call to MessageDialog.open() by TimeZoneTreeView to use the new implementation: if (selectedValue instanceof TimeZone) { TimeZone timeZone = (TimeZone) selectedValue; MessageDialog.openInformation(shell, timeZone.getID(), timeZone.toString()); new TimeZoneDialog(shell, timeZone).open(); } [ 93 ] www.it-ebooks.info Creating JFace Viewers 8. Run the Eclipse instance, double-click on the time zone, and the dialog should appear: What just happened? A double-click listener was added to the viewer by registering it with addDoubleClickListener(). Initially, a standard information dialog was displayed, but then a custom subclass of MessageDialog was used which included a ClockWidget. In order to get the appropriate TimeZone, it was accessed via the currently selected object from the TreeViewer. Selection is managed via an ISelection interface. The viewer's getSelection() method should always return a non-null value, although the value may return true for the isEmpty() call. There are two relevant subinterfaces; IStructuredSelection and ITreeSelection. The ITreeSelection is a subtype of IStructuredSelection, and adds methods specific to trees. This includes the ability to find out what the selected object(s) are and what their parents are in the tree. The IStructuredSelection is the most commanly used interface in dealing with selection types. If the selection is not empty, it is almost always an instance of an IStructuredSelection. As a result, the following snippet of code appears regularly: ISelection sel = viewer.getSelection(); Object selectedValue; if (!(sel instanceof IStructuredSelection) || sel.isEmpty()) { selectedValue = null; } else { selectedValue = ((IStructuredSelection)sel).getFirstElement(); } [ 94 ] www.it-ebooks.info Chapter 3 This snippet gets the selection from the viewer, and if it's not an IStructuredSelection, or it's empty, it assigns the variable selectedValue to null. If it's non-empty, it casts it to the IStructuredSelection interface and calls getFirstElement() to get the single selected value. The selection may have more than one selected value, in which case the getFirstElement() method only returns the first element selected. The IStructuredSelection class provides an iterator to step through all selected objects. E4: In E4, the selected object can be injected via an annotated method: @Inject @Optional void setTZ(@Named(IServiceConstants.ACTIVE_SELECTION) TimeZone timeZone) { } Time for action – showing properties Instead of every object having to have its own custom information dialog, the Eclipse IDE provides a generic Properties view (in the org.eclipse.ui.views plug-in), which can be used to show information about the currently selected object. The properties are discovered generically from an object and accessed through the IPropertySource interface. This allows an object to provide an abstracted way of computing the fields shown in the property view. The easiest way to create a property source is to let the object in question implement its own IPropertySource interface. This works when the source code can be modified, but in many cases (such as the TimeZone, or a Map.Entry containing a String key and a TimeZone) the source code cannot be modified. 1. Open the MANIFEST/META-INF.MF of the plug-in, and add org.eclipse. ui.views as a dependency via the Dependencies tab, or by adding it to the bundles in the Require-Bundle entry. Without this, the IPropertySource interface won't be found. 2. Create a class TimeZonePropertySource in the com.packtpub.e4.clock. ui.internal package that implements the IPropertySource interface. Take a single TimeZone instance in the constructor. public class TimeZonePropertySource implements IPropertySource { private TimeZone timeZone; public TimeZonePropertySource(TimeZone timeZone) { this.timeZone = timeZone; } } [ 95 ] www.it-ebooks.info Creating JFace Viewers 3. The only methods that need to be implemented are getPropertyValue() and getPropertyDescriptors(). (The other methods, such as getEditableValue() and isPropertySet(), can be ignored, because they only get invoked when performing edit operations. These should be left empty or return null/false.) The accessors are called with an identifier; while the latter returns an array of PropertyDescriptors combining pairs of identifiers and a displayable name. Add the following to the TimeZonePropertySource class: private static final Object ID = new Object(); private static final Object DAYLIGHT = new Object(); private static final Object NAME = new Object(); public IPropertyDescriptor[] getPropertyDescriptors() { return new IPropertyDescriptor[] { new PropertyDescriptor(ID, "Time Zone"), new PropertyDescriptor(DAYLIGHT, "Daylight Savings"), new PropertyDescriptor(NAME, "Name") }; } public Object getPropertyValue(Object id) { if (ID.equals(id)) { return timeZone.getID(); } else if(DAYLIGHT.equals(id)) { return timeZone.inDaylightTime(new Date()); } else if (NAME.equals(id)) { return timeZone.getDisplayName(); } else { return null; } } 4. To hook this property source into the Properties view, an adapter is used. It can be specified via a generic IAdaptable interface, which allows a class to virtually implement an interface. Since the TimeZone cannot implement the IAdaptable interface directly, an IAdapterFactory is needed. 5. Create TimeZoneAdapterFactory in the com.packtpub.e4.clock. ui.internal package that implements the IAdapterFactory interface. public class TimeZoneAdapterFactory implements IAdapterFactory { public Class[] getAdapterList() { return new Class[] { IPropertySource.class }; } public Object getAdapter(Object o, Class type) { [ 96 ] www.it-ebooks.info Chapter 3 if(type == IPropertySource.class && o instanceof TimeZone) { return new TimeZonePropertySource((TimeZone)o); } else { return null; } } } 6. To register the adaptor factory with Eclipse, add it to the plugin.xml file declaratively. <extension point="org.eclipse.core.runtime.adapters"> <factory adaptableType="java.util.TimeZone" class= "com.packtpub.e4.clock.ui.internal.TimeZoneAdapterFactory"> <adapter type= "org.eclipse.ui.views.properties.IPropertySource"/> </factory> </extension> 7. Run the Eclipse instance, select a time zone in the tree view, and then open the properties by navigating to Window | Show View | Other | General | Properties. Nothing will be shown. To confirm that the adapter has been wired correctly, add this at the end of the createPartControl() method in the TimeZoneTreeView view creation: System.out.println("Adapter is " + Platform.getAdapterManager(). getAdapter(TimeZone.getDefault(),IPropertySource.class)); 8. Run the Eclipse instance, open the Time Zone Tree View, and check the Console view of the host Eclipse instance. The console should contain an output message similar to the following: Adapter is com.packtpub.e4.clock.ui.internal. [email protected] 9. So what's the missing link? It turns out in this case that the Properties view is not being notified of the change in selection. Adding the following to the createPartControl() method of the TimeZoneTreeView class will solve the problem. System.out.println("Adapter is " + Platform.getAdapterManager(). getAdapter(TimeZone.getDefault(),IPropertySource.class)); getSite().setSelectionProvider(treeViewer); [ 97 ] www.it-ebooks.info Creating JFace Viewers 10. Now, changes in selection are propagated up to the workbench, so that other views can update themselves. When a TimeZone is selected, the Properties, view will be updated automatically. Run the Eclipse instance and open the Time Zone Tree View, select a time zone, and open the Properties view: E4: To hook a viewer up to the selection provider, the code looks similar to this: @Inject ESelectionService selectionService; ISelectionChangedListener selectionListener; @PostConstruct public void postConstruct() { selectionListener = new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent e) { if (selectionService != null) selectionService.setSelection(e.getSelection()); } }; treeViewer.addSelectionChangedListener( selectionListener); } @PreDestroy public void preDestroy() { if(selectionListener != null) treeViewer.removeSelectionChangedListener (selectionListener); selectionListener = null; } [ 98 ] www.it-ebooks.info Chapter 3 What just happened? To update the state of the Workbench's selection, the view's selection provider was connected with that of the page (via the getSite() method). When the selection in the viewer changes, it sends a message to registered listeners of the page's selection service so that they can update their views, if necessary. E4: The selection listener needs to be (un)registered manually to provide a hook between the viewer and the selection service. Instead of being ISelectionService, it's ESelectionService. The interface is slightly different, because the ISelectionService is tied to the IWorkbenchPart class, but the ESelectionService is not. To provide information to the Properties view, an IPropertySource was created for the TimeZone and associated with the Platform IAdapterManager through the declaration in the plugin.xml file. It's generally better to provide hooks declaratively in the plugin.xml file rather than hooking it up with the start() and stop() activator methods. That's because the start on the Activator may not be called until the first class is loaded from the bundle; in the case of the adaptor, the declarative registration can provide the information before it is first required. The adaptor factory provides the getAdapter() method, which wraps or converts the object being passed into one of the desired type. If the object is already an instance of the given type, it can just be returned as it is—but otherwise return a POJO, proxy, or wrapper object that implements the desired interface. It's quite common to have a class (such as TimeZonePropertySupport) whose sole job is to implement the desired interface, and which wraps an instance of the object (TimeZone) to provide the functionality. The IPropertySupport interface provides a basic means to acquire properties from the object, and to do so it uses an identifier for each property. These can be any object type; in the preceding example, new Object instances were used. Although it is possible to use String (plenty of other examples do), this is not recommended, since the value of the String has no importance and it takes up space in the JVM's PermGen memory space. In addition, using a plain Object means that the instance can be compared with == without any concerns, whereas doing so with String is likely to fail the code reviews or automated style checkers. (The preceding example uses the .equals() method to encourage its use when not using an Object, but a decent JIT will in-line it—particularly since the code is sending the message to a static final instance.) [ 99 ] www.it-ebooks.info Creating JFace Viewers Pop quiz – understanding properties Q1. How can TableViewer instances respond to a click? Q2. Why are Dialog subclasses created? Q3. What are property descriptors? Q4. How are properties displayed on the Properties view? Tabular data The tree viewer is used in many situations in Eclipse, but sometimes being able to display more information for a single element is required. JFace provides a TableViewer that is similar to the TreeViewer, except that instead of a single label there are multiple columns available. There is also a combined TableTreeViewer, which combines functionality from the two classes. Time for action – viewing time zones in tables To display the time zones in table form, a new view will be created called Time Zone Table View. 1. 2. Right-click on the com.packtpub.e4.clock.ui project and select Plug-in Tools | Open Manifest. Open the Extensions tab and right-click on the org.eclipse.ui.views, followed by selecting New | View and filling in the following fields: ID: com.packtpub.e4.clock.ui.views.TimeZoneTableView Name: Time Zone Table View Class: com.packtpub.e4.clock.ui.views.TimeZoneTableView Category: com.packtpub.e4.clock.ui Icon: icons/sample.gif The plugin.xml file should now contain the following code snippet: <view category="com.packtpub.e4.clock.ui" class="com.packtpub.e4.clock.ui.views.TimeZoneTableView" icon="icons/sample.gif" id="com.packtpub.e4.clock.ui.views.TimeZoneTableView" name="Time Zone Table View" restorable="true"> </view> [ 100 ] www.it-ebooks.info Chapter 3 3. Create the class using the editor's shortcuts to create a new class, TimeZoneTableView, which extends ViewPart, or with the new class wizard. Once the view is created, add an empty TableViewer, and use an ArrayContentProvider with the set of available TimeZone IDs: public class TimeZoneTableView extends ViewPart { private TableViewer tableViewer; public void createPartControl(Composite parent) { tableViewer=new TableViewer(parent,SWT.H_SCROLL|SWT.V_SCROLL); tableViewer.getTable().setHeaderVisible(true); tableViewer.setContentProvider( ArrayContentProvider.getInstance()); tableViewer.setInput(TimeZone.getAvailableIDs()); } public void setFocus() { tableViewer.getControl().setFocus(); } } E4: If creating a part for an E4 application, remember to use the @Inject annotation for the constructor and @Focus for the setFocus() method. 4. Run the Eclipse instance, and a one-dimensional list of time zones will be shown in the Time Zone Table View: 5. Convert the array of Strings to an array of TimeZones and set that as the input: tableViewer.setInput(TimeZone.getAvailableIDs()); String[] ids = TimeZone.getAvailableIDs(); TimeZone[] timeZones = new TimeZone[ids.length]; for(int i=0;i<ids.length;i++) { timeZones[i] = TimeZone.getTimeZone(ids[i]); } tableViewer.setInput(timeZones); getSite().setSelectionProvider(tableViewer); [ 101 ] www.it-ebooks.info Creating JFace Viewers 6. The selection provider is wired up like in the TimeZoneTreeView example. Make a selection in the table, and see the change in the Properties view: 7. The table shows a list of the ZoneInfo objects. That's because there is no LabelProvider, so they're just being rendered with their toString() representation. Because a table has multiple columns, TableViewers have a number of TableViewerColumn instances. Each one represents a column in the Table, and each has its own size, title, and label provider. Creating a new column often involves setting up standard features (such as the width) as well as hooking in the required fields to display. To make it easy to re-use, create an abstract subclass of ColumnLabelProvider called TimeZoneColumn (in the com.packtpub.e4.clock.ui.internal package) with abstract getText() and getTitle() methods, and a concrete getWidth() method. public abstract class TimeZoneColumn extends ColumnLabelProvider { public abstract String getText(Object element); public abstract String getTitle(); public int getWidth() { return 250; } } 8. Add a helper method to the TimeZoneColumn class, which makes it easier to add it to a viewer: public TableViewerColumn addColumnTo(TableViewer viewer) { TableViewerColumn tableViewerColumn = new TableViewerColumn(viewer,SWT.NONE); TableColumn column = tableViewerColumn.getColumn(); column.setMoveable(true); column.setResizable(true); column.setText(getTitle()); column.setWidth(getWidth()); tableViewerColumn.setLabelProvider(this); return tableViewerColumn; } [ 102 ] www.it-ebooks.info Chapter 3 9. Now create a custom subclass TimeZoneIDColumn in the same package that returns the ID column for a TimeZone: public class TimeZoneIDColumn extends TimeZoneColumn { public String getText(Object element) { if (element instanceof TimeZone) { return ((TimeZone) element).getID(); } else { return ""; } } public String getTitle() { return "ID"; } } 10. Modify the TimeZoneTableView class, and at the end of the createPartControl() method, instantiate the column and call the addColumnTo() method, above the call to setInput(): new TimeZoneIDColumn().addColumnTo(tableViewer); tableViewer.setInput(timeZones); Note that the columns need to be created prior to the setInput() call, otherwise they won't display properly. 11. Run the Eclipse instance, and show the Time Zone Table View. The ID column should be displayed on its own. 12. To add additional columns, copy the TimeZoneIDColumn class, modifying the title returned and the returned property of the associated TimeZone. For example, create a copy of the TimeZoneIDColumn called TimeZoneDisplayNameColumn, and modify the get method and title. return return return return ((TimeZone) element).getID(); ((TimeZone) element).getDisplayName(); "ID"; "Display Name"; 13. Optionally, do the same with the other properties of TimeZone, such as the offset (with getOffset()), and whether it's in summer time or not (with useDaylightTime()). The columns can then be added to the table. new TimeZoneOffsetColumn().addColumnTo(tableViewer); new TimeZoneDisplayNameColumn().addColumnTo(tableViewer); new TimeZoneSummerTimeColumn().addColumnTo(tableViewer); [ 103 ] www.it-ebooks.info Creating JFace Viewers 14. Run the Eclipse instance, go to the Time Zone Table View, and the additional column(s) should be seen: What just happened? A TableViewer was created and multiple ColumnLabelProviders were added to it for displaying individual fields of an object. Subclassing ColumnLabelProvider avoids the need to use anonymous inner classes and it gives a helper function. This can be used to create and wire in the column (with specified title and width), while delegating those properties to the concrete subclasses of TimeZoneIDColumn and so on. This avoids the need for tracking columns by ID. For specific customizations of the columns, the underlying SWT Column is used to set functionality required by the application, including allowing the column to be movable with setMovable(true), and to be resizable with setResizable(true). Similarly, table-wide operations (such as showing the header) are performed by manipulating the underlying SWT Table and invoking setHeaderVisible(true). It's important to note that the columns of the tree viewer are calculated when the setInput() method is called, so columns that are added after this line may not show properly. Generally, the setInput() should be left until the end of the table's construction. All of the other functionality from the other view is portable, for example, by wiring up the selection appropriately, the Properties view can show properties of the selected object. Time for action – syncing selection The TimeZoneTableView and the TimeZoneTreeView can propagate their selection to the Properties view. Responding to selection changes gives a unified feel despite the fact that the views are independent entities. [ 104 ] www.it-ebooks.info Chapter 3 They can be further linked so that when a TimeZone is selected in either of these views, it automatically reveals in the other. To do this, a selection listener will need to be registered, and if the selected object is a type of TimeZone, display it in the view (with the reveal() and setSelection() methods). 1. Create a class TimeZoneSelectionListener (in the com.packtpub. e4.clock.ui.internal package), which implements the ISelectionListener interface. This will take a viewer, and an associated part, to implement the selectionChanged() method. public class TimeZoneSelectionListener implements ISelectionListener { private Viewer viewer; private IWorkbenchPart part; public TimeZoneSelectionListener(Viewer v, IWorkbenchPart p) { this.viewer = v; this.part = p; } public void selectionChanged(IWorkbenchPart p, ISelection sel) { } } 2. 3. The selectionChanged() method needs to: Ignore the event if it was fired by the same part Get the selected object from the event and compare it with the current one If different, and the selected object is a TimeZone, update the viewer Implement it as follows: public void selectionChanged(IWorkbenchPart p, ISelection sel) { if (p != this.part) { IStructuredSelection selected = ((IStructuredSelection)sel) .getFirstElement(); Object current = ((IStructuredSelection)viewer.getSelection()) .getFirstElement(); if(selected != current && selected instanceof TimeZone) { viewer.setSelection(sel); if(viewer instanceof StructuredViewer) { ((StructuredViewer) viewer).reveal(selected); } } } } [ 105 ] www.it-ebooks.info Creating JFace Viewers 4. The selection listeners need to be registered with the views. Open up the TimeZoneTableView class, and at the bottom of the createPartControl() method, add the following: selectionListener = new TimeZoneSelectionListener( tableViewer, getSite().getPart()); getSite().getWorkbenchWindow().getSelectionService() .addSelectionListener(selectionListener); 5. The selectionListener needs to be added as a field, because it will be necessary to remove the listener when the view is disposed: private TimeZoneSelectionListener selectionListener; public void dispose() { if (selectionListener != null) { getSite().getWorkbenchWindow().getSelectionService() .removeSelectionListener(selectionListener); selectionListener = null; } super.dispose(); } 6. A very similar change (only the viewer's variable name is different) needs to be added to the TimeZoneTreeView class: selectionListener = new TimeZoneSelectionListener( tableViewer, getSite().getPart()); getSite().getWorkbenchWindow().getSelectionService() .addSelectionListener(selectionListener); 7. The dispose method is the same for the TimeZoneTreeView class as the TimeZoneTableView. 8. Now run the Eclipse instance, select a time zone in the Time Zone Table View, and the Time Zone Tree View should show the same one. Change the selection of the Time Zone Tree View and the Time Zone Table View should show the same one. What just happened? Selection events occur a lot in the Eclipse workspace, so it is important that the selection listeners be performant. By filtering events fired from the same part, or filtering uninteresting types, the event delivery will be more efficient. In this case, the selection is checked to ensure that the selection contains (at least) one element, which is a TimeZone, before performing any UI updates. [ 106 ] www.it-ebooks.info Chapter 3 The selection of the viewer can be synchronized with the setSelection() call; this saves having to instantiate a new selection object and set the data appropriately. However, setting the selection alone is not enough; the reveal() method needs to be called to ensure that it is appropriately highlighted. If multiple objects are selected, this will only reveal the first element. The reveal() method is only available to StructuredViewers, so the selection stamps the selection as it is, and explicitly sets the IStructuredSelection for those that are StructuredViewers. Finally, the listeners are registered when the view is created, and removed when the view is disposed. To do this, get hold of the ISelectionService via the part and then invoke the addSelectionListener() method to add it, and invoke the removeSelectionListener() method to remove it. E4: If using E4, instead of using the ISelectionService, the ESelectionService is used. This provides a similar, but not identical, interface in that the WorkbenchPart is no longer present. Typically, the ESelectionService is injected into the view and the listener is wired in the @PostConstruct and removed in the @PreDestroy call. Pop quiz – understanding tables Q1. How are a columns' headers enabled in a TableViewer? Q2. What is a TableViewerColumn for? Q3. How is selection synchronized between two views? Summary This chapter covered how to use JFace to build viewers for structured data; both tree-based views (with a TreeViewer) and table-based views (with a TableViewer). It also covered some JFace built-in features for managing fonts and images. To synchronise data between views in Eclipse, services such as the ISelectionService are used (or for E4 the ESelectionService is used). Having views generate and consume selection events provides a visual consistency, even though the views may be exposed by different plug-ins. [ 107 ] www.it-ebooks.info www.it-ebooks.info 4 Interacting with the User In the previous chapter, we looked at some of the basic JFace viewers, which provide a representation of data. However, we need to interact with the user and we can do this in multiple ways, from responding to mouse clicks to processing data-intensive operations in the background. In this chapter we will cover: Creating a menu in response to a user pop up Adding a command and a handler in a menu Using progress managers to report work Adding actions to the progress manager Showing errors and dealing with failure Creating actions, commands, and handlers The first few releases of the Eclipse framework provided Action as a means of contributing to menu items. These were defined declaratively via actionSets in the plugin.xml file, and many tutorials still reference those today. At the programming level, when creating views, Actions are still used to provide context menus programmatically. They were replaced with commands in Eclipse 3, as a more abstract way of decoupling the operation of a command with its representation of the menu. To connect these two together, a handler is used. www.it-ebooks.info Interacting with the User E4: Eclipse 4.x uses the command's model, and decouples it further using the @Execute annotation on the handler class. Commands and views are hooked up with entries on the application's model. The basic ideas covered in this chapter will translate into examples in the E4 in Chapter 7. Time for action – adding context menus A context menu can be added to the TimeZoneTableView class and respond to it dynamically in the view's creation. The typical pattern for Eclipse 3 applications is to create a hookContextMenu() method, which is used to wire up the context menu operation with displaying the menu. A default implementation can be seen by creating an example view, or one can be created from first principles. Eclipse menus are managed by a MenuManager. This is a specialized subclass of a more general ContributionManager, which looks after a dynamic set of contributions that can be made from other sources. When the menu manager is connected to a control, it responds in the standard ways for the platform for showing the menu (typically a context-sensitive click or short key). Menus can also be displayed in other locations, such as a view's or the workspace's coolbar (toolbar). The same MenuManager approach works in these different locations. 1. Open the TimeZoneTableView class and go to the createPartControl() method. 2. At the bottom of the method, add a new MenuManager with the ID #PopupMenu and associate it to the viewer's control. MenuManager manager = new MenuManager("#PopupMenu"); Menu menu = manager.createContextMenu(tableViewer.getControl()); tableViewer.getControl().setMenu(menu); 3. If the Menu is empty, the MenuManager won't show any content, so this currently has no effect. To demonstrate this, an Action will be added to the Menu. An Action has text (for rendering in the pop-up menu, or the menu at the top of the screen), as well as a state (enabled/disabled, selected) and a behavior. These are typically created as subclasses and (although the Action doesn't strictly require it) an implementation of the run() method. Add this to the bottom of the createPartControl() method. Action deprecated = new Action() { public void run() { MessageDialog.openInformation(null, "Hello", "World"); } }; deprecated.setText("Hello"); manager.add(deprecated); [ 110 ] www.it-ebooks.info Chapter 4 4. Run the Eclipse instance, open the Time Zone Table View, and right-click on the table. The Hello menu can be seen, and when selected, an informational dialog is shown. What just happened? The MenuManager (with the id #PopupMenu) was bound to the control, which means when that particular control's context sensitive menu is invoked, the manager will be able to ask to display a menu. The manager is associated with a single Menu object (which is also stamped on the underlying control itself) and is responsible for updating the status of the menu. Actions are deprecated. They are included here since examples on the Internet may have preferred references to them, but it's important to note that while they still work, the way of building user interfaces are with the commands and handlers, shown in the next section. When the menu is shown, the actions that the menu contains are rendered in the order in which they are added. Action are usually subclasses that implement a run() method, which performs a certain operation, and have text which is displayed. Action instances also have other metadata, such as whether they are enabled or disabled. Although it is tempting to override the accessor methods, this behavior doesn't work—the setters cause an event to be sent out to registered listeners, which causes side effects, such as updating any displayed controls. Time for action – creating commands and handlers Since the Action class is deprecated, the supported mechanism is to create a command, a handler, and a menu to display the command in the menu bar. 1. 2. Open the plug-in manifest for the project, or double-click on the plugin.xml file. Edit the source on the plugin.xml tab, and add a definition of a Hello command as follows: <extension point="org.eclipse.ui.commands"> <command name="Hello" description="Says Hello World" id="com.packtpub.e4.clock.ui.command.hello"/> </extension> [ 111 ] www.it-ebooks.info Interacting with the User 3. This creates a command, which is just an identifier and a name. To specify what it does, it must be connected to a handler, which is done by adding the following extension: <extension point="org.eclipse.ui.handlers"> <handler class= "com.packtpub.e4.clock.ui.handlers.HelloHandler" commandId="com.packtpub.e4.clock.ui.command.hello"/> </extension> 4. The handler joins the processing of the command to a class that implements IHandler, typically AbstractHandler. Create a class HelloHandler in a new com.packtpub.e4.clock.ui.handlers package, which implements AbstractHandler (from the org.eclipse.core.commands package). public class HelloHandler extends AbstractHandler { public Object execute(ExecutionEvent event) { MessageDialog.openInformation(null, "Hello", "World"); return null; } } 5. The command's ID com.packtpub.e4.clock.ui.command.hello is used to refer to it from menus or other locations. To place the contribution in an existing menu structure, it needs to be specified by its locationURI, which is a URL that begins with menu: such as menu:window?after=additions or menu:file?after=additions. To place it in the Help menu, add this to the plugin.xml file. <extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI="menu:help?after=additions"> <command commandId="com.packtpub.e4.clock.ui.command.hello" label="Hello" style="push"> </command> </menuContribution> </extension> 6. Run the Eclipse instance, and there will be a Hello menu item under the Help menu. When selected, it will pop up the Hello World message. If the Hello menu is disabled, verify that the handler extension point is defined, which connects the command to the handler class. [ 112 ] www.it-ebooks.info Chapter 4 What just happened? The main issue with the actions framework was that it tightly coupled the state of the command with the user interface. Although an action could be used uniformly between different menu locations, the Action superclass still lives in the JFace package, which has dependencies on both SWT and other UI components. As a result, Action cannot be used in a headless environment. Eclipse 3.x introduced the concept of commands and handlers, as a means of separating their interface from their implementation. This allows a generic command (such as Copy) to be overridden by specific views. Unlike the traditional command design pattern, which provides implementation as subclasses, the command in Eclipse 3.x uses a final class and then a retargetable IHandler to perform the actual execution. E4: In Eclipse 4.x, the concepts of commands and handlers are used extensively to provide the components of the user interface. The key difference is in their definition; for Eclipse 3.x, this typically occurs in the plugin.xml file, whereas in E4 it is part of the application model. In the example, a specific handler was defined for the command, which is valid in all contexts. The handler's class is the implementation; the command ID is the reference. The org.eclipse.ui.menus extension point allows menuContributions to be added anywhere in the user interface. To address where the menu can be contributed to, the locationURI object defines where the menu item can be created. The syntax for the URI is as follows: menu: Menus begin with the menu: protocol (can also be toolbar: or popup:) identifier: This can be a known short name (such as file, window, and help), the global menu (org.eclipse.ui.main.menu), the global toolbar (org.eclipse.ui.main.toolbar), a view identifier (org.eclipse. ui.views.ContentOutline), or an ID explicitly defined in a pop-up menu's registerContextMenu() call. ?after(or before)=key: This is the placement instruction to put this after or before other items; typically additions is used as an extensible location for others to contribute to. The locationURI allows plug-ins to contribute to other menus, regardless of where they are ultimately located. Note, that if the handler implements the IHandler interface directly instead of subclassing AbstractHandler, the isEnabled() method will need to be overridden as otherwise the command won't be enabled, and the menu won't have any effect. [ 113 ] www.it-ebooks.info Interacting with the User Time for action – binding commands to keys To hook up the command to a keystroke a binding is used. This allows a key (or series of keys) to be used to invoke the command, instead of only via the menu. Bindings are set up via an extension point org.eclipse.ui.bindings, and connect a sequence of keystrokes to a command ID. 1. 2. Open the plugin.xml in the clock.ui project. In the plugin.xml tab, add the following: <extension point="org.eclipse.ui.bindings"> <key commandId="com.packtpub.e4.clock.ui.command.hello" sequence="M1+9" contextId="org.eclipse.ui.contexts.window" schemeId= "org.eclipse.ui.defaultAcceleratorConfiguration"/> </extension> 3. Run the Eclipse instance, and press Cmd + 9 (for OS X) or Ctrl + 9 (for Windows/ Linux). The same Hello dialog should be displayed, as if it was shown from the menu. The same keystroke should be displayed in the Help menu. What just happened? The M1 key is the primary meta key, which is Cmd on OS X and Ctrl on Windows/Linux. This is typically used for the main operations; for example M1 + C is copy and M1 + V is paste on all systems. The sequence notation M1 + 9 is used to indicate pressing both keys at the same time. The command that gets invoked is referenced by its commandId. This may be defined in the same plug-in, but does not have to be; it is possible for one application to provide a set of commands and another plug-in to provide keystrokes that bind them. It is also possible to set up a sequence of key presses; for example, M1 + 9 8 7 would require pressing Cmd + 9 or Ctrl + 9 followed by 8 and then 7 before the command is executed. This allows a set of keystrokes to be used to invoke a command; for example, it's possible to emulate an Emacs quit operation with the keybinding Ctrl + X Ctrl + C to the quit command. Other modifier keys include M2 (Shift), M3 (Alt/Option), and M4 (Ctrl on OS X). It is possible to use CTRL, SHIFT, or ALT as long names, but the meta names are preferred, since M1 tends to be bound to different keys on different operating systems. [ 114 ] www.it-ebooks.info Chapter 4 The non-modifier keys themselves can either be single characters (A to Z), numbers (0 to 9), or one of a set of longer name key-codes, such as F12, ARROW_UP, TAB, and PAGE_UP. Certain common variations are allowed; for example, ESC/ESCAPE, ENTER/ RETURN, and so on. Finally, bindings are associated with a scheme, which in the default case should be org. eclipse.ui.defaultAcceleratorConfiguration. Schemes exist to allow the user to switch in and out of keybindings and replace them with others, which is how tools like "vrapper" (a vi emulator) and the Emacs bindings that come with Eclipse by default can be used. (This can be changed via Window | Preferences | Keys menu in Eclipse.) Time for action – changing contexts The context is the location in which this binding is valid. For commands that are visible everywhere—typically the kind of options in the default menu—they can be associated with the org.eclipse.ui.contexts.window context. If the command should also be invoked from dialogs as well, then the org.eclipse.ui.context.dialogAndWindow context would be used instead. 1. 2. Open the plugin.xml file of the clock.ui project. To enable the command only for Java editors, go to the plugin.xml tab, and modify the contextId as follows: <extension point="org.eclipse.ui.bindings"> <key commandId="com.packtpub.e4.clock.ui.command.hello" sequence="M1+9" contextId="org.eclipse.ui.contexts.window" contextId="org.eclipse.jdt.ui.javaEditorScope" schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"/> </extension> 3. Run the Eclipse instance, and create a Java project, a test Java class, and an empty text file. 4. Open both of these in editors. When the focus is on the Java editor, the Cmd + 9 or Ctrl + 9 operation will run the command, but when the focus is on the text editor, the keybinding will have no effect. Unfortunately, it also highlights the fact that just because the keybinding is disabled when in the Java scope, it doesn't disable the underlying command. [ 115 ] www.it-ebooks.info Interacting with the User If there is no change in behavior, try cleaning the workspace of the test instance at launch, by going to the Run | Run ... menu, and choosing Clear on the workspace. This is sometimes necessary when making changes to the plugin. xml file, as some extensions are cached and may lead to strange behavior. What just happened? Context scopes allow bindings to be valid for certain situations, such as when a Java editor is open. This allows the same keybinding to be used for different situations, such as a Format operation—which may have a different effect in a Java editor than an XML editor, for instance. Since scopes are hierarchical, they can be specifically targeted for the contexts in which they may be used. The Java editor context is a subcontext of the general text editor, which in turn is a subcontext of the window context, which in turn is a subcontext of the windowAndDialog context. The available contexts can be seen by editing the plugin.xml file in the plug-in editor; in the extensions tab the binding shows an editor window with a form: [ 116 ] www.it-ebooks.info Chapter 4 Clicking on the Browse… button next to the contextId brings up a dialog, which presents the available contexts: It's also possible to find out all the contexts programmatically or via the running OSGi instance, by navigating to Window | Show View | Console, and then using New Host OSGi Console in the drop-down menu, and then running the following code snippet: osgi> pt -v org.eclipse.ui.contexts Extension point: org.eclipse.ui.contexts [from org.eclipse.ui] Extension(s): ------------------null [from org.eclipse.ant.ui] <context> name = Editing Ant Buildfiles description = Editing Ant Buildfiles Context parentId = org.eclipse.ui.textEditorScope id = org.eclipse.ant.ui.AntEditorScope [ 117 ] www.it-ebooks.info Interacting with the User </context> null [from org.eclipse.compare] <context> name = Comparing in an Editor description = Comparing in an Editor parentId = org.eclipse.ui.contexts.window id = org.eclipse.compare.compareEditorScope </context> Time for action – enabling and disabling the menu's items The previous section showed how to hide or show a specific keybinding depending on the open editor type. However, it doesn't stop the command being called via the menu, or from it showing up in the menu itself. Instead of just hiding the keybinding, the menu can be hidden as well by adding a visibleWhen block to the command. The expressions framework provides a number of variables, including activeContexts, which contains a list of the active contexts at the time. Since many contexts can be active simultaneously, the active contexts is a list (for example, [dialogAndWindows,windows, textEditor,javaEditor]). So, to find an entry (in effect, a contains operation) an iterate operator with the equals expression is used. 1. Open up the plugin.xml file, and update the the Hello command by adding a visibleWhen expression. <extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI="menu:help?after=additions"> <command commandId="com.packtpub.e4.clock.ui.command.hello" label="Hello" style="push"> <visibleWhen> <with variable="activeContexts"> <iterate operator="or"> <equals value="org.eclipse.jdt.ui.javaEditorScope"/> </iterate> </with> </visibleWhen> </command> </menuContribution> </extension> [ 118 ] www.it-ebooks.info Chapter 4 2. Run the Eclipse instance, and verify that the menu is hidden until a Java editor is opened. If this behavior is not seen, run the Eclipse application with the clean argument to clear the workspace. After clearing, it will be necessary to create a new Java project with a Java class, as well as an empty text file, to verify that the menu's visibility is correct. What just happened? Menus have a visibleWhen guard that is evaluated when the menu is shown. If it is false, the menu is hidden. The expressions syntax is based on nested XML elements with certain conditions. For example, an <and> block is true if all of its children are true, whereas an <or> block is true if one of its children is true. Variables can also be used with a property test using a combination of a <with> block (which binds the specified variable to the stack) and an <equals> block or other comparison. In the case of variables that have lists, an <iterate> can be used to step through elements using either operator="or" or operator="and" to dynamically calculate enablement. To find out if a list contains an element, a combination of <iterate> and <equals> operators is the standard pattern. There are a number of variables that can be used in tests; these are listed in the Eclipse help documentation under the Workbench Core Expressions chapter, and include the following variables: activeContexts: List of context IDs that are active at the time activeShell: The active shell (dialog or window) activeWorkbenchWindow: The active window activeEditor: The current or last active editor activePart: The active part (editor or view) selection: The current selection org.eclipse.core.runtime.Platform: The Platform object The Platform object is useful for performing dynamic tests using test, such as the following: <test value="ACTIVE" property="org.eclipse.core.runtime.bundleState" args="org.eclipse.core.expressions"/> <test property="org.eclipse.core.runtime.isBundleInstalled" args="org.eclipse.core.expressions"/> [ 119 ] www.it-ebooks.info Interacting with the User Knowing if a bundle is installed is often useful; it's better to only enable functionality if a bundle is started (or in OSGi terminology, ACTIVE). As a result, use of isBundleInstalled has been replaced by the bundleState=ACTIVE tests. Time for action – reusing expressions Although it's possible to copy and paste expressions between places where they are used, it is preferable to re-use an identical expression. 1. Declare an expression using the expression's extension point, by opening the plugin.xml file of the clock.ui project. <extension point="org.eclipse.core.expressions.definitions"> <definition id="when.hello.is.active"> <with variable="activeContexts"> <iterate operator="or"> <equals value="org.eclipse.jdt.ui.javaEditorScope"/> </iterate> </with> </definition> </extension> If defined via the extension wizard, it will prompt to add dependency on the org. eclipse.core.expressions bundle. This isn't strictly necessary for this example to work. 2. To use the definition, the enablement expressions needs to use the reference. <extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI="menu:help?after=additions"> <command commandId="com.packtpub.e4.clock.ui.command.hello" label="Hello" style="push"> <visibleWhen> <with variable="activeContexts"> <iterate operator="or"> <equals value="org.eclipse.jdt.ui.javaEditorScope"/> </iterate> </with> <reference definitionId="when.hello.is.active"/> </visibleWhen> </command> </menuContribution> </extension> [ 120 ] www.it-ebooks.info Chapter 4 3. Now that the reference has been defined, it can be used to modify the handler as well, so that the handler and menu become active and visible together. Add the following to the Hello handler in the plugin.xml file: <extension point="org.eclipse.ui.handlers"> <handler class="com.packtpub.e4.clock.ui.handlers.Hello" commandId="com.packtpub.e4.clock.ui.command.hello"> <enabledWhen> <reference definitionId="when.hello.is.active"/> </enabledWhen> </handler> </extension> 4. Run the Eclipse application and exactly the same behavior will occur; but should the enablement change, it can be done in one place. What just happened? The org.eclipse.core.expressions extension point defined a virtual condition that could be evaluated when the user's context changes, so both the menu and the handler can be made visible and enabled at the same time. The reference was bound in the enabledWhen condition for the handler, and the visibleWhen condition for the menu. Since references can be used anywhere, expressions can also be defined in terms of other expressions. As long as the expressions aren't recursive, they can be built up in any manner. Time for action – contributing commands to pop-up menus It's useful to be able to add contributions to pop-up menus so that they can be used by different places. Fortunately, this can be done fairly easily with the menuContribution element and a combination of enablement tests. This allows the removal of the Action introduced in the first part of this chapter with a more generic command and handler pairing. There is a deprecated extension point—which still works in Eclipse 4.2 today—called objectContribution, which is a single specialized hook for contributing a pop-up menu to an object. This has been deprecated for some time, but often older tutorials or examples may refer to it. 1. Open the TimeZoneTableView class and add the hookContextMenu() method as follows: private void hookContextMenu(Viewer viewer) { MenuManager manager = new MenuManager("#PopupMenu"); Menu menu = manager.createContextMenu(viewer.getControl()); viewer.getControl().setMenu(menu); getSite().registerContextMenu(manager, viewer); } [ 121 ] www.it-ebooks.info Interacting with the User 2. 3. Add the same hookContextMenu() method to the TimeZoneTreeView class. 4. In the TimeZoneTableView class, at the end of the createPartControl() method, replace the call to the action with a call to hookContextMenu() instead: In the TimeZoneTreeView class, at the end of the createPartControl() method, call hookContextMenu(tableViewer). hookContextMenu(tableViewer); MenuManager manager = new MenuManager("#PopupMenu"); Menu menu = manager.createContextMenu(tableViewer.getControl()); tableViewer.getControl().setMenu(menu); Action deprecated = new Action() { public void run() { MessageDialog.openInformation(null, "Hello", "World"); } }; deprecated.setText("Hello"); manager.add(deprecated); 5. Running the Eclipse instance now and showing the menu results in nothing being displayed, because no menu items have been added to it yet. 6. Create a command and a handler Show the Time. <extension point="org.eclipse.ui.commands"> <command name="Show the Time" description="Shows the Time" id="com.packtpub.e4.clock.ui.command.showTheTime"/> </extension> <extension point="org.eclipse.ui.handlers"> <handler class= "com.packtpub.e4.clock.ui.handlers.ShowTheTime" commandId="com.packtpub.e4.clock.ui.command.showTheTime"/> </extension> 7. Create a class ShowTheTime, in the com.packtpub.e4.clock.ui.handlers package, which extends org.eclipse.core.commands.AbstractHandler, to show the time in a specific time zone. public class ShowTheTime extends AbstractHandler { public Object execute(ExecutionEvent event) { ISelection sel = HandlerUtil.getActiveWorkbenchWindow(event) .getSelectionService().getSelection(); if (sel instanceof IStructuredSelection && !sel.isEmpty()) { Object value = ((IStructuredSelection)sel).getFirstElement(); if (value instanceof TimeZone) { SimpleDateFormat sdf = new SimpleDateFormat(); [ 122 ] www.it-ebooks.info Chapter 4 sdf.setTimeZone((TimeZone) value); MessageDialog.openInformation(null, "The time is", sdf.format(new Date())); } } return null; } } 8. Finally, to hook it up, a menu needs to be added to the special locationURI popup:org.eclipse.ui.popup.any. <extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI="popup:org.eclipse.ui.popup.any"> <command label="Show the Time" style="push" commandId="com.packtpub.e4.clock.ui.command.showTheTime"> <visibleWhen checkEnabled="false"> <with variable="selection"> <iterate ifEmpty="false"> <adapt type="java.util.TimeZone"/> </iterate> </with> </visibleWhen </command> </menuContribution> </extension> 9. Run the Eclipse instance, and open the Time Zone Table view or Time Zone Table view. Right-click on a TimeZone, and the command Show the Time will be displayed (that is, one of the leaves of the tree or one of the rows of the table). Select the command and a dialog should show the time. What just happened? The views from the previous chapter and the knowledge of how to wire up commands in this chapter provided a unified means of adding commands, based on the selected object type. This approach of registering commands is powerful, because any time a time zone is exposed as a selection in the future it will now have a Show the Time menu added to it automatically. The commands define a generic operation, and handlers bind those commands to implementations. The context-sensitive menu is provided by the pop-up menu extension point using the locationURI popup:org.eclipse.ui.popup.any. This allows the menu to be added to any pop-up menu that uses a MenuManager and when the selection contains a TimeZone. The MenuManager is responsible for listening to the mouse gestures to show a menu, and filling it with details when it is shown. [ 123 ] www.it-ebooks.info Interacting with the User In the example, the command was enabled when the object was an instance of a TimeZone, and also if it could be adapted to a TimeZone. This would allow another object type (say, a contact card) to have an adapter to convert it to a TimeZone, and thus show the time in that contact's location. Have a go hero – using view menus and toolbars The way to add a view menu is similar to adding a pop-up menu; the locationURI used is the view's ID rather than the menu item itself. Add a Show the Time menu to the TimeZone view as a view menu. Another way of adding the menu is to add it as a toolbar, which is an icon in the main Eclipse window. Add the Show the Time icon by adding it to the global toolbar instead. To facilitate testing of views, add a menu item that allows you to show the TimeZone views with PlatformUI.getActiveWorkbenchWindow().getActivePage(). showView(id). Pop quiz – understanding menus Q1. What's the difference between an Action and a Command, and which one should be used? Q2. How can a Command be connected to a menu? Q3. What is the M1 key? Q4. How are keystrokes bound to commands? Q5. What is a menu locationURI? Q6. How is a pop-up menu created? Jobs and progress Since the user interface is single threaded, if a command takes a long amount of time it will block the user interface from being redrawn or processed. As a result, it is necessary to run long-running operations in a background thread to prevent the UI from hanging. Although the core Java library contains java.util.Timer, the Eclipse Jobs API provides a mechanism to both run jobs and report progress. It also allows jobs to be grouped together and paused or joined as a whole. [ 124 ] www.it-ebooks.info Chapter 4 Time for action – running operations in the background If the command takes a long time to execute, the user interface can be blocked. This happens because there is only one user interface thread, and because the command is launched from the UI, it will run in the UI thread. Instead, long running operations should run in a background thread, and then once finished, be able to display the results instead. Clearly creating a new Thread (like the clock updates initially) or other techniques like a Timer would work. However, the Eclipse system has a mechanism to provide a Job to do the work instead, or a UIJob to run in the context of the UI thread. 1. Open the HelloHandler and go to the execute() method. Replace its contents with the following code snippet: public Object execute(ExecutionEvent event) { Job job = new Job("About to say hello") { protected IStatus run(IProgressMonitor monitor) { try { Thread.sleep(5000); } catch (InterruptedException e) { } MessageDialog.openInformation(null, "Hello", "World"); return Status.OK_STATUS; } }; job.schedule(); return null; } 2. Run the Eclipse instance, and click on the Help | Hello menu item. (It may be necessary to open a Java file to enable the menu). Open the Progress view, and a Job will be listed with About to say hello running. Unfortunately, an error dialog is then shown: [ 125 ] www.it-ebooks.info Interacting with the User 3. This occurs because the Job runs on a non-UI background thread, so when the MessageDialog is shown an exception occurs. To fix this, instead of showing the MessageDialog directly, a second Job or Runnable can be created to run specifically on the UI thread. Replace the call to the MessageDialog with the following code snippet: MessageDialog.openInformation(null, "Hello", "World"); Display.getDefault().asyncExec(new Runnable() { public void run() { MessageDialog.openInformation(null, "Hello", "World"); } }); This example uses the asyncExec() to run a runnable on the UI thread (similar to the SwingUtilities.invokeLater() method in Swing). 4. Run the Eclipse instance, select the Hello menu, and after a five second pause, the dialog should be shown. What just happened? Every action in the Eclipse UI must run on the UI thread, so if the action takes a significant time to run, it will give the impression that the user interface is blocked or hung. The way to avoid this is to drop out of the UI thread before doing any long term work. Any updates that need to be done involving the UI should be scheduled back on the UI thread. The example used both Job (a mechanism for scheduling named processes that can be monitored via the Progress view), as well as the display's asyncExec() method to launch the resulting message dialog. Both of these situations require the use of inner classes, which can increase the verbosity of the code. With future versions of Java and use of lambda expressions this will be significantly reduced. E4: Since E4 can use different renderers, the Display is not a good target for submitting background UIJob. Instead, use a UISynchronize instance to acquire the asyncExec() or syncExec() method. Have a go hero – using a UI job Instead of scheduling the UI notification piece as a Display.asyncExec() method, create it as a UIJob instead. This works in exactly the same way as a Job does, but you need to override the runInUIThread() method instead of the run() method. This may be useful when there is more UI interaction required, such as asking the user for more information. [ 126 ] www.it-ebooks.info Chapter 4 Time for action – reporting progress Normally when a Job is running, it is necessary to periodically update the user to let them know the state of progress. By default, if a Job provides no information, a generic busy indicator is shown. When a Job is executed, it is passed an IProgressMonitor object, which can be used to notify the user of progress (and provide a way to cancel the operation). A progress monitor has a number of tasks, each of which has a total unit of work that it can do. For jobs that don't have a known amount of work, UNKNOWN can be specified and it will be displayed in a generic busy indicator. 1. Open the HelloHandler and go to the execute() method. In the run() method of the inner Job, add a beginTask() at the beginning, and a worked() method in the loop after each second's sleep, for five iterations. The code will look like the following: protected IStatus run(IProgressMonitor monitor) { try { monitor.beginTask("Preparing", 5000); for(int i=0;i<5;i++) { Thread.sleep(1000); monitor.worked(1000); } } catch (InterruptedException e) { } finally { monitor.done(); } MessageDialog.openInformation(null, "Hello", "World"); return Status.OK_STATUS; } 2. Run the Eclipse instance, and open the Progress view by navigating to Windows | Show View | Other | General | Progress. Now, go to Help | Hello; the Progress view should show the progress as the sleep occurs. 3. To make the reporting more accurate, report the status more frequently. for(int i=0;i<50;i++) { Thread.sleep(100); monitor.worked(100); } 4. Run the Eclipse instance again. When the job is run via the Help | Hello menu, the status will be updated in the Progress view more frequently. [ 127 ] www.it-ebooks.info Interacting with the User What just happened? When running a Job, the progress monitor can be used to indicate how much work has been done. It must start with a beginTask() method—this gives both the total number of work units as well as a textual name that can be used to identify what's happening. If the amount of work is unknown, use IProgressMonitor.UNKNOWN. The unit scale doesn't really matter; it could have been 50 or 50,000. As long as the total number of work units add up, and they're appropriately used, it will give the user a good idea of the operation. Don't just report based on the number of lines (or tasks). If there are four work items but the fifth one takes as long as the previous four, then the amount of work reported needs to be balanced; for example, provide a total of 8 units, with 1 unit for each of the first four and then the remaining four for the fifth item. Finally, done() was called on the progress monitor. This signifies that the Job has been completed, and can be removed from any views that are reporting the status. This is wrapped inside a finally block to ensure that the monitor is completed even if the Job finishes abnormally (for example, if an exception occurs). Time for action – dealing with cancellation Sometimes the user will change their mind; they may have selected the wrong option, or something more important may have come up. The progress monitor allows for two-way communication; the user can signify when they want to cancel as well. There is a method, isCancelled(), which returns true if the user has signified in some way that he/she wishes the Job to finish early. Periodically checking this during the operation of the Job allows the user to cancel a long-running Job before it reaches the end. 1. Modify the for loop in the HelloHandler to check on each iteration whether the monitor is cancelled or not. for(int i=0;i<50 && !monitor.isCanceled(); i++) { ... } if(!monitor.isCancelled()) { Display.getDefault().asyncExec(new Runnable() {…}); } [ 128 ] www.it-ebooks.info Chapter 4 2. Run the Eclipse instance and click on the Hello command. This time, go into the Progress view and click on the red stop square next to the job. The job should cancel and the dialog showing the message shouldn't be shown: What just happened? Being responsive to the user is a key point in implementing plug-ins. If there are long running operations, make sure to check to see if the user has cancelled the operation—there's no point in tying up the CPU if the user doesn't want it to continue. The monitor.isCancelled() method is generally implemented with a single field access, so calling it frequently often has no negative performance implications. Calling the isCancelled() method too many times is never noticed by users, however, not calling it enough certainly is noticed. Time for action – using subtasks and subprogress monitors When performing a set of operations, subtasks can give the user additional details about the state of the operation. A subtask is merely a named message, which is displayed along with the task name in the Progress view. 1. Add monitor.subTask() during the operation to give feedback. for (int i=0; i<50 && !monitor.isCanceled(); i++) { if(i==10) { monitor.subTask("Doing something"); } else if (i==25) { monitor.subTask("Doing something else"); } else if (i==40) { monitor.subTask("Nearly there"); } Thread.sleep(100); monitor.worked(100); } [ 129 ] www.it-ebooks.info Interacting with the User 2. Run the Eclipse instance, and look at the Progress view. The subtask should be shown underneath the status bar: 3. When calling another method with a progress monitor, if the monitor is passed as it is, it can have undesirable effects. Add a new method, checkDozen(), to the Job of HelloHandler and add a condition in the for loop that breaks out if the number of execution reaches 12. protected IStatus run(IProgressMonitor monitor) { ... } else if (i == 12) { checkDozen(monitor); } ... } private void checkDozen(IProgressMonitor monitor) { try { monitor.beginTask("Checking a dozen", 12); for (int i = 0; i < 12; i++) { Thread.sleep(10); monitor.worked(1); } } catch (InterruptedException e) { } finally { monitor.done(); } } 4. Run the Eclipse instance, select the Hello menu and open the Progress view and the progress status completely disappears after it reaches that point: [ 130 ] www.it-ebooks.info Chapter 4 5. To solve this problem, create another IProgressMonitor instance and pass that into the method call using a SubProgressMonitor instance. } else if (i == 12) { checkDozen(new SubProgressMonitor(monitor, 100)); continue; } 6. Now, run the action, and the progress will update as expected. Note, that the continue statement is used here to avoid calling monitor.worked(100). What just happened? The checkDozen() method took an IProgressMonitor instance, and simulated a set of different tasks (with different units of work). Passing the same monitor instance causes problems as the work gets missed between the two. To fix this behavior, a SubProgressMonitor instance was passed in. Because the SubProgressMonitor got 100 units of work from its parent, when the done() method was called on the SubProgressMonitor, the parent saw the completion of the 100 units of work. Importantly, this also allows the child to use a completely different scale of work units and be completely decoupled from the parent's use of work units. Time for action – using null progress monitors and submonitors When a method uses progress monitors extensively, it is inelegant to keep checking whether the monitor is null or not. Instead, the progress monitor can be replaced with a NullProgressMonitor, which acts as a no-op for all monitor calls. 1. Update the checkDozen() method to use a NullProgressMonitor, if null is passed. private void checkDozen(IProgressMonitor monitor) { if(monitor == null) monitor = new NullProgressMonitor(); [ 131 ] www.it-ebooks.info Interacting with the User This allows the remainder of the method to run without modification, and saves any NullPointerExceptions that may result. 2. A similar result is obtained for both the NullProgressMonitor and SubProgressMonitor with a wrapper/factory class called SubMonitor. This provides factory methods to wrap the monitor and creates child progress monitors. protected IStatus run(IProgressMonitor monitor) { try { SubMonitor subMonitor = SubMonitor.convert(monitor,"Preparing", 5000); for (int i = 0; i < 50 && !subMonitor.isCanceled(); i++) { if (i == 10) { subMonitor.subTask("Doing something"); } else if (i == 25) { subMonitor.subTask("Doing something else"); } else if (i == 40) { subMonitor.subTask("Nearly there"); } else if (i == 12) { checkDozen(subMonitor.newChild(100)); continue; } Thread.sleep(100); subMonitor.worked(100); } } catch (InterruptedException e) { } finally { if(monitor != null) monitor.done(); } } 3. Running the code has the same effect as the previous one, but it's more efficient. Note that the subMonitor object is used everywhere in the method until the end, where monitor is used to invoke done(). Since monitor may be null, it is guarded with a test. What just happened? The NullProgressMonitor was replaced with a SubProgressMonitor with a SubMonitor. To convert an arbitrary IProgessMonitor into a SubMonitor, use the convert() factory method. This has the advantage of testing for null (and using an embedded NullProgressMonitor if necessary) as well as facilitating the construction of SubProgressMonitor instances with the newChild() call. [ 132 ] www.it-ebooks.info Chapter 4 Note that the contract of SubMonitor requires the caller to invoke done() on the underlying progress monitor at the end of the method, so it gives an error when it's done an assignment such as monitor = SubMonitor.convert(monitor) in code. Since the isCancelled() check will ultimately call the parent monitor, it doesn't strictly matter whether it is called on the submonitor or the parent monitor. However, if the parent monitor is null, invoking it on the parent will result in a NullPointerException, whereas the SubProgressMonitor will never be null. In situations where there will be lots of recursive tasks, the SubProgessMonitor will handle nesting better than instantiating a SubProgressMonitor each time. That's because the implementation of the newChild() method doesn't necessarily need to create a new SubMonitor instance each time; it can keep track of how many times it has been called recursively. The SubMonitor also has a setWorkRemaining() call, which can be used to reset the amount of work for the outstanding progress monitor. This can be useful if the job doesn't know at the start how much work there is to be done, but it does become known later in the process. Time for action – setting job properties It is possible to associate arbitrary properties with a Job, which can be used to present its progress in different ways. For example, by specifying a command it's possible to click on a running Job and then execute something in the user interface, such as a detailed job description. Job properties are set with setProperty(), and can include any key/value combination. The keys use a QualifiedName, which is like a pair of strings for namespace/value. In the case of the Progress view, there is an IProgressConstants2 interface, which defines values that can be set, including COMMAND_PROPERTY, which can be used to invoke a command. 1. Open the HelloHandler and go to the end of the execute() method. Just before the Job is scheduled, acquire the Command from the ICommandService and then stamp it on the Job as a property. ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class); Command command = service == null ? null : service.getCommand("com.packtpub.e4.clock.ui.command.hello"); if(command != null) { job.setProperty(IProgressConstants2.COMMAND_PROPERTY,command); } job.schedule() return null; [ 133 ] www.it-ebooks.info Interacting with the User E4: In E4, the ICommandService can be obtained via injection, using @Inject ICommandService service. Injection will be covered in more detail in Chapter 7, Understanding the Eclipse 4 Model. 2. Run the Eclipse instance, open the Hello command and go to the Progress view. Nothing will be shown, because the Job expects a ParameterizedCommand instead. Modify the property value, and using the generateCommand() factory of ParameterizedCommand. if(command != null) { job.setProperty(IProgressConstants2.COMMAND_PROPERTY,command); job.setProperty(IProgressConstants2.COMMAND_PROPERTY, ParameterizedCommand.generateCommand(command, null)); } 3. Now run the Eclipse instance, go to the Progress view, and click on the Hello command. Underneath the Progress view, a hyperlink will be provided to allow firing off another Hello command: 4. If the command has no handler (or the handler is disabled), a pop-up error message will be shown: [ 134 ] www.it-ebooks.info Chapter 4 5. If the command is handled, clicking on the link will run the command, which in this case runs the HelloHandler and launches another job instance. Each click will spawn off a new Job: 6. It's possible to change the icon shown in the view by specifying an ImageDescriptor as a Job property with the key ICON_PROPERTY. The image descriptor can be loaded from the createFromURL() method of ImageDescriptor and set as a property. job.setProperty(IProgressConstants2.ICON_PROPERTY, ImageDescriptor.createFromURL( HelloHandler.class.getResource("/icons/sample.gif"))); 7. Run the Eclipse instance, go to the Progress view, and then click on the Hello menu. The icon should be shown against the job: What just happened? Setting properties on the running Job allows viewers to extract information and present it in different ways. Properties are specified with a QualifiedName key and the value is passed in as an object, which is property specific. [ 135 ] www.it-ebooks.info Interacting with the User The purpose of the QualifiedName key is to act as a string identifier, but partitioned into different namespaces. For example, the properties used by the IProgressConstants use org.eclipse.ui.workbench.progress as the namespace qualifier, and shorter strings such as command and icon for individual properties. The benefit of this (instead of org.eclipse.ui.workbench.properties.command) is that the long prefix string is stored once in memory and so doesn't take up repeated space in either the class file or the PermGen space, which can be limited on JDK 7. (JDK 8 will remove the PermGen, so it should be less of an issue in the future.) Valid values for the Job in the Progress view can be found in the IProgressConstants and IProgressContstants2 interfaces. Note that this is not a fixed set; additional Job properties can be added for use both elsewhere in the Eclipse platform and by independent extensions. To associate a Command with a Job, set a property which contains a ParameterizedCommand. The factory method generateCommand() on the ParameterizedCommand class can be used to convert a command into a ParameterizedCommand. The Command can be acquired from the ICommandService, which is acquired via the PlatformUI workbench or through injection in E4. Have a go hero – displaying in the taskbar The IProgressConstants2 interface also defines a property name SHOW_IN_TASKBAR_ ICON_PROPERTY, which shows whether the progress of the Job is exposed to those operating systems that support it. On OS X, a bar will be shown over the Eclipse application icon. Set the property to the value Boolean.TRUE and see the effect it has on the Job. The Job can also indicate if it is running in the foreground or the background, and can query its state via the Job property PROPERTY_IN_DIALOG. This is not intended to be set by clients, but can be read and displayed (or different actions be taken). Pop quiz: understanding jobs Q1. What is the difference between Display.syncExec() and Display.asyncExec()? Q2. What is the difference between Display and UISynchronize? Q3. What is the difference between Job and UIJob? [ 136 ] www.it-ebooks.info Chapter 4 Q4. What is the singleton Status object that indicates everything is ok? Q5. How is the CommandService obtained in Eclipse? Q6. How is an icon associated with a Job in the Progress view? Q7. When should a SubMonitor instead of a SubProgressMonitor be used? Q8. How frequently should the Job cancellation status be checked? Reporting errors As long as everything works as expected, the IDE won't need to tell the user that something has gone wrong. Unfortunately, even the most optimistic programmer won't believe that the code will work in every situation. Bad data, threading issues, simple bugs and environmental issues can result in operations failing, and when it fails, the user needs to be notified. Eclipse has built-in mechanisms to report problems, and these should be used in response to a user interaction that has failed. Time for action – showing errors So far, the code has been using an information dialog as the demonstration of the handler. There's an equivalent method that can be used to create an error message instead. Instead of calling MessageDialog.openInformation(), there's an openError() method, which presents the same kind of dialog, but with an error message instead: [ 137 ] www.it-ebooks.info Interacting with the User Using dialogs to report errors may be useful for certain environments, but unless the user has just invoked something (and the UI is blocked while doing it), reporting errors via a dialog is not a very useful thing to do. Instead, Eclipse offers a standard way to encapsulate both success and failure, in the Status object and the interface IStatus that it implements. When a Job completes, it returns an IStatus object to denote success or failure of the Job execution. 1. Introduce an error into the run() method of HelloHandler, whch will generate a NullPointerException. Add a catch to the existing try block and use that to return an error status. Since the OK_STATUS is a singleton instance of Status that can be used for all successful operations, it is necessary to instantiate a new Status object with the error information enclosed. protected IStatus run(IProgressMonitor monitor) { try { SubMonitor subMonitor = SubMonitor.convert(monitor,"Preparing", 5000); subMonitor = null; // the bug ... } catch (NullPointerException e) { return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Programming bug?", e); } finally { 2. Run the Eclipse instance, and invoke the Hello command. An exception will be generated, and the status object containing the information needed will be passed to Eclipse. If the Job is running in the foreground, the job scheduler will present the status message if it is an error: [ 138 ] www.it-ebooks.info Chapter 4 3. Go to Window | Other | Error Log in the target Eclipse instance to see the error, which has also been written into the workspace/.metadata/.log file: 4. Double-click on the entry in the error log to bring up specific details: [ 139 ] www.it-ebooks.info Interacting with the User 5. Instead of returning an error status, it's also possible to log the error message programmatically, using StatusManager. The StatusManager is a service that can be acquired from a static factory or injected from a service. To just log the information but keep going, execute the following code snippet: } catch (NullPointerException e) { return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Programming bug?", e); StatusManager statusManager = StatusManager.getManager(); Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Programming bug?", e); statusManager.handle(status, StatusManager.LOG); } ... E4: StatusManager has been replaced with StatusReporter, which has a report() method that is equivalent to the handle() method. 6. Run the Eclipse instance, invoke the Hello action and see the error being logged without displaying a dialog. 7. Modify the status flags to add SHOW as well: statusManager.handle(status, StatusManager.LOG | StatusManager.SHOW ); 8. Re-run the Eclipse instance, invoke the Hello action and the error will be shown as well as logged. 9. Finally, remove the bug from the HelloHandler so that it doesn't cause errors. subMonitor = null; // the bug What just happened? First, openError() was used to show an error, which is useful for specific cases—such as when the user is interacting with the UI and has just done an operation that is problematic. The next step looked at status reporting and handling in Eclipse, including how exceptions are captured and associated with a specific plug-in. A Status object was used to indicate a single issue—though there's a MultiStatus, which can be used to add additional Status instances, if required. Generally, the status should be logged but not shown as dialogs popping up in the user's screen (especially from a background job) are a UX anti-pattern. [ 140 ] www.it-ebooks.info Chapter 4 These flags can be combined, so LOG indicates that the status message should be logged but not otherwise displayed to the user, while SHOW indicates that the message should be shown in the standard dialog. Both of these happen asynchronously; the code will continue executing after invoking these calls, regardless of how the messages are shown to the user. There is a BLOCK flag as well, which prevents continued execution of the thread, but this should not be used as it may lead to inadvertent deadlocks. Pop quiz – understanding errors Q1. How is an info/warning/error dialog shown? Q2. What is the difference between StatusManager and StatusReporter? Q3. Are status reports asynchronous or synchronous by default? Q4. How can more than one problem be reported at the same time? Summary This chapter covered how the user interfaces respond to user input by defining menus associated with abstract commands that are associated with handlers to execute code. It also covered how to run code in the background with jobs, and report the errors via the standard error reporting mechanism. The next chapter will look at how to store preferences so that configuration items can be kept between restarts of the Eclipse platform. [ 141 ] www.it-ebooks.info www.it-ebooks.info 5 Storing Preferences and Settings An IDE is powerful as it provides a number of different utility windows to help the developer as they do their job. It becomes more powerful when they can customize it to their own taste, whether it is something as simple as colors, or something more targeted such as filters. The preference store in Eclipse allows users to customize it in the way they want. In this chapter we shall: Read and write preferences from a PreferenceStore Create a PreferencePage using FieldEditors Implement additional FieldEditors Explain the difference between IEclipsePreferences and IPreferenceStore Cover settings and mementos for storing transient state Storing preferences A user preference is a stored configuration option that persists between different Eclipse runtimes. Preferences are simple values (int, String, boolean, and so on) and are identified with a String key, typically prefixed with the plug-in's identifier. Preferences can also be edited via a standard preference panel injected with an extension point. They can also be imported and exported from an Eclipse workbench using File | Import/Export | Preferences and saved as an epf (Eclipse Preference File). www.it-ebooks.info Storing Preferences and Settings Time for action – persisting a value To save and load preferences, an instance of IPreferenceStore is typically obtained from the AbstractUIPlugin subclass, in this case, the Activator of the clock plug-in. The getPreferenceStore() method returns a store which can be used to persist key/value pairs. Perform the following steps: 1. 2. Open the Activator of the clock.ui plug-in. Add the following to the start() method to count the number of times the plug-in has been launched: int launchCount = getPreferenceStore().getInt("launchCount"); System.out.println("I have been launched " + launchCount + "times"); getPreferenceStore().setValue("launchCount",launchCount+1); 3. 4. Run the Eclipse instance and open the Time Zone View. 5. Close the Eclipse instance down and run it again (but do not clear the workspace). Open the Time Zone View again. 6. In the host Eclipse open the Console view. It should now say "I have been launched 1 times". In the host Eclipse, open the Console view. It should say "I have been launched 0 times". What just happened? The IPreferenceStore is usually loaded via the AbstractUIPlugin, which provides a getPreferenceStore(). This API provides get/set methods for primitive types (getInt(), getBoolean(), getString(), and so on) as well as setValue() methods for the same types. If the class does not have AbstractUIPlugin subclass, it can also be acquired by using the following code: IPreferenceStore preferenceStore = new ScopedPreferenceStore(InstanceScope.INSTANCE,ID); In the previous code, ID is the name of the identifier prefix used, typically the bundle's ID. (In AbstractUIPlugin it is calculated as bundle.getSymbolicName()). E4: If using E4, IPreferenceStore can be obtained via injection. Alternatively, a single preference value can be bound with @Preference(name) or @Preference to get the IEclipsePreferences object. It is also possible to track individual preferences being changed with a method parameter injection. [ 144 ] www.it-ebooks.info Chapter 5 The number of times the plug-in is launched will be stored as a value in the preferences store. If run from Eclipse, the message, "I have been launched 0 times" is shown the first time the application is run; the second time, "I have been launched 1 times"; and each restart will update the counter. Note that the preferences API generally shouldn't be used to store plug-in specific state. There's a method getStateLocation() which returns an IPath that can be used for storing such transient state. If "I have been launched 0 times" is displayed repeatedly, Clear workspace before launching may be selected in the launch configurations menu. To disable this, go to Run | Run Configurations... and in the Main tab ensure that the Clear checkbox is deselected. Time for action – creating a preference page Although preferences can be stored, providing a user interface is necessary for end users. A preference page implements the IPreferencePage interface. The easiest way is to use FieldEditorPreferencePage as a super class, which provides most of the standard plug-in behavior needed. Perform the following steps: 1. Open the plugin.xml of the clock.ui plug-in. In order to declare a new preference page, use the org.eclipse.ui.preferencePages extension point. Add the following code: <extension point="org.eclipse.ui.preferencePages"> <page name="Clock" class="com.packtpub.e4.clock.ui.ClockPreferencePage" id="com.packtpub.e4.clock.ui.preference.page"/> </extension> 2. The same effect can be achieved by editing the plugin.xml in the editor and clicking on Add in the Extensions tab and selecting the preferencePages extension point. 3. Create a class ClockPreferencePage that extends FieldEditorPreferencePage in the com.packtpub.e4.clock.ui package as follows: public class ClockPreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { protected void createFieldEditors() { } public void init(IWorkbench workbench) { } } [ 145 ] www.it-ebooks.info Storing Preferences and Settings 4. Run the Eclipse application. Go to the preferences window by clicking on Eclipse | Preferences for OS X, or Window | Preferences for Windows/Linux. In the preferences list a Clock page should be displayed, although at present there will be no content. 5. Add a new IntegerFieldEditor for storing the launch count by adding it to the createFieldEditors() method as follows: protected void createFieldEditors() { addField(new IntegerFieldEditor("launchCount", "Number of times it has been launched" ,getFieldEditorParent())); } 6. Run the Eclipse application, go to Preferences and look at the Clock page. To get it to display the correct value, the FieldEditorPreferencePage needs to be connected to the plug-in's PreferenceStore as follows: public void init(IWorkbench workbench){ setPreferenceStore( Activator.getDefault().getPreferenceStore()); } 7. Now run the Eclipse application, go to the Preferences and look at the Clock page. The number will update based on the number of times the Eclipse application has launched. What just happened? The preferences page was created and connected to the preferences window and the correct preference store (obtained from the plug-in's Activator). Instances of FieldEditor were added to display properties on a per-property basis. The implementation of getFieldEditorParent() is something that needs to be called each time a field editor is created. It might be tempting to refactor this into a common variable, but the JavaDoc says that the value cannot be cached. If the FLAT style is used, then it will create a new instance of the parent each time it is called. [ 146 ] www.it-ebooks.info Chapter 5 Time for action – creating warning and error messages In free-form text fields it's sometimes possible to input a value that doesn't make sense. For example, when asking someone for their e-mail address it might be necessary to validate it against some kind of regular expression like [email protected]+ to provide a simplistic test. Perform the following steps: 1. To test the default validation, run the Eclipse instance and go to the Clock preference page. Type some text in the numeric field. A warning message will be displayed as shown in the following screenshot: 2. To add validation, create a new field called offset which allows valid values between -14 and +12. (By default, IntegerFieldEditor validates against the range 0 to MAX_INT.) Add the following to the createFieldEditors() method: IntegerFieldEditor offset = new IntegerFieldEditor("offset", "Current offset from GMT", getFieldEditorParent()); offset.setValidRange(-14, +12); addField(offset); 3. Run the Eclipse instance, go to the Clock preference page and type in an invalid value as follows: [ 147 ] www.it-ebooks.info Storing Preferences and Settings What just happened? Each field editor can determine what is (or is not) valid, and the validity of the page as a whole is a conjunction of all of the individual field editors' validity. It's also possible to create custom validation rules by creating a subclass of the appropriate FieldEditor by overriding the isValid() method appropriately. The error message will display the warning, "Value must be an Integer between -14 and 12" at the top of the preference page." Time for action – choosing from a list Although free text may be appropriate for some types of preferences, for others, choosing from a set of values may be more appropriate. ComboFieldEditor can be used to present the user with a selection, which can be used to represent the user's favourite TimeZone. The combo dropdown is built from an array of pairs of strings. The first string is the displayed label in the dropdown, while the second value is the string identifier that will be persisted to (and loaded from) the preferences store. Perform the following steps: 1. In the createFieldEditors() method of the ClockPreferencePage class, add the following code to populate a ComboFieldEditor with the list of TimeZone IDs: protected void createFieldEditors() { String[][] data; String[] ids = TimeZone.getAvailableIDs(); Arrays.sort(ids); data = new String[ids.length][]; for (int i = 0; i < ids.length; i++) { data[i] = new String[] { ids[i], ids[i] }; } addField(new ComboFieldEditor("favourite", "Favourite time zone", data, getFieldEditorParent())); } 2. Run the Eclipse instance and go to the Clock preference page. A new dropdown will allow the user to select their favourite TimeZone. Choose a value, then close and re-open the Eclipse instance; the previous value should be stored as seen in the following screenshot: [ 148 ] www.it-ebooks.info Chapter 5 What just happened? Adding new types of field editor allows different types of data to be edited. Since all of the preferences are saved as a string, the ComboFieldEditor takes a set of pairs of strings; one for the display label, and one for the persisted value. The ComboFieldEditor was initialized with a list of the TimeZone IDs, using the ID for both the display label and the persisted value. The display could present more information such as the display name, the offset from GMT, or other metadata. However, the string value which is persisted to the preferences should be unique and not subject to parsing or loading errors. In this case, using the ID means that a later iteration of the preferences plug-in could render the display text in a different form while still persisting the same ID in the preference store. [ 149 ] www.it-ebooks.info Storing Preferences and Settings Time for action – using a grid The preference values weren't lined up as expected. This is because the default preference field editor uses a FLAT style of rendering, which simply lays out the fields similar to a vertical RowLayout. Perform the following steps: 1. Change it to a more natural look by specifying a GRID style of rendering: public ClockPreferencePage() { super(GRID); } 2. Now when the preference page is displayed, it will look more natural as seen in the following screenshot: What just happened? The default or FLAT style does not render well. It was added in 2007 (see Eclipse bug 163281) before the popularity of the grid layout increased, and typically needs to be overridden to provide a decent user interface experience. Switching to GRID does this by working out the label length, field lengths, and setting up the split accordingly. Furthermore, the view is resizable with the fields taking up the additional stretch space. If the layout needs further customization, or the widget set needs to be extended, then it is possible to create a plain subclass of PreferencePage and create the contents in the createContents() method. Then apply any changes in the performOk() method or performApply() methods. [ 150 ] www.it-ebooks.info Chapter 5 Time for action – placing the preferences page When the preference page is created, if it does not specify a location (known as a category in the plugin.xml file and manifest editor), it is inserted into the top level. This is appropriate for some kinds of projects (for example, Mylyn, Java, Plug-in Development); but many plug-ins should contribute to an existing location in the preference page tree. Complete the following steps: 1. Preference pages can be nested by specifying the parent preference page's ID. To move the Clock preference page underneath the General preference page, specify org.eclipse.ui.preferencePages.Workbench as the category as shown in the following code: <extension point="org.eclipse.ui.preferencePages"> <page name="Clock" id="com.packtpub.clock.ui.preference.page" category="org.eclipse.ui.preferencePages.Workbench" class="com.packtpub.e4.clock.ui.ClockPreferencePage"/> </extension> 2. Run the Eclipse instance and look in Preferences. The Clock preference page should now be under the General tree node instead, as seen in the following screenshot: [ 151 ] www.it-ebooks.info Storing Preferences and Settings What just happened? The preference page can be placed anywhere in the hierarchy by specifying the parent page's ID in the category attribute. The parent pages can be listed using Window | Show View | Other | Console, then opening a Host OSGi console followed by running the following command: osgi> pt -v org.eclipse.ui.preferencePages Extension point: org.eclipse.ui.preferencePages [from org.eclipse.ui] Extension(s): ------------------null [from org.eclipse.ant.ui] <page> name = Ant class = org.eclipse.ant.internal.ui.preferences.AntPreferencePage id = org.eclipse.ant.ui.AntPreferencePage </page> Another way of listing the IDs is to use the Browse button via the plugin.xml editor. From the extension point defining the Clock preference page, the Browse button next to the category field will bring up a dialog containing all of the valid IDs, including a search filter to reduce the number of matches, as shown in the following screenshot: [ 152 ] www.it-ebooks.info Chapter 5 Time for action – using other field editors The FieldEditorPreferencePage supports other types of field editor. These different types of editor include BooleanFieldEditor, ColorFieldEditor, ScaleFieldEditor, FileFieldEditor, DirectoryFieldEditor, PathEditor, and RadioGroupFieldEditor. Add a sample of each of these types to the ClockPreferencePage page to find out what they can store. Perform the following steps: 1. Open the createFieldEditors() method of the ClockPreferencePage and add the following code at the bottom of the method: addField(new BooleanFieldEditor("tick","Boolean value", getFieldEditorParent())); addField(new ColorFieldEditor("colour", "Favourite colour", getFieldEditorParent())); addField(new ScaleFieldEditor("scale", "Scale", getFieldEditorParent(), 0, 360, 10, 90)); addField(new FileFieldEditor("file", "Pick a file", getFieldEditorParent())); addField(new DirectoryFieldEditor("dir", "Pick a directory", getFieldEditorParent())); addField(new PathEditor("path","Path", "Directory",getFieldEditorParent())); addField(new RadioGroupFieldEditor("group", "Radio choices", 3, data,getFieldEditorParent(),true)); 2. Run the Eclipse instance and go to the Clock preference page. It should look similar to the following screenshot: [ 153 ] www.it-ebooks.info Storing Preferences and Settings What just happened? This change added different types of standard field editors provided by the JFace package, to give a flavor of the types of data entry element that can be shown. Some of the editors are specific to the kinds of entry points that are used in Eclipse itself, such as file, directory, or path editors. Others are more general such as colour, scale, boolean, or radio choices. The values are persisted in the preferences format in a text values, appropriate to the data type. To see where the values are written to, go to the runtime-EclipseApplication/. metadata/.plugins/org.eclipse.core.runtime/.settings/com.packtpub. e4.clock.ui.prefs file. The contents look similar to the following code: colour=49,241,180 eclipse.preferences.version=1 favourite=Europe/Milton_Keynes group=Europe/Milton_Keynes launchCount=28 scale=78 tick=true The persisted colour value is stored as a red/green/blue triple, while the boolean value is stored as a true or false value. Time for action – adding keywords Eclipse had a search field in the preferences list since version 3.1. This is defined not from UI but from a separate keyword extension instead. The keyword has an ID and a label, but the label isn't shown; rather, it's a space separated list of words which can be used in the filtering dialog. Perform the following steps: 1. To add the keywords offset and timezone to the ClockPreferencePage, create a new extension point in plugin.xml for org.eclipse.ui.keywords: <extension point="org.eclipse.ui.keywords"> <keyword id="com.packtpub.e4.clock.ui.keywords" label="offset timezone"/> </extension> [ 154 ] www.it-ebooks.info Chapter 5 2. Now associate these keywords with the preference page itself as follows: <extension point="org.eclipse.ui.preferencePages"> <page name="Clock" ... > <keywordReference id="com.packtpub.e4.clock.ui.keywords"/> </page> </extension> 3. Run the Eclipse instance, go to the Preferences page and type timezone and offset in the search box. The Clock preference page should be shown in both cases: What just happened? By providing a list of keywords and associating them with the preferences page the user can search for the item in the preferences tree. The same keyword support is used to search for items in other places such as the New dialog wizards and the properties page. Furthermore, these keywords are internationalizable. By specifying a key with a % name, Eclipse can load the keywords from an externalized file called plugin.properties. If %clock.keywords was used as the label and plugin.properties had an entry clock. keywords=timezone offset, then a French translation of the keywords could be provided in a plugin_fr.properties file. [ 155 ] www.it-ebooks.info Storing Preferences and Settings Time for action: using IEclipsePreferences Although JFace defines IPreferenceStore, there's a lower-level interface called IEclipsePreferences. The key difference between IPreferenceStore and IEclipsePreferences is that the latter has support for arbitrary nodes similar to nested HashMaps. IEclipsePreferences is also based upon the OSGi Preferences service and can be used without any UI or JFace involvement. When the IPreferenceStore is obtained from AbstractUIPlugin, it lazily creates a ScopedPreferenceStore using InstanceScope and the bundle's symbolic name as the node name. Perform the following steps: 1. Modify clock.ui's Activator: int launchCount = getPreferenceStore().getInt("launchCount"); IEclipsePreferences eclipsePreferences = InstanceScope.INSTANCE. getNode(PLUGIN_ID); int launchCount2 = eclipsePreferences.getInt("launchCount",-1); System.out.println("I have been launched " + launchCount + " times and " + launchCount2); 2. 3. Run the Eclipse instance and open the Time Zone View. In the host Eclipse instance, go to the Console view, and the output should show the same value being displayed I have been launched 6 times and 6. What just happened? The same preference value was accessed through both as JFace's IPreferenceStore, which is used by UI plug-ins and extension points such as the PreferencePage and through the IEclipsePreferences API that can be used by headless plug-ins. E4: The IEclipsePreferences is used by E4 as the default preferences store. To pass in an existing IEclipsePreferences to an API (like the preference page), an inverse adaptor can be created using the following code: public class EclipsePreferencesScope implements IScopeContext { private IEclipsePreferences preferences; public EclipsePreferencesScope(IEclipsePreferences preferences) { this.preferences = preferences; } public String getName() { return ""; } [ 156 ] www.it-ebooks.info Chapter 5 public IPath getLocation() { return new Path(preferences.absolutePath()); } public IEclipsePreferences getNode(String qualifier) { return preferences; } } Finally the IEclipsePreferences interface is a subtype of Preferences, which is a standard OSGi service. To compile against just the OSGi runtime, use Preferences as the declared type for variables instead of IEclipsePreferences. That way, the code can be compatible with both Eclipse and standard OSGi libraries. Have a go hero – translating into different languages Eclipse's internationalization support is provided by a plugin.properties file. In the plugin.xml, instead of using strings for the values, use %keys. The % instructs the engine to look up a corresponding entry in plugin.properties to display the value. To support different languages use plugin_fr.properties, plugin_de.properties for French and German respectively (don't forget to add these to build.properties otherwise they won't be found). The keys are still the same, but the values can be localized appropriately. The search keywords are an example of something that can be searched as well, so use an online translation service or make-up a translation to test out the effect of running Eclipse in a different language. (Eclipse can be launched in different languages with eclipse -nl de on the command line.) Using IMemento and DialogSettings Preferences are designed to be values persisted in the workspace (or project) that can be consumed by both UI and non-UI components. The preference support also handles changes and default values. However, it's not always the case that values need to be persisted in the preference store. Certain values are specific to views such as the order of columns in a table view, or whether the sort order is increasing or decreasing. Furthermore, it's possible to have multiple views in different windows that have different sort orders. IMementos are a means to store view-specific rendering data in a way that can be saved by the workbench when the window closes, and re-opened when the perspective is brought back. DialogSettings are a more general way of storing values. Despite the name, these are not limited to dialogs. [ 157 ] www.it-ebooks.info Storing Preferences and Settings Time for action – adding a memento for the Time Zone View The TimeZoneView, created in Chapter 2, Creating views with SWT, presents a list of regions as tabs and allows the user to select a region of interest. When the view is closed and then re-opened, the previously selected tab is not remembered. This is the kind of nugget that could be persisted in a memento for later re-use. Perform the following steps: 1. To record what the last selected tab was when it is changed; add the variable lastTabSelected, which contains the last selected tab name, to the TimeZoneView as follows: private transient String lastTabSelected; 2. At the end of createPartControl(), add a selection listener to the tab, such that any time the tab's selection is changed, the name of the tab is remembered: tabs.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { if(e.item instanceof CTabItem) { lastTabSelected = ((CTabItem)e.item).getText(); } } }); 3. When the workbench is closed, save the state by adding a saveState() method: public void saveState(IMemento memento) { super.saveState(memento); memento.putString("lastTabSelected", lastTabSelected); } 4. Restore the data when the view is opened: public void init(IViewSite site, IMemento memento) throws PartInitException { super.init(site, memento); if(memento != null) { lastTabSelected = memento.getString("lastTabSelected"); } } 5. Finally, update createPartControl() to set the selected tab when it's opened: if(lastTabSelected == null) { tabs.setSelection(0); } else { CTabItem[] items = tabs.getItems(); for (CTabItem item : items) { if(lastTabSelected.equals(item.getText())) { [ 158 ] www.it-ebooks.info Chapter 5 tabs.setSelection(item); break; } } } 6. Run the workbench, show the Time Zone View, select a tab, close the workbench and re-open, and the last selected tab should be restored. What just happened? The IMemento is a way of storing content between launches of the workbench. When the workbench shuts down, it sends save() to each of the open views and persists the state such that the workbench state is restored when it is re-opened. If this behavior is not seen while testing, check that the Eclipse workspace isn't being automatically cleaned at startup in the launch configuration. Unfortunately, although the view is saved if it is open when the workbench is closed, if the view is closed first then nothing is saved. This makes the memento pattern one of the most useless patterns in Eclipse. The IMemento pattern should not generally be used; instead, use the preferences store, or the DialogSettings as explained next. E4: In E4, the @PostConstruct annotation can be used to set up values that were saved prior to a view being shut down, and a @PreDestroy to save any state that is required for next time. It is not as fragile as the IMemento pattern. Time for action – using DialogSettings A more useful alternative to the Memento pattern is DialogSettings, which provides a properties-like interface for storing strings and other basic primitive values. This stores its information in an XML file, and can be acquired as a standard extension to the UI plug-in or created from a file location. The settings store is used to store values persistently, and is saved automatically when the plug-in shuts down. At startup, it is loaded automatically. Perform the following steps: 1. To migrate the settings for the last tab selected to use DialogSettings, remove the init() and save() methods from the TimeZoneView and replace them with the following in the createPartControl(): final IDialogSettings settings = Activator.getDefault().getDialogSettings(); lastTabSelected = settings.get("lastTabSelected"); [ 159 ] www.it-ebooks.info Storing Preferences and Settings 2. The call to getDialogSettings() comes from the UIPlugin class. Once the DialogSettings have been acquired, it can be used to store and retrieve values. Update the selection listener to store this in the settings instead: tabs.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { if (e.item instanceof CTabItem) { lastTabSelected = ((CTabItem) e.item).getText(); settings.put("lastTabSelected", lastTabSelected); } } }); 3. Now run the Eclipse instance, go to the Time Zone View and note how the settings are saved when either the view is closed or the application shuts down. What just happened? The calls to IMemento were replaced with DialogSettings, a much more useful mechanism for storing values. In addition, we created a separate group of settings to create a nested namespace with settings.addNewSection("name") and with settings.getSection("name"). The name DialogSettings comes from the fact that was initially used by dialogs with warnings such as "Do not show this message again". In fact, this is so common that a Dialog with a "Do not show this message again" can be created with a MessageDialogWithToggle as follows: if (settings.getBoolean("spamalot")) { MessageDialogWithToggle dialog = MessageDialogWithToggle. openInformation(Display.getCurrent().getActiveShell(), "Spam", "Keep being spammed?", "Do not show this spam again", false, null, null); boolean spamalot = !dialog.getToggleState(); settings.put("spamalot",spamalot); } The MessageDialogWithToggle also has an option to write this to a preference store using the store and the key value in the last two values (null in the example). The DialogSettings can also be used to store items such as the last-value-used or to restore selection. [ 160 ] www.it-ebooks.info Chapter 5 The key difference between using an IPreferenceStore and using DialogSettings is that the former can be used for importing/exporting preferences between Eclipse instances, using the File | Import/Export | Preferences menu. The DialogSettings, on the other hand, are supposed to be transient and recreatable if they are lost. E4: As E4 takes off and provides default support for reading and writing preferences, expect to see more uses of the preferences for storing both transient and non-transient preference data. Pop quiz – understanding preferences Q1. What is the default style used for the FieldEditorPreferencePage, and how can it be changed to something more aesthetically pleasing? Q2. What kinds of primitive values can be edited with a FieldEditorPreferencePage? Q3. How can a preference value be searched for in the preference page? Q4. Which is the preferred API for storing view-specific information; IMemento or IEclipsePreferences? Q5. Which class provides the "Do not show this message again" support? Summary We have covered the mechanisms that Eclipse uses to store metadata values. A preference store is the key/value pair mechanism used by preference pages, as well as being interacted programmatically from headless plug-ins. It is also possible to use IMementos or DialogSettings to store transient information about a view or plug-in. While some code examples demonstrate IMemento, they are essentially useless and can be substituted with DialogSettings without any loss of generality. Some plug-ins just use PreferenceStore and ignore DialogSettings, even though information may not be strictly necessary to persist or share between instances. For that reason, the MessageDialogWithToggle also includes the ability to persist into an associated PreferenceStore. In the next chapter, we will look at how to work with Resources inside Eclipse. [ 161 ] www.it-ebooks.info www.it-ebooks.info 6 Working with Resources As an IDE, Eclipse is used to work with files and folders. Eclipse creates the concept of a workspace (a group of related projects), a number of projects, and then files and folders underneath each. These resources are then used by builders to be able to create derived resources upon change, which is how Eclipse compiles .java source files into .class files. In this chapter we shall: Create a custom editor Read the contents of a file Create a resource file Use a builder to automatically process changes Integrate the builder with a nature Highlight problems in the editor with markers Using the workspace and resources Everything in the Eclipse IDE is based on the concept of a workspace which contains a number of projects, which in turn contain files and folders. Generically, these are all resources which are represented with a path and then with either a set of contents or a set of children. www.it-ebooks.info Working with Resources Time for action – creating an editor The example will be based on a (made-up) markup language called minimark, which is essentially a plain text file with blank delimited paragraphs that can be translated into an HTML file. This will involve creating an editor for text-based content, for minimark files. Perform the following steps: 1. Create a new plug-in project called com.packtpub.e4.minimark.ui by going to File | New | Project | Plug-in project and filling in the following details: 2. Project name: com.packtpub.e4.minimark.ui Click on Next and fill in the following details: ID: com.packtpub.e4.minimark.ui Version: 1.0.0.qualifier Name: Minimark Vendor: PACKTPUB Select the checkbox for Create an Activator Select the checkbox for This plug-in will make contributions to the UI Unselect the checkbox for Create a Rich Client Application 3. 4. Click on Finish and a new plug-in will be created. 5. Go to the Extensions tab and click on Add. The extension points dialog will show; search for editors and it should show up in the list. (If it doesn't, uncheck the Show only extension points from the required plug-ins and it will prompt to add org.eclipse.ui.editors to the required dependencies.) 6. Once the extension point has been added, right-click on the extension point and select New | Editor from the menu. This will add a template extension point; fill it in as follows: The next step is to create an editor for the minimark files. Open the plug-in's manifest by right-clicking on the project and selecting Plug-in Tools | Open Manifest or by double-clicking on the plugin.xml file. id: com.packtpub.e4.minimark.ui.minimarkeditor name: Minimark extensions: minimark class: com.packtpub.e4.minimark.ui.MinimarkEditor [ 164 ] www.it-ebooks.info Chapter 6 7. The resulting plugin.xml will look like the following code: <extension point="org.eclipse.ui.editors"> <editor name="Minimark" extensions="minimark" default="false" class="com.packtpub.e4.minimark.ui.MinimarkEditor" id="com.packtpub.e4.minimark.ui.minimarkeditor"/> </extension> 8. 9. Now, add the required dependencies in the Dependencies tab: org.eclipse.jface.text: Provides text-processing libraries org.eclipse.ui.editors: The general editor support org.eclipse.ui.workbench.texteditor: The general text editor Use the File | New | Class wizard to create MinimarkEditor in the com.packtpub.e4.minimark.ui package as a subclass of AbstractTextEditor: public class MinimarkEditor extends AbstractTextEditor { } 10. Run the Eclipse instance, and create a project with File | New | Project | General Project called EditorTest. Then use the File | New | File to create a file called test.minimark. Double-click on this file and an error will be seen, as shown in the following screenshot: 11. This happens because an editor needs to be hooked up to a document provider which synchronizes the content of the document with any other open editors. (This allows Eclipse to open multiple editors on the same file and still show the same changes in both.) To solve the error, add a constructor that sets the editor's document provider to a general TextFileDocumentProvider: import org.eclipse.ui.editors.text.TextFileDocumentProvider; public class MinimarkEditor extends AbstractTextEditor { public MinimarkEditor() { setDocumentProvider(new TextFileDocumentProvider()); } } [ 165 ] www.it-ebooks.info Working with Resources 12. Run the Eclipse instance again, double-click on the test.minimark file and an empty text editor will be opened. What just happened? A basic text editor was created and associated with files ending in .minimark. To add an editor type, the following bundles are needed: org.eclipse.core.runtime org.eclipse.jface.text org.eclipse.ui org.eclipse.ui.editors org.eclipse.ui.workbench.texteditor The editor needs to be a subtype of an EditorPart. In this case, AbstractTextEditor provides the basic functionality for editing text-based files. It also needs to be registered with the org.eclipse.ui.editors extension point. Note that building editors and the document providers that underpin them is a book in its own right; the implementation of the editor here is to support the resource-processing examples. More information on writing custom editors is available in the online help. Time for action – writing the markup parser First, the format of the markup language needs to be explained. The first line will be a title, and then subsequent paragraphs are blank-line separated. This can be translated into an HTML file as follows: minimark source This is the title A paragraph with some text Another paragraph Translated HTML <html><head><title>This is the title</title></head><body><h1>This is the title</h1><p> A paragraph with some text </p><p> Another paragraph </p></body></html> [ 166 ] www.it-ebooks.info Chapter 6 Perform the following steps: 1. Create a class called MinimarkTranslator in the com.packtpub. e4.minimark.ui package as follows: public class MinimarkTranslator { public static void convert(Reader reader, Writer writer) throws IOException { BufferedReader lines = new BufferedReader(reader); String line; String title = String.valueOf(lines.readLine()); writer.write("<html><head><title>"); writer.write(title); writer.write("</title></head><body><h1>"); writer.write("</h1><p>"); while (null != (line = lines.readLine())) { if ("".equals(line)) { writer.write("</p><p>"); } else { writer.write(line); writer.write('\n'); } } writer.write("</p></body></html>"); writer.flush(); } } 2. Copy the example text from the start of this section and save it as a file input.txt in the com.packtpub.e4.minimark.ui project. 3. Add a main() method to MinimarkTranslator to read in the input.txt file and write it out as output.txt: public static void main(String[] args) throws IOException { convert( new FileReader("in.txt"), new FileWriter("out.txt")); } 4. Run this as a Java application and refresh the project. The file out.txt should be shown, and opening it should show an HTML file like the one at the start of this section. 5. After testing MinimarkConverter works as expected, delete the main() method. Automated plug-in testing will be covered in more detail in Chapter 9, Automated Testing of Plug-ins. [ 167 ] www.it-ebooks.info Working with Resources What just happened? The minimal markup language can take plain ASCII text and be translated into an HTML file. The purpose of this exercise is not to define a fully comprehensive markup processor, but rather to provide a simple translator that can be shown to generate HTML as rendered in a browser from a plain text file. Note that the translator has at least one bug; if the file is empty then the title may well be null, which would result in a title of null in the HTML browser. The reader is invited to replace the translator with a different implementation, such as one of the Markdown parsers available on GitHub or Maven Central. Time for action – building the builder Compilers (and every other kind of translator) in Eclipse are implemented with builders. These are notified when a file or a set of files are changed and can take appropriate action. In the case of the Java builder, it translates the .java source files into the .class files. Perform the following steps: 1. Open the minimark.ui project's .project file. This is visible in the Navigator view, but not in the Package Explorer or other views. Builders are associated to a project within the .project file. The builder ID is referenced via buildCommand, for example have a look at the following code: <projectDescription> <name>com.packtpub.e4.minimark.ui</name> <buildSpec> <buildCommand> <name>org.eclipse.jdt.core.javabuilder</name> </buildCommand> ... 2. To translate a .minimark file into HTML automatically, a builder is needed. A builder is a class that extends IncrementalProjectBuilder and implements a build() method. This is called by the framework when files are saved and either gives a list of changed files or asks that the full project is built. Since this is defined in the core resources bundle, open plugin.xml and add the org.eclipse.core. resources bundle to plugin.xml in the dependency list. 3. Create a class in the com.packtpub.e4.minimark.ui package called MinimarkBuilder: public class MinimarkBuilder extends IncrementalProjectBuilder { protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException { return null; } } [ 168 ] www.it-ebooks.info Chapter 6 4. Builds are called with different kinds of flag, which indicates whether the entire project is being built, or if a subset of the project is being built. For builds that aren't FULL_BUILD there's also a resource delta, which contains the set of resources that have been changed. Calculating a resource delta is a non-free operation so should only be done if needed. The build() method is typically implemented as follows: protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException { if (kind == FULL_BUILD) { fullBuild(getProject(), monitor); } else { incrementalBuild(getProject(), monitor, getDelta(getProject())); } return null; } 5. The fullBuild() and incrementalBuild() methods need to be defined. It is also necessary to handle the case where getDelta() returns a null value, and invoke the full builder accordingly: private void incrementalBuild(IProject project, IProgressMonitor monitor, IResourceDelta delta) throws CoreException { if (delta == null) { fullBuild(project, monitor); } else { System.out.println("Doing an incremental build"); } } private void fullBuild(IProject project, IProgressMonitor monitor) throws CoreException { System.out.println("Doing a full build"); } 6. Finally, to hook up a builder, declare its reference in an extension point org. eclipse.core.resources.builders. This defines a reference (via an ID) to a class that implements IncrementalProjectBuilder. Add the following code to plugin.xml: <extension id="MinimarkBuilder" point="org.eclipse.core.resources.builders"> <builder callOnEmptyDelta="false" hasNature="false" isConfigurable="false" supportsConfigurations="false"> <run class="com.packtpub.e4.minimark.ui.MinimarkBuilder"/> </builder> </extension> [ 169 ] www.it-ebooks.info Working with Resources This extension point requires an ID to be given, since the name defined in the .project file will be the plug-in's ID concatenated with the extension ID. It is conventional, but not necessary, for the full ID to be the name of the class. 7. Run the Eclipse instance. Create a new General project in the test workspace, and once created open the .project file. Add the builder manually by adding in a buildCommand with the ID from the extension point: <buildSpec> <buildCommand> <name>com.packtpub.e4.minimark.ui.MinimarkBuilder</name> </buildCommand> </buildSpec> 8. The message Doing a full build can be seen in the host console when the builder is added, or if the project is cleaned. Edit and save a .minimark file, and the message Doing an incremental build should be displayed. What just happened? The builder is capable of being invoked when files in a project are changed. To associate the builder with the project, it was added as a build command to the project, which is contained in the .project file. The name used for the builder is the extension's unique ID, which is formed as a dot-separated concatenation of the plug-in's ID and the element in the plugin.xml file. The incremental builder has a standard pattern which allows an implementation to determine if it is doing a full or incremental build. There is also a clean() method (which wasn't implemented here) that is used to remove all resources that have been created by a builder. Time for action – iterating through resources A project (IProject) is a top-level unit in the workspace (IWorkspaceRoot). These can contain resources (IResource), which are either folder (IFolder) or file (IFile) objects. They can be iterated with the members() function, but this will result in the creation of IResource for every element processed, even if they aren't relevant. Instead, defer to the platform's internal tree by passing it a visitor that will step through each element required. Perform the following steps: 1. Create a class, MinimarkVisitor, in the com.packtpub.e4.minimark.ui package, that implements IResourceProxyVisitor and IResourceDeltaVisitor interfaces. [ 170 ] www.it-ebooks.info Chapter 6 2. Implement the visit(IResourceProxy) method to get the name of the resource, and display a message if it finds a file whose name ends with .minimark. It should return true to allow child resources to be processed: public boolean visit(IResourceProxy proxy) throws CoreException { String name = proxy.getName(); if(name != null && name.endsWith(".minimark")) { // found a source file System.out.println("Processing " + name); } return true; } 3. Modify incrementalBuild() and fullBuild() to connect the builder to the MinimarkVisitor class as follows: private void incrementalBuild(IProject project, IProgressMonitor monitor, IResourceDelta delta) throws CoreException { if (delta == null) { fullBuild(project, monitor); } else { delta.accept(new MinimarkVisitor()); } } private void fullBuild(IProject project, IProgressMonitor monitor) throws CoreException { project.accept(new MinimarkVisitor(),IResource.NONE); } 4. Run the Eclipse, select a project that has the minimark builder configured and a .minimark file, and do Project | Clean. The host Eclipse instance should have a message saying Processing test.minimark in the Console view. 5. Now create a method in MinimarkVisitor called processResource(). This will get the contents of the file and pass them to the translator. To start with, the translated file will be written to System.out: private void processResource(IResource resource) throws CoreException { if (resource instanceof IFile) { try { IFile file = (IFile) resource; InputStream in = file.getContents(); MinimarkTranslator.convert(new InputStreamReader(in), new OutputStreamWriter(System.out)); } catch (IOException e) { throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed to generate resource", e)); [ 171 ] www.it-ebooks.info Working with Resources } } } 6. Now modify the visit() method to invoke processResource(): public boolean visit(IResourceProxy proxy) throws CoreException { String name = proxy.getName(); if (name != null && name.endsWith(".minimark") { System.out.println("Processing " + name); processResource(proxy.requestResource()); } return true; } The method is called requestResource() instead of getResource() to signify that it isn't just a simple accessor, but that objects are created in calling the method. 7. Run the Eclipse instance, make a change to a .minimark file, and perform a clean build with Project | Clean. The host Eclipse instance should print the translated output in the Console view. What just happened? When notified of changes in the build, the files are processed with a visitor. This abstracts away the need to know how the resources are organized. Resources such as team-private files (.git, .svn, or CVS directories) are automatically excluded from the caller. Using IResourceProxyVisitor to obtain the content is faster than using IResourceVisitor, since the former can be used to test for properties on the name. This provides a much faster way of getting resources that follow a naming pattern as it does not require the creation of an IResource object for every item, some of which may not be necessary. The builder communicates errors through CoreException, which is the standard exception for many of Eclipse's errors. This takes as its parameter a Status object (with an associated exception and plug-in ID). Finally, when a full build is invoked (by performing Project | Clean on the project) the output is seen in the Console view of the development Eclipse. [ 172 ] www.it-ebooks.info Chapter 6 Time for action – creating resources The next step is to create an IFile resource for the .html file (based on the name of the .minimark file). Eclipse uses an IPath object to represent a filename from the root of the workspace. An IPath of /project/folder/file.txt refers to the file.txt file in a folder called folder contained within the project called project. The root path represents the IWorkspaceRoot. Perform the following steps: 1. In the processResource() method of MinimarkVisitor, calculate the new filename, and use it to create an IFile from the file's parent IContainer: try { IFile file = (IFile) resource; String htmlName = file.getName().replace(".minimark", ".html"); IContainer container = file.getParent(); IFile htmlFile = container.getFile(new Path(htmlName)); 2. To create the contents of the file, an InputStream has to be passed to the setContents() method. The easiest way to create this is to pass a ByteArrayOutputStream to MinimarkTranslator, and then use a ByteArrayInputStream to get the contents to set on the file. PipedInput/OutputStream are inherently broken because they use a fixed width pipeline and will block when full. Make the following changes: ByteArrayOutputStream baos = new ByteArrayOutputStream(); MinimarkTranslator.convert(new InputStreamReader(in), new OutputStreamWriter(System.out)); new OutputStreamWriter(baos)); ByteArrayInputStream contents = new ByteArrayInputStream(baos.toByteArray()); 3. Now the contents need to be set on the file. If the file exists, the contents are set with setContents(), but if it doesn't create() needs to be called instead: if (htmlFile.exists()) { htmlFile.setContents(contents, true, false, null); } else { htmlFile.create(contents, true, null); } [ 173 ] www.it-ebooks.info Working with Resources The true parameter is to force the change; that is, the contents will be written even if the resource has been updated elsewhere. The false parameter for the setContents() method is to indicate that changes should not be recorded, and the final null parameter for both methods is to pass an optional ProgressMonitor. 4. Finally the resource needs to be marked as derived, which tells Eclipse that this is an automatically generated file and not a user-edited one: htmlFile.setDerived(true,null); 5. Run the Eclipse instance, do Project | Clean and the corresponding HTML file should be generated. Modify the .minimark file, do a clean again, and the HTML file should be regenerated. What just happened? The build was modified to invoke MinimarkTranslator and create a resource in the filesystem. Since it uses a stream to set the contents a ByteArrayOutputStream is used to build up the translated contents, and a ByteArrayInputStream is used to read it back for the purposes of setting the file's contents. The exists() method check is necessary because setting the contents on a non-existent file throws a CoreException. The files are represented as a generic IPath object, which is concretely implemented with the Path class. Path classes are represented as slash (/) separated filenames, regardless of the operating system, but each path component needs to obey the local filesystem's constraints, such as not allowing colons on Windows. Time for action – implementing incremental builds The final part of the puzzle is to implement the incremental part of the builder. Most of the builds that Eclipse performs are incremental, which means that it only compiles the files that are needed at each point. An incremental build gives a resource delta which says what files have been modified, added, or removed. This is implemented in IResourceDelta which is handed to the IResourceDeltaVisitor visit() method. A resource delta combines an IResource with a flag that says whether it was added or removed. Perform the following steps: 1. Open MinimarkBuilder and go to the visit(IResourceDelta) method. This is used by the incremental build when individual files are changed. Since the delta already has a resource, it can be used to determine if the file is relevant, and if so pass it to the processResource() method: public boolean visit(IResourceDelta delta) throws CoreException { [ 174 ] www.it-ebooks.info Chapter 6 IResource resource = delta.getResource(); if(resource.getName().endsWith(".minimark")) { processResource(resource); } return true; } 2. Run the Eclipse instance, and edit and save the .minimark file. The builder's incremental builder will be invoked with the given resource and the file will be updated. Eclipse's HTML editor won't automatically refresh the change, but if the .html file is opened with a text editor, a side-by-side view shows that the file is being updated with each save. What just happened? An incremental build and a full build are very similar; they both process a set of resources. In the former case, it's the set of files that were changed in a workspace update operation (such as a Save or Save All). In the latter case, it's all files in an individual project. If building is done on independent resources which are unrelated then breaking it apart to a single processResource() method is an efficient way of building new resources. Time for action – handling deletion The incremental builder does not handle deletions in its current implementation. To handle deletion, IResourceDelta needs to be inspected to find out what kind of delta took place, and the delete handled accordingly. Perform the following steps: 1. Run the Eclipse instance and delete a .minimark file. An exception is thrown and reported to the user: [ 175 ] www.it-ebooks.info Working with Resources 2. To fix this issue, modify the check in MinimarkVisitor class' processResource() method to see if the resource exists or not: private void processResource(IResource resource) throws CoreException { if (resource instanceof IFile && resource.exists()) { 3. This solves the NullPointerException, but the generated HTML file is left behind. To clean up the associated .html file if the .minimark file is being deleted, the resource delta's flags can be inspected to see if it is deleted, and if so, the corresponding HTML file can be deleted as well. Modify the visit(IResourceDelta) method as follows: public boolean visit(IResourceDelta delta) throws CoreException { boolean deleted = (IResourceDelta.REMOVED & delta.getKind())!=0; IResource resource = delta.getResource(); String name = resource.getName(); if (deleted) { String htmlName = name.replace(".minimark",".html"); IFile htmlFile = resource.getParent(). getFile(new Path(htmlName)); if (htmlFile.exists()) { htmlFile.delete(true, null); } } else { processResource(resource); } return true; } 4. Run the Eclipse instance, and create a new test.minimark file. Save it, and a corresponding test.html file will be created. Delete the test.minimark file, and the test.html file should also be deleted. 5. Create the test.minimark file again, and the test.html file will be generated. Delete the test.html file, and it won't be regenerated automatically. To fix this, IResourceDelta needs to track deletions of .html files as well, and process the corresponding .minimark resource. Modify the visit(IResourceDelta) as follows: public boolean visit(IResourceDelta delta) throws CoreException { boolean deleted = (IResourceDelta.REMOVED & delta.getKind())!=0; IResource resource = delta.getResource(); String name = resource.getName(); if (name.endsWith(".minimark")) { if (deleted) { String htmlName = name.replace(".minimark",".html"); [ 176 ] www.it-ebooks.info Chapter 6 IFile htmlFile = resource.getParent().getFile( new Path(htmlName)); if (htmlFile.exists()) { htmlFile.delete(true, null); } } else { processResource(resource); } } else if (name.endsWith(".html")) { String minimarkName = name.replace(".html",".minimark"); IFile minimarkFile = resource.getParent().getFile( new Path(minimarkName)); if (minimarkFile.exists()) { processResource(minimarkFile); } } return true; } 6. Run the Eclipse instance and delete the generated test.html file. It should be automatically regenerated by the MinimarkBuilder/MinimarkVisitor. Now, delete the test.html file and the corresponding test.minimark file should be deleted as well. What just happened? When a .minimark file was deleted, a NullPointerException was seen. This was fixed by guarding visit() with a check to see if the resource existed or not. For consistency, the deletion of associated resources was also handled. If the .html file is deleted, it is regenerated (but only if a corresponding .minimark file is present). When the .minimark file is deleted, the corresponding .html file is also deleted. Have a go hero – builder upgrades Along with reacting to changes and creating content, builders are also responsible for removing content when the project is cleaned. The IncrementalProjectBuilder defines a clean() method, which is invoked when the user performs Project | Clean on either that specific project or the workspace as a whole. Implement a clean() method on the MinimarkBuilder which walks the project, and for each .minimark file, deletes any corresponding .html file. Note that not all .html files should be deleted as some may be legitimate source files in a project. [ 177 ] www.it-ebooks.info Working with Resources Using natures Although builders can be configured on a project manually, they aren't usually added directly. Instead, a project may have natures which represent a type of dimension that a project has; and natures can be automatically associated with builders. For example, a Java project is identified with a Java nature and others (such as the PDE project) are identified as both a Java project and an additional nature for PDE processing. Other languages have their own natures, such as C. Time for action – creating a nature A nature is created by implementing the IProjectNature interface. This will be used to create a MinimarkNature, which will allow projects to be associated with the MinimarkBuilder. Perform the following steps: 1. Create a class called MinimarkNature in the com.packtpub.e4.minimark.ui package: public class MinimarkNature implements IProjectNature { public static final String ID = "com.packtpub.e4.minimark.ui.MinimarkNature"; private IProject project; public IProject getProject() { return project; } public void setProject(IProject project) { this.project = project; } public void configure() throws CoreException { } public void deconfigure() throws CoreException { } } 2. The purpose of a nature is to assist by adding (or configuring) the builders, which are associated by an ID. To make cross-referencing possible, define a constant in the MinimarkBuilder which can be used to refer to it by the nature: public class MinimarkBuilder extends IncrementalProjectBuilder { public static final String ID = "com.packtpub.e4.minimark.ui.MinimarkBuilder"; [ 178 ] www.it-ebooks.info Chapter 6 3. The way a builder is added to the project is by accessing the project descriptor and adding a build command. There is no easy way of adding or removing a builder, so acquire the set of commands, search to see if it is present, and then add or remove it. Using the Arrays class takes away some of the pain. Implement the configure() method as follows: public void configure() throws CoreException { IProjectDescription desc = project.getDescription(); List<ICommand> commands = new ArrayList<ICommand>( Arrays.asList(desc.getBuildSpec())); Iterator<ICommand> iterator = commands.iterator(); while (iterator.hasNext()) { ICommand command = iterator.next(); if (MinimarkBuilder.ID.equals(command.getBuilderName())) { return; } } ICommand newCommand = desc.newCommand(); newCommand.setBuilderName(MinimarkBuilder.ID); commands.add(newCommand); desc.setBuildSpec(commands.toArray(new ICommand[0])); project.setDescription(desc,null); } 4. To deconfigure a project, the reverse is done. Implement the deconfigure() method as follows: public void deconfigure() throws CoreException { IProjectDescription desc = project.getDescription(); List<ICommand> commands = new ArrayList<ICommand>( Arrays.asList(desc.getBuildSpec())); Iterator<ICommand> iterator = commands.iterator(); while (iterator.hasNext()) { ICommand command = iterator.next(); if (MinimarkBuilder.ID.equals(command.getBuilderName())) { iterator.remove(); } } desc.setBuildSpec(commands.toArray(new ICommand[0])); project.setDescription(desc,null); } [ 179 ] www.it-ebooks.info Working with Resources 5. Having implemented the nature, it needs to be defined as an extension point within the plugin.xml of the minimark.ui project: <extension id="MinimarkNature" point="org.eclipse.core.resources.natures"> <runtime> <run class="com.packtpub.e4.minimark.ui.MinimarkNature"/> </runtime> </extension> 6. To associate the nature with a project, a menu needs to be added to the Configure menu associated with projects. Create an entry in the plugin.xml file for the Add Minimark Nature command, and put it in the projectConfigure menu: <extension point="org.eclipse.ui.commands"> <command name="Add Minimark Nature" defaultHandler="com.packtpub.e4.minimark.ui.AddMinimarkHandler" id="com.packtpub.e4.minimark.ui.AddMinimarkNature"/> </extension> <extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI= "popup:org.eclipse.ui.projectConfigure?after=additions"> <command label="Add Minimark Nature" style="push" commandId="com.packtpub.e4.minimark.ui.AddMinimarkNature"/> </menuContribution> </extension> 7. Create a new class in the com.packtpub.e4.minimark.ui package called AddMinimarkNature, as follows: public class AddMinimarkHandler extends AbstractHandler { public Object execute(ExecutionEvent event) throws ExecutionException { ISelection sel = HandlerUtil.getCurrentSelection(event); if (sel instanceof IStructuredSelection) { Iterator<?> it = ((IStructuredSelection)sel).iterator(); while (it.hasNext()) { Object object = (Object) it.next(); if(object instanceof IProject) { try { addProjectNature((IProject) object,MinimarkNature.ID); } catch (CoreException e) { throw new ExecutionException("Failed to set nature on" + object,e); } } } } [ 180 ] www.it-ebooks.info Chapter 6 return null; } private void addProjectNature(IProject project, String nature) throws CoreException { IProjectDescription description = project.getDescription(); List<String> natures = new ArrayList<String>( Arrays.asList(description.getNatureIds())); if(!natures.contains(nature)) { natures.add(nature); description.setNatureIds(natures.toArray(new String[0])); project.setDescription(description, null); } } } 8. Run the Eclipse instance, create a new General project, and open the .project file using the Navigator view. Now right-click on the project, and select Configure | Add Minimark Nature to add the nature. When the nature is added, it will add the commands automatically and the changes will be visible in the .project file. What just happened? The MinimarkNature class was created to inject a builder into the project description when added. Changing the .project file manually does not add the builder, so an action to programmatically add the nature was created and added to the standard Configure menu. Both the Nature and the Builder are referred to via IDs; these are stored in the .project and .classpath files. Since these may be checked into a version control system, the names of these IDs should be consistent between releases. (It's best practice to add these to version control.) The project descriptor contains the content from the .project file and stores an array of nature IDs and commands. To add a nature to a project, its identifier is appended to this list. Note that the change only takes effect when the updated project descriptor is set on the project. Since the nature's modifications only take effect when set programmatically, the Add Minimark Nature command was created to add the nature. The command was put into the menu popup org.eclipse.ui.projectConfigure?after=additions, which is the standard location for adding and configuring natures. Conventionally, either a separate command to Add Minimark Nature and Remove Minimark Nature is used, or a Toggle Minimark Nature could be used for both actions. The advantage of the separate Add/Remove menu items is that their visibility can be controlled based on whether the project already has the nature or not. [ 181 ] www.it-ebooks.info Working with Resources The handler class used HandlerUtil to extract the current selection; though this just extracts the object from the parameter map via the variable name selection. To avoid spelling errors, it makes sense to define constants as static final variables. If they are related with class names, it can be better to use class.getName() as the identifier so that if they are renamed then the identifiers are automatically updated as well. Alternatively, they can be created from a concatenation with the plug-in's ID (in this case, via Activator.ID). Have a go hero – enabling for a selected object type It is conventional to only show the configure option if it is strictly necessary. In the case where projects already have MinimarkNature, we should not show the command. Use the visibleWhen property to target the selection and only enable it if the projectNature of the selected object is that of the minimark builder. Using markers The final section in this chapter is devoted to markers. These are the errors and warnings that the compiler shows when it detects problems in files. They can be created automatically, typically from a builder. Time for action – error markers if the file is empty Errors and warning markers are used to indicate if there are problems in the source files. These are used by the Eclipse compiler to indicate Java compile errors, but they are also used for non-Java errors as well. For example, text editors also show warnings when words are misspelled. A warning can be shown if the .minimark file is empty and the title is missing. There isn't a simple way of accessing the file's size in Eclipse, so we'll use a heuristic that if the generated HTML file is less than about 100 bytes then there probably wasn't much to start with anyway. Perform the following steps: 1. 2. Open MinimarkVisitor and go to the processResource() method. When the HTML file is generated, put in a test to determine if the size is less than 100 bytes: ByteArrayOutputStream baos = new ByteArrayOutputStream(); MinimarkTranslator.convert(new InputStreamReader(in), new OutputStreamWriter(baos)); ByteArrayInputStream contents = new ByteArrayInputStream( baos.toByteArray()); if(baos.size() < 100) { System.out.println("Minimark file is empty"); } [ 182 ] www.it-ebooks.info Chapter 6 3. The problem with printing out an error message is that it won't be seen by the user. Instead, it can represented with an IMarker object, created on the source resource, and with additional properties to set the type and location of the error: System.out.println("Minimark file is empty"); IMarker marker = resource.createMarker(IMarker.PROBLEM); marker.setAttribute(IMarker.SEVERITY,IMarker.SEVERITY_ERROR); marker.setAttribute(IMarker.MESSAGE,"Minimark file is empty"); marker.setAttribute(IMarker.LINE_NUMBER,0); marker.setAttribute(IMarker.CHAR_START,0); marker.setAttribute(IMarker.CHAR_END,0); 4. Run the Eclipse instance and create a new empty .minimark file. When it is saved, an error will be reported in the Problems view. If it's not shown, it can be opened with Window | Show View | Other | General | Problems: What just happened? A heuristic that detected when the input file was likely to be empty was used to generate a marker in the build view. While creating an empty file, the builder runs and the problem is generated in the problems view. The marker also allows additional fields to be set if the location of the problem is known. These are optional fields but can be used to give a user more information as to the source of the problem. In the case of a misspelled word, the CHAR_START and CHAR_END can be used to pinpoint the exact word on a line; in the case of more general errors the LINE_NUMBER can be used to indicate the approximate location in the file. Each time the file is changed, additional markers are created in the problems view. Even if content is added, the existing errors aren't removed. That will be fixed in the next section. [ 183 ] www.it-ebooks.info Working with Resources Time for action – registering a marker type The current implementation has a flaw, in that it doesn't appear to fix the problems after they have been resolved. That's because the marker types are kept associated with a resource, even if that resource is changed. The reason this isn't seen in Java files is that the builder wipes out all (Java) errors prior to the start of a build, and then adds new ones as applicable. To avoid wiping out other plug-in's markers, each marker has an associated marker type. JDT uses this to distinguish between its markers and others contributed by different systems. This can be done for MinimarkMarkers as well. Perform the following steps: 1. Open plugin.xml of the minimark.ui project. Add the following extension to define a MinimarkMarker: <extension id="com.packtpub.e4.minimark.ui.MinimarkMarker" name="Minimark Marker" point="org.eclipse.core.resources.markers"> <persistent value="false"/> <super type="org.eclipse.core.resources.problemmarker"/> </extension> 2. To use this marker when the error is created, instead of using IMarker.PROBLEM, use its extension ID from plugin.xml in the processResource() method of the MinimarkVisitor: IMarker marker = resource.createMarker(IMarker.PROBLEM); IMarker marker = resource.createMarker( "com.packtpub.e4.minimark.ui.MinimarkMarker"); 3. At the start of the processResource() method, flush all the markers associated with the resource of this type: resource.deleteMarkers( "com.packtpub.e4.minimark.ui.MinimarkMarker", true, IResource.DEPTH_INFINITE); 4. Run the Eclipse instance and verify that as soon as some content is put in a .minimark file that the errors are cleared. Delete the content, save the file, and the errors should reappear. 5. Finally, the editor doesn't show up warnings in the column. To make that happen, change the super class of MinimarkEditor from AbstractEditor to AbstractDecoratedTextEditor. Run the Eclipse instance again. Now errors will be reported in the editor as well: [ 184 ] www.it-ebooks.info Chapter 6 What just happened? Now the custom markers for the resource are being deleted at the start of a build and if there's a problem a marker will be automatically added. When the problem is resolved, the resource's markers are automatically deleted anyway. In the deletion code, the boolean argument says whether to delete markers that are a subtype of that marker type or not. The second argument says what happens in the case that it's a folder or other container, and if deletion should recurse. Generally, builders delete and create a specific type of problem marker so that they do not affect the other that may be associated with that resource. This allows other contributors (such as the spell checking editor) to raise warnings or informational dialogs that are not cleaned by a particular builder. Have a go hero – working out when the file is really empty Fix the file detection so that it works out when the source file is really empty. Do this by using EFS and the file's locationURI to get a FileInfo, which contains the file's size. [ 185 ] www.it-ebooks.info Working with Resources Pop quiz – understanding resources, builders, and markers Q1. How is an error with a missing document provider fixed? Q2. What is an IResourceProxy and why is it useful? Q3. What is an IPath? Q4. What is a nature, and how is one set on a project? Q5. How are markers created on a project? Summary In this chapter we looked at how resources are accessed, created, and processed. We used an example of a markup language to associate a builder that translated markup items into HTML when the source files were saved. We then hooked up the builder with a nature so that a project can automatically be configured with content when needed. The next chapter will look at the E4 model, and how it differs from Eclipse 3.x in creating views and contributing to a Rich Client Platform. [ 186 ] www.it-ebooks.info 7 Understanding the Eclipse 4 Model The last major change to Eclipse was with the 3.0 release when it migrated to OSGi. The Eclipse 4 model provides a significant departure from the Eclipse 3.x line, with the user interface being represented as a dynamic EMF model. In addition, both the model and views can be represented as simple POJOs with services provided by dependency injection. There is also a separate rendering mechanism which allows an E4 application to be hosted by different UIs, although we'll look at the SWT renderer specifically. In this chapter, we'll take a look at the differences and how you can evolve Eclipse plug-ins forward. In this chapter we shall: Set up an Eclipse 4 instance for development Create an E4 application with parts Style UI with CSS Send and receive events Create commands, handlers, and menus Integrate with preferences Inject custom POJOs www.it-ebooks.info Understanding the Eclipse 4 Model Working with the Eclipse 4 model Since Eclipse was first released in November 2001, its user interface has remained mostly static. Each window has a perspective, which contains zero or one editor area, and zero or more view parts. In early releases, every perspective had exactly one editor area, and it was not until the release of Eclipse RCP with Eclipse 3.0 in 2004 that it was possible to disable the editor, and have a custom application suitable for a non-IDE use. However, the presentation of the perspective always proved difficult to customize, such as changing the background color or arrangement of the windows or toolbars. The Eclipse 4 model provides a way to model an application both at design time, and also interpret and modify it at runtime. An application has a top-level model, but may also have additional model fragments contributed by different bundles. Additionally, the separate rendering framework allows the UI to be represented with different tools such as JavaFX and HTML. In this book, the default SWT rendering tool will be used, since that closely matches the existing Eclipse 3.x UI. The other significant change is that it is no longer necessary to create subclasses of specific classes to contribute to the Eclipse infrastructure. Instead, classes are created with dependency injection (similar to how Spring components are configured) and may consume platform-level services separately to the user interface. Instead of referring to global singletons through accessor methods, these are now available through injection. This allows components to be built as simple POJOs (Plain Old Java Objects) which allows them to be tested headlessly and provides a looser binding to the services that they consume. Time for action – installing E4 tooling To work with Eclipse 4 modeled applications, it is necessary to install the E4 tools to allow the application XMI to be edited through an editor. These are not shipped with the standard Eclipse distribution package, and must be installed separately. 1. Add the following update site by going to Window | Preferences | Install/Update | Available Software Sites, then click on Add... and put in: In OS X, the Preferences menu is located under the Eclipse menu. Juno (4.2): http://download.eclipse.org/e4/updates/0.13 Kepler (4.3): http://download.eclipse.org/e4/updates/0.14 [ 188 ] www.it-ebooks.info Chapter 7 Alternatively, the tools can be found by installation through the Eclipse Marketplace. 2. Click on OK in the Add Site dialog and on OK in the Preferences page to add it. Once added, go to Help | Install New Software and then select the newly added site in the list. Select the the following features: CSS file editor CSS spy for Eclipse 4 Eclipse 4 core tools These add the ability to edit both css and e4xmi files, and create Eclipse 4 applications by a wizard, which are used to create and render the user interface in Eclipse 4. [ 189 ] www.it-ebooks.info Understanding the Eclipse 4 Model 3. 4. Restart Eclipse after the install has completed. Go to File | New | Project and verify that Eclipse 4 | Eclipse 4 Application Project is seen in the new project wizard's choices. What just happened? Installing the Eclipse E4 tools and E4 CSS spy provides a custom editor for e4xmi files, as well as css files. In addition, the CSS spy can be called with Alt + Shift + F5, or via the Quick Access search bar. To run the CSS spy in a launched application, add org.eclipse.e4.tools.css.spy to the required bundles in the launch configuration. (Don't add them to the product definition unless the product's end users need to be able to invoke the CSS spy.) There is also a live EMF editor which can be called with Alt + Shift + F9. This is useful for exploring the Eclipse runtime, either in the IDE or in the launched application. To add the live EMF editor, add org.eclipse.e4.tools.emf.liveeditor to the launch configuration. Don't forget to click on Validate plug-ins and if necessary Add required plug-ins to ensure that the tools' dependencies are present in the launched product. Time for action – creating an E4 application Eclipse applications use an application ID to launch and start execution. For E4 applications, org.eclipse.e4.ui.workbench.swt.E4Application is used. Since this is specified within a product, to start with a new E4 application will be created. 1. Go to the File | New | Project... menu and choose Eclipse 4 | Eclipse 4 Application Project from the list. [ 190 ] www.it-ebooks.info Chapter 7 If the Eclipse 4 Application Project is not shown here, check that the E4 tools are installed correctly. 2. Create a project with the name com.packtpub.e4.application, and step through the wizard. Choose the default values for each field. [ 191 ] www.it-ebooks.info Understanding the Eclipse 4 Model 3. On the last page, ensure that the Create sample contents option is selected. This will ensure that the E4 tools creates standard parts and menus for the default application. 4. 5. Click on Finish and the project will be created. Right-click on the com.packtpub.e4.application project and choose Run As | Eclipse Application. This launches a new version of the IDE, which isn't what might be expected. [ 192 ] www.it-ebooks.info Chapter 7 6. Double-click on the product file (called com.packtpub.e4.application.product) and click on the run icon in the top-right corner: If you get a web browser launched when invoking the product, it may be that RAP is installed. If this happens, the product can be launched by going to the Run | Run As... and selecting the Eclipse Application type, followed by Run a product: com.packtpub.e4.application.product. Alternatively, the Overview tab has a Testing section, which includes a link to Launch an Eclipse Application. 7. When the application runs, an error may be seen: !MESSAGE Application error !STACK 1 java.lang.RuntimeException: No application id has been found. This indicates that the product was missing one or more dependencies in the product list. Some older versions of the E4 tools did not include required plug-ins such as javax.xml. As a result, the runtime application fails to install, with the cryptic error that it can't find the application. [ 193 ] www.it-ebooks.info Understanding the Eclipse 4 Model 8. To resolve the problem, click on the Add Required Plug-ins for the product (the button on the right of the product editor, as seen in the preceding screenshot). Now, when it is run, the default window should be shown: What just happened? The new E4 wizard created a simple E4 rich client application, including creating some sample content (menus, commands, and so on). After fixing issues with missing dependencies, product launch operation starts the application successfully. When running any kind of Eclipse application, it is good practice to stop the launch when there are missing dependencies. The launch configuration, visible via the Run | Run Configurations... menu, has an option on the plug-ins tab Validate plug-ins automatically prior to launching: [ 194 ] www.it-ebooks.info Chapter 7 Now, when launching the product without adding the necessary plug-ins, a warning will be shown. Clicking on the Validate Plug-ins button can run this validation at any time. Time for action – creating a part Having created a sample application, the next step is to create a view, known as a part in E4. Parts are the generic name for views, editors, and other grouping components in an E4 application. Unlike views in Eclipse 3, the view class doesn't have to have any references to the Eclipse APIs. This makes it particularly easy to build and test in isolation. 1. Open the plugin.xml file of the com.packtpub.e4.application project, and go to the Dependencies tab. Add a dependency to the org.eclipse.ui.di bundle, if it's not already added. 2. Create a new class called Hello in the com.packtpub.e4.application.parts package. 3. 4. Add a field called label of type Label. Add a create() method, annotated with @PostConstruct that instantiates the Label and sets its text to Hello. If @PostConstruct appears not to work, ensure javax.annotation is added as a package import to the bundle (see http://www.vogella.com/articles/ EclipseRCP/article.html#tutorial_api2). 5. Add a focus() method, annotated with @Focus. The focus() method is required in Eclipse 4.2 but optional in 4.3 onwards. 6. The class will look like: package com.packtpub.e4.application.parts; import javax.annotation.PostConstruct; import org.eclipse.e4.ui.di.Focus; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; public class Hello { private Label label; @PostConstruct public void create(Composite parent) { label = new Label(parent, SWT.NONE); [ 195 ] www.it-ebooks.info Understanding the Eclipse 4 Model label.setText("Hello"); } @Focus public void focus() { label.setFocus(); } } 7. Double-click on the Application.e4xmi file and it should open up in the application editor (if it doesn't, install the E4 tools from the update site earlier in this chapter). 8. Navigate to Application | Windows | Trimmed Window | Controls | Perspective Stack | Perspective | Controls | PartSashContainer | Part Stack. 9. Delete the Sample Part, if present, by right-clicking on it and choosing Remove. 10. Right-click on the Part Stack, and select Add child followed by Part. The part should be created as follows: ID: com.packtpub.e4.application.part.0 Label: Hello Class URI: Click on Find and enter Hello as the class name, and it will add bundleclass://com.packtpub.e4.application/ com.packtpub.e4.application.parts.Hello Closeable: Ensure Closeable is not selected To Be Rendered: Ensure To Be Rendered is selected Visible: Keep Visible selected [ 196 ] www.it-ebooks.info Chapter 7 11. Finally, save the e4xmi file and then launch the product. If all has gone well, Hello should be displayed in the generated window: 12. If nothing is shown, the first debug point is to delete the workspace of the runtime application. The launch configuration can be set to clear the contents of the workspace each time it launches, which is a good idea for E4 development as often the new files aren't copied over or stale files can be left behind. [ 197 ] www.it-ebooks.info Understanding the Eclipse 4 Model Go to the Run | Run Configurations... menu, select the com.packtpub. e4.application configuration. On the Main tab there is an option to Clear: 13. Leave the option to Ask for confirmation before clearing selected to be prompted with a dialog each time the product is run, or deselected to delete the contents of the runtime workspace each time. 14. If still not shown, put a breakpoint in the @PostConstruct method to verify that it is being called. If not, check @javax.annotation.PostConstruct annotation is on the method, that the right class name in the e4xmi file, and that everything is saved before launching. Another approach is to delete the launch configuration and launch a new one via the product launch as before. What just happened? Eclipse 4 applications are modeled with an e4xmi file, which defines both visual and non-visual contents. At runtime, an Eclipse 4 application is booted with org.eclipse. e4.ui.workbench.swt.E4Application, which reads the file specified in the product's applicationXMI property. The default name created by the wizard is Application. e4xmi, but this can be replaced if necessary. Other renderers exist, such as e(fx)clipse's implementation built on JavaFX. [ 198 ] www.it-ebooks.info Chapter 7 Parts in E4 are the equivalent of views/editors in Eclipse 3.x. The structure of viewable content in the default E4 application is: 1. Application, which contains 2. (Trimmed) windows, which contains 3. Perspective stacks, which contains 4. Perspectives, which contains 5. PartSashContainer, which contains 6. Part stacks, which contains 7. Parts In addition, Trimmed Windows can also contain Menus and Menu Items. Menu Items are covered later in this chapter. In the default application, the window uses a Trimmed Window, which means it can have toolbars such as the save and open tools. (These are shown as Handled Tool Item or Direct in the application editor.) Each element can have a control which allows parts to be mixed and matched; for example, it's not mandatory for a Window to use any perspectives at all; they can just contain controls if desired. The Hello part was created and added into the application by adding it to the model. At runtime, the class is instantiated, followed by an invocation of the method annotated with @PostConstruct. Any required arguments are injected in automatically, which are obtained from the runtime context which is akin to a HashMap of services by class name. Finally, the @Focus call is invoked when the part gets the focus; it should delegate that call to the key widget within the part. Note, that since the hook between the application model and the code is the pointer in the Class URI reference to the fully qualified class name, when renaming Java class names or packages, it's important to select the option that allows the fully qualified name to be replaced in other files, as otherwise links between the application model and the parts may be broken. It may be necessary to clear the workspace when starting. This is because the application model is not only used for an initial starting point of the application; it's also used to model the runtime of the application as well. Any changes to the model (creating new views, resizing the parts) updates the runtime model. When the application shuts down normally, the updated state of the model is saved and used for subsequent launches. As a result, a newly launched application with the same workspace will show the state of the workspace at the last time it shut down, not any new state that may have been added at development time. [ 199 ] www.it-ebooks.info Understanding the Eclipse 4 Model Time for action – styling the UI with CSS The user interface for Eclipse 4 is styled with CSS. Although this is loosely based on the CSS syntax used in browsers, the content that can be used is interpreted by the Eclipse 4 runtime. CSS stylesheets are composed of selectors and style rules. A selector can be one of a widget name (for example, Button), a model class name (for example, .MPartStack) or an identifier (for example, #PerspectiveSwitcher). 1. The default Eclipse 4 application generated by the wizard with sample content will have an empty CSS file called css/default.css. Open this file, and add the following rule: Shell { background-color: blue; } 2. Run the application, and the background of the window (Shell) will be shown in blue. 3. Basic CSS color names are supported, but hex values can also be used. Modify the default.css file as follows: Shell { background-color: #00FF00; } 4. 5. Run the application, and the background color will be shown in green. It's possible to support vertical gradients in colors by specifying more than one color in the list. Modify the default.css as follows: Shell { background-color: yellow blue; } 6. 7. Run the application, and the background will be a gradient from yellow to blue. The colors split at the 50 percent mark by default, but it is possible to specify where the break occurs as a percentage. Using 25 percent makes the switch to blue happen at the top quarter of the screen—conversely, using 75 percent makes the switch to blue happen at the bottom quarter of the screen. Modify the default.css as follows: Shell { background-color: yellow blue 25%; } 8. Run the application, and the background will be a gradient from yellow to blue but with the split nearer the top. [ 200 ] www.it-ebooks.info Chapter 7 9. If more than two colors are specified, then multiple gradient points are specified. This creates a rainbow-style effect. Modify the default.css as follows: Shell { background-color: red orange yellow green blue indigo violet 15% 30% 45% 60% 75% 90%; } 10. Run the application, and the background will be rainbow style: 11. In addition to using Java class names as selectors, IDs and CSS classes can also be used. For example, to target the Hello part, its ID can be used. The default one will be com.packtpub.e4.application.part.0 if it is not explicitly specified at creation. [ 201 ] www.it-ebooks.info Understanding the Eclipse 4 Model Go to the Part in the application viewer to see the ID. To translate it to CSS, use the # selector and replace . with – in the name. To place the rainbow only on that specific part, use this rule instead: #com-packtpub-e4-application-part-0 { background-color: red orange yellow green blue indigo violet 15% 30% 45% 60% 75% 90%; } The Eclipse 4.2 tools may show this as an error in the CSS file, but this is a bug in the CSS editor and not in the CSS rule itself running the application, and the rainbow part should now be targeted to just the Hello part. 12. Another way of coloring each part is to use the pseudo class .MPart, which allows a rule to be targeted to all parts in the UI: .MPart { background-color: red orange yellow green blue indigo violet 15% 30% 45% 60% 75% 90%; } [ 202 ] www.it-ebooks.info Chapter 7 What just happened? The default.css file, created with the application wizard, was modified to explore how to style properties declaratively. Although loosely based on the CSS specification, some differences are apparent. Style selectors can be: Widget (SWT) class names, like Button, Label, and Shell CSS class names like .MPart, .MPartStack, and .MTrimmedWindow CSS IDs like #IDEWindow, #org-eclipse-jdt-ui-MembersViewMStack, and #left There is also a "pseudo selector" which can be used. These apply to certain subsets of the classes: Shell:active used for applying styles to the active Shell Button:checked used if a Button is checked :selected used if a tab folder/item is selected In addition, CSS selectors can be combined. For example, to have the same rules applied to .MPart and an .MPartStack, use .MPart, .MPartStack as a selector. The comma represents "either". Dependencies can be combined; .MPart Label will apply to Label elements which are contained (anywhere) inside an .MPart. To restrict it to direct descendants, use Shell > Label. This will apply only to those Label elements which are immediately inside a Shell, but not those Label elements which exist in Container instances in the Shell. In addition to background-color, other CSS properties can be used, such as: alignment: Used for Buttons (for example, up) or Label (for example left/ right/center) border-visible: Is true if the border should be shown for CTabFolder background-image: An image referenced as a URL url('platform:/plugin/ com.packtpub.e4.application/icons/icon.gif') color: As with background-color; names or hex values font: Used to specify font name (for example, Courier New) and size (for example, 128px) font-family: The font name (for example, Courier New) font-size: The font size (for example, 128px) [ 203 ] www.it-ebooks.info Understanding the Eclipse 4 Model font-adjust: The font size adjustment, not supported in 4.2 (CSS3 name is font-size-adjust, so this may change) font-stretch: The font stretch size, not supported in 4.2 font-style: Can be italic, or bold font-variant: The font variant, not supported in 4.2 (normal, small-caps, and inherit) font-weight: The font weight, not supported in 4.2 (normal, bold, bolder, lighter, and inherit)—use font-style instead margin: The space (in pixels) around the content (-top, -bottom, -left, and -right) maximized: If the widget is maximized or not minimized: If the widget is minimized or not padding: The space (in pixels) between elements (-top, -bottom, -left, and -right) text–align: Can be left, right, or center text-transform: Can be capitalize, uppercase, or lowercase There are also some Eclipse-specific properties as well: eclipse-perspective-keyline-color: The color of perspective lines. swt-background-mode: The background of the composite to be none, default, or force, corresponding to the Java call Composite. setBackgroundMode(INHERIT_NONE/DEFAULT/FORCE). This ensures the background of the children either override or inherit their parent's background. swt-corner-radius: The size (in pixels) of the corner radius. swt-inner-keyline-color: The color of the inside line of the tabs, drawn by the CTabRenderer (see swt-tab-renderer). swt-keyline-color: The keyline color. swt-maximize-visible: Is true or false if the maximize icon is shown. swt-maximized: Is true or false if the view is maximized (used as a selector) swt-minimize-visible: Is true or false if the minimize icon is shown. swt-minimized: Is true or false if the view is minimized (used as a selector). swt-mru-visible: Is true (for "Indigo-like" tab behavior) or false (default). swt-outer-keyline-color: The color of the outside line of the tabs, drawn by the CTabRenderer (see swt-tab-renderer) [ 204 ] www.it-ebooks.info Chapter 7 swt-selected-tabs-background: The background color of selected tabs. swt-selected-tab-fill: The fill color of the selected tab. swt-show-close: Is true or false if the close icon is shown. swt-shadow-visible: Is true or false if the shadow is visible. swt-shadow-color: The color of shadows, if visible. swt-simple: Is true (for "new style" tabs) or false (for "old style" tabs, default). swt-tab-outline: Is true or false if the tab should have an outline. swt-tab-renderer: Is null for classic style, or a class URL like url('bundleclass://org.eclipse.e4.ui.workbench.renderers.swt/ org.eclipse.e4.ui.workbench.renderers.swt.CTabRendering'). swt-tab-height: Height, in pixels, of the tabs. swt-text-align: Can be left, right, up, down, center, lead, or trail. swt-unselected-close-visible: Is true or false if the close icon is shown on unselected tabs. swt-unselected-tabs-color: The color of the unselected tabs. swt-unselected-image-visible: Is true or false if the image is shown on unselected tabs. The reference to the default.css file is specified in the plugin.xml; the product property applicationCSS points to the top-level CSS file. It can also be overridden with command line arguments -applicationCSS and -applicationCSSResources, both of which use a URL for identifying the location of the main CSS file and its associated resources. Have a go hero – using the theme manager Eclipse 4 ships with a "theme manager" which can be used to swap between themes (in essence, separate CSS files). The theme manager is available for inclusion in E4 based applications by adding the org.eclipse.e4.ui.css.swt plug-in as a dependency to the application, and by adding one or more org.eclipse.e4.ui.css.swt.theme extension points. To switch between different themes, create a handler that has an org.eclipse.e4.ui. css.swt.theme.IThemeEngine injected, and invoke engine.setTheme(id,persist), where id is the reference to the theme defined in the extension point, and persist is a boolean denoting whether the theme switch should be saved and used by default in the next launch. [ 205 ] www.it-ebooks.info Understanding the Eclipse 4 Model Using services and contexts An IDE is more than a collection of its component parts, and the Eclipse 4 framework allows these to coordinate and communicate with each other. In prior releases of Eclipse, the Platform (or PlatformUI) object would act as an oracle of the known services in the runtime infrastructure, as well as providing hooks for accessing those services, for example: IExtensionRegistry registry = Platform.getExtensionRegistry(); IWorkbench workbench = PlatformUI.getWorkbench(); Although this provides a programmatic way of making the services available, it has two key disadvantages: The provider of the interface is tightly coupled with the bundle containing the accessor, even if they are unrelated Introducing new services requires a code change to a core object, and has disadvantages in being introduced to existing systems The goal of E4 is to decouple service providers and service consumers through the use of OSGi services. These are contributed to the runtime, and can be looked up via interface name using either standard OSGi mechanisms, or through E4 injection. Although that is the general trend, some platform services accessed by the IServiceLocator aren't backed by OSGi services. Time for action – adding logging The OSGi platform defines a LogService which allows messages to be logged to a central collector. In the E4 platform, an instance of LogService is available as part of the platform, routing error messages through to the console. 1. 2. 3. 4. Open the Hello class and add a private field LogService log. Add an @Inject annotation above the LogService field. In the create() method, add a call to the log service. The Hello class will look like: import javax.inject.Inject; import org.osgi.service.log.LogService; public class Hello { @Inject private LogService log; @PostConstruct public void create(Composite parent) { [ 206 ] www.it-ebooks.info Chapter 7 label = new Label(parent, SWT.NONE); label.setText("Hello"); log.log(LogService.LOG_ERROR,"Hello"); } } 5. Run the application, and a log message will be printed out to the console of the host Eclipse: !ENTRY org.eclipse.e4.ui.workbench 4 0 2013-01-24 23:15:51.543 !MESSAGE Hello What just happened? The E4 runtime infrastructure injected the reference into the class when it was constructed. The instance was obtained from the E4 context, which looks through a hierarchy of contexts until it can find an instance which matches the class type. At the root of the context tree, the list of OSGi services is consulted. If a LogService cannot be found in the context, the part will fail to be created with an error message: !ENTRY org.eclipse.e4.ui.workbench 4 0 2013-01-24 23:19:04.328 !MESSAGE Unable to create class 'com.pactpub.e4.app.parts.Hello' from bundle '29' !STACK 0 org.eclipse.e4.core.di.InjectionException: Unable to process "Hello.log": no actual value was found for the argument "LogService" at org.eclipse.e4.core.internal.di.InjectorImpl. reportUnresolvedArgument(InjectorImpl.java:394) To mark the service as optional (that is it can be null), annotate it with @Optional: import org.eclipse.e4.core.di.annotations.Optional; public class Hello { @Inject @Optional private LogService log; @PostConstruct public void create(Composite parent) { if (log != null) { log.log(LogService.LOG_ERROR, "Hello"); } Remember that @Optional annotated fields or parameters have to be guarded against the possibility of being null. If a service arrives after instantiation, the service will be injected into the part. [ 207 ] www.it-ebooks.info Understanding the Eclipse 4 Model Time for action – getting the window In an Eclipse 3.x application, the main window is typically accessed via a static accessor like Display.getDisplay() or workbench.getWorkbenchWindows(). Both of these assume there is a way of getting to this global list in the first place, often through tightly coupled code references. In addition to OSGi services, E4 can also be used to inject references to GUI components. However, rather than accessing the GUI components directly, models are used instead. As a result, components in E4 tend to start with M (for Model) – such as MPart, MWindow, and MPerspective. 1. To obtain the reference to the window, add a field MWindow window to the Hello class, along with an @Inject annotation. 2. Modify the create() method so that the label of the text is taken from the window's title (label). The class will look like: import org.eclipse.e4.ui.model.application.ui.basic.MWindow; public class Hello { @Inject private MWindow window; @PostConstruct public void create(Composite parent) { Label label = new Label(parent, SWT.NONE); label.setText(window.getLabel()); } } 3. Run the application, and the name of the label should be the name of the window, which is com.packtpub.e4.application. 4. Open the Application.e4xmi file, and go to Applications | Windows | Trimmed Window which is where the label is defined. Modify the label to Hello E4 and save the file. 5. 6. Run the application, and the name of the label and window should be Hello E4. Go back to the Application.e4xmi file, select the Trimmed Window node and do Edit | Copy. Select the Windows node and do Edit | Paste. This will create a duplicate node for the Trimmed Window. Modify the label to Other E4 and save the file. 7. Run the application, and two windows should be displayed. The first will contain a label "Hello E4" while the other will contain Other E4. 8. Finally, modify the Other E4 part to be invisible by unchecking the Visible checkbox on the Trimmed Window. [ 208 ] www.it-ebooks.info Chapter 7 What just happened? A reference to the enclosing MWindow was acquired, using the same mechanism as for the OSGi LogService. However, while the LogService is a global instance, the MWindow is local to the currently selected part. The lookup strategy for objects follows a hierarchical set of contexts, which are hash table like structures that contain named objects. These are represented with the MContext interface, which is implemented by: MApplication MWindow MPerspective MPart MPopupMenu The context search starts at the most specific element, and works its way up to the top level. If they aren't found in the MApplication, the OSGi services are consulted. Each of these are organized into a hierarchy with IEclipseContext, which maintains parent/child relationships between them. The lookup automatically follows the chain if an object cannot be located in the current context. In the last example, having two separate windows means that two contexts are used, and so although the code is identical between the two, one has the "Hello E4" window model injected, while the other has the "Other E4" window model injected. Time for action – obtaining the selection The current selection can be obtained through the selection service with a listener, similar to Eclipse 3.x. However, the ISelectionService in Eclipse 3.x has been replaced with an almost identical ESelectionService in Eclipse 4.x. (Other than the minor lack of JavaDoc and change of package name, the only significant difference between the two is that there is no add/removePostSelection methods.) 1. Create a class called Rainbow in the com.packtpub.e4.application.parts package. Add a static final array of strings with colors of the rainbow. 2. Add a create() method, along with a @PostConstruct annotation, that takes a Composite parent. Inside, create a ListViewer and set the input to the array of rainbow colors. The class will look like: public class Rainbow { private static final Object[] rainbow = { "Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet" }; [ 209 ] www.it-ebooks.info Understanding the Eclipse 4 Model @PostConstruct public void create(Composite parent) { ListViewer lv = new ListViewer(parent, SWT.NONE); lv.setContentProvider(new ArrayContentProvider()); lv.setInput(rainbow); } } 3. Open the Application.e4xmi and go to Application | Windows | Trimmed Window | Controls | Perspective Stack | Perspective | Controls | Part Stash Container | Part Stack. Right-click on the Part Stack and choose Add child followed by Part. Set the label as Rainbow and find the Rainbow class with the Find button, or by using the bundleclass URI bundleclass://com.packtpub. e4.application/com.pactpub.e4.app.parts.Rainbow. 4. Run the application, and two tabs should be shown. Next to the Hello tab, the Rainbow tab will be shown, containing the rainbow colors in a list. 5. To synchronize the selection between the two tabs, add a field of type ESelectionService called selectionService, along with an @ Inject annotation. In the create() method, add an anonymous ISelectionChangedListener to the ListViewer, such that if a selection event is received, the selection is reflected in the selectionService. The implementation will look like: @Inject private ESelectionService selectionService; @PostConstruct public void create(Composite parent) { ListViewer lv = new ListViewer(parent, SWT.NONE); lv.setContentProvider(new ArrayContentProvider()); lv.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { selectionService.setSelection(event.getSelection()); } }); ... } 6. When a selection is made on the ListViewer, it will send a selection event to the platform's selection service. To determine what objects are selected, the Hello part can register for changes in the selection, and use that to display the text in the label. Add a setSelection() method on the Hello class as follows: @Inject @Optional public void setSelection( [ 210 ] www.it-ebooks.info Chapter 7 @Named(IServiceConstants.ACTIVE_SELECTION) Object selection) { if (selection != null) { label.setText(selection.toString()); } } 7. Run the application, switch to the Rainbow part, and select the Red color from the list. Switch back to the Hello part and it should show [Red] in the label. What just happened? The list viewer is similar to the previous example. Here, a simple array of String values is being used, backed with an ArrayContentProvider. By creating a new part, it is possible to switch between the tabs to see the effect of changing the selection. In order to hook up the viewer's selection with the platform's selection, the viewer's selectionChanged event needs to be delegated to the platform. To do that, the ESelectionService needs to be injected. The E4 context contains a set of name/value pairs (like a HashMap), and one of these is used to track the current selection. When the selection changes, the new value is set into the context, and this triggers the method calls on the corresponding parts. Because there may not be a selection when the part is created, it is necessary to annotate it with @Optional. If a method is marked with @Optional then it won't be called at all; if a parameter is marked as @Optional then the method will be called, but with a null parameter. In general, for methods that receive events, mark them as @Optional so that they are not called at creation time. E4 can automatically inject the active selection as it changes. The context contains an object with a key of IServiceConstants.ACTIVE_SELECTION (which has the value org. eclipse.ui.selection), which can be injected as a @Named parameter in a method call. Since there may not be a selection, the @Optional annotation must be used, as otherwise exceptions will be reported when the part is created. [ 211 ] www.it-ebooks.info Understanding the Eclipse 4 Model Time for action – dealing with events There's a more generic way of passing information between components in Eclipse 4, using the OSGi EventAdmin service. This is a message bus, like JMS, but operates in memory. There is also an Eclipse-specific EventBroker, which provides a slightly simpler API to send messages. 1. 2. 3. Add the following bundles as dependencies to the com.packtpub. e4.application project, by double-clicking on the project's META-INF/ MANIFEST.MF file and going to the Dependencies tab: org.eclipse.osgi.services org.eclipse.e4.core.services org.eclipse.e4.core.di.extensions Open the Rainbow class and obtain an instance of EventBroker through injection. Modify the selectionChanged() method, so that instead of setting a selection, it uses the EventBroker to post() the color asynchronously to the rainbow/ colour topic. public void selectionChanged(SelectionChangedEvent event) { selectionService.setSelection(event.getSelection()); IStructuredSelection sel = (IStructuredSelection) event.getSelection(); Object colour = sel.getFirstElement(); broker.post("rainbow/colour",colour); } 4. Open the Hello class, and add a new method receiveEvent(). It should be annotated with @Inject and @Optional. The parameter should be defined with an annotation @EventTopic("rainbow/colour") to pick up the data from the event. It will look like: @Inject @Optional public void receiveEvent( @EventTopic("rainbow/colour") String data) { label.setText(data); } 5. Run the application, and switch to the Rainbow tab. Select an item in the list, and go back to the Hello tab. Nothing will be displayed. Open the Console view in the host Eclipse, and an error will be visible: !MESSAGE Exception while dispatching event org.osgi.service.event.Event [topic=rainbow/colour] to handler [ 212 ] www.it-ebooks.info Chapter 7 org.eclipse.e4.core.di.internal.extensions. [email protected] !STACK 0 org.eclipse.e4.core.di.InjectionException: org.eclipse.swt.SWTException: Invalid thread access at org.eclipse.e4.core.internal.di.MethodRequestor. execute(MethodRequestor.java:63) 6. This happens because the dispatched event runs on a non-UI thread by default. There are two ways of solving this problem; either re-dispatch the call to the UI thread, or use @UIEventTopic instead of @EventTopic. Modify the receiveEvent() as follows: public void receiveEvent( @EventTopic("rainbow/colour") String data) { @UIEventTopic("rainbow/colour") String data) { label.setText(data); } 7. Run the application, go to the Rainbow tab and select a color. Switch back to the Hello tab and the text of the label will be updated appropriately. What just happened? Events allow components to communicate in a highly decoupled mechanism. Using events to pass data means that the only shared context is the name of the topic. The OSGi EventAdmin service is a key component and will always be available in current Eclipse 3.x and E4 applications, since much of the lower-level implementations are based on events. Either the Eclipse EventBroker wrapper or the EventAdmin can be used, depending on personal preferences. However, if the code is to be used in other OSGi systems, building directly on top of the EventAdmin will give the greatest portability. The Event object is either created automatically using EventBroker or can be created manually. Each Event has an associated topic, which is a String identifier that allows producers and consumers to coordinate with each other. Map<String, Object> properties = new HashMap<String,Object>(); properties.put("message","Hello World"); properties.put(IEventBroker.DATA,"E4 Data Object"); eventAdmin.postEvent(new Event("topic/name",properties)); Note, that if the object passed in is a Map or Dictionary, it gets passed to the EventAdmin as is. To pass a Map and receive it using the E4 tools, another Map, must be created and passed in with the IEventBroker.DATA key. Alternatively, the EventAdmin service can be used directly. [ 213 ] www.it-ebooks.info Understanding the Eclipse 4 Model The Event can be posted synchronously (that is on the same thread as the delivery agent) or asynchronously (on a non-background thread). Synchronously, using sendEvent() or send() Asynchronously, using postEvent() or post() Synchronous or Asynchronous? Generally using asynchronous delivery is recommended, since synchronous delivery will block the calling thread. When delivering events from the UI asynchronous delivery should always be used, since it is not possible to place any bounds on how long the event receivers may take to execute. To receive an event, a listener needs to be registered with the topic. This can be done via the OSGi EventAdmin service, or with the @EventTopic and @UIEventTopic annotations on a method marked with @Inject @Optional. If an Event needs to be processed on the UI thread, it should not be sent synchronously from the UI thread. Doing so invites delays and blocking the UI, since it is possible for other listeners to pick up on the event and do excessive computation on an unnecessary thread. Instead, send it from a non-UI thread, and in the event hander, delegate it to the UI thread or consume it via the @UIEventTopic annotation. The topic name is specified in the annotation (or via the subscription in the EventHandler interface). Topic names can be any string, but are typically separated with / characters. This is because the OSGi specification allows for subscription to both topics by exact match and to partial matches. The subscription topic/* will pick up both topic/name and topic/another/example. Note that it is not a regular expression; the topics are explicitly delimited by the / character, and /* means "and everything below". So topic/n*e won't match anything, and nor will topic/*/example. To be more selective about the topics subscribed, use the EventAdmin EVENT_FILTER to specify an LDAP-style query. Subscribe to the highest level that makes sense (such as topic/*) and then use an LDAP filter to refine it further, using event.filter with (event.topic=topic/n*e). Currently, the annotations cannot be used to apply an LDAP filter, but it's possible to register an EventHandler interface which supplies this property. Finally, it is conventional for the topic name to be constructed from the same kind of reverse domain names used for bundles, with . replaced with / for example com/packtpub/e4/app/. [ 214 ] www.it-ebooks.info Chapter 7 Time for action – calculating values on demand The Eclipse context can supply not only services but also dynamically calculated values. These are supplied via an interface IContextFuction. By registering a service with that class name, and a key name with the service.context.key it is possible to create a value upon request. 1. Create a class called RandomFunction which extends ContextFunction and which returns a random value: package com.packtpub.e4.application; import org.eclipse.e4.core.contexts.IContextFunction; import org.eclipse.e4.core.contexts.IEclipseContext; public final class RandomFunction extends ContextFunction { @Override public Object compute(final IEclipseContext context) { return Math.random(); } } 2. To allow E4 to recognize the function, register an instance with the OSGi runtime. This could be done within the Activator, but currently a bug prevents this from happening. Instead, register it using declarative services. Create a file called random.xml in a folder called OSGI-INF with the following content: <?xml version="1.0" encoding="UTF-8"?> <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="math.random"> <implementation class="com.packtpub.e4.application.RandomFunction"/> <service> <provide interface="org.eclipse.e4.core.contexts.IContextFunction"/> </service> <property name="service.context.key" type="String" value="math.random"/> </scr:component> 3. Add the OSGI-INF folder to the build.properties, to ensure that it gets added when the bundle is built: bin.includes = OSGI-INF/,\ META-INF/,\ 4. To allow DS to load and create the component, add this header into META-INF/ MANIFEST.MF: Service-Component: OSGI-INF/*.xml [ 215 ] www.it-ebooks.info Understanding the Eclipse 4 Model 5. Finally, inject this value into the application. In the Hello part, add the following: @Inject @Named("math.random") private Object random; @PostConstruct public void create(Composite parent) { label = new Label(parent, SWT.NONE); label.setText(window.getLabel() + " " + random); } 6. Run the application, and this time, the Hello tab starts out with a random value appended to the window's title text. Each time the application is started, a different value is calculated. What just happened? The IEclipseContext can acquire calculated values as well as values inserted into the runtime. The calculation is done through a function, registered with the IContextFunction interface—though currently the only way to register it is with the declarative services model, as shown previously. The implementation class should extend ContextFunction, as the interface IContextFunction is marked as @NoImplement. This allows additional methods to be added. In Eclipse 4.3, a new method was added to the interface compute(IEclipseContext,String) which was also added to the ContextFunction parent class. The OSGi declarative services API allows a service to be created and made available to clients that need to use it. To register a service with DS, a service document is created (in the example this is random.xml), and it is referred to with the Service-Component header in the MANIFEST.MF file. When the plug-in is installed, the DS component notices this header, reads the service document, and then creates the class. This class then becomes available through the E4 context for inclusion in the injection of parts. Note that the value of the calculated function is cached upon first calculation. So even if the code is changed to inject the value as a method parameter, only the first value calculated will be seen. Although the IEclipseContext has a method to remove the value, it doesn't necessarily trigger the removal from all contexts. The recommendation is to use an OSGi service for data that must be calculated each time. [ 216 ] www.it-ebooks.info Chapter 7 Time for action – using preferences In addition to injecting in specific elements from the context, it is also possible to acquire preferences from the Eclipse preference store. Recall that preferences are stored in a hierarchical node structure, with each node having an identifier (conventionally the plug-in name) and a number of key/value pairs. An annotation @Preference allows these to be accessed easily. 1. Add a String greeting field to the Hello part. To obtain a preference, annotate it with @Inject and @Preference as follows: @Inject @Preference(nodePath="com.packtpub.e4.application", value="greeting") private String greeting; 2. Modify the create() method to use this greeting value as the initial value for the Hello label. @PostConstruct public void create(Composite parent) { label = new Label(parent, SWT.NONE); label.setText(greeting+" "+window.getLabel()+" "+random); 3. 4. Run the application, and the Hello label will show a null value for the greeting. To set a preference value, an IEclipsePreferences object needs to be injected into the Hello part. Add a new field called prefs which is used to interact with the preferences store: @Inject @Preference(nodePath="com.packtpub.e4.application") private IEclipsePreferences prefs; If the preference's nodePath is not specified, a runtime error occurs. 5. Modify the receiveEvent() method created previously and set the value of the color as the greeting: @Inject @Optional public void receiveEvent( @UIEventTopic("rainbow/colour") String data) { label.setText(data); prefs.put("greeting", "I like " + data); prefs.sync(); } [ 217 ] www.it-ebooks.info Understanding the Eclipse 4 Model 6. Now run the application, go to the Rainbow tab and select a color. Switch back to the Hello tab and verify that the event was received. Now, close down the application and re-open the application; the persisted greeting should be visible (provided that the workspace is not being cleaned upon each launch). 7. Changes to the preference value gets injected dynamically into the part, but as it stands there is no notification when an injected field value has been changed. To be notified when a new value is set, the preference can be injected as an annotated @ Preference parameter on an @Optional method: @Inject @Optional void setText(@Preference(nodePath="com.packtpub.e4.application", value="greeting") String text) { if (text != null && label != null && !label.isDisposed()) { // NB Run in UI thread! label.setText(text); } } 8. Now, if the preference is set, the label's text will be updated automatically. However, the preference invocation is not necessarily on the UI thread, so the call should be delegated appropriately. It may also be the case that this method is called during startup or as the runtime is closing down, in which case, the setting of any UI components need to be guarded against either being null or being disposed. What just happened? Acquiring preferences with E4 injection makes it trivial to obtain and set preference values. Using a single preference value is the easiest way to get individual values; however, if the preferences need to be mutated then a reference to the IEclipsePreferences store is required. If the user interface needs to react to changes in the preference, the preference value should be injected via a setter. When the preference value changes, the setter is invoked and it can update the UI. Note that the preference value setter may be invoked on any thread; if the UI needs to be updated then this should be done via the UI thread. How to do this is covered in the next section. [ 218 ] www.it-ebooks.info Chapter 7 Time for action – interacting with the UI Sometimes it is necessary to write code to run in the UI thread, but when called back via a handler it's not always clear if the method is in the UI thread or not. In Eclipse 3.x there is a Display.getDefault().syncExec() for running Runnables inside the UI thread, or .asyncExec() for a non-UI thread. Eclipse 4 introduces the UISynchronize class, which is an abstract mechanism for executing code on the UI thread. (It's like an interface for Display, except that Display doesn't implement it and it's not an interface.) This provides syncExec() and asyncExec() methods which can be used to schedule Runnable events. If a long calculation needs to update the UI after concluding, using UISynchronize allows the UI update to be scheduled on the right thread. 1. Create a new Button as a field in the Hello part, and attach a selection listener such that when it is pressed, it invokes setEnabled(false) on itself. At the same time, schedule a Job to run after one second that invokes setEnabled(true) again: private Button button; @PostConstruct public void create(Composite parent) { button = new Button(parent, SWT.PUSH); button.setText("Do not push"); button.addSelectionListener(new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { button.setEnabled(false); new Job("Button Pusher") { @Override protected IStatus run(IProgressMonitor monitor) { button.setEnabled(true); return Status.OK_STATUS; } }.schedule(1000); } @Override public void widgetDefaultSelected(SelectionEvent e) { } }); ... } [ 219 ] www.it-ebooks.info Understanding the Eclipse 4 Model 2. When the application runs and the button is pushed, the button will be disabled (grayed out) immediately. One second later, an exception will be logged to the console with an "Invalid thread access" message: !MESSAGE An internal error occurred during: "Button Pusher". !STACK 0 org.eclipse.swt.SWTException: Invalid thread access at org.eclipse.swt.SWT.error(SWT.java:4361) 3. The error occurs because the setEnabled() call must be made on the UI thread. Although Display.getDefault().syncExec() can be used to do this, E4 provides an annotation-based way of doing the same thing. Inject an instance of the UISynchronize into the Hello part: @Inject private UISynchronize ui; 4. Modify the Job implementation in the create() method as follows: protected IStatus run(IProgressMonitor monitor) { ui.asyncExec(new Runnable() { @Override public void run() { button.setEnabled(true); } }); return Status.OK_STATUS; } 5. Now run the application and press the button. It will be disabled for one second, and then re-enabled afterwards. What just happened? Using UISynchronize provides a way to interact with the UI thread safely. Another way of achieving this would be to use a UIJob. One advantage of using UISynchronize is that it is not necessarily tied down to SWT. E4 provides the option of having different part renderers, which could allow for future runtimes based on HTML or Swing, or JavaFX such as e(fx)clipse. When building plug-ins that will be shared between E4 and Eclipse 3.x systems, continue to use Display.getDefault() or Display.getCurrent() in order to schedule UI updates, as the UISynchronize class is not present in earlier releases. [ 220 ] www.it-ebooks.info Chapter 7 Using Commands, Handlers, and MenuItems The command and handlers in Eclipse 4 work the same way as in Eclipse 3. A command represents a generic operation, and the handler is the code that implements the operation. However, the implementation for the handler takes advantage of E4's annotations, instead of a custom subclass. Time for action – wiring a menu to a command with a handler As with Eclipse 3.x, a command has an identifier and an associated Handler class, which can be bound to Menus. Unlike Eclipse 3.x, it is not specified in the plugin.xml file; instead, it is specified in the Application.e4xmi file. 1. Open the Application.e4xmi file in the com.packtpub.e4.application project. 2. Navigate to the Application | Commands node in the tree, and click on Add child to add a new command: ID: com.packtpub.e4.application.command.hello Name: helloCommand Description: Says Hello [ 221 ] www.it-ebooks.info Understanding the Eclipse 4 Model 3. Create a HelloHandler class in the com.packtpub.e4.application. handlers package. It doesn't need to have any specific superclass or method implementation. Instead, create a method called hello()which takes no arguments, and prints a message to System.out. The method needs the @Execute annotation: package com.packtpub.e4.application.handlers; import org.eclipse.e4.core.di.annotations.Execute; public class HelloHandler { @Execute public void hello() { System.out.println("Hello World"); } } 4. The handler needs to be defined in the Application.e4xmi. Navigate to the Application | Handlers node in the tree, and right-click and Add child to add a new handler. Fill in the details as follows: ID: com.packtpub.e4.application.handler.hello Command: helloCommand – com.packtpub.e4.application. command.hello Class URI: bundleclass://com.packtpub.e4.application/ com.packtpub.e4.application.handlers.HelloHandler [ 222 ] www.it-ebooks.info Chapter 7 5. Finally, to associate the handler with a menu, go into the Application | Windows | Trimmed Window | Main Menu | File and click on Add child to add a new Handled MenuItem. This can also be done by right-clicking on the Menu – File node and choosing Add child | Handled MenuItem. Add it as follows: ID: com.packtpub.e4.application.handledmenuitem.hello Label: Hello Tooltip: Says Hello World 6. Command: helloCommand – com.packtpub.e4.application. command.hello Save the Application.e4xmi and run the application. Go to the File menu, and click on Hello from the menu item to display Hello World in the Console view of the host Eclipse. What just happened? The HelloHandler class provides a simple message output and an entry point annotated with @Execute. The handler can also report whether it can be executed through a boolean returning method annotated with @CanExecute, but this defaults to true if a handler is always valid. [ 223 ] www.it-ebooks.info Understanding the Eclipse 4 Model A command is a generic ID that can be associated with one or more handlers and with one or more UI elements, such as MenuItems, Buttons, or programmatic execution. The helloCommand is associated by default to the HelloHandler. Finally, the command is associated with the File | Hello menu item so that it can be invoked. Time for action – passing command parameters Displaying a message to System.out shows that the command works, but what if the command needed to pick up a local state? Fortunately, the @Named and @Inject annotations allow objects to be injected into the method when it is called. 1. Modify the hello() method so that instead of printing a message to System.out, it opens a dialog window, using the active shell: public void hello(@Named(IServiceConstants.ACTIVE_SHELL) Shell s){ MessageDialog.openInformation(s, "Hello World", "Welcome to Eclipse 4 technology"); } 2. Other arguments can be passed in from the context, managed by the IEclipseContext interface. For example, using the math.random function from earlier, a value could be injected into the handler like this: public void hello(@Named(IServiceConstants.ACTIVE_SHELL) Shell s, @Named("math.random") double value) { 3. If the same handler is being used for different functions (for example, Paste and Paste Special) they can be disambiguated by passing in a hard-coded value in the command. Modify the helloCommand to add a parameter, by opening the Application.e4xmi and navigating to the Application | Commands | helloCommand, right-click and go to Add child | Command Parameter to add a command parameter: ID: com.packtpub.e4.application.commandparameter.hello. value Name: hello.value Optional: Ensure that Optional is selected [ 224 ] www.it-ebooks.info Chapter 7 4. To pass a value into the command, go to the Application.e4xmi and navigate to Application | Windows | Main Menu | Menu (File) | HandledMenuItem (Hello) | Parameters. Right-click and go to Add child | Parameter to create a parameter: ID: com.packtpub.e4.application.parameter.hello.value Name: com.packtpub.e4.application.commandparameter.hello. value Value: Hello World Parameter [ 225 ] www.it-ebooks.info Understanding the Eclipse 4 Model 5. Finally, modify the command handler so that it receives the value encoded with the handler: public void hello(@Named(IServiceConstants.ACTIVE_SHELL) Shell s, @Optional @Named( "com.packtpub.e4.application.commandparameter.hello.value") String hello) @Named("math.random") double value) { MessageDialog.openInformation(s, "Hello World", hello+value); } } 6. Run the application, and go to the File | Hello menu. The parameter will be passed in to the handler What just happened? Any values can be injected into the method when it is invoked, provided that they are available in the context when the handler is called. These can be taken from standard constants (such as those in IServiceConstants) or be custom values injected at runtime. Other values include: ACTIVE_WINDOW: The currently displayed window ACTIVE_PART: The currently selected part ACTIVE_SELECTION: The current selection If the values are one of a set of values, they can be encoded in the menu or other command invocation. They can also be set via code which calls IEclipseContext.set() with an appropriate value. Time for action – creating a direct menu and keybindings Although using commands and handlers provides a generic way for reusing content, it is possible to provide a shorter route to implementing menus with a Direct MenuItem. The difference between this and a Handled MenuItem is that Direct just contains a reference to the @Executable class. 1. To add a new direct menu item, open the Application.e4xmi file and navigate to the Application | Windows | Trimmed Window | Main Menu | Menu (File). Rightclick on the menu and choose Add child | Direct MenuItem. In the dialog shown, fill in the details, including the class URI link to the HelloHandler, defined previously: ID: com.packtpub.e4.application.directmenuitem.hello Label: Direct Hello [ 226 ] www.it-ebooks.info Chapter 7 2. 3. Class URI: bundleclass://com.packtpub.e4.application/ com.packtpub.e4.application.handlers.HelloHandler Run the application, choose File | Direct Hello to show the same message as before. Keys can be bound to commands in an application, and can be enabled in one of the several UI contexts. By default, these include In Dialogs and Windows, In Dialogs, and In Windows; additional contexts can be created. To set a keybinding to the menu, open the Application.e4xmi and navigate to the Application | BindingTables | BindingTable – In Dialog and Windows. Right-click on the binding table and Add child | KeyBinding. Fill in the fields as follows: ID: leave blank Sequence: M1+L [ 227 ] www.it-ebooks.info Understanding the Eclipse 4 Model 4. Command: helloCommand - com.packtpub.e4.application. command.hello Run the application, and press M1 + L (Cmd + L on OS X, Alt + L on Windows/Linux). The Hello command will be run. What just happened? A Direct MenuItem is a way of hooking up a menu item directly to an executable method in a very simple way, without needing to have a separate command and handler defined. For application-wide operations, such as quitting the application, using a Direct MenuItem may be appropriate. However, if the command needs to be handled in a different context then it is more appropriate to define a handler which can be replaced in different scopes. Unlike a Handled MenuItem, the Direct MenuItem cannot have command parameters associated with it. Nor can a Direct MenuItem have a keybinding assigned. [ 228 ] www.it-ebooks.info Chapter 7 To associate a keybinding with a command, an associated context must be selected. This is typically the In Dialogs and Windows, although other contexts can be selected as well (such as In Dialogs and In Windows). These are represented as the Binding Table in the Application.e4xmi node. The sequence can be a single character, or it can be a sequence of characters. The meta characters (M1, M2, M3, M4, and so on) are defined in the org.eclipse.ui.bindings extension point. M1 is the Cmd key on OS X and Ctrl on Windows M2 is Shift on all platforms M3 is Alt on all platforms M4 is Ctrl on OS X When the binding is invoked, it will execute the command specified in the list. As a command, it can have associated parameters like the Handled MenuItems. Time for action – creating a pop-up menu and a view menu Pop-up and view menus are defined declaratively in the Application.e4xmi file. These are specific to a part, so the option is defined underneath the part declaration. 1. 2. Open the Application.e4xmi file. 3. Right-click on the Menus node and go to Add child | Popup Menu. Now right-click on the Popup Menu and do Add child | HandledMenuItem. This is exactly the same as for other menus; fill in the details as follows: Navigate to Application | Windows | Trimmed Window | Controls | Perspective Stack | Perspective | Controls | PartSashContainer | Part Stack | Part (Hello) | Menus. Label: Hello [ 229 ] www.it-ebooks.info Understanding the Eclipse 4 Model Command: helloCommand - com.packtpub.e4.application. command.hello 4. Right-click on the Menus node again, and go to Add child | View Menu. Give the menu a label View Menu and right-click to Add child | Handled MenuItem. Use the same label and command as for the pop-up menu. 5. Run the application. On the top-right, there will be a triangular drop-down icon which should contain the view menu. However, the pop-up menu won't be triggered, because the SWT component has to be bound to the pop-up menu through its ID. [ 230 ] www.it-ebooks.info Chapter 7 6. Go to Popup Menu and set the ID to com.packtpub.e4.application. popupmenu.hello: 7. Add the org.eclipse.e4.ui.workbench.swt dependency to the dependencies tab of the plugin.xml editor. 8. Add a line to the Hello class' create() method, that registers the context menu with the ID specified. To do this, a new parameter EMenuService menu needs to be passed, from which registerContextMenu() can be called: public void create(Composite parent, EMenuService menu) { menu.registerContextMenu(parent, "com.packtpub.e4.application.popupmenu.hello"); 9. Run the application, and right-click on the Hello label or elsewhere in the Hello part. The pop-up menu should be shown, and the Hello command can be run. What just happened? The pop-up menu can be associated with a part, but it doesn't get shown by default. Instead, it has to be registered with a SWT widget. The pop-up menu can be for the entire part's component, or it can be just for specific components in the part. [ 231 ] www.it-ebooks.info Understanding the Eclipse 4 Model The EMenuService is the interface to the E4 menus. It gets injected into the creation of the widget and provides the detector to listen to the mouse and keyboard events that trigger the pop-up menu. Adding a View Menu is exactly the same as a Popup Menu, except that no additional code is required to make it happen. Creating custom injectable classes The injection framework in E4 allows custom injectable classes and services. In addition to registering OSGi services, POJOs can be defined and instantiated on demand. The rules for allowing a type to be instantiated automatically are: It must be a non-abstract class. It must have a non-private default constructor. It must be annotated with @Creatable. Time for action – creating a simple service POJOs can be instantiated and made available in the E4 context, such that they can be injected into other classes or created on demand. This allows an application to be built in a flexible manner without tight coupling between services. 1. Create a class in the com.packtpub.e4.application package called StringService with a @Creatable annotation, and a process() method that takes a String and returns an upper-case version: import org.eclipse.e4.core.di.annotations.Creatable; @Creatable public class SimpleService { public String process(String string) { return string.toUpperCase(); } } 2. Add an injectable instance of StringService to the Rainbow class: @Inject private StringService stringService; 3. Use the injected service to process the string color choice before posting the event to the event broker: public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection sel = (IStructuredSelection) [ 232 ] www.it-ebooks.info Chapter 7 event.getSelection(); Object colour = sel.getFirstElement(); broker.post("rainbow/colour", stringService.process(colour.toString())); } 4. Run the application. Go to the Rainbow part and select a color; switch back to the Hello part, and the color should be shown, but in uppercase. What just happened? By denoting a POJO with @Creatable, when the dependency injection in E4 needs to satisfy a type it knows that it can create an instance of the class to satisfy the injection demand. It will invoke the default constructor and assign the result to the injected field. Note that the resulting instance is not stored in the context; as a result, if additional instances are required (either in a separate field in the same part, or in an alternative part) the dependency injection will create new instances for each injection. Generally, the use of injectable POJOs in this way should be restricted to stateless services. If the service needs a state that should be shared by multiple callers, register an OSGi service instead, or use a singleton service injected in the context. Time for action – injecting subtypes Although creating a POJO can be an efficient way of creating simple classes, it can be limiting to have a concrete class definition scattered through the class definitions. It is a better design to use either an abstract class or an interface as the service type. 1. Create a new interface in the com.packtpub.e4.application package, called IStringService. Define the process() method as abstract: public interface IStringService { public abstract String process(String string); } 2. Modify the reference in the Rainbow class to refer to the IStringService interface instead of the StringService class: @Inject private IStringService stringService; [ 233 ] www.it-ebooks.info Understanding the Eclipse 4 Model 3. Run the application, switch to the Rainbow tab, and a dependency injection fault will be shown in the host Eclipse instance's Console view: org.eclipse.e4.core.di.InjectionException: Unable to process "Rainbow.stringService": no actual value was found for the argument "IStringService". 4. Although the runtime knows that the StringService is a @Creatable instance, it doesn't look for subtypes of an interface by default. To inject an alternative type, modify the Activator and add the following: public class Activator implements BundleActivator { public void start(BundleContext bundleContext) throws Exception{ Activator.context = bundleContext; InjectorFactory.getDefault(). addBinding(IStringService.class). implementedBy(StringService.class); } } 5. Run the application and the part should be created correctly. What just happened? Using an interface for the service type is best practice, since it further decouples the use of the service with its implementation. In order for the dependency injection framework to provide an instance of an abstract type (whether an interface or abstract class, or even a concrete class without a @Creatable annotation) a binding needs to be created for the injector. The binding created above tells the InjectorFactory to create an instance of StringService when an IStringService is required. Have a go hero – using the tools bridge Although Eclipse 3.x parts can run in an Eclipse 4 IDE, to take advantage of the E4 model the code has to be implemented as a POJO such that it can be registered with a model. To fit an E4 POJO into an Eclipse 3.x IDE, the E4 bridge has to be used. Install the "Eclipse E4 Tools Bridge for 3.x" from the E4 update site, which provides the compatibility views. Now, create a class called HelloView which extends DIViewPart<Hello> and passes the instance of Hello.class to the superclass' constructor. Register the HelloView in the plugin.xml as would be the case with Eclipse 3.x views, and the part is now visible either as a standalone part in Eclipse 4 or as a wrapped view in Eclipse 3.x. [ 234 ] www.it-ebooks.info Chapter 7 Pop quiz – understanding E4 Q1. What is the application model, and what is it used for? Q2. What is the different between a part and a view? Q3. Are extension points still used in Eclipse 4? Q4. How can Eclipse 4 parts be styled? Q5. What is the Eclipse 4 context? Q6. What annotations are used by Eclipse 4, and what are their purpose? Q7. How are preferences accessed in Eclipse 4? Q8. How can messages be sent and received on the event bus? Q9. How is the selection accessed in Eclipse 4? Summary Eclipse 4 is a new way of building Eclipse applications, and provides a number of features that make creating parts (views/editors), as well as obtaining service references and communication between parts much easier. If you are building Eclipse-based RCP applications, then there is no reason not to jump on the Eclipse 4 framework to take advantage of its features. If you are building plug-ins that will run on both Eclipse 3.x and Eclipse 4, then you have to consider backward compatibility requirements before you can make the switch. One way of supporting both is to use the workbench compatibility plug-in (which is what Eclipse 4.x uses if you download the SDK or one of the EPP packages) and continue to use the Eclipse 3.x APIs. However, this means the code cannot take advantage of the Eclipse 4.x mechanisms. Another approach is to write Eclipse 4 based plug-ins, and then wrap them in a reverse compatibility layer. Such a layer is provided in the "Eclipse E4 Tools Bridge for 3.x" feature, which is available from the E4 tools update site. This provides classes DIViewPart, DISaveableViewPart, and DIEditorPart, which can be used to provide an adapter for an E4 part in an Eclipse 3.x view extension point. In the next chapter, we'll look at how to create features and update sites that allows the plug-ins written so far to be served and installed into other Eclipse applications. [ 235 ] www.it-ebooks.info www.it-ebooks.info 8 Creating Features, Update Sites, Applications, and Products Eclipse is much more than just an application; its plug-in architecture allows additional functionality to be installed. Plug-ins can be grouped into features, and both can be hosted on an update site. These allow functionality to be installed into an existing application, but it's also possible to build your own applications and products. In this chapter, we shall: Create a feature that combines plug-ins Generate an update site containing features and plug-ins Categorize the update site Create an application Create and export a product Grouping plug-ins with features Although functionality is provided in Eclipse through the use of plug-ins, typically individual plug-ins aren't installed separately. Historically, the Eclipse platform only dealt with features, a means of grouping a number of plug-ins together. Although the P2 update system (used in Eclipse since 3.5) is capable of installing plug-ins separately, almost all functionality used in Eclipse runtimes is installed through features. www.it-ebooks.info Creating Features, Update Sites, Applications, and Products Time for action – creating a feature A feature project is used in Eclipse to create, test, and export features. Features are used to group many plug-ins together into a coherent unit. For example, the JDT feature consists of 26 separate plug-ins. Features are also used in the construction of update sites, which are covered later in this chapter. 1. Create a feature project by going to File | New | Project... and then selecting Feature Project. 2. Name the project com.packtpub.e4.feature and this will be used as the default name for the Feature ID. As with plug-ins, they are named in reverse domain name format, though typically they end with feature to distinguish them from the plug-in that they represent. The version number defaults to 1.0.0.qualifier. The feature name is used as the text name which is shown to the user when it's installed, and will default to the last segment of the the project name. [ 238 ] www.it-ebooks.info Chapter 8 3. Click on Next and it will prompt for plug-ins to be chosen. Choose com.packtpub. e4.clock.ui from the list. 4. 5. Click on Finish to create the feature project. 6. Add additional information such as feature descriptions, copyright notices, and license agreements via the Information tab. Double-click on the feature.xml file to open it in an editor, go to the Plug-ins tab, and verify that the clock plug-in has been added as part of the feature. What just happened? A feature project called com.packtpub.e4.feature was created with a feature.xml file. The information specified in the dialog can be seen in this file, and changed later if necessary. <feature id="com.packtpub.e4.feature" label="Feature" version="1.0.0.qualifier" provider-name="PACKTPUB"> <plugin id="com.packtpub.e4.clock.ui" [ 239 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products download-size="0" install-size="0" version="0.0.0" unpack="false"/> </feature> The feature ID must be globally unique, as this is the identifier Eclipse and P2 will use for installation. The feature version follows the same format as plug-in versions; major.minor. micro.qualifier, where: Increments of major versions indicate backward incompatible changes Increments of minor versions indicate new functionality with backward compatibility Increments of micro versions indicate no new functionality other than bug fixes The qualifier can be any textual value. The special keyword qualifier is used by Eclipse to substitute the build number, which if not specified is formed from the date and timestamp. The plug-in listed here is the one chosen from the wizard. It will default to 0.0.0—but when the feature is published, it will choose the highest version available and then replace the version string for the plug-in. There may also be other elements in the feature.xml file, such as license, description, and copyright. These are optional, but if present, will be displayed in the update dialog when installing. Time for action – exporting a feature Once a feature has been created and has one or more plug-ins added, they can be exported from the workbench. An exported feature can be installed into other Eclipse instances, as the next section will demonstrate. Note that exporting a feature also builds and exports all the associated plug-ins as well. 1. To export a plug-in, go to File | Export | Deployable features. This will launch a dialog with the option to select any features in the workspace. 2. Choose the com.packtpub.e4.feature and give a suitable directory location. [ 240 ] www.it-ebooks.info Chapter 8 3. 4. Click on Finish and the feature and all of its plug-ins will be exported. Open the destination location in a file explorer and see the files created: artifacts.jar content.jar features/com.packtpub.e4.feature_1.0.0.201305070958.jar plugins/com.packtpub.e4.clock.ui_1.0.0.201305070958.jar What just happened? The File | Export | Deployable features did a number of steps under the covers. First, it compiled the referenced plug-ins into their own JARs. It then zipped up the contents of the feature project, and finally moved them both into the directory under the features and plugins subdirectories. When a feature is exported, the associated plug-ins are built, and so problems in exporting are often caused by problems compiling the plug-ins. [ 241 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products To debug problems with plug-in compilation, check the build.properties file. This is used to control the ant-based build that PDE uses under the covers. Sometimes, PDE will flag warnings or errors in this file, especially if the source or compilation folders are moved or renamed after creating a project. A build.properties file looks similar to the following: source.. = src/ output.. = bin/ bin.includes = plugin.xml,\ META-INF/,\ .,\ icons/ If there are any problems, verify that these correspond to paths in the plug-in's directory. The source.. is actually a reference to the current directory. If there are multiple JARs being created, then this will read source.ajar and source.anotherjar. The source directive is used if plug-ins have source exported; classes and other compiled output comes from the output property. If the output directory is renamed (for example target/ classes) then ensure the output.. property is updated in the build.properties file. If there are non-Java assets that need to be exported, they must be explicitly listed in this file. If a directory (such as icons/) is included, then this path will be re-created in the plugin's JAR structure when it is created. Individual assets underneath the icons/ folder do not need to be explicitly listed. Oracle Java 1.7 on OSX Exporting a feature on OSX with Oracle's Java 1.7 may give an error such as, /Library/Java/JavaVirtualMachines/.../Contents/ Home/Classes does not exist. To fix this, go into the Home directory mentioned in the error, and run sudo ln -s jre/lib Classes to re-create it. Time for action – installing a feature Now that the feature has been exported, it can be installed into Eclipse. Either the current Eclipse instance can be used for this, or a new instance of Eclipse can be created by running the eclipse executable with a different workspace. (On OS X double-clicking on the Eclipse. app will show the current Eclipse instance again; to run a second instance on OSX, open up the application in the terminal and run eclipse from the home of the application folder.) 1. To install the feature into Eclipse, go to Help | Install New Software.... [ 242 ] www.it-ebooks.info Chapter 8 2. In the dialog that appears, type in the directory's URL in the work with box. If the feature was exported into /tmp/exported, then put file:///tmp/exported into the work with field. If it was exported on Windows, say to c:\temp\exported then use a URL such as file:///c:/temp/exported. Note that on Windows the directory slashes are reversed in the URL. 3. After the URL has been put in, hit Enter. The message may say no categorized items—but deselect the checkbox at the bottom Group items by category and the feature should appear: 4. Select the checkbox next to the feature and click on Finish to install the feature and associated plug-in to the Eclipse workspace. 5. 6. Restart Eclipse to complete the install. Verify that the various Clock views are available by going to Window | Show View | Other | Timekeeping. What just happened? The feature exported from the earlier step was imported from the same location. Unless the feature is categorized, it won't show up on the list of things to install. Deselecting the Group items by category checkbox allows all features to be shown, not just categorized ones. The Feature name here is derived from the default value in the feature.xml file; it can be replaced with a more appropriate value if desired. [ 243 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products If the Eclipse instance isn't showing changes (for example, because the feature or plug-in has been modified), go to the preferences and find Install/Update | Available Software Sites and click on the exported repository. The Reload button on the right-hand side will be enabled, and clicking on that will refresh from disk the contents of the repository. Once the repository is reloaded, go back into the Help | Install New Software and the update should be seen. If it is not, verify that the version ends with .qualifier on the end of the features' and plug-ins' version number. Without a monotonically increasing version number, Eclipse gets confused and cannot detect that the plug-in or feature has been changed and so refuses to re-install it. Check also that the exported version of the plug-in and feature ends with a more recent date, or remove the contents of the exported folder, export, and then reload. Time for action – categorizing the update site The Group items by category mechanism allows a small subset of features to be shown in the list, grouped by category. Eclipse is a highly modular application, and a regular install is likely to include over 400 features and over 600 plug-ins. A one-dimensional list of all of the features will take up a significant amount of UI space and would not provide the best user experience; and in any case, many of the features are subsets of the core functionality (Mylyn alone will install over 150 features depending on what combinations are selected in the install). [ 244 ] www.it-ebooks.info Chapter 8 This categorization works by providing a category.xml file (also known as site.xml) which defines a category and a collection of features (from Eclipse 4.3 onwards, plug-ins as well as features). When the Group items by category checkbox is selected, only the groups and features defined in the category.xml file are shown, and the rest of the features and plug-ins are hidden. These are usually done via a separate Update Site Project. 1. Create a new project called com.packtpub.e4.update as an Update Site Project. This will create a new project with a site.xml file. (If it is renamed from site.xml to category.xml it will fail; so don't do that.) 2. 3. Double-click on the site.xml file and it will open an editor. Click on the New Category button and enter the following: ID: com.packtpub.e4.category Name: PacktPub Example E4 Category Description: Contains features for the PacktPub E4 book by Alex Blewitt 4. Ensure that the com.packtpub.e4.category is selected, and click on the Add Feature button. [ 245 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products 5. Select the com.packtpub.e4.feature from the pop-up menu, and this will add it to the highlighted category: 6. Click on the Build All button, and an update site will be materialized into the project folder: 7. Finally, check that the categories are set up correctly by going into Help | Install New Software and placing the path to the workspace in use. [ 246 ] www.it-ebooks.info Chapter 8 8. Ensure the Group items by category checkbox is selected, and the category containing the feature should be seen: What just happened? The site.xml file is not required by modern Eclipse runtimes, but the artifacts.jar and content.jar contain XML files required by P2 to perform installation. These contain a list of all of the features and plug-ins, and what constraints are required for their installation (such as the packages a bundle exports or imports). [ 247 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products P2 generates a categorization from either a site.xml or category.xml file. These are essentially the same, but the update site has a nicer UI for editing and generating the content required. <site> <feature id="com.packtpub.e4.feature" version="1.0.0.qualifier" url="features/com.packtpub.e4.feature_1.0.0.qualifier.jar"> <category name="com.packtpub.e4.category"/> </feature> <category-def name="com.packtpub.e4.category" label="PacktPub E4 Example Category"> <description> Contains features for the PacktPub E4 book by Alex Blewitt </description> </category-def> </site> This file contains a list of features (which themselves contain plug-ins) and can be used to generate an update site, which is a features and plugins directory along with the top-level content. If the artifacts.jar or content.jar is missing (and there is no site.xml file), Eclipse will be unable to install content from the repository. The ability to load content from a repository only containing a site.xml has been removed from Eclipse 4.3 onwards. When the update site is built, it will replace the .qualifier with a build identifier, which is derived and then composed of the year/month/day/time. It is possible to override this with a different value if desired. The artifacts.jar and content.jar files are ZIP files which contain a single xml file. This xml file is put in a JAR solely for compression; it can be served (though less efficiently) as artifacts.xml or content.xml as well. This update site can be transferred to host onto a remote server to allow installation into other Eclipse instances. Eclipse supports HTTP as well as FTP by default, although it can be extended to allow other protocols. Generally, only top-level features should be exposed in the update site. These may include other features or plug-ins, but only the top-level features are shown in the update site and in the installed list in Eclipse via Help | About | Installation Details. [ 248 ] www.it-ebooks.info Chapter 8 Time for action – depending on other features If a feature needs functionality provided by another feature, it can be declared via the feature.xml file of the feature itself. For example, installing the E4 feature may depend on some runtime components provided by JGit, so installing the JGit feature will mean that everything required is present. To add JGit as a dependency to the E4 feature: 1. Edit the feature.xml file and go to the Dependencies tab. 2. Click on Add Feature and select org.eclipse.jgit from the list. It will fill in a version range using the exact version specified in the plug-in; invariably it is better to substitute that with a lower-bound version number since that will allow the feature to be installed with a dependency that is slightly lower. This will result in a feature.xml file that looks similar to the following: <feature id="com.packtpub.e4.feature" label="Feature" version="1.0.0.qualifier" provider-name="PACKTPUB"> <requires> <import feature="org.eclipse.jgit" version="2.0.0"/> </requires> <plugin id="com.packtpub.e4.clock.ui" download-size="0" install-size="0" [ 249 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products version="0.0.0" unpack="false"/> <feature/> 3. Run the Build All again. The features/ directory will contain just the com.packtpub.e4.feature, and the plugins/ directory will contain just the com.packtpub.e4.clock.ui plug-in. 4. Install the feature again (or do Help | Check for Updates if the directory has already been added). This time, as well as installing the E4 feature, it should prompt to install JGit as well, which it will get from the standard Eclipse update sites. What just happened? By adding a dependency on another feature, when it is installed into a running Eclipse platform it requires that the other feature be present. If the Consult all update sites checkbox is selected, and if the feature is not installed and cannot be found from the current update site, other update sites will be consulted to acquire the missing feature. Note that the JGit feature will not be present in the exported site. This is generally desirable since it is unnecessary to duplicate features that are available elsewhere. However, if this is desired then remove the dependency from the Dependency tab and add it to the Included features tab. This will result in the requires dependency being changed to an includes dependency in the feature.xml file. Time for action – branding features Features generally don't show up in the About dialog of Eclipse, as there is only space for a handful of features to appear there. Only top-level features which have branding information associated with them are shown in the dialog. 1. Go to Help | About (on OS X, this is under Eclipse | About Eclipse) and there will be a number of icons present, consisting of the top-level branded features that have been installed. These features have an associated branding plug-in which contains a file called about.ini that supplies the information: [ 250 ] www.it-ebooks.info Chapter 8 2. First, set up an association between the feature and its branding plug-in, by re-using the com.packtpub.e4.clock.ui plug-in from before. Open the feature.xml file, go to the Overview tab and add the name of the branding plug-in as com. packtpub.e4.clock.ui: [ 251 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products 3. Now, create a file in the com.packtpub.e4.clock.ui plug-in called about.ini with the following content: featureImage=icons/sample.gif aboutText=\ Clock UI plug-in\n\ \n\ Example of how to use plug-ins to modularise applications\n 4. Build the update site, and install the plug-in into Eclipse. After restarting, go to the Help | About menu. Unfortunately, the about text won't be present. That's because despite the about.ini file being part of the plug-in, Eclipse doesn't bundle it into the plug-in's JAR. There is another change which is required in the build. properties file in the com.packtpub.e4.clock.ui plug-in to include the about.ini file explicitly: bin.includes = plugin.xml,\ META-INF/,\ .,\ icons/,\ about.ini,\ ... 5. 6. Export the update site and the plug-in JAR will now have the about.ini included. 7. Run Help | Check for Updates to pick up the changes, and restart Eclipse. Reload the update site from the Window | Preferences | Install/Update | Available Update Sites window and by doing a Reload on the exported repository. [ 252 ] www.it-ebooks.info Chapter 8 8. Go into the About screen to show the generic icon used by the feature: What just happened? A feature is associated with a branding plug-in, which contains a section of text and an icon. This feature branding consists of an about.ini file, which is included in the associated branding plug-in, and optionally a 32 x 32 icon. The icon generated by the same wizard is 16 x 16, so it shows up as a quarter of the size of other icons in this list. A feature icon should be 32 x 32 size, otherwise the inconsistent size will be seen. Supplying a 32 x 32 feature icon is left as an exercise for the reader. Have a go hero – publishing the content remotely Since update sites can be served on websites, upload the update content to a remote web server and install it from there. Alternatively, use a web server such as Apache or those built into the operating system to serve the content via a local web server. [ 253 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products Building applications and products An Eclipse runtime consists of groups of features which are themselves groups of plug-ins. The application that they all live within is referred to as the product. A product has top-level branding, dictates what the name of the application is, and coordinates what platforms the code will run on, including ensuring that any necessary operating system specific functionality is present. In the previous chapter, a product based on E4 was created; but products work in the same way for both Eclipse 3.x and Eclipse 4.x. Time for action – creating a headless application A product hands the runtime off to an application, which can be thought of as a custom Eclipse Runnable class. This is the main entry point to the application which is responsible for setting up and tearing down the content of the application. 1. Create a new plug-in, called com.packtpub.e4.headless.application. Ensure that the This plug-in will make contributions to the UI checkbox is deselected and Would you like to create a rich client application is set to No: [ 254 ] www.it-ebooks.info Chapter 8 2. 3. Click on Finish and the project will be created. 4. Click on Add and then type applications into the box. Ensure the Show only extension points from the required plug-ins checkbox is deselected. Choose the org.eclipse.core.runtime.applications extension point: Open the project and select Plug-in Tools | Open Manifest and go to the Extensions tab. This is where Eclipse keeps its list of extensions to the system, and where an application is defined. [ 255 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products 5. When Finish is selected, a dialog may ask if the plug-in should add the dependency org.eclipse.equinox.app. If so, say Yes to this: 6. The editor will switch to a tree-based view. Right-click on (application) and choose New | run to create a new application reference: [ 256 ] www.it-ebooks.info Chapter 8 7. Use com.packtpub.e4.headless.application.Application as the class name and click on the underlined class*: link on the left to open a new class wizard, which pre-fills the class name and supplies the IApplication interface: 8. Implement the class as follows: public class Application implements IApplication { public Object start(IApplicationContext c) throws Exception { System.out.println("Headless Application"); return null; } public void stop() { } } [ 257 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products 9. Run the application, by going to the Extensions tab of the manifest and clicking on the play button at the top right, or via the Launch an Eclipse application hyperlink. Headless Application will be displayed to the Console view. What just happened? Creating an application requires an extension point and a class that implements the IApplication interface. Using the wizard, an application was created and the start method was implemented with a simple display message. When play is clicked, Eclipse will create a new launch configuration which points to an application: [ 258 ] www.it-ebooks.info Chapter 8 This references the automatically generated ID of the application from the plugin.xml. When Eclipse starts the runtime, it will bring up the runtime environment and then hand runtime control over to the application instance. At the end of the start method's execution, the application will terminate. Time for action – creating a product An Eclipse product is a branding and a reference to an application. The product also has control over what features or plug-ins will be available, and whether those plug-ins will be started or not (and if so, in what order). Chapter 7, Understanding the Eclipse 4 Model created a product to bootstrap the E4 application (provided by the org.eclipse.e4.ui.workbench.swt.E4Application class) but this section will create a product that binds to the headless application created previously to demonstrate how the linkage works. 1. Use File | New | Other | Plug-in Development | Product Configuration to bring up the product wizard. 2. Select the com.packtpub.e4.headless.application project and put headless as the filename. 3. 4. 5. Leave Create a configuration file with the basic settings selected. 6. Click on Finish and it will open up headless.product in an editor. Fill in the details as follows: ID: com.packtpub.e4.headless.application.product Version: 1.0.0 Name: Headless Product Click on New on the right of the product definition section, which will launch a dialog to create a product. Fill in the dialog as follows: Defining plug-in: com.packtpub.e4.headless.application Product ID: product [ 259 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products Application: com.packtpub.e4.headless.application.id It may say id1, id2, and so on. This comes from the plugin.xml file in the previous step. 7. Click on Run on the top-right corner of the product to launch the product: [ 260 ] www.it-ebooks.info Chapter 8 8. 9. There will be an error reported java.lang.ClassNotFoundException: org. eclipse.core.runtime.adaptor.EclipseStarter because the runtime can't find the required plug-ins. Switch to the Dependencies tab, and add: com.packtpub.e4.headless.application org.eclipse.core.runtime Click on Add Required Plug-ins and the rest of the dependencies will be added: [ 261 ] www.it-ebooks.info Creating Features, Update Sites, Applications, and Products 10. Run the product and the same Headless Application should be displayed as before. 11. Export the product, either via File | Export | Plug-in Development | Eclipse Product, or via the Export button at the top of the product editor, to a local directory. 12. From the directory where the product was exported, run eclipse to see the message being printed. On Windows, run eclipsec.exe to see the output. On OS X, run Eclipse.app/Contents/MacOS/eclipse. What just happened? Using and running a product may not seem that different from running an application, but the key difference between the two is that an application is a start point and one which can be installed into an existing Eclipse runtime, whereas a product is a standalone system that can be run independently. A product defines the look and feel of the application's launch icons, specifies what will be bundled, and how it is launched. The product then hands over control to an application, which executes the runtime code. The editor is a GUI for the product file, which is an XML file that will look similar to the following: <?xml version="1.0" encoding="UTF-8"?> <?pde version="3.5"?> <product name="Headless Product" uid="com.packtpub.e4.headless.application.product" id="com.packtpub.e4.headless.application.product" application="com.packtpub.e4.headless.application.id" version="1.0.0" useFeatures="false" includeLaunchers="true"> <configIni use="default"/> <launcherArgs> <vmArgsMac>-XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts</vmArgsMac> </launcherArgs> <launcher> <solaris/> <win useIco="false"> <bmp/> </win> </launcher> <vm/> <plugins> <plugin id="com.packtpub.e4.headless.application"/> <plugin id="org.eclipse.core.contenttype"/> <plugin id="org.eclipse.core.jobs"/> [ 262 ] www.it-ebooks.info Chapter 8 <plugin id="org.eclipse.core.runtime"/> <plugin id="org.eclipse.core.runtime.compatibility.registry" fragment="true"/> <plugin id="org.eclipse.equinox.app"/> <plugin id="org.eclipse.equinox.common"/> <plugin id="org.eclipse.equinox.preferences"/> <plugin id="org.eclipse.equinox.registry"/> <plugin id="org.eclipse.osgi"/> </plugins> </product> Have a go hero – creating a product based on features The product created previously specified an exact set of plug-ins that are needed to run the code. Many Eclipse applications are based on features, and products can also be defined by features as well. Move the plug-in dependencies from the product to a feature, and then have the product depend on the feature instead. That way, when the feature is updated, it can be done externally to the product definition. Pop Quiz – understanding features, applications, and products Q1. What is the keyword used in the version number that gets replaced by the timestamp? Q2. What files get generated in an update site build? Q3. What is the name of the file that allows an update site to be categorized? Q4. What is the difference between feature "requires" and "includes"? Q5. What is the difference between an application and a product? Q6. What is an application's entry point? Summary In this chapter, we covered how to create features and update sites, which allows plug-ins to be exported and installed into different Eclipse instances. The contents of the update site can be published to a web server and registered with the Eclipse marketplace to gain wide visibility. We also covered how to create applications and products which can be used to export top-level applications. In the next chapter, we will look at how to write automated tests for Eclipse plug-ins. [ 263 ] www.it-ebooks.info www.it-ebooks.info 9 Automated Testing of Plug-ins JUnit is the testing framework of choice for Eclipse applications, and can be used to run either pure Java tests or plug-in tests. If user interfaces need to be exercised, SWTBot provides a facade onto the underling Eclipse application, and can be used to drive menus, dialogs and views. In this chapter, we will do the following: Create a JUnit test running as pure Java code Create a JUnit test running as a plug-in Write a UI test using SWTBot Interrogate views and work with dialogs Wait for a condition to occur before continuing Using JUnit for automated testing One of the original automated unit testing frameworks, JUnit has been in use at Eclipse for over a decade. Eclipse's quality can be partly attributed to the set of automated unit tests that exercise both UI and non-UI (headless) components. JUnit works by creating a test case with one or more tests, which usually correspond to a class or methods respectively. Conventionally, test classes end with Test, but this is not a requirement. Multiple test cases can be aggregated into test suites, although implicitly a project becomes its own test suite. www.it-ebooks.info Automated Testing of Plug-ins Time for action – writing a simple JUnit test case This section explains how to write and run a simple JUnit case in Eclipse. 1. 2. 3. Create a new Java project called com.packtpub.e4.junit.example. Create a class called MathUtil in com.packtpub.e4.junit.example. Create a public static method called isOdd() that takes an int value, and returns a boolean value if it is an odd number (using value % 2 == 1). 4. Create a new class called MathUtilTest in the package com.packtpub. e4.junit.example. 5. Create a method called testOdd() with an annotation @Test, which is how JUnit 4 signifies that this method is a test case. 6. Click on the quick-fix saying Add JUnit 4 library to the build path, or edit the build path manually to point to Eclipse's plugins/org.junit_4.*.jar file. 7. Implement the testOdd() method as follows: assertTrue(MathUtil.isOdd(3)); assertFalse(MathUtil.isOdd(4)); 8. 9. Add a static import to org.junit.Assert.* to fix the compile-time errors. Right-click on the project and go to Run As | JUnit Test, and the JUnit test view should be shown with a green test result: [ 266 ] www.it-ebooks.info Chapter 9 10. Verify that the test works, by modifying the isOdd() method to return false and re-run it—a red-colored test-failure text should be seen instead. What just happened? The example project demonstrated how JUnit tests are written and executed in Eclipse. The example works for both OSGi and non-OSGi projects, provided JUnit can be resolved and executed accordingly. Remember to annotate the test methods with @Test, otherwise they won't run. It can sometimes be helpful to write a method that knowingly fails at first, and then run the tests, just to confirm it's actually being run. There's nothing more useless than a green test bar with tests that are never run, but would fail when they are run. It is also possible to re-run tests from the JUnit view; the green play button allows all tests to be re-run, while the one with a red cross allows just the tests that have failed to be reexecuted (shown as disabled in the previous example). Time for action – writing a plug-in test Although Java projects and Java plug-in projects both use Java and JUnit to execute, plug-ins typically need to have access to them (provided by the runtime platform), which is only available if running in an OSGi or Eclipse environment. 1. 2. Create a new plug-in project called com.packtpub.e4.junit.plugin. 3. Create a method called testPlatform(), which asserts that the Platform is running: Create a new JUnit test called PlatformTest in the com.packtpub.e4.junit. plugin package. @Test public void test() { assertTrue(Platform.isRunning()); } 4. Click on the quick-fix to add org.junit to the required bundles. Alternatively, open up the project's manifest by right-clicking on it and going to Plug-in Tools | Open Manifest. Go to the Dependencies tab and click on Add, and select org.junit from the dialog. Ensure that org.eclipse.core.runtime is also added as a dependency. [ 267 ] www.it-ebooks.info Automated Testing of Plug-ins 5. Run the test by right-clicking on the project and going to Run As | JUnit Test. You will see the error message fail (with an assertion error). 6. Run the test as a plug-in, by right-clicking on the project and going to Run As | JUnit Plug-in Test. You will see the test pass. What just happened? Although the test code is exactly the same, the way in which the tests are run is slightly different. In the first instance, it uses the standard JUnit test runner, which executes the code in a standalone JVM. Since this doesn't have the full Eclipse runtime inside, the test fails. The plug-in test is launched in a different way: a new Eclipse instance is created, the plug-in is exported and installed into the runtime, the various OSGi services that are needed to power Eclipse are brought up, and then the test runner executes the plugin in place. As a result, running a plug-in test can add latency to the test process, because the platform has to be booted first. Sometimes, quick-tests are run as standalone Java tests, while integration tests run in the context of a full plug-in environment. Code sections that depend on OSGi and Platform services need to be run as plug-in tests. Using SWTBot for user interface testing SWTBot is an automated testing framework that allows the Eclipse user interface and SWT applications to be tested in place. Although writing tests and exercising the models behind an application can be essential, sometimes it is necessary to test the interaction of the user interface itself. Time for action – writing an SWTBot test The first step is to install SWTBot from the Eclipse update site. These examples were tested with Version 2.1.0, downloaded from http://download.eclipse.org/technology/ swtbot/releases/latest/. Note that Eclipse Kepler (4.3) requires SWTBot 2.1.1 or above. 1. Go to Help | Install New Software and enter the SWTBot update site. [ 268 ] www.it-ebooks.info Chapter 9 2. Select everything except the GEF feature: 3. 4. 5. Click on Next to install SWTBot. 6. 7. 8. 9. Restart Eclipse, when prompted. Add the following bundle dependencies to the plug-in manifest for the com. packtpub.e4.junit.plugin project: org.eclipse.swtbot.junit4_x org.eclipse.swtbot.forms.finder org.eclipse.swtbot.eclipse.finder org.eclipse.ui Create a class called UITest in the com.packtpub.e4.junit.plugin package. Add a class annotation @RunWith(SWTBotJunit4ClassRunner.class). Create a method called testUI() with an annotation @Test. Inside the testUI() method, create an instance of SWTWorkbenchBot. [ 269 ] www.it-ebooks.info Automated Testing of Plug-ins 10. Iterate through the bot's shells() method and assert that the one that is visible has a title Java – Eclipse SDK (this is the title of the Eclipse window, and may be different in your case). Here's what the code looks like: package com.packtpub.e4.junit.plugin; import static org.junit.Assert.assertEquals; import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot; import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner; import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(SWTBotJunit4ClassRunner.class) public class UITest { @Test public void testUI() { SWTWorkbenchBot bot = new SWTWorkbenchBot(); SWTBotShell[] shells = bot.shells(); for (int i = 0; i < shells.length; i++) { if (shells[i].isVisible()) { assertEquals("Java - Eclipse SDK", shells[i].getText()); } } } } 11. Run the test by right-clicking on the project and going to Run As | SWTBot Test. 12. Verify that the JUnit tests have passed. What just happened? SWTBot is a UI testing mechanism that allows the state of the user interface to be driven programmatically. In this test, a new SWTWorkbenchBot was created to interact with the Eclipse workbench. (For pure SWT applications, there is the SWTBot class.) The bot iterates through the available shells once the workspace has been opened. Although more than one shell is returned in the list, only one of them is visible. The shell's title can be obtained through the getText() method, which returns Java – Eclipse SDK if the Eclipse SDK package opens on the Java perspective by default—but this value may differ depending on what perspective and which Eclipse package is being used. Substitute it as necessary for the title shown in the dialog if the test fails on this specific comparison. [ 270 ] www.it-ebooks.info Chapter 9 This application is similar to an Eclipse product launch; combinations of plug-ins, start-up properties, and product or application choices can be made via the Run or Debug configurations menu. As with ordinary JUnit tests, the launch can be invoked in Debug mode and breakpoints can be set. Time for action – working with menus Note that SWTBot works on a non-UI thread by default, so as to avoid a deadlock with modal dialogs and other user interface actions. If the tests need to interact with specific SWT widgets, it is necessary to invoke a runnable via the UI thread. To make this easier, the SWTBot framework has several helper methods that can provide a facade of the workspace, including the ability to click on buttons and displaying menus. 1. Create a new test method called createProject() in the UITest class with a @Test annotation. 2. 3. 4. Create a new SWTWorkbenchBot instance. Use the bot's menu() method to navigate to File | Project..., and perform a click(). Use the bot's shell() method to get the newly opened shell with a title New Project. Activate the shell to ensure that it has focus. 5. Use the bot's tree() method to find a tree in the shell and expand the General node, and finally select Project. 6. Invoke the Next > button with a click() method. Note the space between Next and the > symbol. 7. 8. 9. Find the label titled Project name: and set its text to SWTBot Test Project. Click on the Finish button. The code will look like the following block: @Test public void createProject() { SWTWorkbenchBot bot = new SWTWorkbenchBot(); bot.menu("File").menu("Project...").click(); SWTBotShell shell = bot.shell("New Project"); shell.activate(); bot.tree().expandNode("General").select("Project"); bot.button("Next >").click(); bot.textWithLabel("Project name:"). setText("SWTBot Test Project"); bot.button("Finish").click(); } 10. Run the test as a SWTBot Test, and verify createProject() success. [ 271 ] www.it-ebooks.info Automated Testing of Plug-ins What just happened? After creating the workspace bot, go to File | Project.... Since this opens up a new dialog, a handle needs to be acquired to point to the newly created shell. To do this, a new SWTBotShell is created, which is a handle to the displayed shell. The title is used as a key to find a given shell. If one is not currently visible, it polls (every 500 milliseconds by default) until one is found or the default timeout period (5 seconds) ends when a WidgetNotFoundException is thrown. The activate() method waits until the dialog has focus. To navigate through a dialog, methods such as tree() and textWithLabel() allow specific elements to be pulled out from the UI, with exceptions being raised if these are not found. If there is only one element of a particular type, then simple accessors like tree() may be sufficient; if not, there are xxxWithLabel() and xxxWithId() accessors that can find a specific element in a particular section. To set an ID on an object so that it can be found using the withId() method, call widget. setData(SWTBotPreferences.DEFAULT_KEY,"theWidgetId"). When objects are accessed, they aren't directly the underlying SWT widgets. Instead, they are wrappers, much like SWTWorkspaceBot is a wrapper for the workspace. Although the code is calling setText() on what looks like a label, the code is in fact running on a non-UI thread. It posts a runnable to the UI thread, with an instruction to set the text; all of this is done "under the covers" by SWTBot. One thing that's immediately obvious from this is that when using labels, the tests are highly specific to the localization of the product. The tests will fail if the application is run in a different language, for example. They are also implicitly tied to the structure of the application—if the UI changes significantly then it may be necessary to re-write or update the tests. Have a go hero – using resources Automated testing exercises code paths, but it is often necessary to verify that not only has the user interface reacted in the right way, but also that the side effects have happened correctly. In this case, to find out if a project has been created, use the ResourcesPlugin (from the org.eclipse.core.resources bundle) to get the workspace, from which the root will provide a means of accessing an IProject object. Use the exists() method of the project to determine that the project has been successfully created. Amend the createProject() method to verify that the project does not exist at the start of the method, and does exist at the end of the method. [ 272 ] www.it-ebooks.info Chapter 9 Note that the getProject() method of IWorkspaceRoot will return a non-null value regardless of whether the project exists or not. Working with SWTBot There are some techniques that help us while writing SWTBot tests, such as organizing the test code and hiding the welcome screen at the start, which otherwise might distort the test run. Time for action – hiding the welcome screen When Eclipse starts, it typically displays a welcome page. Since this often gets in the way of automated user testing, it is useful to close this at startup. 1. In the createProject() method, within a try block obtain a view with the title Welcome. 2. 3. Invoke the close() method. The code will change to look like this: SWTWorkbenchBot bot = new SWTWorkbenchBot(); try { bot.viewByTitle("Welcome").close(); } catch (WidgetNotFoundException e) { // ignore } 4. Run the test—the welcome screen should be closed before the test is run. What just happened? Upon startup, the IDE will show a welcome screen. This is shown in a view with a Welcome title. Using the viewByTitle() accessor, the SWTBot wrapper view can be accessed. If the view doesn't exist then an exception will be thrown for a safety check; catch any WidgetNotFoundException since not finding the welcome screen is not a failure. Having found the welcome page, invoking the close() method will close the view. [ 273 ] www.it-ebooks.info Automated Testing of Plug-ins Time for action – avoiding SWTBot runtime errors As more test methods are added, the runtime may start throwing spurious errors. This is because the order of the tests may cause changes, and the ones that ran previously may modify the state of the workbench. This can be mitigated by moving the common setup and tear-down routines to a single place: 1. 2. 3. Create a static method beforeClass(). 4. The code looks like this: Add the annotation @BeforeClass from the org.junit package. Move references to create a SWTWorkbenchBot to the static method, and save the value in a static field. private static SWTWorkbenchBot bot; @BeforeClass public static void beforeClass() { bot = new SWTWorkbenchBot(); try { bot.viewByTitle("Welcome").close(); } catch (WidgetNotFoundException e) { // ignore } } 5. Run the tests and ensure that they pass appropriately. What just happened? The JUnit annotation @BeforeClass allows a single static method to be executed prior to any of the tests running in the class. This is used to create an instance of SWTWorkbenchBot, which is then used by all other tests in the class. This is also an opportune location to close the Welcome view, if it is shown, so that all other tests can assume that the window has been cleaned up. Do not call bot.resetWorkbench(), otherwise subsequent tests will fail in the test cases. Working with views As with menus and dialogs, views that have been created can be interrogated with SWTBot as well. [ 274 ] www.it-ebooks.info Chapter 9 Time for action – showing views To show other views, the same mechanism is followed in the UI tests, as a user would do; by going to Window | Show View | Other. 1. 2. 3. 4. Create a new method, testTimeZoneView(), with a @Test annotation. 5. 6. 7. 8. Click on the OK button to have the view shown. From the bot, open the Other dialog by going to Window | Show View. Get the shell with the title Show View and activate it. Expand the Timekeeping node and select the Time Zone View node (the view created in Chapter 2, Creating Views with SWT). Use the bot.viewByTitle() method to acquire a reference to the view. Assert that the view is not null. The code looks like this: @Test public void testTimeZoneView() { bot.menu("Window").menu("Show View").menu("Other...").click(); SWTBotShell shell = bot.shell("Show View"); shell.activate(); bot.tree().expandNode("Timekeeping").select("Time Zone View"); bot.button("OK").click(); SWTBotView timeZoneView = bot.viewByTitle("Time Zone View"); assertNotNull(timeZoneView); } 9. Run the tests and ensure that they were successful. What just happened? Using the built-in Eclipse mechanism to switch views, the bot navigated to the Time Zone View menu inside Window | Show View | Other | Timekeeping to bring the view to the screen. Once shown, the viewByTitle() method of the bot can be used to get a reference to the widget; verify that it is not null. Being able to select a view programmatically is such a common occurrence that it can help to have a utility method to open a view on demand. [ 275 ] www.it-ebooks.info Automated Testing of Plug-ins Time for action – interrogating views Having been able to acquire a reference to the view, the next step is to deal with specific user-interface components. For standard controls such as Button and Text labels, the bot provides standard methods. To get hold of other components, the widget hierarchy will have to be interrogated directly. 1. In the testTimeZoneView() method, get the Widget from the returned SWTBotView. 2. 3. Create a Matcher that is based on widgetsOfType(CTabItem.class). Use bot.widgets() to search for a list of CTabItem instances in the view's widget. 4. Ensure that 18 elements are returned. Check the running application to verify how many tabs there are in the time zone view; there may be more or less, depending on how many timezones your computer has. 5. The code looks like this: SWTBotView timeZoneView = bot.viewByTitle("Time Zone View"); assertNotNull(timeZoneView); Widget widget = timeZoneView.getWidget(); org.hamcrest.Matcher<CTabItem> matcher = WidgetMatcherFactory.widgetOfType(CTabItem.class); final java.util.List<? extends CTabItem> ctabs = bot.widgets(matcher,widget); assertEquals(18,ctabs.size()); 6. Run the tests and ensure that they are successful. What just happened? It is possible to write code to walk the user interface tree directly. However, because the widgets have to be interrogated on the UI thread, care has to be taken to find the children. SWTBot provides a generic Matcher mechanism, which is a predicate that can return true if a certain condition occurs. The Matcher provided by widgetOfType() matches items that have a certain class type; similarly many other matchers can be instantiated with the withXxx() calls, such as withLabel() and withId(). Matchers can be combined with the allOf() and anyOf() methods, by providing AND/OR logic respectively. The widgets() call walks through the tree recursively to find all widgets that match a particular specification. A single-argument version finds all elements from the active shell; the two-argument version allows a specific parent to be searched, which in this case is the TimeZoneView itself. [ 276 ] www.it-ebooks.info Chapter 9 Finally, the size of the list is compared with the number of time-zone groups, which in this case is 18. Interacting with the UI While interrogating the user interface, care needs to be taken as to which thread is used to access the UI. This section will show how to obtain values and wait for asynchronous results that are to be delivered by the workbench. Time for action – getting values from the UI Note that if the test tries to access a property from the returned widget, there may be an invalid thread-access error. For example, ctabs.get(0).getText() will result in an Invalid thread access SWT error. To perform tests on widgets, the code has to be run in the UI thread. Either the Display. getDefault().syncExec() or the equivalent Synchronizer class can be used, but SWTBot has a general interface called StringResult, which is like a Runnable method that can return a String value through syncExec() on the bot. 1. In the last testTimeZone() method of the UITest class, create a new StringResult and pass it to UIThreadRunnable.syncExec(). 2. 3. 4. In the run() method, get the first cTabItem and return its text value. After the Runnable method has been run, assert that the value is Africa. The code looks like this: String tabText = UIThreadRunnable.syncExec(new StringResult() { @Override public String run() { return ctabs.get(0).getText(); } }); assertEquals("Africa", tabText); 5. Run the tests and ensure they are successful. What just happened? To interact with widgets, code must be run on the UI thread. To run code on the UI thread in SWT, it needs to be wrapped into a Runnable, which needs to be posted to the display (or Synchronizer) and executed there. [ 277 ] www.it-ebooks.info Automated Testing of Plug-ins Using a syncExec() means that the result is guaranteed to be available for testing. If an asyncExec() operation is used, the result may not be available by the time the following assert operation runs. To pass a value back from the UI thread to a non-UI thread, the result has to be stored in a variable. This has to be either a field on the class or a value in a final array. The StringResult of the SWTBot package wraps this up effectively in the UIThreadRunnable, in which an ArrayList is created to hold the single element. Java 8 will make this significantly easier in that it will allow for lambda methods to be executed. At the time of writing, the syntax of Java 8 was not finalized. Time for action – waiting for a condition Typically, an action may require a result to happen in the user interface before testing can continue. Since SWTBot can run much faster than a human can, waiting for a result of an action may be necessary. To demonstrate this, create a Java project with a single source file and then use the conditions to wait until the class file is compiled. 1. 2. Create a new method called createJavaProject() in the UITest class. Use the bot to create a new Java project by copying the createProject() method as a template. 3. 4. Add org.eclipse.core.resources as a dependency to the plug-in. 5. 6. 7. 8. Use the getProject() with the test project to get the src folder . 9. Use the bot.waitUntil() call to pass in a new anonymous subclass of DefaultCondition. Add a method getProject(), which returns an IProject from ResourcesPlugin.getWorkspace().getRoot().getProject(). If the folder does not exist, create it. Get the file called Test.java from src. Create it with the contents from the "class {}".getBytes() bytes as a ByteArrayInputStream. 10. In the test() method of the condition, return if the project's bin folder has a file called Test.class. 11. In the getFailureMessage() of the condition, return a suitable message. 12. The code looks like the following snippet: @Test public void createJavaProject() throws Exception { String projectName = "SWTBot Java Project"; [ 278 ] www.it-ebooks.info Chapter 9 bot.menu("File").menu("Project...").click(); SWTBotShell shell = bot.shell("New Project"); shell.activate(); bot.tree().expandNode("Java").select("Java Project"); bot.button("Next >").click(); bot.textWithLabel("Project name:").setText(projectName); bot.button("Finish").click(); final IProject project = getProject(projectName); assertTrue(project.exists()); final IFolder src = project.getFolder("src"); final IFolder bin = project.getFolder("bin"); if (!src.exists()) { src.create(true, true, null); } IFile test = src.getFile("Test.java"); test.create(new ByteArrayInputStream( "class Test{}".getBytes()), true, null); bot.waitUntil(new DefaultCondition() { @Override public boolean test() throws Exception { return project.getFolder("bin"). getFile("Test.class").exists(); } public String getFailureMessage() { return "File bin/Test.class was not created"; } }); assertTrue(bin.getFile("Test.class").exists()); } 13. Run the test and verify that it was successful. 14. Comment out the waitUntil() call and verify that the test fails. What just happened? When the Test.java file is created in the project, an event is fired that runs the Java compiler. This in turn results in the creation of both the bin folder, as well as the Test.class file that is being tested. However, both of these operations occur on different threads, and so while the test is running, if it needs to act on the generated file, it must wait until that file is created. [ 279 ] www.it-ebooks.info Automated Testing of Plug-ins Although this example could have been implemented outside of SWTBot, it provides a simple way to block the execution until a particular condition occurs. This can be useful if the user interface is running some kind of long-running action and the code needs to wait until a certain dialog message is shown, or something that can be determined programmatically (such as the file's existence, as in this case). Other types of conditionals and tests are possible as well; there is a waitWhile(), which is similar to the bot's waitUntil() but has the opposite behavior. Note that when the wait condition is commented out, the test fails, because the test execution thread will hit the assertion before the Java compiler has been run. One advantage of using the wait code in SWTBot is that if the condition doesn't occur within a given timeout then an exception is generated and the test will fail. Since the same wait condition is used elsewhere in SWTBot, the delay is configurable and can be changed externally. Have a go hero – driving the New Class wizard Instead of using the source file as a text file, use SWTBot to open the New Class wizard by going to the File menu. Pass in the name of the project, the package, and the class; the source and class files should be created in the background. This is how integration tests (that show the application working from a user's perspective) can be implemented, instead of having a set of tests that show only the underlying libraries at work. Pop quiz – understanding SWTBot Q1. What is the name of the JUnit test runner that is required for SWTBot? Q2. How are views shown with SWTBot? Q3. How do you get the text value of a field in a dialog? Q4. What is a Matcher and when is it to be used? Q5. How can values from the UI be returned to the user without having to worry about thread interaction? Q6. When some asynchronous events are happening in the background, how can the test wait (without blocking the test) until a particular condition occurs? [ 280 ] www.it-ebooks.info Chapter 9 Summary Being able to test code automatically is a key part of creating quality software. Whether the tests exercise the underlying models or the user interface—or ideally a combination of both—more tests help to highlight problems that occur when changes happen to the underlying framework, or when dependencies change and introduce unwanted side-effects. The final chapter will show how to integrate everything to an automated build. [ 281 ] www.it-ebooks.info www.it-ebooks.info 10 Automated Builds with Tycho The final part of the puzzle is how to build plug-ins automatically. Most plug-ins are now built with Tycho, a Maven plugin infrastructure for building Eclipse plug-ins. In this chapter, we will do the following: Automate a plug-in build Automate a feature build Create an update site Execute UI and non-UI tests Sign the plug-ins Learn how to publish the update site Using Maven to build Eclipse plug-ins with Tycho Maven is an automated build tool that builds using a declarative file called pom.xml that tells it how and what to build. Maven projects have a group, an artifact, and a version, all of which help in identifying them in repositories such as the Central repository, and also a packaging type that tells Maven what it is trying to build. The default is jar since the widest use of Maven is for building Java archives; for Tycho, we need to use a variety of other types. Maven Tycho is a set of plug-ins that allow the building of Eclipse plug-ins. Tycho requires at least Maven 3.0 to work; the instructions in this chapter have been tested against Maven 3.0.5 and Tycho 0.18.0. www.it-ebooks.info Automated Builds with Tycho Time for action – installing Maven In this section, we will install and use Maven to build a simple Java project to ensure that the tool is configured appropriately. The first time it runs, it will cache many JAR files from the Central repository into a folder ${user.home}/.m2/repository; for subsequent runs it will be much faster. 1. Go to http://maven.apache.org/ and download the Maven 3.0.5.zip package (for Windows) or Maven 3.0.5.tgz (for Mac OS X/Linux). 2. Unzip/untar the package and install Maven at a convenient directory, referred to in these instructions as MAVEN_HOME. 3. Either add MAVEN_HOME/bin to the PATH environment variable or specify the full path to the Maven executable JAR. Run mvn --version and if all works well, a version message should be printed out. 4. To create a new Maven project, run mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.1 5. 6. 7. When prompted for the groupId, enter com.packtpub.e4. 8. Finally, hit Enter to create the project: When prompted for the artifactId, enter com.packtpub.e4.tycho. When prompted for the version and package, hit Enter to take the defaults of 1.0-SNAPSHOT and com.packtpub.e4 respectively. Define value for property 'groupId': : com.packtpub.e4 Define value for property 'artifactId': : com.packtpub.e4.tycho Define value for property 'version': 1.0-SNAPSHOT: : Define value for property 'package': com.packtpub.e4: : Confirm properties configuration: groupId: com.packtpub.e4 artifactId: com.packtpub.e4.tycho version: 1.0-SNAPSHOT package: com.packtpub.e4 Y: : Y 9. Change into the com.packtpub.e4.tycho directory created and run mvn package to run the tests and create the package: [INFO] Scanning for projects [INFO] [ 284 ] www.it-ebooks.info Chapter 10 [INFO] Building com.packtpub.e4.tycho 1.0-SNAPSHOT [INFO] T E S T S Running com.packtpub.e4.AppTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] Building jar: com.packtpub.e4.tycho-1.0-SNAPSHOT.jar BUILD SUCCESS Total time: 1.977s Final Memory: 15M/136M What just happened? The Maven launcher knows how to connect to the Central repository and download additional plug-ins. When it is launched for the first time, it will download a set of plug-ins, which in turn have dependencies on other JAR files that will be resolved automatically prior to project building. Fortunately, these are cached in the local Maven repository (~/.m2/repository by default), so this is done only once. The repository can be cleaned or removed; the next time Maven runs, it will download any needed plug-ins again. When mvn archetype:generate is executed, a sample Java project is created. This creates a pom.xml file with the groupId, artifactId, and version given, and sets it up for a Java project. When mvn package is executed, the operation depends on the packaging type of the project. This will be jar if not specified, and the default package operation for jar is to run the compile, then the run test, and finally create the JAR file of the package. The maven quickstart plugin is a useful way of creating a pom.xml file with known good values, and a way of verifying connectivity to the outside before moving ahead with the Eclipse-specific Tycho builds. If there's a problem with these steps, check the troubleshooting guides at http://maven.apache.org/users/ for assistance. [ 285 ] www.it-ebooks.info Automated Builds with Tycho Time for action – building with Tycho Now that Maven is installed, it's time to build a plug-in with Tycho. Tycho is a set of plug-ins for Maven 3 that emulates the older PDE build used by Eclipse. The Eclipse platform has moved to building with Tycho and Maven 3 under the name Common Build Infrastructure (http://wiki.eclipse.org/CBI). 1. Change into the com.packtpub.e4.clock.ui project created in Chapter 2, Creating Views with SWT. (If you don't have this project, see the book's GitHub repository for sample code.) 2. Create a file called pom.xml at the root of the project, with the following empty contents: <?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/ POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> </project> This can be copied from the pom.xml file generated in the previous section, since every pom.xml has this same signature. 3. Give the project a unique groupId, artifactId, and version, by placing the following after the modelVersion tag: <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.clock.ui</artifactId> <version>1.0.0-SNAPSHOT</version> The version here has to match the one in the plugin.xml file, with .qualifier replaced with -SNAPSHOT. The artifactId has to be the name of the fully qualified plug-in name (the Bundle-SymbolicName in the MANIFEST.MF file) 4. Define the packaging type to be eclipse-plugin: <packaging>eclipse-plugin</packaging> 5. If the build is now run with mvn package, an error message, Unknown packaging: eclipse-plugin, will be displayed. To fix this, add Tycho as a build plugin: <build> <plugins> <plugin> [ 286 ] www.it-ebooks.info Chapter 10 <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-maven-plugin</artifactId> <version>0.18.0</version> <extensions>true</extensions> </plugin> </plugins> </build> 6. Run the build again. This time it will complain of an "unsatisfiable" build error: [ERROR] Internal error: java.lang.RuntimeException: "No solution found because the problem is unsatisfiable.": ["Unable to satisfy dependency from com.packtpub.e4.clock.ui 1.0.0.qualifier to bundle org.eclipse.ui 0.0.0."] -> [Help 1] 7. Add the juno (or kepler, luna) release repository: <repositories> <repository> <id>juno</id> <layout>p2</layout> <url>http://download.eclipse.org/releases/juno</url> </repository> <!-- repository> <id>kepler</id> <layout>p2</layout> <url>http://download.eclipse.org/releases/kepler</url> </repository --> <!-- repository> <id>luna</id> <layout>p2</layout> <url>http://download.eclipse.org/releases/luna</url> </repository --> </repositories> 8. Now run mvn clean package and the plug-in should be built. What just happened? All Maven projects have a pom.xml file that controls their build process, and Eclipse plug-ins are no different. The header for a pom.xml file doesn't change, and so generally this is copied from an existing one (or auto-generated by tools) rather than being typed in by hand. [ 287 ] www.it-ebooks.info Automated Builds with Tycho Each Maven pom.xml file needs to have a unique groupId / artifactId / version. For eclipse-plugin projects, the name of the artifactId must be the same as the Bundle-SymbolicName in MANIFEST.MF, otherwise an error is thrown: [ERROR] Failed to execute goal org.eclipse.tycho:tycho-packaging-plugin:0.18.0:validate-id (default-validate-id) on project com.packtpub.e4.clock.uix: The Maven artifactId (currently: "com.packtpub.e4.clock.uix") must be the same as the bundle symbolic name (currently: "com.packtpub.e4.clock.ui") -> [Help 1] The same is true for the version in the pom.xml file, which must match the version in MANIFEST.MF. Without this, the build will fail with a different error: [ERROR] Failed to execute goal org.eclipse.tycho:tycho-packaging-plugin:0.18.0:validate-version (default-validate-version) on project com.packtpub.e4.clock.ui: Unqualified OSGi version 1.0.0.qualifier must match unqualified Maven version 1.0.1-SNAPSHOT for SNAPSHOT builds -> [Help 1] Tycho knows how to build Eclipse plug-ins by setting the packaging type to eclipseplugin. However, in order for Maven to know about the eclipse-plugin type, Tycho has to be defined as a Maven plugin for the build. Importantly, it needs to be defined as an extension, otherwise it doesn't contribute to the packaging type: <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-maven-plugin</artifactId> <version>0.18.0</version> <extensions>true</extensions> </plugin> Although it's possible to hard-code the version of the Tycho plug-in like this, it's conventional to replace it with a property instead. We will replace the version number with a property when we create the parent project later. Finally, an Eclipse repository was added to pom.xml so that the build could resolve any additional plug-ins and features. This needs to be defined as a p2 repository type, to distinguish it from the default type, which stores Maven artifacts. Note that it is best practice to not put repositories in pom.xml files in general. Instead, this can be extracted to a settings.xml file. This allows the same project to be built against different versions of Eclipse in the future without changing the source, or to run against a closer mirror of the same. A settings file can be passed to Maven with mvn -s /path/ to/settings.xml that allows the plug-in's dependencies to be varied over time without mutating the pom.xml file. [ 288 ] www.it-ebooks.info Chapter 10 Have a go hero – using target platforms Although it is possible to build an Eclipse-based application by pointing to a repository, this does not provide reproducibility of the build. For example, a build might be run against the Kepler release repository in July 2013 when Kepler is released as 4.3.0, and the same build can be run again in December 2013 when Kepler 4.3.1 is released. The results of these two builds will be different, even if no source code changes have occurred in the meantime. A target platform can be defined in Eclipse by going to the Target Platform preferences page by navigating to Window | Preferences | Plug-in Development. Create a new target definition, consisting of the Base RCP, and then click on Share to allow the .target file to be saved on the filing system. A corresponding eclipse-target-definition packaging type can be used to define a GAV co-ordinate for the .target file, and when combined with the target-platform-configuration plug-in, it can be specified to build just against those components. See the Tycho documentation or the book's GitHub repository for more examples. Building features and update sites with Tycho The process for building features and update sites is similar to that for plug-ins, but with different packaging types. However, it's common for features and plug-ins to be built in the same Maven build, which requires a little reorganization of the projects. These are typically organized into a "parent" project and then into several "child" projects. Time for action – creating a parent project It's common for the parent and child projects to be located outside the workspace. For historic reasons, Eclipse doesn't deal well with nested projects in the workspace. It's also common for the parent project to host all the Tycho configuration information, which makes setting up the child projects a lot easier. 1. 2. 3. 4. 5. 6. 7. Create a General project by navigating to File | New | Project | General | Project. Unselect use default location. Put in a location that is outside the Eclipse workspace. Name the project com.packtpub.e4.parent. Click on Finish. Create a new file pom.xml in the root of the project. Copy the content of the plug-in's pom.xml file to the parent, but change the artifactId to com.packtpub.e4.parent and the packaging to pom. [ 289 ] www.it-ebooks.info Automated Builds with Tycho 8. Create a properties element in the pom.xml file. Inside, create two child tags: tycho-version (which has the content 0.18.0) and eclipse (with the value http://download.eclipse.org/releases/juno). 9. Modify the reference to 0.18.0 in the existing Tycho plugin and replace it with ${tycho-version}. 10. Modify the reference to http://download.eclipse.org/releases/juno in the existing repositories URL and replace it with ${eclipse}. 11. Move the com.packtpub.e4.clock.ui plug-in underneath the parent project. 12. Add a modules element to the pom.xml file, and underneath a module element with the value com.packtpub.e4.clock.ui. 13. The parent pom.xml should look like: <?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/ POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <tycho-version>0.18.0</tycho-version> <eclipse>http://download.eclipse.org/releases/ juno</eclipse> </properties> <modules> <module>com.packtpub.e4.clock.ui</module> </modules> <build> <plugins> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-maven-plugin</artifactId> <version>${tycho-version}</version> <extensions>true</extensions> </plugin> </plugins> </build> <repositories> <repository> <id>juno</id> [ 290 ] www.it-ebooks.info Chapter 10 <layout>p2</layout> <url>${eclipse}</url> </repository> </repositories> </project> 14. Modify the com.packtpub.e4.clock.ui/pom.xml file and add a parent element with a groupId, artifactId, and version that are the same as the parent. It is also possible to remove the version and groupId from the child pom.xml file, as it will default to the parent's groupId and version, if not specified: <parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> 15. Remove the plugins and repositories elements from the pom.xml file of com. packtpub.e4.clock.ui. 16. Now, change into the parent project and run mvn clean package. The parent will be built, which in turn builds all the modules in the list: [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] Reactor Summary: com.packtpub.e4.parent ............ SUCCESS [0.049s] com.packtpub.e4.clock.ui .......... SUCCESS [1.866s] BUILD SUCCESS What just happened? Each plug-in is its own Eclipse project, and therefore its own Maven project. To build a set of projects together, there needs to be a parent pom.xml file, which acts as an aggregator. At build time, Maven calculates the order in which projects need to be built, and then arranges the build steps accordingly. The other benefit provided by a parent pom.xml is the ability to specify standard build plug-ins and configuration information. In this case, the parent specifies the link with Tycho and its versions. This simplifies the implementation of the other plug-ins and features that lie underneath the parent project. [ 291 ] www.it-ebooks.info Automated Builds with Tycho Time for action – building a feature Features can be built in the same way as plug-ins, although this time the packaging type is eclipse-feature. 1. Move the com.packtpub.e4.feature project underneath the com.packtpub. e4.parent project. 2. Add the line <module>com.packtpub.e4.feature</module> to the parent pom.xml file. 3. 4. 5. 6. Copy the pom.xml file from the clock plugin to the feature project. Modify the packaging to <packaging>eclipse-feature</packaging>. Change the artifactId to com.packtpub.e4.feature. The resulting pom.xml file will look like: <?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/ POM/4.0.0 http://maven.apache.org/xsd/maven4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.feature</artifactId> <!-- version>1.0.0-SNAPSHOT</version --> <packaging>eclipse-feature</packaging> </project> 7. Run mvn clean package from the parent and it should build both the plug-in and the feature: [INFO] Reactor Summary: [INFO] [INFO] com.packtpub.e4.parent .................. SUCCESS [0.070s] [INFO] com.packtpub.e4.clock.ui ................ SUCCESS [1.872s] [INFO] com.packtpub.e4.feature ................. SUCCESS [0.080s] [INFO] BUILD SUCCESS [ 292 ] www.it-ebooks.info Chapter 10 What just happened? By adding the feature into the list of modules, the feature is built at the same time as everything else. The version of the plug-in built earlier is used to compose the feature contents. If the plug-in wasn't listed as part of the Maven build modules and it couldn't be resolved from a remote repository, then the build would fail. The child module will inherit the groupId and version of the parent project, if specified. As a result, the version can be commented out (or removed), which makes managing the versions easier. At present, the feature and plug-in are built but cannot be easily installed into an existing Eclipse instance. The assumption is that the plug-ins and features can be tested using PDE directly in Eclipse, and that, therefore, there's no need to directly install a plug-in or feature from the result of a build. It's necessary to define an additional module—an update site—that will allow the plugin to be installed or hosted. Time for action – building an update site The update site created in Chapter 8, Creating Features, Update Sites, Applications, and Products, is used to provide a standard hosting mechanism for Eclipse plug-ins and features. This can be built automatically with Tycho as well. 1. Move the com.packtpub.e4.update project underneath the com.packtpub.e4.parent project. 2. Add the line <module>com.packtpub.e4.update</module> to the parent pom.xml file. 3. 4. 5. 6. Copy the pom.xml file from the clock plugin to the update project. Modify the packaging to <packaging>eclipse-repository</packaging>. Change the artifactId to com.packtpub.e4.update. The resulting pom.xml file will look like this: <?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/ POM/4.0.0 http://maven.apache.org/xsd/maven4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.parent</artifactId> <version>1.0.0-SNAPSHOT</version> [ 293 ] www.it-ebooks.info Automated Builds with Tycho </parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.update</artifactId> <!-- version>1.0.0-SNAPSHOT</version --> <packaging>eclipse-repository</packaging> </project> 7. Rename the site.xml file to category.xml. (This is an entirely pointless change required by p2 since the files are identical in format.) 8. Verify that the category.xml file does not contain a url attribute and that the version attribute is 0.0.0, otherwise the message Unable to satisfy dependencies may be seen. <feature id="com.packtpub.e4.feature" version="0.0.0"> 9. Run mvn package from the parent project and it should now build the update site as well: [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] Reactor Summary: com.packtpub.e4.parent .................. com.packtpub.e4.clock.ui ................ com.packtpub.e4.feature ................. com.packtpub.e4.update .................. BUILD SUCCESS SUCCESS SUCCESS SUCCESS SUCCESS [0.048s] [1.416s] [0.074s] [2.727s] 10. In Eclipse, install the update site by going to Help | Install New Software and typing file:///path/to/com.packtpub.e4.parent/com.packtpub. e4.update/target/repository into the work with field, and installing the feature shown there. What just happened? Creating an update site is no different from creating a plug-in or feature project. There is a pom.xml file that defines the name of the update site itself, and it uses or detects the file called category.xml to generate the update site. The category.xml file is functionally equivalent to the site.xml file created previously, and is a name change introduced by p2 some four or five releases ago. However, the update site project still generates it using the old name, so it may be necessary to rename it from site.xml to category.xml in order to be built with Tycho. (There is an eclipse-update-site packaging type that uses the site.xml file as-is, but this is deprecated and may be removed in future versions.) [ 294 ] www.it-ebooks.info Chapter 10 As with the Eclipse update project, when the update site is built, the versions of the features and plug-ins in the category.xml file are replaced with the versions of the features and plug-ins just built. Time for action – building a product A product (a branded Eclipse application, or one that is launched from eclipse -application from the command line) can also be built with Tycho using the eclipserepository packaging type. To do this, the app project needs to be built with Tycho and made available in the feature, and a new project for the product needs to be created. 1. Move the com.packtpub.e4.application project under the com.packtpub.e4.parent project. 2. Add the line <module>com.packtpub.e4.application</module> to the parent pom.xml file. 3. 4. 5. Copy the pom.xml file from the clock plugin to the application project. Change the artifactId to com.packtpub.e4.application. The resulting pom.xml file will look like this: <?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.application</artifactId> <!-- version>1.0.0-SNAPSHOT</version --> <packaging>eclipse-plugin</packaging> </project> 6. Modify com.packtpub.e4.feature/feature.xml to add the reference to the application plug-in: <feature> ... <pluginid="com.packtpub.e4.application" downloadsize="0" install-size="0" version="0.0.0" unpack="false"/> </feature> [ 295 ] www.it-ebooks.info Automated Builds with Tycho 7. Run mvn clean package from the parent, and the build should complete with the application plug-in. 8. Now, create an additional project as a Java plug-in project, called com.packtpub. e4.product. Move the com.packtpub.e4.application.product file from the com.pactkpub.e4.application project into the product project. 9. Copy com.packtpub.e4.application/pom.xml into the com.packtpub. e4.product project, and modify the artifactId to be com.packtpub. e4.product. Change the packaging type to eclipse-repository. 10. Add the product to the parent by adding the module to the pom.xml file with <module>com.packtpub.e4.product</module>. 11. Run mvn clean package from the parent, and it will complain with an error: [ERROR] Failed to execute goal org.eclipse.tycho:tycho-p2-publisherplugin:0.18.0:publish-products (default-publish-products) on project com.packtpub.e4.application: The product file com.packtpub.e4.application.product does not contain the mandatory attribute 'uid' -> [Help 1] 12. Edit the com.packtpub.e4.application/com.packtpub.e4.application. product and add uid as a copy of the id attribute. 13. Switch to a feature-based build (if not already done so) and depend on the org.eclipse.rcp and com.packtpub.e4.feature features, removing the plugins element: <product name="com.packtpub.e4.application" uid="com.packtpub.e4.application.product" id="com.packtpub.e4.application.product" application="org.eclipse.e4.ui.workbench.swt.E4Application" version="1.0.0.qualifier" useFeatures="true" includeLaunchers="true"> <features> <feature id="com.packtpub.e4.feature" version="0.0.0"/> <feature id="org.eclipse.rcp" version="0.0.0"/> </features> <plugins/> ... </product> 14. Run mvn package from the parent again, and the build should succeed. The com. packtpub.e4.product/target/repository now contains the product for the target platform you're running on. [ 296 ] www.it-ebooks.info Chapter 10 15. To build for more than one platform, add the following to either the product's pom.xml file, or the parent pom.xml: <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>target-platform-configuration</artifactId> <version>${tycho-version}</version> <configuration> <environments> <environment> <os>win32</os> <ws>win32</ws> <arch>x86_64</arch> <!--arch>x86</arch--> </environment> <environment> <os>linux</os> <ws>gtk</ws> <arch>x86_64</arch> <!--arch>x86</arch--> </environment> <environment> <os>macosx</os> <ws>cocoa</ws> <arch>x86_64</arch> <!--arch>x86</arch--> </environment> </environments> </configuration> </plugin> 16. Now, run the build and a product per OS should be built. The example builds for Windows, Linux and OS X on a 64-bit architecture; to build for the 32-bit versions, duplicate the environment block and use <arch>x86</arch> instead. 17. To materialize the products, and not just provide a p2 repository for them, add the materialize-products goal to the com.packtpub.e4.application/pom. xml file: <build> <plugins> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-p2-director-plugin</artifactId> <version>${tycho-version}</version> <configuration> <formats> <win32>zip</win32> <linux>tar.gz</linux> <macosx>tar.gz</macosx> </formats> </configuration> <executions> [ 297 ] www.it-ebooks.info Automated Builds with Tycho <execution> <id>materialize-products</id> <goals> <goal>materialize-products</goal> </goals> </execution> <execution> <id>archive-products</id> <goals> <goal>archive-products</goal> </goals> </execution> </executions> </plugin> </plugins> </build> 18. Run the build from the parent with mvn clean package and the build will create com.packtpub.e4.application/target/products/os/ws/arch, as well as creating archives (ZIP files) of them. 19. When running the product that is generated, an error may be seen (or can be seen running eclipse -consoleLog): java.lang.IllegalStateException: Unable to acquire application service. Ensure that the org.eclipse.core.runtime bundle is resolved and started (see config.ini). 20. This is an Eclipse product error. In essence, Eclipse products need to have a number of plugins started at boot time in order to run eclipse -application. Add this to the com.packtpub.e4.application.product file: <configurations> <plugin id="org.eclipse.core.runtime" autoStart="true" startLevel="4"/> <plugin id="org.eclipse.equinox.common" autoStart="true" startLevel="2"/> <plugin id="org.eclipse.equinox.ds" autoStart="true" startLevel="2"/> <!-- for 'dropins' directory support --> <!-- plugin id="org.eclipse.equinox.p2.reconciler.dropins" autoStart="true" startLevel="4"/ --> <plugin id="org.eclipse.equinox.simpleconfigurator" autoStart="true" startLevel="1"/> <!-- disable old update manager --> <property name="org.eclipse.update.reconcile" value="false"/> <!-- for 'new' update manager support --> <!-- plugin id="org.eclipse.update.configurator" autoStart="true" startLevel="4"/ --> </configurations> [ 298 ] www.it-ebooks.info Chapter 10 21. Run mvn clean package and try running the product again. This time it should succeed. What just happened? Eclipse applications are built and made available as a p2 repository or as archived downloads. The p2 repository allows the product to be updated using the standard update mechanisms; this is how updates from 4.2.0 to 4.2.1 to 4.2.2 occur. The archives are used to provide direct download links, as are found at http://download.eclipse.org for the standard packages. For RCP-based applications, it is easier to build on the RCP feature, which provides the necessary platform-specific fragments for SWT and file-systems. Although building a product based on plug-ins is possible, almost all RCP and SDK applications are built upon either the RCP feature or the IDE feature respectively. For Eclipse to launch successfully, a number of plug-ins need to be started when the application starts. This is controlled using the config.ini file, which in turn is read by simpleconfigurator, so this needs to be started at the started at the launch of the runtime. In addition to this, the E4 platform requires Declarative Services (DS) to be installed and started, as well as the runtime bundle. By decomposing the projects into a feature, and then having the same feature used for both the SWTBot and product definitions, it becomes the de-facto place for adding new content. Then these changes to the feature are automatically visible in the product, the automated tests, and in the update site. Have a go hero – depending on Maven components Sometimes, it is necessary to depend on components that have been built by ordinary Maven jobs. Although it's not possible to mix and match Tycho and ordinary Maven reactor builds in the same build, it is possible to allow Tycho to resolve Maven components as part of the target platform. Since ordinary Maven dependencies are represented by the <dependencies> tag, it is possible to define additional dependencies that can be consumed or used by Tycho builds. Normally Tycho won't use this information, but to allow Tycho to resolve those and make them available as OSGi bundles for the purposes of the Eclipse build, modify the configuration for target-platform-configuration and add the line <pomDependencies>consider</ pomDependencies> to the <configuration> element. Note that only OSGi bundles are added to the dependencies list; others are silently ignored. [ 299 ] www.it-ebooks.info Automated Builds with Tycho Testing and releasing The final step of an Eclipse build is to ensure that all the automated tests are run, and that all the versions that need to be bumped for the release stage are done prior to the code being published. Time for action – running automated tests Although a plug-in's code-based tests (those under src/test/java) will be run automatically as part of a Maven build, very often it is necessary to test them in a running Eclipse application. The previous chapter covered creating automated UI tests; now they will be run as part of the automated build. 1. Move the com.packtpub.e4.junit.plugin project underneath the com. packtpub.e4.parent project. 2. Add the line <module>com.packtpub.e4.junit.plugin</module> to the parent pom.xml file. 3. Add the SWTBot repository to the parent pom.xml file: <properties> <swtbot>http://download.eclipse.org/technology /swtbot/releases/latest</swtbot> </properties> ... <repositories> <repository> <id>swtbot</id> <layout>p2</layout> <url>${swtbot}</url> </repository> </repositories> For Kepler (4.3), SWTBot must be version 2.1.1 or higher. 4. 5. 6. Copy the pom.xml file from the clock plugin to the junit.plugin project. Modify the packaging to <packaging>eclipse-test-plugin</packaging>. Change the artifactId to com.packtpub.e4.junit.plugin. [ 300 ] www.it-ebooks.info Chapter 10 7. To run the Tycho tests, add the following as a build plugin: <build> <sourceDirectory>src</sourceDirectory> <plugins> <plugin> <groupId>org.eclipse.tycho</groupId> <artifactId>tycho-surefire-plugin</artifactId> <version>${tycho-version}</version> <configuration> <useUIHarness>true</useUIHarness> <useUIThread>false</useUIThread> <product>org.eclipse.sdk.ide</product> <application>org.eclipse.ui.ide.workbench</application> </configuration> </plugin> </plugins> </build> 8. Now, running mvn integration-test should run the tests when not run on OS X. If run on an OS X, a line must be added to the configuration for JVM: <configuration> <useUIHarness>true</useUIHarness> <useUIThread>false</useUIThread> <argLine>-XstartOnFirstThread</argLine> ... </configuration> Although the tests run, they fail, because the SWTBot environment is giving the bare minimum dependencies required for the JUnit plug-in. In this case, it doesn't even include the clock plug-in developed earlier. To fix this, a dependency needs to be added to the pom.xml file so that the runtime can instantiate the correct workspace, including both the clock plug-in and the Eclipse SDK—since the tests rely on the workbench for the Open View command and the JDT for the Java project: <configuration> <useUIHarness>true</useUIHarness> <useUIThread>false</useUIThread> <dependencies> <dependency> <type>p2-installable-unit</type> <groupId>com.packtpub.e4</groupId> <artifactId>com.packtpub.e4.clock.ui</artifactId> </dependency> <dependency> <type>p2-installable-unit</type> [ 301 ] www.it-ebooks.info Automated Builds with Tycho <artifactId>org.eclipse.sdk.feature.group</artifactId> </dependency> </dependencies> ... </configuration> 9. Finally, run mvn integration-test and the tests should run and pass. What just happened? The tycho-surefire-plugin allows SWTBot applications to be launched. The tests are executed and then a return value indicates whether or not they were successful to be passed back to the Maven build process. Although it may seem that specifying the product or application will also bring in the necessary dependencies, that isn't the case. When the SWTBot test is run, it appears to pay no attention to the application or product when considering dependencies. As a result, these have to be added manually to the pom.xml file so that SWTBot sets up the right environment for the tests to run. The SWTBot tests written previously also had an implicit dependency on the SDK, from asserting the title of the workbench window to expecting the Show View menu to be present. These are examples of loosely coupled dependencies—they aren't code related, but to run, they do require the environment to be pre-seeded with the necessary plug-ins and features. Normally, if applications or features are being developed then these can be used to add the required dependencies instead of using individual plug-ins. In the example, the clock. ui plugin was added explicitly and the org.eclipse.sdk.feature was added as well. Note that the naming convention for the p2 feature's installable units is to add the suffix .feature.group to the end of the name; so the clock.ui plug-in dependency could be replaced with com.packtpub.e4.feature.feature.group as a dependency instead. Using features for dependencies makes it easier to maintain, as the test project can depend only on the feature, and the feature can depend on the necessary plug-ins. If a dependency needs to be added, it need only be added to the feature and it will apply to both the update site, as well as runtime and test projects. Finally, it is possible to determine whether or not a build is running on a Mac box dynamically, and switch in a value for the -XstartOnFirstThread argument. This can be achieved by setting a property using profiles which are automatically selected based on the operating system: <profiles> <profile> [ 302 ] www.it-ebooks.info Chapter 10 <id>OSX</id> <activation> <os> <family>mac</family> </os> </activation> <properties> <swtbot.args>-Xmx1024m -XstartOnFirstThread</swtbot.args> </properties> </profile> <profile> <id>NotOSX</id> <activation> <os> <family>!mac</family> </os> </activation> <properties> <swtbot.args>-Xmx1024m</swtbot.args> </properties> </profile> </profiles> The OSX profile is automatically enabled for the mac family builds, and the NotOSX profile is automatically enabled for any non-mac family builds (with the negation character ! at the start of the family name). Time for action – changing the version numbers When a new version of the project is released, the plug-in and feature numbers need to be updated. This can be done manually, or by modifying the pom.xml file and MANIFEST.MF version numbers, or by running a tool to do this. 1. From the parent directory, run the following (all on one line): mvn org.eclipse.tycho:tycho-versions-plugin:set-version -DnewVersion=1.2.3-SNAPSHOT 2. The output should say SUCCESS for the parent and SKIPPED for the others: [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] Reactor Summary: com.packtpub.e4.parent .................. com.packtpub.e4.clock.ui ................ com.packtpub.e4.junit.plugin ............ com.packtpub.e4.feature ................. com.packtpub.e4.update .................. [ 303 ] www.it-ebooks.info SUCCESS [5.569s] SKIPPED SKIPPED SKIPPED SKIPPED Automated Builds with Tycho 3. Now, run a build to verify that the versions were updated correctly: [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] [INFO] 4. Building com.packtpub.e4.parent 1.2.3-SNAPSHOT Building com.packtpub.e4.clock.ui 1.2.3-SNAPSHOT Building com.packtpub.e4.junit.plugin 1.2.3-SNAPSHOT Building com.packtpub.e4.feature 1.2.3-SNAPSHOT Building com.packtpub.e4.update 1.2.3-SNAPSHOT Reactor Summary: com.packtpub.e4.parent ........... com.packtpub.e4.clock.ui ......... com.packtpub.e4.junit.plugin ..... com.packtpub.e4.feature .......... com.packtpub.e4.update ........... SUCCESS SUCCESS SUCCESS SUCCESS SUCCESS [0.001s] [0.561s] [0.176s] [0.071s] [2.764s] Finally, once the development is complete, build with a release version: mvn org.eclipse.tycho:tycho-versions-plugin:set-version -DnewVersion=1.2.3.RELEASE What just happened? The Tycho set-versions plugin is very similar to the Maven version:set plugin. However, Tycho makes changes for both META-INF/MANIFEST.MF (needed by Eclipse) and pom.xml (needed by Maven). Development version numbers in Maven end in -SNAPSHOT to indicate that they are a mutable release, and there's special handling in Maven builds to get "the latest" snapshot build. For Eclipse builds, the equivalent special name is .qualifier, which is appended onto the end of the plug-in and feature builds. For simple projects, where there is a single plug-in and feature, it can often make sense to have the two versions kept in sync. Sometimes, when there are two highly related plug-ins in the same feature (for example, JDT and JDT UI) then it can also make sense in keeping them in sync. For larger projects where a single build may have multiple modules, it can make sense to have different version numbers on a plug-in by plug-in basis. The version numbers in OSGi and, therefore, Eclipse plug-ins follow semantic versioning (see http://semver.org), in which the version-number component consists of a major version, a minor version, and a micro version, as well as an optional qualifier. The major, minor, and micro versions default to 0 if not present, while the qualifier defaults to the empty string. Typically, the qualifier is used to encode either a build timestamp or a build revision identifier (such as that produced by git describe on modern version control systems). While the major/minor/micro versions are sorted numerically, the qualifier is sorted alphabetically. [ 304 ] www.it-ebooks.info Chapter 10 Unfortunately, OSGi version numbers and Maven version numbers differ in agreement on what the "highest" value is. For Maven, the empty qualifier is the highest (that is, 1.2.3.build < 1.2.3), whereas for OSGi it is the other way around (1.2.3.build > 1.2.3). As a result, organizations such as SpringSource have created a de-facto policy of using a qualifier of RELEASE to indicate the release build, (1.2.3.build < 1.2.3.RELEASE). They also use M1, M2, and M3 for milestone releases and RC1, RC2 for release candidates, since all of these are less than RELEASE. As a result, the progression for Eclipse build qualifiers tends to follow -SNAPSHOT, M1, M2, RC1, RC2, RELEASE. Have a go hero – enabling builds for other plug-ins Apply the same pom.xml builds to allow the other plug-ins built in the other chapters as part of the automated build. This includes the headless application (the product can be moved into the same product) and the Minimark editor. The standalone JUnit test will need to be built as a jar instead of an eclipse-plugin, and examples are available at the Maven homepage or the book's GitHub repository. Signing update sites When installing content into a repository, Eclipse will report whether the plug-ins are "signed" or not. Digital signatures ensure that the content of the plug-ins have not changed, and the identity of the signer can be verified. Time for action – creating a self-signed certificate To sign content, a private key and a public key must be used. The private key is used for signing the content, and the public key is used for verifying that the content has not been modified. A key-pair can be created using the Java keytool utility on the command line. 1. 2. Run keytool to see a list of options, and to verify that it is on the path. Create a new key-pair by running the following code (all on one line): keytool -genkey -alias packtpub -keypass SayK3ys -keystore /path/to/keystore -storepass BarC0der -dname "cn=packtpub,ou=pub,o=packt" 3. Verify that the key-pair was generated correctly: keytool -list -keystore /path/to/keystore storepass BarC0der [ 305 ] www.it-ebooks.info Automated Builds with Tycho 4. Create a JAR file for testing purposes, by zipping the contents of the directory: jar cf test.jar . 5. Sign the JAR file to verify that it works, by running the following command (all on one line): jarsigner -keypass SayK3ys -storepass BarC0der -keystore /path/to/keystore test.jar packtpub 6. Verify the JAR signature by running the following command: jarsigner -verify test.jar What just happened? The Java keytool program manages keys and certificates for Java programs wanting to sign content. Each entry in the keystore has an alias (to allow for ease of reference if there are many) and an associated key password and store password. The keystore is created at the location given, protected with a store password BarC0der. To use any of the keys in the keystore, the store needs to be unlocked with this password first. To use the private key, we need to give the key password, which is SayK3ys. Typically, the key passwords will be different from the store password. If multiple keys are present, it is good practice to have a different password for each one. The distinguished name (dname) is an LDAP identifier for the "owner" of the key. This is represented as a series of name=value comma-separated pairs. At the minimum, they need a common name (cn) and then some kind of organizational identifier. In this case, the organizational unit (ou) is pub and the organization (o) is packt. Another common way of representing ownership is to use the domain components (dc), so an alternative is to use something like cn=pakcktpub, dc=packtpub, dc=com where each element in the packtpub.com domain is split into its own dc element in the distinguished name. Note that the order of elements is significant. The jarsigner tool is used to sign a JAR file and needs access to the store, the store's password, and the key's password. The alias can be supplied, in which case it will use that one—but if it is left out then it will use any matching key in the chain (which assumes that the passwords are unique for keys, as is best practice). Finally, the jarsigner tool can also be used to verify whether a signature is correct or not using the -verify argument. [ 306 ] www.it-ebooks.info Chapter 10 Time for action – signing the plug-ins Integrating signatures into a Tycho build is a matter of adding a plug-in to the build script. In addition, Java properties need to be passed in to provide access to the arguments required by the jarsigner tool. 1. Add the plug-in to the parent pom.xml file: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jarsigner-plugin</artifactId> <version>1.2</version> <executions> <execution> <id>sign</id> <goals> <goal>sign</goal> </goals> </execution> </executions> </plugin> 2. Run mvn package and an error is shown: [ERROR] Failed to execute goal org.apache.maven.plugins:maven-jarsignerplugin:1.2:sign (sign) on project com.packtpub.e4.parent: The parameters 'alias' for goal org.apache.maven.plugins:maven-jarsigner-plugin:1.2:sign are missing or invalid -> [Help 1] 3. Pass in the arguments required by jarsigner, which are supplied as Java system properties with a jarsigner prefix as follows (all on one line): mvn package -Djarsigner.alias=packtpub -Djarsigner.keypass=SayK3ys -Djarsigner.storepass=BarC0der -Djarsigner.keystore=/path/to/keystore 4. If it is successful, the output should be as follows: [INFO] --- maven-jarsigner-plugin:1.2:sign (sign) @ com.packtpub.e4.clock.ui --[INFO] 1 archive(s) processed [INFO] --- maven-jarsigner-plugin:1.2:sign (sign) @ com.packtpub.e4.feature --[ 307 ] www.it-ebooks.info Automated Builds with Tycho [INFO] 1 archive(s) processed [INFO] --- maven-jarsigner-plugin:1.2:sign (sign) @ com.packtpub.e4.update --[INFO] 1 archive(s) processed 5. To run the sign step conditionally, a profile can be used. Move the sign plug-in from build to a separate top-level element profiles in pom.xml: <profiles> <profile> <id>sign</id> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jarsigner-plugin</artifactId> ... </plugin> </plugins> </build> </profile> </profiles> 6. 7. Now run the build with mvn package, and verify that it runs without signing. 8. To automatically enable the sign profile whenever the jarsigner.alias property is provided, add the following to the profile: Run the build with signing enabled by running mvn package -Psign to enable the sign profile; it should ask for the alias, as before. <profile> <id>sign</id> <activation> <property> <name>jarsigner.alias</name> </property> </activation> <build> ... </build> </profile> 9. Now, run the build as mvn package -Djarstore.alias=packtpub ... to verify that signing runs without needing to specify the -Psign argument. [ 308 ] www.it-ebooks.info Chapter 10 What just happened? By adding the maven-jarsigner-plugin to the build, Maven signed any JAR file that was built (including the content.jar and artifacts.jar files, which don't really need to be signed). This is a standard pattern for building any signed Java content in Maven and isn't Tycho or Eclipse-specific. The parameters to jarsigner are specified as system properties. The -D flag for Maven, like Java, is used to specify a system property on the command line. The maven-jarsigner-plugin reads its properties with a prefix of jarsigner, so the alias is passed as jarsigner.alias and the keystore as jarsigner.store. Note that the location of the store needs to be specified as a full path, since the plug-in will run with different directories (specifically the "target" directory of the build). Attempting to use a relative path will fail. Time for action – serving an update site Now that the update site has been developed, tested, and automatically built, the final stage is to upload the contents of the update site (under com.packtpub.e4.update/target/ repository) and make it available on a website or ftp server so that others can install it. If Python 2.7 or higher is installed, run a simple web server as follows: 1. 2. Change to the directory com.packtpub.e4.update/target/repository. Run Python's SimpleHTTPServer: python -m SimpleHTTPServer 8080 Serving HTTP on 0.0.0.0 port 8080 ... 3. Verify the update site by adding http://localhost:8080/ as a remote update site in Eclipse. If you don't have Python installed, then some operating systems have a means to serve web-based content already, or another web server can be used. OS X has Web Sharing where files in ~/Sites are served from; Linux systems typically have Apache configured to allow per-user web sharing in ~/public_html, and Microsoft Windows has IIS where the default location is c:\intepub\wwwroot. See the operating system's documentation for details. What just happened? An update site is simply an HTTP server that serves the contents of the content.jar and artifacts.jar files, along with their plugins and features directories. [ 309 ] www.it-ebooks.info Automated Builds with Tycho If Python 2.7 is installed, a module called SimpleHTTPServer exists that can be run from the update/target/repository directory to allow the update site to be installed from http://localhost:8080/. Uploading the contents of the repository to a remote website is left as an exercise for the reader. For Python 3, the command is python3 -m http.server. Finally, once published to a publicly visible website, it's possible to register the location of the update site at the Eclipse marketplace at http://marketplace.eclipse.org, so that other Eclipse users can find the update site from the Marketplace client. Pop quiz – understanding automated builds and update sites Q1. What is a GroupId, ArtifactId, and Version (GAV)? Q2. What are the four types of packaging types needed to build plug-ins, features, products, and update sites? Q3. How can the version numbers of plug-ins and features be updated in Maven? Q4. Why and how are JAR files signed? Q5. How can a simple HTTP server be run in Python? Q6. Where are Eclipse features typically registered for others to find? Summary This chapter concludes the creation of Eclipse plug-ins and features. Since the final step is building and making it available to others, the steps in this chapter focused on how to automate the builds with Tycho and then take the published update site and make it available for others. [ 310 ] www.it-ebooks.info Pop Quiz Answers Chapter 1, Creating Your First Plug-in Pop quiz – Eclipse workspaces and plug-ins Q1 An Eclipse workspace is the location where all the projects are stored. Q2 The naming convention for Eclipse plug-in projects is to use a reverse domain name prefix, such as com.packtpub. Additionally, UI projects typically have a UI in their name. Q3 The three key files in an Eclipse plug-in are META-INF/MANIFEST.MF, plugin.xml, and build.properties. Pop quiz – launching Eclipse Q1 1. Quit the application by navigating to File | Exit. Q2 Launch configurations are similar to precanned scripts, which can start up an application, set its working directory and environment, and run a class. Q3 Launch configurations are modified by navigating to the Run | Run Configurations... or Debug | Debug Configurations... menus. 2. Use the Stop button from the Debug or Console views. www.it-ebooks.info Pop Quiz Answers Pop quiz – debugging Q1 Navigate to the Debug | Debug configurations or Debug | Debug As... menus. Q2 Set step filters via the preferences menu to avoid certain package names. Q3 Breakpoints can be conditional, method entry/exit, enabled/disabled, or number of iterations. Q4 Set a breakpoint and set it after a hit count of 256. Q5 Use a conditional breakpoint and set argument==null as the condition. Q6 Inspecting an object means opening it up in the viewer so that the values of the object can be interrogated and expanded. Q7 The expression watches the window that allows arbitrary expressions to be set. Chapter 2, Creating Views with SWT Pop quiz – understanding views Q1 In the Eclipse 3.x model, views must be subclasses of ViewPart. In the Eclipse 4.x model, parts do not need to have an explicit superclass. Q2 In the Eclipse 3.x model, views are registered via an org.eclipse.ui.views extension point in the plugin.xml file. Q3 The two arguments that most SWT objects have are a Composite parent and an integer flags field. Q4 When a widget is disposed, it will have its native resources released to the operating system. Any subsequent actions will throw an SWTException with a Widget is disposed message. Q5 The Canvas has many drawing operations; to draw a circle, use drawArc() and specify a full orbit. Q6 To receive drawing events, a PaintListener must be created and associated with the control by using the addPaintListener method. Q7 UI updates not on the UI thread will generate an SWTException with an Invalid thread access error. Q8 To perform an update on a widget from a non-UI thread, use the asyncExec() or syncExec() methods from Display (3.x) or UISynchronize (4.x) to wrap a runnable that will run on the UI thread. Q9 SWT.DEFAULT is used to indicate default options in the flags parameter that is passed to the construction of an SWT widget. Q10 Create a RowData object with the given size, and associate it with each Widget. [ 312 ] www.it-ebooks.info Appendix Pop quiz – understanding resources Q1 Resource leaks occur when an SWT resource is acquired from the OS, but then not returned to it via a dispose() method prior to the object being garbage collected. Q2 The different types of resources are Color, Cursor, Font, GC, Image, Path, Pattern, Region, TextLayout, and Transform. Q3 Run the Eclipse instance in tracing mode with org.eclipse.ui/debug and org. eclipse.ui/trace/graphics set, specified in a debug file and launched with -debug. Q4 Use the displayed data to get the object's arrays, and iterate through them. Q5 The right way is to register a dispose listener with the view, and the wrong way is to override the dispose method. Pop quiz – understanding widgets Q1 Use the setFocus() method to set the focus on a particular widget. Q2 Invoking redraw() will allow the widget to redraw itself. Q3 The Combo can have a SelectionListener associated with it. Q4 The widgetDefaultSelected() is what is called when the default value is used, typically an empty value. Pop quiz – using SWT Q1 Use the Tray and TrayItem widgets. Q2 The SWT.NO_TRIM style means don't draw the edges of the window, or the close/maximize/minimize buttons. Q3 Use setAlpha() to control a widget's transparency, including Shell. Q4 Use setRegion() with a path describing the shape. Q5 A group allows you to group things together with a standard item. Q6 Most Composite use a null LayoutManager by default; it's only Shell and Dialog that have a non-default value. Q7 Use a ScrolledComposite. [ 313 ] www.it-ebooks.info Pop Quiz Answers Chapter 3, Creating JFace Viewers Pop quiz – understanding JFace Q1 getImage() for showing an Image for an entry, and getText() for showing a text value of an entry. Q2 The hasChildren() method is used to determine whether or not an element is shown with an expandable element, and getChildren() is used to calculate a list of children. Q3 An ImageRegistry is used to share images between plug-ins or different views in plug-ins, with a means of clearing up the resources when the view is disposed. Q4 Entries can be styled with an IStyledLabelProvider. Pop quiz – understanding sorting and filters Q1 Specifying a ViewerComparator can allow elements to be sorted in a different order other than the default one. Q2 The select() method is used to filter elements, which is originally derived from the Smalltalk terminology. Q3 Multiple filters can be combined by setting an array of filters, or by writing a filter to combine two or more filters together. Pop quiz – understanding properties Q1 Add a DoubleClickListener to the view. Q2 The Dialog subclasses are used to create a dialog with custom content. Q3 Property descriptors are used to represent keys for properties on a particular object. Q4 Properties are displayed on a Properties view by having an object that is adaptable (either directly through the IAdaptable interface, or indirectly via the IAdapterManager) such that it returns a property source instance, which will return the property descriptors. [ 314 ] www.it-ebooks.info Appendix Pop quiz – understanding tables Q1 To get the headers shown, get the Table from the Viewer, and use it to call the setHeaderVisible(true) method. Q2 TableViewerColumns are used to set properties on individual columns and to bind the label provider for the columns. Q3 Synchronization is achieved by registering the site as a selection provider (so that the selection events are sent to the workbench) and to listen for incoming selection events and adjusting the view as necessary. Care must be taken to avoid recursive selection calls in this case. Chapter 4, Interacting with the User Pop quiz – understanding menus Q1 Action are the old way of attaching executable content to menu items. A command is an abstract representation of the effect, which does not need to have a UI component. Action should not be used as they have been depreciated for some time and may be removed from a future version of the platform. Q2 A command can be associated with a handler to provide a menu item. Handlers are indirection mechanisms that allow the same menu (for example, copy) to take on different commands based on which context they are in. It is also possible to have a default command ID associated with a menu to avoid this indirection. Q3 The M1 key is an alias for Cmd on OS X, and for Ctrl on other platforms. When defining standard commands such as copy (M1 + C) it has the expected behavior on both platforms (Cmd + C for OS X and Ctrl + C for others). Q4 Keystrokes are bound to commands via a binding, which lists the key(s) necessary to invoke and the associated command/handler. Q5 A menu's locationURI is where it will contribute the entry to the UI. These are specified either as relative to an existing menu's contribution, or to its generic additions entry. It is also possible to specify custom ones, which are associated with custom code. Q6 A pop-up menu is created by using the special locationURI popup:org.eclipse. ui.popup.any. Note that the objectContribution is not as flexible and may be removed from the platform in future. [ 315 ] www.it-ebooks.info Pop Quiz Answers Pop quiz – understanding jobs Q1 The syncExec() method will block and wait for the job to complete before continuing the code. The asyncExec() method will continue to run after posting the job but before it completes. Q2 The Display class runs jobs on the SWT UI thread, but the UISynchronize instance can be used to run jobs on non-SWT UI threads as well. For E4 applications, the UISynchronize is preferred. Q3 The UIJob will always run on the UI thread of the runtime, and direct access of widgets will not run into a thread error. Care should be taken to minimize the amount of time spent in the UI thread so as not to block Eclipse. The job will run on a non-UI thread, and so it does not have access to acquire or modify UI threaded objects. Q4 The Status.OK_STATUS singleton is used to indicate success in general. Although it is possible to instantiate a Status object with an OK code, doing so only increases the garbage collection as the Status result is typically discarded after execution. Q5 The CommandService can be obtained via the general PlatformUI. getWorkbench().getService() call, which takes a class and returns an instance of that class. The same technique can be used to acquire a UISynchronize instance. Q6 An icon can be displayed by setting a property on the job with the name IProgressConstants2.ICON_NAME. Q7 SubMonitor are generally easier to use at the start of a method, to ensure that the monitor being passed in is correctly partitioned as appropriate for the task in hand. The SubProgressMonitor should generally not be used. Q8 The cancellation should be checked as frequently as possible so that as soon as the user chooses to cancel the Job, the Job is aborted. Pop quiz – understanding errors Q1 An informational dialog is shown with MessageDialog.openInformation() (and also .openWarning() and .openError() as well). There is also a MessageDialog.openConfirmation() call, which returns the value of a yes/no answer to the user. Q2 The StatusManager is an Eclipse 3.x class, which is tightly coupled to the UI. The StatusReporter provides the same basic intent, but without a UI association. Q3 The status reporting is asynchronous by default; although a BLOCK option exists to make it synchronous. Q4 To combine the results of many things into one report, use a MultiStatus object. [ 316 ] www.it-ebooks.info Appendix Chapter 5, Storing Preferences and Settings Pop quiz – understanding preferences Q1 The default style is FLAT but this can be overridden to provide GRID which provides a better layout for preference pages. Q2 There are many subclasses of FieldEditor, which include editors for Boolean, Color, Combo, Font, List, RadioGroup, Scale, String, Integer, Directory, and File. Q3 To provide searching in a preference page, keywords must be registered via the extension point. Q4 Don't use IMemento for anything. Use anything else including IEclipsePreferences or DialogSettings. Q5 The MessageDialogWithToggle class provides the "Do not show this message again" support. Chapter 6, Working with Resources Pop quiz – understanding resources, builders, and markers Q1 If an editor complains of a missing document provider, install an instance of TextFileDocumentProvider with the setDocumentProvider() method on the editor. Q2 An IResourceProxy is used by a builder to provide a wrapper around an IResource, but one which doesn't require the construction of an IResource image. Q3 An IPath is a generic file component that is used to navigate files in folders and projects. Q4 A nature is a flavor of a project, which enables certain behaviors. It is installed with an update to the project descriptor for the given project. Q5 Markers are generally created by a builder though they can be created by any plug-in on a resource. There is a specific function on resource which can be used to create a marker of a specific type. [ 317 ] www.it-ebooks.info Pop Quiz Answers Chapter 7, Understanding the Eclipse 4 Model Pop quiz – understanding E4 Q1 The application model is stored in the e4xmi file, and provides a way of representing the entire state of the application's UI. It is also persisted on save and then reloaded at startup, so positions of parts and their visibility is persisted. The model is also accessible at runtime via the various M classes such as MApplication and MPart, and can be queried and mutated at runtime. Q2 Parts are a more generic form of views and editors. Unlike Eclipse 3.x, not everything needs to fit into a View or Editor category; they are all just Parts which contain UI components underneath, and can be organized appropriately. Q3 Although extension points aren't used for things like commands, keybindings, or views, they are still used to define other extensions to Eclipse such as builders, marker types, and language parsers. The only thing that the Eclipse 4 model moves out of the extension points are the UI-related concepts. Even then, in the Eclipse 4 IDE the backward compatibility mode ensures that all the UI-related extension points are still rendered. For developing IDE plugins, the Eclipse 3.x APIs are likely to be around for the next couple of Eclipse releases. Q4 The Eclipse 4 parts can be styled with CSS, and the underlying renderer applies the styles on the fly, including if the CSS styles change. This allows theme managers to apply different color combinations in Eclipse 4 in ways which are not possible in Eclipse 3. Q5 The Eclipse 4 contexts are essentially a series of HashMaps that contain values (objects) associated with keys. Parts can dynamically obtain content from their context, which include all of the injectable services as well as dynamically changing content such as the current selection. A context is implicit in every Part, and inherits up the containment chain, terminating with the OSGi runtime. [ 318 ] www.it-ebooks.info Appendix Q6 There are several annotations used by Eclipse 4, including: @Inject (used to provide a general "insert-value-here" instruction to Eclipse) @Optional (meaning it can be null) @Named (to pull out a specific named value from the context) @PostConstruct (called just after the object is created) @PreDestroy (called just before the object is destroyed) @Preference (to pull out a specific preference value or the preference store) @EventTopic and @UIEventTopic (for receiving events via the event admin service and on the UI thread respectively) @Persist and @PersistState (for saving data and viewing data) @Execute and @CanExecute (for showing what method to execute, and a boolean conditional which has a boolean return to indicate if it can run) @Creatable (to indicate that the object can be instantiated) @GroupUpdate (to indicate that updates can be deferred). Q7 Preferences are accessed with the @Preference annotation which can inject a value into a field. If updates are needed it should be set as a method parameter, which will be called when the preference value is changed. Q8 Messages are sent via the EventBroker, which is accessible from the injection context. This can have sendEvent() or postEvent() to send data. On the receiving side, using the @UIEventTopic or @EventTopic annotations is the easiest way to receive values. As with preferences, if it's set up as a method parameter then the changes will be notified. Q9 Selection can be accessed using the value from the context with a method injection or value injection using @Named(IServiceConstants.ACTIVE_SELECTION). [ 319 ] www.it-ebooks.info Pop Quiz Answers Chapter 8, Creating Features, Update Sites, Applications, and Products Pop quiz – understanding features, applications, and products Q1 The keyword qualifier is replaced with a timestamp when plug-ins or features are built. Q2 The files are artifacts.jar and content.jar as well as one file per feature/plug-in built. Q3 The older site.xml can be used, or a category.xml file which is essentially equivalent. Q4 If a feature requires another, then it must be present in the Eclipse instance in order to install. If a feature includes another, then a copy of that included feature is included in the update site when built. Q5 An application is a standalone application which can be run in any Eclipse instance when it is installed. A product affects the Eclipse instance as a whole, replacing the launcher, icons, and default application launched. Q6 An application is a class that implements IApplication and has a start() method. It is referenced in the plugin.xml file and can be invoked by id with -application on the command line. Chapter 9, Automated Testing of Plug-ins Pop quiz – understanding SWTBot Q1 The JUnit Runner that is required is SWTBotJunit4ClassRunner, which is set up with an annotation @RunWith(SWTBotJunit4ClassRunner.class). Q2 Views are set up by driving the menu to perform the equivalent of navigating to Window | Show View | Other and driving the value of the dialog. Q3 To get the text value of a dialog, use textWithLabel() to find the text field next to the associated label, and then get or set the text from that. Q4 A Matcher is used to encode a specific condition, such as a view or window with a particular title. It can be handed over to the SWTBot runner to execute in the UI thread and return a value when it is done. Q5 To get values from the UI, use a StringResult (or other equivalent types) and pass that to the UIThreadRunnable's syncExec(). It will execute the code, return the value, and then pass that to the calling thread. Q6 Use the bot's waitUntil() or waitWhile() methods, which block execution of the test until a certain condition occurs. [ 320 ] www.it-ebooks.info Appendix Chapter 10, Automated Builds with Tycho Pop quiz – understanding automated builds and update sites Q1 The GroupId, ArtifactId, and Version are a set of co-ordinates (collectively known as GAV) that Maven uses to identify dependencies and plugins. The group is a means of associating multiple artifacts together, and the artifact is the individual component name. In OSGi and Eclipse builds, the group is typically the first few segments of the bundle name, and the artifact is the bundle name. The version follows the same syntax as the bundle's version, except that .qualifier is replaced with -SNAPSHOT. Q2 The four types are pom (used for the parent), eclipse-plugin (for plug-ins), eclipse-feature, (for features) and eclipse-repository (for update sites and products) Q3 Version numbers can be updated with mvn org.eclipse.tycho:tychoversions-plugin:set-version -DnewVersion=version.number. Note that while mvn version:set exists, it will not update the plug-in versions, if chosen. Q4 JAR files are signed to ensure that the contents of the JAR file have not been modified after creation. Eclipse looks at these JAR files at run-time to ensure that they are not modified, and warns if they are unsigned or if the signatures are invalid. The standard JDK tool, jarsigner is used to sign and verify JAR files; the JDK tool, keytool is used to manipulate keypairs. Q5 A simple HTTP server can be launched using the command python -m SimpleHTTPServer. In Python 3.0, the command is python3 -m http.server. Q6 Eclipse features are typically published in the Eclipse Marketplace at http://marketplace.eclipse.org. This includes both open source and commercial plug-ins. [ 321 ] www.it-ebooks.info www.it-ebooks.info Index Symbols .class files 163 @Execute annotation 110 .java source files 163 A AbstractUIPlugin 144 actions about 111 creating 109 addColumnTo() method 103 addDoubleClickListener() method 92 addSelectionListener() method 59, 107 automated tests JUnit, using for 265 running 300-302 B BooleanFieldEditor 153 bot 270 breakpoint about 25 method breakpoints 25 builder building 168-170 builds enabling, for plug-ins 305 C Central repository 283 clock.ui plug-in creating 36 Clock View custom view, drawing 38-40 second hand, animating 42, 43 second hand, drawing 41, 42 UI thread, running 43, 44 ClockWidget creating 45, 46 hours hand, drawing 50 layouts, using 47-49 minute hand, drawing 50 updating 60 collapseAll() method 90 collapseToLevel() method 91 ColorFieldEditor 153 ColorRegistry 81 Combo 57 ComboFieldEditor 148 command parameters passing 224, 226 commands about 221 binding, to keys 114, 115 contributing, to pop-up menus 121-123 creating 109 creating, for menu bar 111-113 menu, wiring to 221-224 Common Build Infrastructure 286 compare() method 88 compilers 168 Composite 49 computeSize() method 46 www.it-ebooks.info conditional breakpoints about 26 setting 26-28 ConsoleViewer 76 ContentViewers TableViewer 76 TreeViewer 76 context modifying 115-117 using 206 values, calculating on demands 215, 216 context menus about 110 adding 110, 111 createContents() method 150 createCustomArea() method 93 createFieldEditors() method 146, 147, 153 createPartControl() method 38, 58, 77, 82, 84, 88, 89, 92, 97, 103, 158 customArea() method 93 custom injectable classes creating 232 service, creating 232, 233 subtypes, injecting 233, 234 D debugging, Eclipse plug-in about 18, 20 step filters, using 23 Declarative Services (DS) 299 DelegatingStyledCellLabelProvider 84, 85 DetailedProgressViewer 76 DialogSettings about 157 using 159, 160 direct menu creating 226-229 Direct MenuItem 228 DirectoryFieldEditor 153 dispose method 106 dispose() method 50 double-click listener adding, to tree view 92 E E4 application creating 190-194 parts, creating 195-199 theme manager, using 205 UI, styling with CSS 200-205 E4 tooling installing 188, 189 Eclipse about 7, 163 feature, creating 238 headless application, creating 254-257 JUnit case, writing 266, 267 launching 15, 17 plug-ins, grouping with features 237 product, creating 259-262 Eclipse 4 model about 187 working with 188 Eclipse Classic download link 7 Eclipse plug-in about 7 breakpoint types, using 25 building, Maven used 283 building, with Tycho 286-288 code, updating in debugger 22 conditional breakpoints, using 26 creating 11-13 debugging 18-21 debugging, step filters used 23 exceptional breakpoints, using 28 key files 13 running 15 signing 307-309 Eclipse SDK download link 7 setting up 8-10 editor creating 164-166 epf (Eclipse Preference File) 143 error markers 182, 183 errors reporting 137-140 [ 324 ] www.it-ebooks.info events dealing with 212-214 exceptional breakpoints about 28 exceptions, catching 29, 30 Expressions view 32 Variables view 31 execute() method 31 expandAll() method 90 expandToLevel() method 91 expressions reusing 120, 121 getImage() method 81 getPropertyDescriptors() method 96 getPropertyValue() method 96 getSelection() method 94 getStateLocation() method 145 getStyledText() method 84 getSystemColor() method 50 graphics context (GC) 40 H F feature about 292 branding 250-253 building 292, 293 creating 238-240 depending, on other features 249, 250 exporting 240-242 installing 242-244 update site, categorising 244-248 FieldEditorPreferencePage 145 field editors BooleanFieldEditor 153 ColorFieldEditor 153 DirectoryFieldEditor 153 FileFieldEditor 153 PathEditor 153 RadioGroupFieldEditor 153 ScaleFieldEditor 153 FileFieldEditor 153 filtering, JFace 89 FontRegistry 81 G getAdapter() method 99 getChildren() method 79 getEditableValue() method 96 getElements() method 78 getFieldEditorParent() method 146 getFirstElement() method 95 getFont() method 85 Handled MenuItem 228 handlers about 221 creating 109 creating, for menu bar 111-113 menu, wiring to command 221-224 hasChildren() method 78 headless application creating 254-257 hookContextMenu() method 110 HTML 188 I IDoubleClickListener interface 92 IEclipsePreferences about 156 using 156 ImageRegistry 81 IMementos 157 incremental builds implementing 174, 175 installation, E4 tooling 188-190 installation, Maven 284, 285 IntegerFieldEditor 147 IPreferenceStore 144 IPropertySource interface 95 IPropertySupport interface 99 isOdd() method 266 isPropertySet() method 96 IStructuredSelection class 94, 95 IStyledLabelProvider interface adding, to TimeZoneLabelProvider 84 isValid() method 148 ITreeSelection 92, 94 [ 325 ] www.it-ebooks.info J M jarsigner tool 306 Java URL 8 JavaFX 188 java.util.Date() 32 JFace about 75 festures 75 filtering 89 resource registries 81 sorting 86 JFaceRegistry 82 job properties setting 133-136 jobs 42, 124 JUnit about 265 using, for automated testing 265 JUnit case writing, in Eclipse 266, 267 Juno (4.2) URL 188 MANIFEST.MF file 37 markers using 182 marker type registering 184, 185 markup language example about 164 builder, building 168-170 deletion, handling 175-177 editor, creating 164-166 files, iterating through resources 170-172 incremental builds, implementing 174, 175 markup parser, writing 166, 167 resources, creating 173, 174 markup parser writing 166, 167 Maven about 283 installing 284, 285 used, for building Eclipse plug-ins 283 maven quickstart plugin 285 memento adding, for Time Zone View 158 menu wiring, to command 221-224 menu items disabling 118-120 enabling 118-120 MessageDialogWithToggle 160 method breakpoints 25 minimark 164 K Kepler (4.3) URL 188 keybindings creating 226-229 key files, Eclipse plug-ins build.properties 14 META-INF/MANIFEST.MF 13 plugin.xml file 14 keys commands, binding to 114, 115 keystore 306 keytool program 306 L LocalResourceManager 82 logging adding 206, 207 LogService 206 N nature creating 178-181 using 178 New Class wizard dividing 280 NullPointerException 30 null progress monitors using 131-133 [ 326 ] www.it-ebooks.info O openInformation() method 31 operations running, in background 125, 126 OSGi 187 OSGi services about 206 dealing with, events 212-214 logging, adding 206, 207 selection service, obtaining 209-211 window, obtaining 208, 209 P paintControl() method 40, 41, 45, 51, 58 parent project creating 289-291 parts about 195 creating 195-199 PathEditor 153 performApply() method 150 performOk() method 150 plug-in. See Eclipse plug-in Plug-in Development Environment (PDE) 8 plug-ins development 7 plug-in test writing 267, 268 plug-in wizard MANIFEST.MF 37 plugin.xml file 38 plugin.xml file 38, 109 POJOs (Plain Old Java Objects) 188 pom.xml file 283 pop-menus command, contributing to 121-123 pop-up menu about 229 creating 229-231 preferences about 143 error messages, creating 147 field editors, using 153, 154 grid, using 150 IEclipsePreferences, using 156, 157 keywords, adding 154 list, selecting from 148 preference page, creating 145, 146 preferences page, placing 151 using 217, 218 value, persisting 144 warning, creating 147 product about 295 building 295-299 creating 259, 261 creating, based on features 263 progress about 124 reporting, for tasks 127, 128 progress monitor dealing with cancellations 128, 129 project about 170 version number, modifying 303-305 public static method 68 R RadioGroupFieldEditor 153 removeSelectionListener() method 107 resource management, SWT about 50 colorful option, adding 51 leak, plugging 54-56 leak, searching 52, 54 resource registries ColorRegistry 81 FontRegistry 81 ImageRegistry 81 resources about 163 creating 173, 174 files, iterating through 170-172 folders, iterating through 170-172 using 163 reusable widget creating 45 reveal() method 91, 107 S saveState() method 158 selectionChanged() method 105 [ 327 ] www.it-ebooks.info selectionListener 106 select() method 89 self-signed certificate creating 305, 306 semantic versioning 304 services using 206 setFocus() method 38 using 58 setOffset() method 59 setSelection() method 107 setValue() method 144 sorting, JFace about 87 view-specific sorting 87 SourceViewer 76 step filtering setting up 23, 24 submonitors using 131-133 subprogress monitors using 130, 131 subtasks about 129 using 130, 131 SWT about 35 resources, managing 50 reusable widget, creating 45 user interaction 56 view, creating 36 SWTBot about 265, 268, 270 installing 268 used, for user interface testing 268 welcome screen, hiding 273 working with 273 working with menus 271, 272 SWTBot runtime errors avoiding 274 SWTBot test writing 269-271 SWT rendering tool 188 SWT tools update site referenec link 56 SWT widgets about 60 groups and tab folders 66-71 items, adding to tray 60, 61 modal window, creating 64 shell effects 64, 65 user, responding to 62, 63 syncExec() method 43 T TableTreeViewer creating 100 selection, syncing 104-106 time zones, viewing in tables 100 TableViewer about 76 creating 76-79 images, adding for regions 86 images, using 81-83 label providers, styling 84, 85 target platform 289 test case about 265 writing, in Eclipse 266 testOdd() method 266 testPlatform() method 267 test suites 265 TextViewer 76 theme manager using 205 TimeZoneDisplayNameColumn 103 TimeZoneIDColumn class 103 TimeZoneLabelProvider 81 time zones displaying, in tables 100-103 Time Zone Table View 100 Time Zone View memento, adding 158, 159 TimeZoneViewerComparator 88 toolbars using 124 tools bridge using 234 toString() method 78, 87 TreeViewer about 76 double-click listener, adding 92-95 items, filtering 89, 91 [ 328 ] www.it-ebooks.info items, sorting 87, 88 properties, displaying 95-99 Tycho about 283, 286 plug-in, building 286-288 tycho-surefire-plugin 302 U UI conditions, using 278, 279 interacting with 277 values, obtaining from 277, 278 UI, for E4 application interacting with 219, 220 styling, with CSS 200-205 UI job using 126 update site about 293 building 293, 294 serving 309 signing 305 user interactions, SWT about 56 focus, switching 57 input, responding to 58, 59 user interface testing SWTBot used 268 user preference 143 V values obtaining, from UI 277, 278 view creating 36, 37 custom view, drawing 38 displaying 275 interrogating 276 working with 274 viewByTitle() method 275 ViewerFilter class 89 viewers ContentViewers 76 view menu about 229 creating 229-232 using 124 W warning markers 182 window 208 workspace about 163 using 163 [ 329 ] www.it-ebooks.info www.it-ebooks.info Thank you for buying Eclipse 4 Plug-in Development by Example Beginner's Guide About Packt Publishing Packt, pronounced 'packed', published its first book "Mastering phpMyAdmin for Effective MySQL Management" in April 2004 and subsequently continued to specialize in publishing highly focused books on specific technologies and solutions. Our books and publications share the experiences of your fellow IT professionals in adapting and customizing today's systems, applications, and frameworks. Our solution based books give you the knowledge and power to customize the software and technologies you're using to get the job done. Packt books are more specific and less general than the IT books you have seen in the past. Our unique business model allows us to bring you more focused information, giving you more of what you need to know, and less of what you don't. Packt is a modern, yet unique publishing company, which focuses on producing quality, cutting-edge books for communities of developers, administrators, and newbies alike. For more information, please visit our website: www.packtpub.com. About Packt Open Source In 2010, Packt launched two new brands, Packt Open Source and Packt Enterprise, in order to continue its focus on specialization. This book is part of the Packt Open Source brand, home to books published on software built around Open Source licences, and offering information to anybody from advanced developers to budding web designers. The Open Source brand also runs Packt's Open Source Royalty Scheme, by which Packt gives a royalty to each Open Source project about whose software a book is sold. Writing for Packt We welcome all inquiries from people who are interested in authoring. Book proposals should be sent to [email protected] If your book idea is still at an early stage and you would like to discuss it first before writing a formal book proposal, contact us; one of our commissioning editors will get in touch with you. We're not just looking for published authors; if you have strong technical skills but no writing experience, our experienced editors can help you develop a writing career, or simply get some additional reward for your expertise. www.it-ebooks.info Instant Eclipse 4 RCP Development How-to [Instant] ISBN: 978-1-78216-952-9 Paperback: 68 pages Over 10 practical recipes for creating rich client applications using Eclipse 4 1. Learn something new in an Instant! A short, fast, focused guide delivering immediate results 2. Produce rich client standalone applications using Eclipse 4 3. Create an application user interface using an application model 4. Customize and package your applications for multiple target platforms Java EE Development with Eclipse ISBN: 978-1-78216-096-0 Paperback: 426 pages Develop Java EE applications with Eclipse and commonly used technologies and frameworks 1. Each chapter includes an end-to-end sample application 2. Develop applications with some of the commonly used technologies using the project facets in Eclipse 3.7 3. Clear explanations enriched with the necessary screenshots Please check www.PacktPub.com for information on our titles www.it-ebooks.info Instant Eclipse Application Testing How-to [Instant] ISBN: 978-1-78216-324-4 Paperback: 62 pages An easy-to-use guide on how to test Java applications of any scope using Eclipse IDE 1. Learn something new in an Instant! A short, fast, focused guide delivering immediate results 2. Learn how to install Eclipse and Java for any platform 3. Get to grips with how to efficiently navigate in the Eclipse environment using shortcuts 4. Create your own Java sample app and learn how to test and debug it using a rich set of Eclipse debugging tools Getting Started with Eclipse Juno ISBN: 978-1-78216-094-6 Paperback: 277 pages A friendly and engaging tutorial on Eclipse Juno IDE that will help you to get up to speed with Eclipse 1. Learn subjects ranging from basic Java development to web app development, version control, and GUI programming 2. Discover how to use Eclipse to develop, test, and debug basic desktop Java applications proficiently 3. Integrate JUnit 4, the most widely used unit testing framework, into Eclipse 4. Get to grips with how Eclipse can be used to develop web-based Java applications that employ Java Servlets and JavaServer Pages Please check www.PacktPub.com for information on our titles www.it-ebooks.info
* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project
advertisement