1 /******************************************************************************* 2 * Copyright (c) 2011 Google, Inc. 3 * All rights reserved. This program and the accompanying materials 4 * are made available under the terms of the Eclipse Public License v1.0 5 * which accompanies this distribution, and is available at 6 * http://www.eclipse.org/legal/epl-v10.html 7 * 8 * Contributors: 9 * Google, Inc. - initial API and implementation 10 *******************************************************************************/ 11 package org.eclipse.wb.core.controls; 12 13 import org.eclipse.swt.SWT; 14 import org.eclipse.swt.events.FocusAdapter; 15 import org.eclipse.swt.events.FocusEvent; 16 import org.eclipse.swt.events.KeyAdapter; 17 import org.eclipse.swt.events.KeyEvent; 18 import org.eclipse.swt.events.SelectionAdapter; 19 import org.eclipse.swt.events.SelectionEvent; 20 import org.eclipse.swt.graphics.Color; 21 import org.eclipse.swt.graphics.Point; 22 import org.eclipse.swt.graphics.Rectangle; 23 import org.eclipse.swt.widgets.Button; 24 import org.eclipse.swt.widgets.Composite; 25 import org.eclipse.swt.widgets.Display; 26 import org.eclipse.swt.widgets.Event; 27 import org.eclipse.swt.widgets.Layout; 28 import org.eclipse.swt.widgets.Spinner; 29 import org.eclipse.swt.widgets.Text; 30 31 import java.text.DecimalFormat; 32 import java.text.MessageFormat; 33 import java.text.ParseException; 34 35 /** 36 * Custom implementation of {@link Spinner}. 37 * 38 * @author scheglov_ke 39 * @coverage core.control 40 */ 41 public class CSpinner extends Composite { 42 private static final Color COLOR_VALID = Display.getCurrent().getSystemColor( 43 SWT.COLOR_LIST_BACKGROUND); 44 private static final Color COLOR_INVALID = new Color(null, 255, 230, 230); 45 private int m_minimum = 0; 46 private int m_maximum = 100; 47 private int m_increment = 1; 48 private int m_value = 0; 49 private int m_multiplier = 1; 50 private String m_formatPattern = "0"; 51 private DecimalFormat m_format = new DecimalFormat(m_formatPattern); 52 //////////////////////////////////////////////////////////////////////////// 53 // 54 // GUI fields 55 // 56 //////////////////////////////////////////////////////////////////////////// 57 private final Button m_button; 58 private final Text m_text; 59 private final Spinner m_spinner; 60 private Composite win32Hack; 61 62 //////////////////////////////////////////////////////////////////////////// 63 // 64 // Constructor 65 // 66 //////////////////////////////////////////////////////////////////////////// 67 public CSpinner(Composite parent, int style) { 68 super(parent, style); 69 m_button = new Button(this, SWT.ARROW | SWT.DOWN); 70 { 71 int textStyle = SWT.SINGLE | SWT.RIGHT; 72 if (IS_OS_MAC_OSX_COCOA) { 73 textStyle |= SWT.BORDER; 74 } 75 m_text = new Text(this, textStyle); 76 m_text.setText("" + m_value); 77 m_text.addKeyListener(new KeyAdapter() { 78 @Override 79 public void keyPressed(KeyEvent e) { 80 if (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.ARROW_DOWN) { 81 e.doit = false; 82 updateValue(e.keyCode); 83 } 84 } 85 86 @Override 87 public void keyReleased(KeyEvent e) { 88 try { 89 m_value = (int) (m_format.parse(m_text.getText()).doubleValue() * m_multiplier); 90 if (m_value < m_minimum || m_value > m_maximum) { 91 m_text.setBackground(COLOR_INVALID); 92 setState(MessageFormat.format( 93 Messages.CSpinner_outOfRange, 94 m_value, 95 m_minimum, 96 m_maximum)); 97 notifySelectionListeners(false); 98 } else { 99 setState(null); 100 notifySelectionListeners(true); 101 } 102 } catch (ParseException ex) { 103 setState(MessageFormat.format( 104 Messages.CSpinner_canNotParse, 105 m_text.getText(), 106 m_formatPattern)); 107 notifySelectionListeners(false); 108 } 109 } 110 }); 111 } 112 if (!IS_OS_MAC_OSX) { 113 win32Hack = new Composite(this, SWT.NONE); 114 win32Hack.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE)); 115 win32Hack.moveAbove(null); 116 win32Hack.moveBelow(m_text); 117 } 118 { 119 m_spinner = new Spinner(this, SWT.VERTICAL); 120 m_spinner.setMinimum(0); 121 m_spinner.setMaximum(50); 122 m_spinner.setIncrement(1); 123 m_spinner.setPageIncrement(1); 124 m_spinner.setSelection(25); 125 m_spinner.addFocusListener(new FocusAdapter() { 126 @Override 127 public void focusGained(FocusEvent e) { 128 setFocus(); 129 } 130 }); 131 m_spinner.addSelectionListener(new SelectionAdapter() { 132 @Override 133 public void widgetSelected(SelectionEvent e) { 134 m_text.forceFocus(); 135 if (m_spinner.getSelection() > 25) { 136 updateValue(SWT.ARROW_UP); 137 } else { 138 updateValue(SWT.ARROW_DOWN); 139 } 140 m_spinner.setSelection(25); 141 } 142 }); 143 setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE)); 144 if (IS_OS_WINDOWS_XP || IS_OS_WINDOWS_2003) { 145 setLayout(new WindowsXpLayout()); 146 } else if (IS_OS_WINDOWS_VISTA || IS_OS_WINDOWS_7) { 147 setLayout(new WindowsVistaLayout()); 148 } else if (IS_OS_LINUX) { 149 setLayout(new LinuxLayout()); 150 } else if (IS_OS_MAC_OSX) { 151 if (IS_OS_MAC_OSX_COCOA) { 152 setLayout(new MacCocoaLayout()); 153 } else { 154 setLayout(new MacLayout()); 155 } 156 } else { 157 setLayout(new WindowsXpLayout()); 158 } 159 } 160 } 161 162 //////////////////////////////////////////////////////////////////////////// 163 // 164 // Access 165 // 166 //////////////////////////////////////////////////////////////////////////// 167 @Override 168 public void setEnabled(boolean enabled) { 169 super.setEnabled(enabled); 170 m_text.setEnabled(enabled); 171 m_spinner.setEnabled(enabled); 172 } 173 174 /** 175 * Sets the number of decimal places used by the receiver. 176 * <p> 177 * See {@link Spinner#setDigits(int)}. 178 */ 179 public void setDigits(int digits) { 180 m_formatPattern = "0."; 181 m_multiplier = 1; 182 for (int i = 0; i < digits; i++) { 183 m_formatPattern += "0"; 184 m_multiplier *= 10; 185 } 186 m_format = new DecimalFormat(m_formatPattern); 187 updateText(); 188 } 189 190 /** 191 * Sets minimum and maximum using single invocation. 192 */ 193 public void setRange(int minimum, int maximum) { 194 setMinimum(minimum); 195 setMaximum(maximum); 196 } 197 198 /** 199 * @return the minimum value that the receiver will allow. 200 */ 201 public int getMinimum() { 202 return m_minimum; 203 } 204 205 /** 206 * Sets the minimum value that the receiver will allow. 207 */ 208 public void setMinimum(int minimum) { 209 m_minimum = minimum; 210 setSelection(Math.max(m_value, m_minimum)); 211 } 212 213 /** 214 * Sets the maximum value that the receiver will allow. 215 */ 216 public void setMaximum(int maximum) { 217 m_maximum = maximum; 218 setSelection(Math.min(m_value, m_maximum)); 219 } 220 221 /** 222 * Sets the amount that the receiver's value will be modified by when the up/down arrows are 223 * pressed to the argument, which must be at least one. 224 */ 225 public void setIncrement(int increment) { 226 m_increment = increment; 227 } 228 229 /** 230 * Sets the <em>value</em>, which is the receiver's position, to the argument. If the argument is 231 * not within the range specified by minimum and maximum, it will be adjusted to fall within this 232 * range. 233 */ 234 public void setSelection(int newValue) { 235 newValue = Math.min(Math.max(m_minimum, newValue), m_maximum); 236 if (newValue != m_value) { 237 m_value = newValue; 238 updateText(); 239 // set valid state 240 setState(null); 241 } 242 } 243 244 private void updateText() { 245 String text = m_format.format((double) m_value / m_multiplier); 246 m_text.setText(text); 247 m_text.selectAll(); 248 } 249 250 /** 251 * @return the <em>selection</em>, which is the receiver's position. 252 */ 253 public int getSelection() { 254 return m_value; 255 } 256 257 //////////////////////////////////////////////////////////////////////////// 258 // 259 // Update 260 // 261 //////////////////////////////////////////////////////////////////////////// 262 /** 263 * Updates {@link #m_value} into given direction. 264 */ 265 private void updateValue(int direction) { 266 // prepare new value 267 int newValue; 268 { 269 newValue = m_value; 270 if (direction == SWT.ARROW_UP) { 271 newValue += m_increment; 272 } 273 if (direction == SWT.ARROW_DOWN) { 274 newValue -= m_increment; 275 } 276 } 277 // update value 278 setSelection(newValue); 279 notifySelectionListeners(true); 280 } 281 282 /** 283 * Sets the valid/invalid state. 284 * 285 * @param message 286 * the message to show, or <code>null</code> if valid. 287 */ 288 private void setState(String message) { 289 m_text.setToolTipText(message); 290 if (message == null) { 291 m_text.setBackground(COLOR_VALID); 292 } else { 293 m_text.setBackground(COLOR_INVALID); 294 } 295 } 296 297 /** 298 * Notifies {@link SWT#Selection} listeners with value and state. 299 */ 300 private void notifySelectionListeners(boolean valid) { 301 Event event = new Event(); 302 event.detail = m_value; 303 event.doit = valid; 304 notifyListeners(SWT.Selection, event); 305 } 306 307 //////////////////////////////////////////////////////////////////////////// 308 // 309 // Windows XP 310 // 311 //////////////////////////////////////////////////////////////////////////// 312 /** 313 * Implementation of {@link Layout} for Windows XP. 314 */ 315 private class WindowsXpLayout extends Layout { 316 @Override 317 protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { 318 Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 319 size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width; 320 // add Text widget margin 321 size.y += 2; 322 // apply hints 323 if (wHint != SWT.DEFAULT) { 324 size.x = Math.min(size.x, wHint); 325 } 326 if (hHint != SWT.DEFAULT) { 327 size.y = Math.min(size.y, hHint); 328 } 329 // OK, final size 330 return size; 331 } 332 333 @Override 334 protected void layout(Composite composite, boolean flushCache) { 335 Rectangle cRect = composite.getClientArea(); 336 if (cRect.isEmpty()) { 337 return; 338 } 339 // prepare size of Text 340 Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 341 // prepare size of Spinner 342 Point sSize; 343 sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache); 344 sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height)); 345 sSize.x = Math.min(sSize.x, cRect.width); 346 // prepare width of arrows part of Spinner 347 int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; 348 // set bounds for Spinner and Text 349 m_spinner.setBounds( 350 cRect.x + cRect.width - sSize.x + 1, 351 cRect.y - 1, 352 sSize.x, 353 cRect.height + 2); 354 m_text.setBounds(cRect.x, cRect.y + 1, cRect.width - arrowWidth, tSize.y); 355 win32Hack.setBounds(cRect.x, cRect.y, cRect.width - arrowWidth, sSize.y); 356 } 357 } 358 //////////////////////////////////////////////////////////////////////////// 359 // 360 // Windows Vista 361 // 362 //////////////////////////////////////////////////////////////////////////// 363 /** 364 * Implementation of {@link Layout} for Windows Vista. 365 */ 366 private class WindowsVistaLayout extends Layout { 367 @Override 368 protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { 369 Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 370 size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width; 371 // add Text widget margin 372 size.y += 3; 373 // apply hints 374 if (wHint != SWT.DEFAULT) { 375 size.x = Math.min(size.x, wHint); 376 } 377 if (hHint != SWT.DEFAULT) { 378 size.y = Math.min(size.y, hHint); 379 } 380 // OK, final size 381 return size; 382 } 383 384 @Override 385 protected void layout(Composite composite, boolean flushCache) { 386 Rectangle cRect = composite.getClientArea(); 387 if (cRect.isEmpty()) { 388 return; 389 } 390 // prepare size of Text 391 Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 392 // prepare size of Spinner 393 Point sSize; 394 sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache); 395 sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height)); 396 sSize.x = Math.min(sSize.x, cRect.width); 397 // prepare width of arrows part of Spinner 398 int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; 399 // set bounds for Spinner and Text 400 m_spinner.setBounds( 401 cRect.x + cRect.width - sSize.x + 1, 402 cRect.y - 1, 403 sSize.x, 404 cRect.height + 2); 405 m_text.setBounds(cRect.x, cRect.y + 1, cRect.width - arrowWidth, tSize.y); 406 win32Hack.setBounds(cRect.x, cRect.y, cRect.width - arrowWidth, sSize.y); 407 } 408 } 409 //////////////////////////////////////////////////////////////////////////// 410 // 411 // Linux 412 // 413 //////////////////////////////////////////////////////////////////////////// 414 /** 415 * Implementation of {@link Layout} for Linux. 416 */ 417 private class LinuxLayout extends Layout { 418 @Override 419 protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { 420 Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 421 size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width; 422 // apply hints 423 if (wHint != SWT.DEFAULT) { 424 size.x = Math.min(size.x, wHint); 425 } 426 if (hHint != SWT.DEFAULT) { 427 size.y = Math.min(size.y, hHint); 428 } 429 // OK, final size 430 return size; 431 } 432 433 @Override 434 protected void layout(Composite composite, boolean flushCache) { 435 Rectangle cRect = composite.getClientArea(); 436 if (cRect.isEmpty()) { 437 return; 438 } 439 // prepare size of Text 440 Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 441 // prepare size of Spinner 442 Point sSize; 443 sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache); 444 sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height)); 445 sSize.x = Math.min(sSize.x, cRect.width); 446 // prepare width of arrows part of Spinner 447 int arrowWidth; 448 { 449 m_spinner.setSize(sSize); 450 arrowWidth = sSize.x - m_spinner.getClientArea().width; 451 } 452 // set bounds for Spinner and Text 453 m_spinner.setBounds(cRect.x + cRect.width - sSize.x, cRect.y - 2, sSize.x, cRect.height + 4); 454 m_text.setBounds(cRect.x, cRect.y, cRect.width - arrowWidth, tSize.y); 455 } 456 } 457 //////////////////////////////////////////////////////////////////////////// 458 // 459 // MacOSX 460 // 461 //////////////////////////////////////////////////////////////////////////// 462 /** 463 * Implementation of {@link Layout} for MacOSX. 464 */ 465 private class MacLayout extends Layout { 466 @Override 467 protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { 468 Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 469 size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width; 470 // add Text widget margin 471 size.y += 4; 472 // apply hints 473 if (wHint != SWT.DEFAULT) { 474 size.x = Math.min(size.x, wHint); 475 } 476 if (hHint != SWT.DEFAULT) { 477 size.y = Math.min(size.y, hHint); 478 } 479 // OK, final size 480 return size; 481 } 482 483 @Override 484 protected void layout(Composite composite, boolean flushCache) { 485 Rectangle cRect = composite.getClientArea(); 486 if (cRect.isEmpty()) { 487 return; 488 } 489 // prepare size of Text 490 Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 491 tSize.y += 4; 492 // prepare size of Spinner 493 Point sSize; 494 sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache); 495 sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height)); 496 sSize.x = Math.min(sSize.x, cRect.width); 497 // prepare width of arrows part of Spinner 498 int arrowWidth = m_button.computeSize(-1, -1).x; 499 // set bounds for Spinner and Text 500 m_spinner.setBounds(cRect.x + cRect.width - sSize.x, cRect.y, sSize.x, cRect.height); 501 m_text.setBounds(cRect.x, cRect.y + 2, cRect.width - arrowWidth - 2, tSize.y); 502 } 503 } 504 /** 505 * Implementation of {@link Layout} for MacOSX Cocoa. 506 */ 507 private class MacCocoaLayout extends Layout { 508 @Override 509 protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { 510 Point textSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT); 511 Point spinnerSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT); 512 int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; 513 int width = textSize.x + arrowWidth; 514 int height = Math.max(spinnerSize.y, textSize.y); 515 // apply hints 516 if (wHint != SWT.DEFAULT) { 517 width = Math.min(width, wHint); 518 } 519 if (hHint != SWT.DEFAULT) { 520 height = Math.min(height, hHint); 521 } 522 return new Point(width, height); 523 } 524 525 @Override 526 protected void layout(Composite composite, boolean flushCache) { 527 Rectangle clientArea = composite.getClientArea(); 528 if (clientArea.isEmpty()) { 529 return; 530 } 531 // prepare size of Spinner 532 Point spinnerSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache); 533 // prepare width of arrows part of Spinner 534 int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; 535 m_spinner.setBounds(clientArea.x + clientArea.width - arrowWidth - 1, clientArea.y 536 + clientArea.height 537 - spinnerSize.y, arrowWidth + 2, spinnerSize.y); 538 m_text.setBounds( 539 clientArea.x + 2, 540 clientArea.y + 2, 541 clientArea.width - arrowWidth - 5, 542 clientArea.y + clientArea.height - 4); 543 } 544 } 545 546 //////////////////////////////////////////////////////////////////////////// 547 // 548 // System utils 549 // 550 //////////////////////////////////////////////////////////////////////////// 551 private static final String OS_NAME = System.getProperty("os.name"); 552 private static final String OS_VERSION = System.getProperty("os.version"); 553 private static final String WS_TYPE = SWT.getPlatform(); 554 private static final boolean IS_OS_MAC_OSX = isOS("Mac OS X"); 555 private static final boolean IS_OS_MAC_OSX_COCOA = IS_OS_MAC_OSX && "cocoa".equals(WS_TYPE); 556 private static final boolean IS_OS_LINUX = isOS("Linux") || isOS("LINUX"); 557 private static final boolean IS_OS_WINDOWS_XP = isWindowsVersion("5.1"); 558 private static final boolean IS_OS_WINDOWS_2003 = isWindowsVersion("5.2"); 559 private static final boolean IS_OS_WINDOWS_VISTA = isWindowsVersion("6.0"); 560 private static final boolean IS_OS_WINDOWS_7 = isWindowsVersion("6.1"); 561 562 private static boolean isOS(String osName) { 563 return OS_NAME != null && OS_NAME.startsWith(osName); 564 } 565 566 private static boolean isWindowsVersion(String windowsVersion) { 567 return isOS("Windows") && OS_VERSION != null && OS_VERSION.startsWith(windowsVersion); 568 } 569 } 570