/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.notification; import java.awt.event.*; import java.util.*; import util.GraphicsUtils; import com.growl.GrowlWrapper; import com.leafdigital.irc.api.*; import com.leafdigital.notification.api.*; import com.leafdigital.prefs.api.*; import com.leafdigital.prefsui.api.PreferencesUI; import com.leafdigital.ui.api.*; import leafchat.core.api.*; /** Plugin provides notification facilities (popups etc) */ public class NotificationPlugin implements Plugin,Notification { private NotificationListMsgOwner owner=new NotificationListMsgOwner(); private GrowlWrapper growl; private PluginContext context; private final static String NOTIFICATION_USERCOMMAND="Scripted /popup events"; @Override public void init(PluginContext context, PluginLoadReporter reporter) throws GeneralException { // Wait until plugins are loaded before setting up Growl context.requestMessages(SystemStateMsg.class,this); context.registerMessageOwner(owner); // Handle /popup context.requestMessages(UserCommandMsg.class,this); context.requestMessages(UserCommandListMsg.class,this); context.requestMessages(NotificationListMsg.class,this); // Become a notification singleton context.registerSingleton(Notification.class,this); // Register prefs page PreferencesUI preferencesUI = context.getSingle(PreferencesUI.class); preferencesUI.registerPage(this,(new NotificationPage(context)).getPage()); // Need to know about preferences changes (in case of change to minimise-to-tray) context.requestMessages(PreferencesChangeMsg.class, this); this.context=context; } /** Message owner for notification messages */ public class NotificationListMsgOwner extends BasicMsgOwner { @Override public String getFriendlyName() { return "Request supported notification types"; } @Override public Class<? extends Msg> getMessageClass() { return NotificationListMsg.class; } } private boolean isUsingSystemTray; private Object trayIcon=null; boolean isUsingSystemTray() { return isUsingSystemTray; } @Override public boolean hasTrayIcon() { return trayIcon != null; } void updateTrayIcon() { if(!isUsingSystemTray) return; // Are any notifications turned on? boolean enabled = false; NotificationListMsg list = getNotifications(); TreeSet<String> allTypes = new TreeSet<String>(Arrays.asList(list.getTypes())); HashSet<String> defaultTypes = new HashSet<String>(Arrays.asList(list.getDefaultTypes())); Preferences p = context.getSingle(Preferences.class); PreferencesGroup group = p.getGroup(context.getPlugin()); for(String name : allTypes) { if(p.toBoolean(group.get("enabled-"+NotificationPlugin.getPrefName(name), defaultTypes.contains(name) ? "t" : "f"))) { enabled = true; break; } } // Or does the user have minimise-to-tray on? group = p.getGroup(p.getPluginOwner("com.leafdigital.ui.UIPlugin")); enabled = enabled || group.get(UIPrefs.PREF_MINIMISE_TO_TRAY, UIPrefs.PREFDEFAULT_MINIMISE_TO_TRAY).equals("t"); if(enabled && trayIcon==null) { // Let's show the tray icon then try { Class<?> trayClass = Class.forName("java.awt.SystemTray"); if(((Boolean)trayClass.getMethod("isSupported").invoke( null)).booleanValue()) { Object tray = trayClass.getMethod("getSystemTray").invoke( null); // Load icon of appropriate size int iconSize= ((java.awt.Dimension)trayClass.getMethod("getTrayIconSize", new Class[0]).invoke( tray, new Object[0])).height; java.awt.Image icon; if(iconSize <= 16) { icon = GraphicsUtils.loadImage(getClass().getResource("icon16.png")); } else if(iconSize<=32) { icon = GraphicsUtils.loadImage(getClass().getResource("icon32.png")); } else { icon = GraphicsUtils.loadImage(getClass().getResource("icon48.png")); } Class<?> trayIconClass = Class.forName("java.awt.TrayIcon"); trayIcon = trayIconClass. getConstructor(new Class[] {java.awt.Image.class, String.class}).newInstance( new Object[] {icon, "leafChat notifications"}); trayIconClass.getMethod("setImageAutoSize", new Class[] { boolean.class }).invoke( trayIcon, new Object[] { Boolean.TRUE }); MouseListener mouseListener = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if(e.getButton() == MouseEvent.BUTTON1) { context.getSingle(UI.class).activate(); } } }; ActionListener actionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { context.getSingle(UI.class).activate(); context.getSingle(UI.class).showLatest(); } }; trayIconClass.getMethod("addMouseListener", new Class[] { MouseListener.class }).invoke( trayIcon, new Object[] { mouseListener }); trayIconClass.getMethod("addActionListener", new Class[] { ActionListener.class }).invoke( trayIcon, new Object[] { actionListener }); trayClass.getMethod("add",new Class[] {trayIcon.getClass()}).invoke( tray, new Object[] {trayIcon}); } } catch(Exception e) { isUsingSystemTray = false; trayIcon = null; // This happens on Ubuntu where Java incorrectly reports // isSupported = true when it actually throws an exception. // So to avoid causing unnecessary user annoyance, log this // instead of reporting it as a visible error. context.getSingle(SystemLog.class).log(this, "Unable to add icon to system tray", e); } } else if(!enabled && trayIcon != null) { // Get rid of the icon! try { Class<?> trayClass = Class.forName("java.awt.SystemTray"); Object tray = trayClass.getMethod("getSystemTray").invoke(null); trayClass.getMethod("remove", new Class[] {trayIcon.getClass()}).invoke( tray, new Object[] {trayIcon}); } catch(Exception e) { } trayIcon = null; } } /** * Message: system state. Used to set up notification once system starts. * @param msg Message */ public synchronized void msg(SystemStateMsg msg) { if(msg.getType() == SystemStateMsg.PLUGINSLOADED) { // Get list of notifications NotificationListMsg list = getNotifications(); // Construct a Growl wrapper growl = new GrowlWrapper("leafChat", "leafChat", list.getTypes(), list.getDefaultTypes()); if(growl.getState() == GrowlWrapper.GROWL_NOT_MAC) { try { Class<?> trayClass = Class.forName("java.awt.SystemTray"); if(((Boolean)trayClass.getMethod("isSupported").invoke( null)).booleanValue()) { trayClass.getMethod("getSystemTray").invoke(null); isUsingSystemTray = true; } } catch(ClassNotFoundException e) { } catch(Exception e) { ErrorMsg.report("Unable to check popup notification system",e); } } updateTrayIcon(); } } /** * Message: preference changed. If it was the minimise-to-tray preference, * we may need to hide or show the icon. * @param msg Message */ public synchronized void msg(PreferencesChangeMsg msg) { if(msg.getName().equals(UIPrefs.PREF_MINIMISE_TO_TRAY)) { updateTrayIcon(); } } /** * @return List of all notifications */ NotificationListMsg getNotifications() { NotificationListMsg list = new NotificationListMsg(); owner.getDispatch().dispatchMessage(list, true); return list; } /** * Message: notification list. Adds user-command notification to list. * @param msg Message */ public synchronized void msg(NotificationListMsg msg) { msg.addType(NOTIFICATION_USERCOMMAND, true); } /** * Message: user command. Implements the /popup command. * @param msg Message */ public synchronized void msg(UserCommandMsg msg) { if("popup".equals(msg.getCommand())) { String params = msg.getParams().trim(); if(params.length() == 0) { msg.getMessageDisplay().showError( "/popup requires parameters: /popup Title, or /popup Title / Text"); } else { String title, text; if(params.indexOf('/') == -1) { title = params; text = ""; } else { title = params.substring(0, params.indexOf('/')).trim(); text = params.substring(params.indexOf('/') + 1).trim(); } notify(NOTIFICATION_USERCOMMAND, title, text); } msg.markHandled(); } } /** * Message: Listing available commands. * @param msg Message */ public void msg(UserCommandListMsg msg) { msg.addCommand(false, "popup", UserCommandListMsg.FREQ_OBSCURE, "/popup <title> [/ <text>]", "<key>Scripting:</key> Show a notification popup with the given title and " + "optional text (after a slash)"); } @Override public void notify(String type, String title, String message) { // Let's see if we can use Java 6 to notify if(trayIcon != null) { // OK, let's see if it's turned on... NotificationListMsg list = getNotifications(); HashSet<String> defaultTypes = new HashSet<String>(Arrays.asList(list.getDefaultTypes())); Preferences p = context.getSingle(Preferences.class); PreferencesGroup group = p.getGroup(context.getPlugin()); boolean enabled = p.toBoolean(group.get("enabled-" + getPrefName(type), defaultTypes.contains(type) ? "t" : "f")); if(enabled) { try { Class<?> messageType=Class.forName("java.awt.TrayIcon$MessageType"); trayIcon.getClass().getMethod("displayMessage", new Class[] { String.class, String.class, messageType }).invoke(trayIcon, new Object[] { title, message, messageType.getField("INFO").get(null) }); } catch(Exception e) { ErrorMsg.report("Error generating popup notification", e); } } } else { growl.notify(type, title, message); } } GrowlWrapper getGrowl() { return growl; } @Override public void close() throws GeneralException { if(trayIcon != null) { try { Class<?> trayClass = Class.forName("java.awt.SystemTray"); Object tray = trayClass.getMethod("getSystemTray").invoke(null); trayClass.getMethod("remove", new Class[] {trayIcon.getClass()}).invoke( tray, new Object[] {trayIcon}); } catch(Exception e) { } trayIcon = null; } } @Override public String toString() { // Used to display in system log etc. return "Notification plugin"; } static String getPrefName(String notification) { return notification.toLowerCase().replaceAll("[^a-z0-9]", "_"); } }