From d665f29f2811e03967501c98d0c6a738465f09e3 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 21 Oct 2016 12:36:55 +0200 Subject: [PATCH] new API front-end + bound backend for SwingUI --- src/dorkbox/systemTray/Status.java | 24 - .../systemTray/swingUI/SwingEntry.java | 208 ------- .../swingUI/SwingEntryCheckbox.java | 128 ---- .../systemTray/swingUI/SwingEntryItem.java | 113 ---- .../systemTray/swingUI/SwingEntryStatus.java | 71 --- .../systemTray/swingUI/SwingEntryWidget.java | 61 -- src/dorkbox/systemTray/swingUI/SwingMenu.java | 378 ++++-------- .../systemTray/swingUI/SwingMenuItem.java | 161 +++++ .../swingUI/SwingMenuItemCheckbox.java | 171 ++++++ ...rator.java => SwingMenuItemSeparator.java} | 45 +- .../swingUI/SwingMenuItemStatus.java | 76 +++ .../systemTray/swingUI/_AppIndicatorTray.java | 195 +++--- .../swingUI/_GtkStatusIconTray.java | 185 +++--- .../systemTray/swingUI/_SwingTray.java | 184 +++--- src/dorkbox/systemTray/util/EntryHook.java | 9 + src/dorkbox/systemTray/util/ImageUtils.java | 106 +++- src/dorkbox/systemTray/util/MenuBase.java | 579 +++++------------- .../systemTray/util/MenuCheckboxHook.java | 20 + src/dorkbox/systemTray/util/MenuHook.java | 12 + src/dorkbox/systemTray/util/MenuItemHook.java | 19 + .../systemTray/util/MenuStatusHook.java | 9 + src/dorkbox/systemTray/util/Status.java | 67 ++ 22 files changed, 1244 insertions(+), 1577 deletions(-) delete mode 100644 src/dorkbox/systemTray/Status.java delete mode 100644 src/dorkbox/systemTray/swingUI/SwingEntry.java delete mode 100644 src/dorkbox/systemTray/swingUI/SwingEntryCheckbox.java delete mode 100644 src/dorkbox/systemTray/swingUI/SwingEntryItem.java delete mode 100644 src/dorkbox/systemTray/swingUI/SwingEntryStatus.java delete mode 100644 src/dorkbox/systemTray/swingUI/SwingEntryWidget.java create mode 100644 src/dorkbox/systemTray/swingUI/SwingMenuItem.java create mode 100644 src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java rename src/dorkbox/systemTray/swingUI/{SwingEntrySeparator.java => SwingMenuItemSeparator.java} (57%) create mode 100644 src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java create mode 100644 src/dorkbox/systemTray/util/EntryHook.java create mode 100644 src/dorkbox/systemTray/util/MenuCheckboxHook.java create mode 100644 src/dorkbox/systemTray/util/MenuHook.java create mode 100644 src/dorkbox/systemTray/util/MenuItemHook.java create mode 100644 src/dorkbox/systemTray/util/MenuStatusHook.java create mode 100644 src/dorkbox/systemTray/util/Status.java diff --git a/src/dorkbox/systemTray/Status.java b/src/dorkbox/systemTray/Status.java deleted file mode 100644 index f6506da..0000000 --- a/src/dorkbox/systemTray/Status.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2015 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dorkbox.systemTray; - -/** - * This represents a common menu-status entry, that is cross platform in nature - */ -public -interface Status { -} diff --git a/src/dorkbox/systemTray/swingUI/SwingEntry.java b/src/dorkbox/systemTray/swingUI/SwingEntry.java deleted file mode 100644 index 495d582..0000000 --- a/src/dorkbox/systemTray/swingUI/SwingEntry.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright 2014 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.systemTray.swingUI; - -import java.io.File; -import java.io.InputStream; -import java.net.URL; - -import javax.swing.JComponent; -import javax.swing.JMenuItem; - -import dorkbox.systemTray.Entry; -import dorkbox.systemTray.Menu; -import dorkbox.systemTray.util.ImageUtils; -import dorkbox.systemTray.util.MenuBase; -import dorkbox.systemTray.util.SystemTrayFixes; - -abstract -class SwingEntry implements Entry, SwingUI { - private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement(); - - private final SwingMenu parent; - final JComponent _native; - - // this have to be volatile, because they can be changed from any thread - private volatile String text; - - // this is ALWAYS called on the EDT. - SwingEntry(final SwingMenu parent, final JComponent menuItem) { - this.parent = parent; - this._native = menuItem; - - parent._native.add(menuItem); - } - - @Override - public - Menu getParent() { - return parent; - } - - /** - * must always be called in the EDT thread - */ - abstract - void renderText(final String text); - - /** - * Not always called on the EDT thread - */ - abstract - void setImage_(final File imageFile); - - /** - * Enables, or disables the sub-menu entry. - */ - @Override - public - void setEnabled(final boolean enabled) { - _native.setEnabled(enabled); - } - - @Override - public - void setShortcut(final char key) { - if (_native instanceof JMenuItem) { - // yikes... - final int vKey = SystemTrayFixes.getVirtualKey(key); - - parent.dispatch(new Runnable() { - @Override - public - void run() { - ((JMenuItem) _native).setMnemonic(vKey); - } - }); - } - } - - @Override - public - String getText() { - return text; - } - - @Override - public - void setText(final String newText) { - this.text = newText; - - parent.dispatch(new Runnable() { - @Override - public - void run() { - renderText(newText); - } - }); - } - - @Override - public - void setImage(final File imageFile) { - if (imageFile == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile)); - } - } - - @Override - public final - void setImage(final String imagePath) { - if (imagePath == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath)); - } - } - - @Override - public final - void setImage(final URL imageUrl) { - if (imageUrl == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl)); - } - } - - @Override - public final - void setImage(final String cacheName, final InputStream imageStream) { - if (imageStream == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream)); - } - } - - @Override - public final - void setImage(final InputStream imageStream) { - if (imageStream == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream)); - } - } - - @Override - public final - void remove() { - parent.dispatchAndWait(new Runnable() { - @Override - public - void run() { - removePrivate(); - parent._native.remove(_native); - } - }); - } - - // called when this item is removed. Necessary to cleanup/remove itself - abstract - void removePrivate(); - - @Override - public final - int hashCode() { - return id; - } - - - @Override - public final - boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - - SwingEntry other = (SwingEntry) obj; - return this.id == other.id; - } -} diff --git a/src/dorkbox/systemTray/swingUI/SwingEntryCheckbox.java b/src/dorkbox/systemTray/swingUI/SwingEntryCheckbox.java deleted file mode 100644 index 7df49d1..0000000 --- a/src/dorkbox/systemTray/swingUI/SwingEntryCheckbox.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2014 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.systemTray.swingUI; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; - -import javax.swing.ImageIcon; -import javax.swing.JMenuItem; - -import dorkbox.systemTray.Checkbox; -import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.util.ImageUtils; - -class SwingEntryCheckbox extends SwingEntry implements Checkbox { - - private final ActionListener swingCallback; - - private volatile ActionListener callback; - - private static ImageIcon checkedIcon; - private static ImageIcon uncheckedIcon; - private volatile boolean isChecked = false; - - - // this is ALWAYS called on the EDT. - SwingEntryCheckbox(final SwingMenu parent, final ActionListener callback) { - super(parent, new AdjustedJMenuItem()); - this.callback = callback; - - if (checkedIcon == null) { - // from Brankic1979, public domain - File checkedFile = ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, ImageUtils.class.getResource("checked_32.png")); - checkedIcon = new ImageIcon(checkedFile.getAbsolutePath()); - - File uncheckedFile = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); - uncheckedIcon = new ImageIcon(uncheckedFile.getAbsolutePath()); - } - - ((JMenuItem) _native).setIcon(uncheckedIcon); - - if (callback != null) { - _native.setEnabled(true); - swingCallback = new ActionListener() { - @Override - public - void actionPerformed(ActionEvent e) { - // we want it to run on the EDT - if (isChecked) { - ((JMenuItem) _native).setIcon(uncheckedIcon); - } else { - ((JMenuItem) _native).setIcon(checkedIcon); - } - isChecked = !isChecked; - - handle(); - } - }; - - ((JMenuItem) _native).addActionListener(swingCallback); - } else { - _native.setEnabled(false); - swingCallback = null; - } - } - - /** - * @return true if this checkbox is selected, false if not - */ - public - boolean getState() { - return isChecked; - } - - @Override - public - void setCallback(final ActionListener callback) { - this.callback = callback; - } - - private - void handle() { - ActionListener cb = this.callback; - if (cb != null) { - try { - cb.actionPerformed(new ActionEvent((Checkbox)this, ActionEvent.ACTION_PERFORMED, "")); - } catch (Throwable throwable) { - SystemTray.logger.error("Error calling menu entry {} click event.", getText(), throwable); - } - } - } - - // checkbox image is always present - @Override - public - boolean hasImage() { - return true; - } - - @Override - void removePrivate() { - ((JMenuItem) _native).removeActionListener(swingCallback); - } - - // always called in the EDT - @Override - void renderText(final String text) { - ((JMenuItem) _native).setText(text); - } - - @Override - void setImage_(final File imageFile) { - } -} diff --git a/src/dorkbox/systemTray/swingUI/SwingEntryItem.java b/src/dorkbox/systemTray/swingUI/SwingEntryItem.java deleted file mode 100644 index 0af6d26..0000000 --- a/src/dorkbox/systemTray/swingUI/SwingEntryItem.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2014 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.systemTray.swingUI; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; - -import javax.swing.ImageIcon; -import javax.swing.JMenuItem; - -import dorkbox.systemTray.SystemTray; -import dorkbox.util.SwingUtil; - -class SwingEntryItem extends SwingEntry { - - private final ActionListener swingCallback; - - private volatile boolean hasLegitIcon = false; - private volatile ActionListener callback; - - // this is ALWAYS called on the EDT. - SwingEntryItem(final SwingMenu parent, final ActionListener callback) { - super(parent, new AdjustedJMenuItem()); - this.callback = callback; - - - if (callback != null) { - _native.setEnabled(true); - swingCallback = new ActionListener() { - @Override - public - void actionPerformed(ActionEvent e) { - // we want it to run on the EDT - handle(); - } - }; - - ((JMenuItem) _native).addActionListener(swingCallback); - } else { - _native.setEnabled(false); - swingCallback = null; - } - } - - @Override - public - void setCallback(final ActionListener callback) { - this.callback = callback; - } - - private - void handle() { - ActionListener cb = this.callback; - if (cb != null) { - try { - cb.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")); - } catch (Throwable throwable) { - SystemTray.logger.error("Error calling menu entry {} click event.", getText(), throwable); - } - } - } - - @Override - public - boolean hasImage() { - return hasLegitIcon; - } - - @Override - void removePrivate() { - ((JMenuItem) _native).removeActionListener(swingCallback); - } - - // always called in the EDT - @Override - void renderText(final String text) { - ((JMenuItem) _native).setText(text); - } - - @SuppressWarnings("Duplicates") - @Override - void setImage_(final File imageFile) { - hasLegitIcon = imageFile != null; - - SwingUtil.invokeLater(new Runnable() { - @Override - public - void run() { - if (imageFile != null) { - ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); - ((JMenuItem) _native).setIcon(origIcon); - } - else { - ((JMenuItem) _native).setIcon(null); - } - } - }); - } -} diff --git a/src/dorkbox/systemTray/swingUI/SwingEntryStatus.java b/src/dorkbox/systemTray/swingUI/SwingEntryStatus.java deleted file mode 100644 index 6a5e24e..0000000 --- a/src/dorkbox/systemTray/swingUI/SwingEntryStatus.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2014 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.systemTray.swingUI; - -import java.awt.Font; -import java.awt.event.ActionListener; -import java.io.File; - -import javax.swing.JMenuItem; - -import dorkbox.systemTray.Status; - -class SwingEntryStatus extends SwingEntry implements Status { - - // this is ALWAYS called on the EDT. - SwingEntryStatus(final SwingMenu parent, final String label) { - super(parent, new JMenuItem()); - setText(label); - } - - // called in the EDT thread - @Override - void renderText(final String text) { - Font font = _native.getFont(); - Font font1 = font.deriveFont(Font.BOLD); - _native.setFont(font1); - - ((JMenuItem) _native).setText(text); - - // this makes sure it can't be selected - _native.setEnabled(false); - } - - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - } - - @Override - public - void setShortcut(final char key) { - } - - @Override - public - boolean hasImage() { - return false; - } - - @Override - public - void setCallback(final ActionListener callback) { - - } -} diff --git a/src/dorkbox/systemTray/swingUI/SwingEntryWidget.java b/src/dorkbox/systemTray/swingUI/SwingEntryWidget.java deleted file mode 100644 index 9e58078..0000000 --- a/src/dorkbox/systemTray/swingUI/SwingEntryWidget.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2016 dorkbox, llc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dorkbox.systemTray.swingUI; - -import java.awt.event.ActionListener; -import java.io.File; - -import javax.swing.JComponent; - -// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. -class SwingEntryWidget extends SwingEntry implements dorkbox.systemTray.Separator { - - // this is ALWAYS called on the EDT. - SwingEntryWidget(final SwingMenu parent, JComponent widget) { - super(parent, widget); - - _native.setEnabled(true); - } - - // called in the EDT thread - @Override - void renderText(final String text) { - } - - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - } - - @Override - public - void setShortcut(final char key) { - } - - @Override - public - boolean hasImage() { - return false; - } - - @Override - public - void setCallback(final ActionListener callback) { - } -} diff --git a/src/dorkbox/systemTray/swingUI/SwingMenu.java b/src/dorkbox/systemTray/swingUI/SwingMenu.java index be281be..3356389 100644 --- a/src/dorkbox/systemTray/swingUI/SwingMenu.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenu.java @@ -16,55 +16,51 @@ package dorkbox.systemTray.swingUI; -import java.awt.event.ActionListener; import java.io.File; -import java.util.concurrent.atomic.AtomicReference; import javax.swing.ImageIcon; import javax.swing.JComponent; -import javax.swing.JMenuItem; import dorkbox.systemTray.Checkbox; import dorkbox.systemTray.Entry; import dorkbox.systemTray.Menu; -import dorkbox.systemTray.Status; +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.Separator; import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.util.MenuBase; +import dorkbox.systemTray.util.MenuHook; +import dorkbox.systemTray.util.Status; import dorkbox.systemTray.util.SystemTrayFixes; import dorkbox.util.SwingUtil; -// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both +// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both (and duplicate code) @SuppressWarnings("ForLoopReplaceableByForEach") -class SwingMenu extends MenuBase implements SwingUI { +class SwingMenu implements MenuHook { - // sub-menu = AdjustedJMenu - // systemtray = TrayPopup - volatile JComponent _native; + private final SwingMenu parent; + final JComponent _native; - // this have to be volatile, because they can be changed from any thread - private volatile String text; private volatile boolean hasLegitIcon = false; - /** - * Called in the EDT - * - * @param systemTray the system tray (which is the object that sits in the system tray) - * @param parent the parent of this menu, null if the parent is the system tray - * @param _native the native element that represents this menu - */ - SwingMenu(final SystemTray systemTray, final Menu parent, final JComponent _native) { - super(systemTray, parent); - this._native = _native; + // This is NOT a copy constructor! + @SuppressWarnings("IncompleteCopyConstructor") + SwingMenu(final SwingMenu parent) { + this.parent = parent; + + if (parent == null) { + this._native = new TrayPopup(); + } + else { + this._native = new AdjustedJMenu(); + parent._native.add(this._native); + } } - @Override protected final void dispatch(final Runnable runnable) { // this will properly check if we are running on the EDT SwingUtil.invokeLater(runnable); } - @Override protected final void dispatchAndWait(final Runnable runnable) { // this will properly check if we are running on the EDT @@ -75,131 +71,16 @@ class SwingMenu extends MenuBase implements SwingUI { } } - // always called in the EDT - protected final - void renderText(final String text) { - ((JMenuItem) _native).setText(text); - } - - @Override - public final - String getText() { - return text; - } - - @Override - public final - void setText(final String newText) { - text = newText; - dispatch(new Runnable() { - @Override - public - void run() { - renderText(newText); - } - }); - } - - /** - * Will add a new menu entry - * NOT ALWAYS CALLED ON EDT - */ - protected final - Entry addEntry_(final String menuText, final File imagePath, final ActionListener callback) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference value = new AtomicReference(); - - // must always be called on the EDT - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = new SwingEntryItem(SwingMenu.this, callback); - entry.setText(menuText); - entry.setImage(imagePath); - - menuEntries.add(entry); - value.set(entry); - } - } - }); - - return value.get(); - } - - /** - * Will add a new checkbox menu entry - * NOT ALWAYS CALLED ON DISPATCH - */ - @Override - protected - Checkbox addCheckbox_(final String menuText, final ActionListener callback) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference value = new AtomicReference(); - - // must always be called on the EDT - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = new SwingEntryCheckbox(SwingMenu.this, callback); - entry.setText(menuText); - - menuEntries.add(entry); - value.set((Checkbox) entry); - } - } - }); - - return value.get(); - } - - /** - * Will add a new sub-menu entry - * NOT ALWAYS CALLED ON EDT - */ - protected final - Menu addMenu_(final String menuText, final File imagePath) { - if (menuText == null) { - throw new NullPointerException("Menu text cannot be null"); - } - - final AtomicReference value = new AtomicReference(); - - // must always be called on the EDT - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = new SwingMenu(getSystemTray(), SwingMenu.this, new AdjustedJMenu()); - _native.add(((SwingMenu) entry)._native); // have to add it separately - - entry.setText(menuText); - entry.setImage(imagePath); - - menuEntries.add(entry); - value.set((Menu) entry); - } - } - }); - - return value.get(); - } - - - - // public here so that Swing/Gtk/AppIndicator can override this public - void setImage_(final File imageFile) { + boolean hasImage() { + return hasLegitIcon; + } + + // is overridden in tray impl + @Override + public + void setImage(final MenuItem menuItem) { + final File imageFile = menuItem.getImage(); hasLegitIcon = imageFile != null; dispatch(new Runnable() { @@ -208,156 +89,115 @@ class SwingMenu extends MenuBase implements SwingUI { void run() { if (imageFile != null) { ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); - ((JMenuItem) _native).setIcon(origIcon); + ((AdjustedJMenu) _native).setIcon(origIcon); } else { - ((JMenuItem) _native).setIcon(null); + ((AdjustedJMenu) _native).setIcon(null); } } }); } - - - - - - + // is overridden in tray impl @Override public - boolean hasImage() { - return hasLegitIcon; - } - - // public here so that Swing/Gtk/AppIndicator can override this - @Override - public - void setEnabled(final boolean enabled) { + void setEnabled(final MenuItem menuItem) { dispatch(new Runnable() { @Override public void run() { - _native.setEnabled(enabled); + _native.setEnabled(menuItem.getEnabled()); } }); } + // is overridden in tray impl + @Override + public + void setText(final MenuItem menuItem) { + dispatch(new Runnable() { + @Override + public + void run() { + ((AdjustedJMenu) _native).setText(menuItem.getText()); + } + }); + } + + @Override + public + void setCallback(final MenuItem menuItem) { + // can't have a callback for menus! + } + + // is overridden in tray impl + @Override + public + void setShortcut(final MenuItem menuItem) { + char shortcut = menuItem.getShortcut(); + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(shortcut); + + dispatch(new Runnable() { + @Override + public + void run() { + ((AdjustedJMenu) _native).setMnemonic(vKey); + } + }); + } + + @Override + public + void add(final Menu parentMenu, final Entry entry, final int index) { + // must always be called on the EDT + dispatch(new Runnable() { + @Override + public + void run() { + if (entry instanceof Menu) { + SwingMenu swingMenu = new SwingMenu(SwingMenu.this); + ((Menu) entry).bind(swingMenu, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Separator) { + SwingMenuItemSeparator swingEntrySeparator = new SwingMenuItemSeparator(SwingMenu.this); + entry.bind(swingEntrySeparator, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Checkbox) { + SwingMenuItemCheckbox swingEntryCheckbox = new SwingMenuItemCheckbox(SwingMenu.this); + ((Checkbox) entry).bind(swingEntryCheckbox, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof Status) { + SwingMenuItemStatus swingEntryStatus = new SwingMenuItemStatus(SwingMenu.this); + ((Status) entry).bind(swingEntryStatus, parentMenu, parentMenu.getSystemTray()); + } + else if (entry instanceof MenuItem) { + SwingMenuItem swingMenuItem = new SwingMenuItem(SwingMenu.this); + ((MenuItem) entry).bind(swingMenuItem, parentMenu, parentMenu.getSystemTray()); + } + } + }); + } + /** - * NOT ALWAYS CALLED ON EDT + * This removes all menu entries from this menu AND this menu from it's parent */ @Override - public final - void addSeparator() { - dispatch(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - synchronized (menuEntries) { - Entry entry = new SwingEntrySeparator(SwingMenu.this); - menuEntries.add(entry); - } - } - } - }); - } - -// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. -// public -// Entry addWidget(final JComponent widget) { -// if (widget == null) { -// throw new NullPointerException("Widget cannot be null"); -// } -// -// final AtomicReference value = new AtomicReference(); -// -// dispatchAndWait(new Runnable() { -// @Override -// public -// void run() { -// synchronized (menuEntries) { -// // must always be called on the EDT -// Entry entry = new SwingEntryWidget(SwingMenu.this, widget); -// value.set(entry); -// menuEntries.add(entry); -// } -// } -// }); -// -// return value.get(); -// } - - - // public here so that Swing/Gtk/AppIndicator can access this - public final - void setStatus(final String statusText) { - final SwingMenu _this = this; - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - // status is ALWAYS at 0 index... - SwingEntry menuEntry = null; - if (!menuEntries.isEmpty()) { - menuEntry = (SwingEntry) menuEntries.get(0); - } - - if (menuEntry instanceof Status) { - // set the text or delete... - - if (statusText == null) { - // delete - remove(menuEntry); - } - else { - // set text - menuEntry.setText(statusText); - } - - } else { - // create a new one - menuEntry = new SwingEntryStatus(_this, statusText); - // status is ALWAYS at 0 index... - menuEntries.add(0, menuEntry); - } - } - } - }); - } - - @Override - public final - void setShortcut(final char key) { - if (_native instanceof JMenuItem) { - // yikes... - final int vKey = SystemTrayFixes.getVirtualKey(key); - dispatch(new Runnable() { - @Override - public - void run() { - ((JMenuItem) _native).setMnemonic(vKey); - } - }); - } - } - - @Override - public final + public synchronized void remove() { - dispatchAndWait(new Runnable() { + dispatch(new Runnable() { @Override public void run() { _native.setVisible(false); - if (_native instanceof TrayPopup) { - ((TrayPopup) _native).close(); - } + _native.removeAll(); - SwingMenu parent = (SwingMenu) getParent(); if (parent != null) { parent._native.remove(_native); + } else { + // have to dispose of the tray popup hidden frame, otherwise the app will never close (because this will hold it open) + ((TrayPopup) _native).close(); } } }); diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItem.java b/src/dorkbox/systemTray/swingUI/SwingMenuItem.java new file mode 100644 index 0000000..3383983 --- /dev/null +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItem.java @@ -0,0 +1,161 @@ +/* + * Copyright 2014 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.systemTray.swingUI; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; + +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.util.MenuItemHook; +import dorkbox.systemTray.util.SystemTrayFixes; +import dorkbox.util.SwingUtil; + +class SwingMenuItem implements MenuItemHook { + + private final SwingMenu parent; + private final JMenuItem _native = new AdjustedJMenuItem(); + + private volatile boolean hasLegitIcon = false; + private volatile ActionListener swingCallback; + + // this is ALWAYS called on the EDT. + SwingMenuItem(final SwingMenu parent) { + this.parent = parent; + parent._native.add(_native); + } + + public + boolean hasImage() { + return hasLegitIcon; + } + + @Override + public + void setImage(final MenuItem menuItem) { + final File imageFile = menuItem.getImage(); + hasLegitIcon = imageFile != null; + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (imageFile != null) { + ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); + _native.setIcon(origIcon); + } + else { + _native.setIcon(null); + } + } + }); + } + + @Override + public + void setEnabled(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setEnabled(menuItem.getEnabled()); + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setText(menuItem.getText()); + } + }); + } + + @Override + public + void setCallback(final MenuItem menuItem) { + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + } + + if (menuItem.getCallback() != null) { + _native.setEnabled(true); + swingCallback = new ActionListener() { + @Override + public + void actionPerformed(ActionEvent e) { + // we want it to run on the EDT, but with our own action event info (so it is consistent across all platforms) + ActionListener cb = menuItem.getCallback(); + if (cb != null) { + try { + cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, "")); + } catch (Throwable throwable) { + SystemTray.logger.error("Error calling menu entry {} click event.", menuItem.getText(), throwable); + } + } + } + }; + + _native.addActionListener(swingCallback); + } + else { + _native.setEnabled(false); + swingCallback = null; + } + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + char shortcut = menuItem.getShortcut(); + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(shortcut); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setMnemonic(vKey); + } + }); + } + + @Override + public + void remove() { + //noinspection Duplicates + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + swingCallback = null; + } + parent._native.remove(_native); + _native.removeAll(); + } + }); + } +} diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java b/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java new file mode 100644 index 0000000..3ce13d6 --- /dev/null +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItemCheckbox.java @@ -0,0 +1,171 @@ +/* + * Copyright 2014 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.systemTray.swingUI; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.ImageIcon; +import javax.swing.JMenuItem; + +import dorkbox.systemTray.Checkbox; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.util.ImageUtils; +import dorkbox.systemTray.util.MenuCheckboxHook; +import dorkbox.systemTray.util.SystemTrayFixes; +import dorkbox.util.SwingUtil; + +class SwingMenuItemCheckbox implements MenuCheckboxHook { + + private final SwingMenu parent; + private final JMenuItem _native = new AdjustedJMenuItem(); + + private volatile boolean isChecked = false; + + private volatile ActionListener swingCallback; + + private static ImageIcon checkedIcon; + private static ImageIcon uncheckedIcon; + + // this is ALWAYS called on the EDT. + SwingMenuItemCheckbox(final SwingMenu parent) { + this.parent = parent; + parent._native.add(_native); + + if (checkedIcon == null) { + // from Brankic1979, public domain + File checkedFile = ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, ImageUtils.class.getResource("checked_32.png"), true); + checkedIcon = new ImageIcon(checkedFile.getAbsolutePath()); + + File uncheckedFile = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); + uncheckedIcon = new ImageIcon(uncheckedFile.getAbsolutePath()); + } + } + + // checkbox image is always present + public + boolean hasImage() { + return true; + } + + + @Override + public + void setEnabled(final Checkbox menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setEnabled(menuItem.getEnabled()); + } + }); + } + + @Override + public + void setText(final Checkbox menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setText(menuItem.getText()); + } + }); + } + + @Override + public + void setCallback(final Checkbox menuItem) { + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + } + + swingCallback = new ActionListener() { + @Override + public + void actionPerformed(ActionEvent e) { + // this will run on the EDT, since we are calling it from the EDT + menuItem.setChecked(!isChecked); + + // we want it to run on the EDT, but with our own action event info (so it is consistent across all platforms) + ActionListener cb = menuItem.getCallback(); + if (cb != null) { + try { + cb.actionPerformed(new ActionEvent(menuItem, ActionEvent.ACTION_PERFORMED, "")); + } catch (Throwable throwable) { + SystemTray.logger.error("Error calling menu entry {} click event.", menuItem.getText(), throwable); + } + } + } + }; + + _native.addActionListener(swingCallback); + } + + @Override + public + void setShortcut(final Checkbox menuItem) { + char shortcut = menuItem.getShortcut(); + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(shortcut); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + _native.setMnemonic(vKey); + } + }); + } + + @Override + public + void setChecked(final Checkbox menuItem) { + this.isChecked = menuItem.getChecked(); + + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (isChecked) { + _native.setIcon(checkedIcon); + } else { + _native.setIcon(uncheckedIcon); + } + } + }); + } + + @Override + public + void remove() { + //noinspection Duplicates + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + if (swingCallback != null) { + _native.removeActionListener(swingCallback); + swingCallback = null; + } + + parent._native.remove(_native); + _native.removeAll(); + } + }); + } +} diff --git a/src/dorkbox/systemTray/swingUI/SwingEntrySeparator.java b/src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java similarity index 57% rename from src/dorkbox/systemTray/swingUI/SwingEntrySeparator.java rename to src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java index d5a23ee..3afa4b0 100644 --- a/src/dorkbox/systemTray/swingUI/SwingEntrySeparator.java +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItemSeparator.java @@ -15,37 +15,22 @@ */ package dorkbox.systemTray.swingUI; -import java.awt.event.ActionListener; -import java.io.File; - import javax.swing.JSeparator; -class SwingEntrySeparator extends SwingEntry implements dorkbox.systemTray.Separator { +import dorkbox.systemTray.util.EntryHook; +import dorkbox.util.SwingUtil; + +class SwingMenuItemSeparator implements EntryHook { + + private final SwingMenu parent; + private final JSeparator _native = new JSeparator(JSeparator.HORIZONTAL); // this is ALWAYS called on the EDT. - SwingEntrySeparator(final SwingMenu parent) { - super(parent, new JSeparator(JSeparator.HORIZONTAL)); + SwingMenuItemSeparator(final SwingMenu parent) { + this.parent = parent; + parent._native.add(_native); } - // called in the EDT thread - @Override - void renderText(final String text) { - } - - @Override - void setImage_(final File imageFile) { - } - - @Override - void removePrivate() { - } - - @Override - public - void setShortcut(final char key) { - } - - @Override public boolean hasImage() { return false; @@ -53,6 +38,14 @@ class SwingEntrySeparator extends SwingEntry implements dorkbox.systemTray.Separ @Override public - void setCallback(final ActionListener callback) { + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + parent._native.remove(_native); + _native.removeAll(); + } + }); } } diff --git a/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java b/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java new file mode 100644 index 0000000..1630f58 --- /dev/null +++ b/src/dorkbox/systemTray/swingUI/SwingMenuItemStatus.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dorkbox.systemTray.swingUI; + +import java.awt.Font; + +import javax.swing.JMenuItem; + +import dorkbox.systemTray.util.MenuStatusHook; +import dorkbox.systemTray.util.Status; +import dorkbox.util.SwingUtil; + +class SwingMenuItemStatus implements MenuStatusHook { + + private final SwingMenu parent; + private final JMenuItem _native = new AdjustedJMenuItem(); + + // this is ALWAYS called on the EDT. + SwingMenuItemStatus(final SwingMenu parent) { + this.parent = parent; + + // status is ALWAYS at 0 index... + parent._native.add(_native, 0); + } + + public + boolean hasImage() { + return false; + } + + + @Override + public + void setText(final Status menuItem) { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + Font font = _native.getFont(); + Font font1 = font.deriveFont(Font.BOLD); + _native.setFont(font1); + + _native.setText(menuItem.getText()); + + // this makes sure it can't be selected + _native.setEnabled(false); + } + }); + } + + @Override + public + void remove() { + SwingUtil.invokeLater(new Runnable() { + @Override + public + void run() { + parent._native.remove(_native); + _native.removeAll(); + } + }); + } +} diff --git a/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java b/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java index 0053948..63f54c1 100644 --- a/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java +++ b/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java @@ -24,7 +24,9 @@ import com.sun.jna.NativeLong; import com.sun.jna.Pointer; import com.sun.jna.ptr.PointerByReference; +import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.Tray; import dorkbox.systemTray.jna.linux.AppIndicator; import dorkbox.systemTray.jna.linux.AppIndicatorInstanceStruct; import dorkbox.systemTray.jna.linux.GEventCallback; @@ -81,7 +83,7 @@ import dorkbox.util.SwingUtil; */ @SuppressWarnings("Duplicates") public -class _AppIndicatorTray extends SwingMenu { +class _AppIndicatorTray extends Tray implements SwingUI { private volatile AppIndicatorInstanceStruct appIndicator; private boolean isActive = false; private final Runnable popupRunnable; @@ -101,6 +103,7 @@ class _AppIndicatorTray extends SwingMenu { // is the system tray visible or not. private volatile boolean visible = true; + private volatile File image; // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) // they ALSO do not support tooltips, so we cater to the lowest common denominator @@ -108,9 +111,109 @@ class _AppIndicatorTray extends SwingMenu { public _AppIndicatorTray(final SystemTray systemTray) { - super(systemTray,null, new TrayPopup()); + super(); - TrayPopup popupMenu = (TrayPopup) _native; + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final SwingMenu swingMenu = new SwingMenu(null) { + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); + + if (visible && !enabled) { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); + visible = false; + } + else if (!visible && enabled) { + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + visible = true; + } + } + }); + } + + @Override + public + void setImage(final MenuItem menuItem) { + image = menuItem.getImage(); + if (image == null) { + return; + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + AppIndicator.app_indicator_set_icon(appIndicator, image.getAbsolutePath()); + + if (!isActive) { + isActive = true; + + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + + // now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set + hookMenuOpen(); + } + } + }); + + + // needs to be on EDT + dispatch(new Runnable() { + @Override + public + void run() { + ((TrayPopup) _native).setTitleBarImage(image); + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + if (!shuttingDown.getAndSet(true)) { + // must happen asap, so our hook properly notices we are in shutdown mode + final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; + appIndicator = null; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); + Pointer p = savedAppIndicator.getPointer(); + Gobject.g_object_unref(p); + } + }); + + // does not need to be called on the dispatch (it does that) + Gtk.shutdownGui(); + + super.remove(); + } + } + }; + + + TrayPopup popupMenu = (TrayPopup) swingMenu._native; popupMenu.pack(); popupMenu.setFocusable(true); popupMenu.setOnHideRunnable(new Runnable() { @@ -123,7 +226,7 @@ class _AppIndicatorTray extends SwingMenu { } // Such ugly hacks to get AppIndicator support properly working. This is so horrible I am ashamed. - Gtk.dispatch(new Runnable() { + Gtk.dispatchAndWait(new Runnable() { @Override public void run() { @@ -141,7 +244,7 @@ class _AppIndicatorTray extends SwingMenu { Point point = MouseInfo.getPointerInfo() .getLocation(); - TrayPopup popupMenu = (TrayPopup) _native; + TrayPopup popupMenu = (TrayPopup) swingMenu._native; popupMenu.doShow(point, SystemTray.DEFAULT_TRAY_SIZE); } }; @@ -162,6 +265,8 @@ class _AppIndicatorTray extends SwingMenu { }); Gtk.waitForStartup(); + + bind(swingMenu, null, systemTray); } private @@ -195,87 +300,9 @@ class _AppIndicatorTray extends SwingMenu { AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu); } - public final - void shutdown() { - if (!shuttingDown.getAndSet(true)) { - // must happen asap, so our hook properly notices we are in shutdown mode - final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; - appIndicator = null; - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); - Pointer p = savedAppIndicator.getPointer(); - Gobject.g_object_unref(p); - } - }); - - // does not need to be called on the dispatch (it does that) - Gtk.shutdownGui(); - - // uses EDT - removeAll(); - remove(); // remove ourselves from our parent - } - } - @Override public final boolean hasImage() { - return true; - } - - @Override - public final - void setImage_(final File imageFile) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - AppIndicator.app_indicator_set_icon(appIndicator, imageFile.getAbsolutePath()); - - if (!isActive) { - isActive = true; - - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); - - // now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set - hookMenuOpen(); - } - } - }); - - - // needs to be on EDT - dispatch(new Runnable() { - @Override - public - void run() { - ((TrayPopup) _native).setTitleBarImage(imageFile); - } - }); - } - - @Override - public final - void setEnabled(final boolean setEnabled) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - if (visible && !setEnabled) { - // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); - visible = false; - } - else if (!visible && setEnabled) { - AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); - visible = true; - } - } - }); + return image != null; } } diff --git a/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java b/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java index 5f95fc5..2356737 100644 --- a/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java +++ b/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java @@ -27,7 +27,9 @@ import javax.swing.JPopupMenu; import com.sun.jna.NativeLong; import com.sun.jna.Pointer; +import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.Tray; import dorkbox.systemTray.jna.linux.GEventCallback; import dorkbox.systemTray.jna.linux.GdkEventButton; import dorkbox.systemTray.jna.linux.Gobject; @@ -41,7 +43,7 @@ import dorkbox.systemTray.jna.linux.Gtk; */ @SuppressWarnings("Duplicates") public -class _GtkStatusIconTray extends SwingMenu { +class _GtkStatusIconTray extends Tray implements SwingUI { private volatile Pointer trayIcon; // http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c @@ -50,20 +52,114 @@ class _GtkStatusIconTray extends SwingMenu { // have to save these in a field to prevent GC on the objects (since they go out-of-scope from java) private final List gtkCallbacks = new ArrayList(); - // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) private AtomicBoolean shuttingDown = new AtomicBoolean(); private volatile boolean isActive = false; // is the system tray visible or not. private volatile boolean visible = true; + private volatile File image; // called on the EDT public _GtkStatusIconTray(final SystemTray systemTray) { - super(systemTray, null, new TrayPopup()); + super(); - JPopupMenu popupMenu = (JPopupMenu) _native; + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final SwingMenu swingMenu = new SwingMenu(null) { + @Override + public + void setEnabled(final MenuItem menuItem) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); + + if (visible && !enabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, enabled); + visible = false; + } + else if (!visible && enabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, enabled); + visible = true; + } + } + }); + } + + @Override + public + void setImage(final MenuItem menuItem) { + image = menuItem.getImage(); + if (image == null) { + return; + } + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_status_icon_set_from_file(trayIcon, image.getAbsolutePath()); + + if (!isActive) { + isActive = true; + Gtk.gtk_status_icon_set_visible(trayIcon, true); + } + } + }); + + // needs to be on EDT + dispatch(new Runnable() { + @Override + public + void run() { + ((TrayPopup) _native).setTitleBarImage(image); + } + }); + } + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) + if (!shuttingDown.getAndSet(true)) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // this hides the indicator + Gtk.gtk_status_icon_set_visible(trayIcon, false); + Gobject.g_object_unref(trayIcon); + + // mark for GC + trayIcon = null; + gtkCallbacks.clear(); + } + }); + + // does not need to be called on the dispatch (it does that) + Gtk.shutdownGui(); + + super.remove(); + } + } + }; + + + JPopupMenu popupMenu = (JPopupMenu) swingMenu._native; popupMenu.pack(); popupMenu.setFocusable(true); @@ -74,15 +170,11 @@ class _GtkStatusIconTray extends SwingMenu { Point point = MouseInfo.getPointerInfo() .getLocation(); - TrayPopup popupMenu = (TrayPopup) _native; + TrayPopup popupMenu = (TrayPopup) swingMenu._native; popupMenu.doShow(point, 0); } }; - // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) - // they ALSO do not support tooltips, so we cater to the lowest common denominator - // trayIcon.setToolTip("app name"); - Gtk.startGui(); Gtk.dispatch(new Runnable() { @@ -100,7 +192,7 @@ class _GtkStatusIconTray extends SwingMenu { // BUTTON_PRESS only (any mouse click) if (event.type == 4) { // show the swing menu on the EDT - dispatch(popupRunnable); + swingMenu.dispatch(popupRunnable); } } }; @@ -140,76 +232,13 @@ class _GtkStatusIconTray extends SwingMenu { } } }); + + bind(swingMenu, null, systemTray); } - - @SuppressWarnings("FieldRepeatedlyAccessedInMethod") + @Override public - void shutdown() { - if (!shuttingDown.getAndSet(true)) { - - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - // this hides the indicator - Gtk.gtk_status_icon_set_visible(trayIcon, false); - Gobject.g_object_unref(trayIcon); - - // mark for GC - trayIcon = null; - gtkCallbacks.clear(); - } - }); - - // does not need to be called on the dispatch (it does that) - Gtk.shutdownGui(); - - // uses EDT - removeAll(); - remove(); // remove ourselves from our parent - } - } - - public - void setImage_(final File iconFile) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - Gtk.gtk_status_icon_set_from_file(trayIcon, iconFile.getAbsolutePath()); - - if (!isActive) { - isActive = true; - Gtk.gtk_status_icon_set_visible(trayIcon, true); - } - } - }); - - // needs to be on EDT - dispatch(new Runnable() { - @Override - public - void run() { - ((TrayPopup) _native).setTitleBarImage(iconFile); - } - }); - } - - public - void setEnabled(final boolean setEnabled) { - Gtk.dispatch(new Runnable() { - @Override - public - void run() { - if (visible && !setEnabled) { - Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled); - visible = false; - } else if (!visible && setEnabled) { - Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled); - visible = true; - } - } - }); + boolean hasImage() { + return image != null; } } diff --git a/src/dorkbox/systemTray/swingUI/_SwingTray.java b/src/dorkbox/systemTray/swingUI/_SwingTray.java index 3a5113e..0fc493e 100644 --- a/src/dorkbox/systemTray/swingUI/_SwingTray.java +++ b/src/dorkbox/systemTray/swingUI/_SwingTray.java @@ -26,6 +26,9 @@ import java.io.File; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; +import dorkbox.systemTray.MenuItem; +import dorkbox.systemTray.Tray; + /** * Class for handling all system tray interaction, via Swing. * @@ -35,8 +38,8 @@ import javax.swing.JPopupMenu; * https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028 */ @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"}) -public -class _SwingTray extends SwingMenu { +public final +class _SwingTray extends Tray implements SwingUI { private volatile SystemTray tray; private volatile TrayIcon trayIcon; @@ -46,7 +49,7 @@ class _SwingTray extends SwingMenu { // Called in the EDT public _SwingTray(final dorkbox.systemTray.SystemTray systemTray) { - super(systemTray, null, new TrayPopup()); + super(); if (!SystemTray.isSupported()) { throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " + @@ -54,87 +57,124 @@ class _SwingTray extends SwingMenu { } _SwingTray.this.tray = SystemTray.getSystemTray(); - } - public - void shutdown() { - dispatchAndWait(new Runnable() { + // we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization. + final SwingMenu swingMenu = new SwingMenu(null) { @Override public - void run() { - removeAll(); - remove(); + void setEnabled(final MenuItem menuItem) { + dispatch(new Runnable() { + @Override + public + void run() { + boolean enabled = menuItem.getEnabled(); - tray.remove(trayIcon); - } - }); - } - - public - void setImage_(final File iconFile) { - dispatch(new Runnable() { - @Override - public - void run() { - // stupid java won't scale it right away, so we have to do this twice to get the correct size - final Image trayImage = new ImageIcon(iconFile.getAbsolutePath()).getImage(); - trayImage.flush(); - - if (trayIcon == null) { - // here we init. everything - trayIcon = new TrayIcon(trayImage); - - JPopupMenu popupMenu = (JPopupMenu) _native; - popupMenu.pack(); - popupMenu.setFocusable(true); - - // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) - // they ALSO do not support tooltips, so we cater to the lowest common denominator - // trayIcon.setToolTip("app name"); - - trayIcon.addMouseListener(new MouseAdapter() { - @Override - public - void mousePressed(MouseEvent e) { - TrayPopup popupMenu = (TrayPopup) _native; - popupMenu.doShow(e.getPoint(), 0); + if (visible && !enabled) { + tray.remove(trayIcon); + visible = false; + } + else if (!visible && enabled) { + try { + tray.add(trayIcon); + visible = true; + } catch (AWTException e) { + dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray", e); + } } - }); - - try { - tray.add(trayIcon); - } catch (AWTException e) { - dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e); } - } else { - trayIcon.setImage(trayImage); - } - - ((TrayPopup) _native).setTitleBarImage(iconFile); + }); } - }); - } - @SuppressWarnings("Duplicates") - public - void setEnabled(final boolean setEnabled) { - dispatch(new Runnable() { @Override public - void run() { - if (visible && !setEnabled) { - tray.remove(trayIcon); - visible = false; + void setImage(final MenuItem menuItem) { + final File image = menuItem.getImage(); + if (image == null) { + return; } - else if (!visible && setEnabled) { - try { - tray.add(trayIcon); - visible = true; - } catch (AWTException e) { - dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray", e); + + dispatch(new Runnable() { + @Override + public + void run() { + // stupid java won't scale it right away, so we have to do this twice to get the correct size + final Image trayImage = new ImageIcon(image.getAbsolutePath()).getImage(); + trayImage.flush(); + + if (trayIcon == null) { + // here we init. everything + trayIcon = new TrayIcon(trayImage); + + JPopupMenu popupMenu = (JPopupMenu) _native; + popupMenu.pack(); + popupMenu.setFocusable(true); + + // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) + // they ALSO do not support tooltips, so we cater to the lowest common denominator + // trayIcon.setToolTip("app name"); + + trayIcon.addMouseListener(new MouseAdapter() { + @Override + public + void mousePressed(MouseEvent e) { + TrayPopup popupMenu = (TrayPopup) _native; + popupMenu.doShow(e.getPoint(), 0); + } + }); + + try { + tray.add(trayIcon); + } catch (AWTException e) { + dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e); + } + } else { + trayIcon.setImage(trayImage); + } + + ((TrayPopup) _native).setTitleBarImage(image); } - } + }); } - }); + + @Override + public + void setText(final MenuItem menuItem) { + // no op + } + + @Override + public + void setShortcut(final MenuItem menuItem) { + // no op + } + + @Override + public + void remove() { + dispatch(new Runnable() { + @Override + public + void run() { + if (trayIcon != null) { + tray.remove(trayIcon); + trayIcon = null; + } + + tray = null; + } + }); + + super.remove(); + } + }; + + + bind(swingMenu, null, systemTray); + } + + @Override + public + boolean hasImage() { + return tray.getTrayIcons().length > 0; } } diff --git a/src/dorkbox/systemTray/util/EntryHook.java b/src/dorkbox/systemTray/util/EntryHook.java new file mode 100644 index 0000000..98b3522 --- /dev/null +++ b/src/dorkbox/systemTray/util/EntryHook.java @@ -0,0 +1,9 @@ +package dorkbox.systemTray.util; + +/** + * + */ +public +interface EntryHook { + void remove(); +} diff --git a/src/dorkbox/systemTray/util/ImageUtils.java b/src/dorkbox/systemTray/util/ImageUtils.java index 988250b..584d58a 100644 --- a/src/dorkbox/systemTray/util/ImageUtils.java +++ b/src/dorkbox/systemTray/util/ImageUtils.java @@ -24,6 +24,7 @@ import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -44,6 +45,7 @@ import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.jna.Windows.User32; import dorkbox.util.CacheUtil; import dorkbox.util.FileUtil; +import dorkbox.util.IO; import dorkbox.util.LocationResolver; import dorkbox.util.OS; import dorkbox.util.process.ShellProcessBuilder; @@ -357,12 +359,16 @@ class ImageUtils { } public static synchronized - File resizeAndCache(final int size, final File file) { - return resizeAndCache(size, file.getAbsolutePath()); + File resizeAndCache(final int size, final File file, final boolean cacheResult) { + return resizeAndCache(size, file.getAbsolutePath(), cacheResult); } public static synchronized - File resizeAndCache(final int size, final String fileName) { + File resizeAndCache(final int size, final String fileName, final boolean cacheResult) { + if (fileName == null) { + return null; + } + // check if we already have this file information saved to disk, based on size final String cacheName = size + "_" + fileName; @@ -393,7 +399,11 @@ class ImageUtils { @SuppressWarnings("Duplicates") public static synchronized - File resizeAndCache(final int size, final URL imageUrl) { + File resizeAndCache(final int size, final URL imageUrl, final boolean cacheResult) { + if (imageUrl == null) { + return null; + } + final String cacheName = size + "_" + imageUrl.getPath(); // if we already have this fileName, reuse it @@ -449,16 +459,90 @@ class ImageUtils { } } } + @SuppressWarnings("Duplicates") + public static synchronized + File resizeAndCache(final int size, final Image image, final boolean cacheResult) { + if (image == null) { + return null; + } + + +// final String cacheName = size + "_" + imageUrl.getPath(); +// +// // if we already have this fileName, reuse it +// final File check = getIfCachedOrError(cacheName); +// if (check != null) { +// return check; +// } +// +// // no cached file, so we resize then save the new one. +// boolean needsResize = true; +// try { +// InputStream inputStream = imageUrl.openStream(); +// Dimension imageSize = getImageSize(inputStream); +// //noinspection NumericCastThatLosesPrecision +// if (size == ((int) imageSize.getWidth()) && size == ((int) imageSize.getHeight())) { +// // we can reuse this URL (it's the correct size). +// needsResize = false; +// } +// } catch (IOException e) { +// // have to serve up the error image instead. +// SystemTray.logger.error("Error resizing image. Using error icon instead", e); +// return getErrorImage(cacheName); +// } +// +// if (needsResize) { +// // we have to hop through hoops. +// try { +// File resizedFile = resizeFileNoCheck(size, imageUrl); +// +// // now cache that file +// try { +// return CacheUtil.save(cacheName, resizedFile); +// } catch (IOException e) { +// // have to serve up the error image instead. +// SystemTray.logger.error("Error caching image. Using error icon instead", e); +// return getErrorImage(cacheName); +// } +// +// } catch (IOException e) { +// // have to serve up the error image instead. +// SystemTray.logger.error("Error resizing image. Using error icon instead", e); +// return getErrorImage(cacheName); +// } +// +// } else { +// // no resize necessary, just cache as is. +// try { +// return CacheUtil.save(cacheName, imageUrl); +// } catch (IOException e) { +// // have to serve up the error image instead. +// SystemTray.logger.error("Error caching image. Using error icon instead", e); +// return getErrorImage(cacheName); +// } +// } + return null; + } @SuppressWarnings("Duplicates") public static synchronized - File resizeAndCache(final int size, String cacheName, final InputStream imageStream) { - if (cacheName == null) { - cacheName = CacheUtil.createNameAsHash(imageStream); + File resizeAndCache(final int size, InputStream imageStream, final boolean cacheResult) { + if (imageStream == null) { + return null; + } + + // have to make a copy of the inputStream. + try { + ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream); + imageStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + } catch (IOException e) { + throw new RuntimeException("Unable to read from inputStream.", e); } // check if we already have this file information saved to disk, based on size - cacheName = size + "_" + cacheName; + final String cacheName = size + "_" + CacheUtil.createNameAsHash(imageStream); + ((ByteArrayInputStream) imageStream).reset(); + // if we already have this fileName, reuse it final File check = getIfCachedOrError(cacheName); @@ -513,12 +597,6 @@ class ImageUtils { } } - public static - File resizeAndCache(final int size, final InputStream imageStream) { - return resizeAndCache(size, null, imageStream); - } - - /** * Resizes the given URL to the specified size. No checks are performed if it's the correct size to begin with. * diff --git a/src/dorkbox/systemTray/util/MenuBase.java b/src/dorkbox/systemTray/util/MenuBase.java index c9e5046..b064d08 100644 --- a/src/dorkbox/systemTray/util/MenuBase.java +++ b/src/dorkbox/systemTray/util/MenuBase.java @@ -16,44 +16,27 @@ package dorkbox.systemTray.util; -import java.awt.event.ActionListener; -import java.io.File; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; import java.util.Iterator; -import java.util.concurrent.atomic.AtomicInteger; -import dorkbox.systemTray.Checkbox; import dorkbox.systemTray.Entry; import dorkbox.systemTray.Menu; -import dorkbox.systemTray.Separator; -import dorkbox.systemTray.Status; import dorkbox.systemTray.SystemTray; // this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both @SuppressWarnings("ForLoopReplaceableByForEach") public abstract -class MenuBase implements Menu { - public static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); - private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement(); - - protected final java.util.List menuEntries = new ArrayList(); - - private final SystemTray systemTray; - private final Menu parent; - - /** - * Called in the EDT/GTK dispatch threads - * - * @param systemTray the system tray (which is the object that sits in the system tray) - * @param parent the parent of this menu, null if the parent is the system tray - */ - public - MenuBase(final SystemTray systemTray, final Menu parent) { - this.systemTray = systemTray; - this.parent = parent; - } +class MenuBase extends Menu { +// /** +// * Called in the EDT/GTK dispatch threads +// * +// * @param systemTray the system tray (which is the object that sits in the system tray) +// * @param parent the parent of this menu, null if the parent is the system tray +// */ +// public +// MenuBase(final SystemTray systemTray, final Menu parent) { +// setSystemTray(systemTray); +// setParent(parent); +// } protected abstract void dispatch(final Runnable runnable); @@ -62,31 +45,43 @@ class MenuBase implements Menu { void dispatchAndWait(final Runnable runnable); + + + + + + + + + + + + /** * Will add a new menu entry * NOT ALWAYS CALLED ON DISPATCH */ - protected abstract - Entry addEntry_(final String menuText, final File imagePath, final ActionListener callback); +// protected abstract +// Entry addEntry_(final String menuText, final File imagePath, final ActionListener callback); /** * Will add a new checkbox menu entry * NOT ALWAYS CALLED ON DISPATCH */ - protected abstract - Checkbox addCheckbox_(final String menuText, final ActionListener callback); +// protected abstract +// Checkbox addCheckbox_(final String menuText, final ActionListener callback); /** * Will add a new sub-menu entry * NOT ALWAYS CALLED ON DISPATCH */ - protected abstract - Menu addMenu_(final String menuText, final File imagePath); +// protected abstract +// Menu addMenu_(final String menuText, final File imagePath); - // public here so that Swing/Gtk/AppIndicator can override this - protected abstract - void setImage_(final File imageFile); +// // public here so that Swing/Gtk/AppIndicator can override this +// protected abstract +// void setImage_(final File imageFile); @@ -95,30 +90,8 @@ class MenuBase implements Menu { - @Override - public final - Menu getParent() { - return parent; - } - @Override - public final - SystemTray getSystemTray() { - return systemTray; - } - // public here so that Swing/Gtk/AppIndicator can access this - public final - String getStatus() { - synchronized (menuEntries) { - Entry entry = menuEntries.get(0); - if (entry instanceof Status) { - return entry.getText(); - } - } - - return null; - } // TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. // public @@ -146,316 +119,88 @@ class MenuBase implements Menu { // } - @Override - public final - Entry get(final String menuText) { - if (menuText == null || menuText.isEmpty()) { - return null; - } - - // Must be wrapped in a synchronized block for object visibility - synchronized (menuEntries) { - for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { - final Entry entry = menuEntries.get(i); - - if (entry instanceof Separator || entry instanceof Status) { - continue; - } - - String text = entry.getText(); - - // text can be null - if (menuText.equals(text)) { - return entry; - } - } - } - - return null; - } - - // ignores status + separators - @Override - public final - Entry getFirst() { - return get(0); - } - - // ignores status + separators - @Override - public final - Entry getLast() { - // Must be wrapped in a synchronized block for object visibility - synchronized (menuEntries) { - if (!menuEntries.isEmpty()) { - Entry entry; - for (int i = menuEntries.size()-1; i >= 0; i--) { - entry = menuEntries.get(i); - - if (!(entry instanceof Separator || entry instanceof Status)) { - return entry; - } - } - } - } - - return null; - } - - // ignores status + separators - @Override - public final - Entry get(final int menuIndex) { - if (menuIndex < 0) { - return null; - } - - // Must be wrapped in a synchronized block for object visibility - synchronized (menuEntries) { - if (!menuEntries.isEmpty()) { - int count = 0; - for (Entry entry : menuEntries) { - if (entry instanceof Separator || entry instanceof Status) { - continue; - } - - if (count == menuIndex) { - return entry; - } - - count++; - } - } - } - - return null; - } - - @Override - public final - Entry addEntry(String menuText, ActionListener callback) { - return addEntry(menuText, (String) null, callback); - } - - @Override - public final - Entry addEntry(String menuText, String imagePath, ActionListener callback) { - if (imagePath == null) { - return addEntry_(menuText, null, callback); - } - else { - return addEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath), callback); - } - } - - @Override - public final - Entry addEntry(String menuText, URL imageUrl, ActionListener callback) { - if (imageUrl == null) { - return addEntry_(menuText, null, callback); - } - else { - return addEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl), callback); - } - } - - @Override - public final - Entry addEntry(String menuText, String cacheName, InputStream imageStream, ActionListener callback) { - if (imageStream == null) { - return addEntry_(menuText, null, callback); - } - else { - return addEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream), callback); - } - } - - @Override - public final - Entry addEntry(String menuText, InputStream imageStream, ActionListener callback) { - if (imageStream == null) { - return addEntry_(menuText, null, callback); - } - else { - return addEntry_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream), callback); - } - } - - - @Override - public - Checkbox addCheckbox(final String menuText, final ActionListener callback) { - return addCheckbox_(menuText, callback); - } - - - - @Override - public final - Menu addMenu(String menuText) { - return addMenu(menuText, (String) null); - } - - @Override - public final - Menu addMenu(String menuText, String imagePath) { - if (imagePath == null) { - return addMenu_(menuText, null); - } - else { - return addMenu_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath)); - } - } - - @Override - public final - Menu addMenu(String menuText, URL imageUrl) { - if (imageUrl == null) { - return addMenu_(menuText, null); - } - else { - return addMenu_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl)); - } - } - - @Override - public final - Menu addMenu(String menuText, String cacheName, InputStream imageStream) { - if (imageStream == null) { - return addMenu_(menuText, null); - } - else { - return addMenu_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream)); - } - } - - @Override - public final - Menu addMenu(String menuText, InputStream imageStream) { - if (imageStream == null) { - return addMenu_(menuText, null); - } - else { - return addMenu_(menuText, ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream)); - } - } +// public final +// Entry get(final String menuText) { +// if (menuText == null || menuText.isEmpty()) { +// return null; +// } +// +// // Must be wrapped in a synchronized block for object visibility +// synchronized (menuEntries) { +// for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { +// final Entry entry = menuEntries.get(i); +// +// if (entry instanceof Separator || entry instanceof Status) { +// continue; +// } +// +//// String text = entry.getText(); +// +// // text can be null +//// if (menuText.equals(text)) { +//// return entry; +//// } +// } +// } +// +// return null; +// } - @Override - public final - void setImage(final File imageFile) { - if (imageFile == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile)); - } - } - @Override - public final - void setImage(final String imagePath) { - if (imagePath == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath)); - } - } - - @Override - public final - void setImage(final URL imageUrl) { - if (imageUrl == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl)); - } - } - - @Override - public final - void setImage(final String cacheName, final InputStream imageStream) { - if (imageStream == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream)); - } - } - - @Override - public final - void setImage(final InputStream imageStream) { - if (imageStream == null) { - setImage_(null); - } - else { - setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream)); - } - } - - - @Override - public final - void setCallback(final ActionListener callback) { - } - - - /** - * This removes a menu entry from the dropdown menu. - * - * @param entry This is the menu entry to remove - */ - @Override - public final - void remove(final Entry entry) { - if (entry == null) { - throw new NullPointerException("No menu entry exists for entry"); - } - - dispatchAndWait(new Runnable() { - @Override - public - void run() { - remove__(entry); - } - }); - } - - /** - * This removes a sub-menu entry from the dropdown menu. - * - * @param menu This is the menu entry to remove - */ - @Override - public final - void remove(final Menu menu) { - final Menu parent = getParent(); - if (parent == null) { - // we are the system tray menu, so we just remove from ourselves - dispatchAndWait(new Runnable() { - @Override - public - void run() { - remove__(menu); - } - }); - } else { - final Menu _this = this; - // we are a submenu - dispatchAndWait(new Runnable() { - @Override - public - void run() { - ((MenuBase) parent).remove__(_this); - } - }); - } - } +// /** +// * This removes a menu entry from the dropdown menu. +// * +// * @param entry This is the menu entry to remove +// */ +// @Override +// public final +// void remove(final Entry entry) { +// if (entry == null) { +// throw new NullPointerException("No menu entry exists for entry"); +// } +// +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// remove__(entry); +// } +// }); +// } +// +// /** +// * This removes a sub-menu entry from the dropdown menu. +// * +// * @param menu This is the menu entry to remove +// */ +// @Override +// public final +// void remove(final Menu menu) { +// final Menu parent = getParent(); +// if (parent == null) { +// // we are the system tray menu, so we just remove from ourselves +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// remove__(menu); +// } +// }); +// } else { +// final Menu _this = this; +// // we are a submenu +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// ((MenuBase) parent).remove__(_this); +// } +// }); +// } +// } // NOT ALWAYS CALLED ON EDT protected @@ -493,68 +238,44 @@ class MenuBase implements Menu { } } - /** - * This removes a menu entry or sub-menu (via the text label) from the dropdown menu. - * - * @param menuText This is the label for the menu entry or sub-menu to remove - */ - public final - void remove(final String menuText) { - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - Entry entry = get(menuText); - - if (entry != null) { - remove(entry); - } - } - } - }); - } - - @Override - public final - void removeAll() { - dispatch(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - // have to make copy because we are deleting all of them, and sub-menus remove themselves from parents - ArrayList menuEntriesCopy = new ArrayList(MenuBase.this.menuEntries); - for (Entry entry : menuEntriesCopy) { - entry.remove(); - } - menuEntries.clear(); - } - } - }); - } - - @Override - public final - int hashCode() { - return id; - } - - - @Override - public final - boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - - MenuBase other = (MenuBase) obj; - return this.id == other.id; - } +// /** +// * This removes a menu entry or sub-menu (via the text label) from the dropdown menu. +// * +// * @param menuText This is the label for the menu entry or sub-menu to remove +// */ +// public final +// void remove(final String menuText) { +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// synchronized (menuEntries) { +// Entry entry = get(menuText); +// +// if (entry != null) { +// remove(entry); +// } +// } +// } +// }); +// } +// +// @Override +// public final +// void removeAll() { +// dispatch(new Runnable() { +// @Override +// public +// void run() { +// synchronized (menuEntries) { +// // have to make copy because we are deleting all of them, and sub-menus remove themselves from parents +// ArrayList menuEntriesCopy = new ArrayList(MenuBase.this.menuEntries); +// for (Entry entry : menuEntriesCopy) { +// entry.remove(); +// } +// menuEntries.clear(); +// } +// } +// }); +// } } diff --git a/src/dorkbox/systemTray/util/MenuCheckboxHook.java b/src/dorkbox/systemTray/util/MenuCheckboxHook.java new file mode 100644 index 0000000..ccea5cc --- /dev/null +++ b/src/dorkbox/systemTray/util/MenuCheckboxHook.java @@ -0,0 +1,20 @@ +package dorkbox.systemTray.util; + +import dorkbox.systemTray.Checkbox; + +/** + * + */ +public +interface MenuCheckboxHook extends EntryHook { + + void setEnabled(Checkbox menuItem); + + void setText(Checkbox menuItem); + + void setCallback(Checkbox menuItem); + + void setShortcut(Checkbox menuItem); + + void setChecked(Checkbox checkbox); +} diff --git a/src/dorkbox/systemTray/util/MenuHook.java b/src/dorkbox/systemTray/util/MenuHook.java new file mode 100644 index 0000000..9218dc1 --- /dev/null +++ b/src/dorkbox/systemTray/util/MenuHook.java @@ -0,0 +1,12 @@ +package dorkbox.systemTray.util; + +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; + +/** + * + */ +public +interface MenuHook extends MenuItemHook { + void add(Menu parentMenu, Entry entry, int index); +} diff --git a/src/dorkbox/systemTray/util/MenuItemHook.java b/src/dorkbox/systemTray/util/MenuItemHook.java new file mode 100644 index 0000000..c7dd351 --- /dev/null +++ b/src/dorkbox/systemTray/util/MenuItemHook.java @@ -0,0 +1,19 @@ +package dorkbox.systemTray.util; + +import dorkbox.systemTray.MenuItem; + +/** + * + */ +public +interface MenuItemHook extends EntryHook { + void setImage(MenuItem menuItem); + + void setEnabled(MenuItem menuItem); + + void setText(MenuItem menuItem); + + void setCallback(MenuItem menuItem); + + void setShortcut(MenuItem menuItem); +} diff --git a/src/dorkbox/systemTray/util/MenuStatusHook.java b/src/dorkbox/systemTray/util/MenuStatusHook.java new file mode 100644 index 0000000..6d6144b --- /dev/null +++ b/src/dorkbox/systemTray/util/MenuStatusHook.java @@ -0,0 +1,9 @@ +package dorkbox.systemTray.util; + +/** + * + */ +public +interface MenuStatusHook extends EntryHook { + void setText(Status menuItem); +} diff --git a/src/dorkbox/systemTray/util/Status.java b/src/dorkbox/systemTray/util/Status.java new file mode 100644 index 0000000..5b5e9c5 --- /dev/null +++ b/src/dorkbox/systemTray/util/Status.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dorkbox.systemTray.util; + +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.SystemTray; + +/** + * This represents a common menu-status entry, that is cross platform in nature + */ +public +class Status extends Entry { + private volatile String text; + + public + Status() { + } + + /** + * @param hook the platform specific implementation for all actions for this type + * @param parent the parent of this menu, null if the parent is the system tray + * @param systemTray the system tray (which is the object that sits in the system tray) + */ + public synchronized + void bind(final MenuStatusHook hook, final Menu parent, final SystemTray systemTray) { + super.bind(hook, parent, systemTray); + + hook.setText(this); + } + + /** + * @return the text label that the menu entry has assigned + */ + public final + String getText() { + return text; + } + + /** + * Specifies the new text to set for a menu entry + * + * @param text the new text to set + */ + public + void setText(final String text) { + this.text = text; + + if (hook != null) { + ((MenuStatusHook) hook).setText(this); + } + } +}