#!/usr/bin/env python

# This file is part of Window-Switch.
# Copyright (c) 2009-2013 Antoine Martin <antoine@nagafix.co.uk>
# Window-Switch is released under the terms of the GNU GPL v3

import sys
from winswitch.util.simple_logger import Logger
from winswitch.util.simple_logger import set_log_filename, set_log_dir, rotate_log_file
from winswitch.util.paths import CLIENT_DIR, APPLET_LOG
set_log_dir(CLIENT_DIR)
set_log_filename(APPLET_LOG)
logger = Logger("applet")
debug_import = logger.get_debug_import()

debug_import("pygtk/gtk/gobject")
import pygtk
pygtk.require("2.0")
import gtk
import gobject

logger.slog("python version %s" % sys.version)
logger.slog("gtk version %s, pygtk version %s" % (gtk.gtk_version, gtk.pygtk_version))

debug_import("os/time/thread,webbrowser,...")
import os
import time
import webbrowser

debug_import("main_loop")
from winswitch.util.main_loop import loop_init, loop_run, loop_exit, callLater
reactor = loop_init(True)

debug_import("consts")
# Our imports
from winswitch.consts import APPLICATION_NAME, ROOT_WINDOW_UUID_PROPERTY, ROOT_WINDOW_DESKTOPSESSION_PROPERTY, ROOT_WINDOW_PULSE_CLONE_DEVICE, \
	SSH_TYPE, XPRA_TYPE, NX_TYPE, VNC_TYPE, GSTVIDEO_TYPE, VIRTUALBOX_TYPE, X11_TYPE, SCREEN_TYPE, LIBVIRT_TYPE, WINDOWS_TYPE, OSX_TYPE, ALL_TYPES, \
	SESSION_EVENT_NAMES, NOTIFY_ERROR, MODIFIER_KEY_ANY, MODIFIER_KEY_ALL_LIST, MENU_SIZES, \
	SELINUX_FAQ_URL, PROTOCOL_NAMES

import glib
glib.set_application_name(APPLICATION_NAME)

debug_import("globals")
from winswitch.globals import WIN32, OSX, LINUX
debug_import("session")
from winswitch.objects.session import Session
debug_import("server_command")
from winswitch.objects.server_command import ServerCommand
debug_import("server_config")
from winswitch.objects.server_config import ServerConfig
debug_import("config")
from winswitch.util.config import modify_settings, modify_server_config
debug_import("common")
from winswitch.util.common import is_valid_file
debug_import("selinux_util")
from winswitch.util.selinux_util import check_selinux, is_selinux_checked
debug_import("error_handling")
from winswitch.util.error_handling import display_error
debug_import("gdk_debug")
from winswitch.ui.gdk_debug import set_gdk_debug
debug_import("version_check")
from winswitch.util.version_check import version_update_check
debug_import("icon_util")
from winswitch.util.icon_util import HAS_PIXBUFLOADER
debug_import("icon_cache")
from winswitch.util.icon_cache import populate_pixmap_lookup
debug_import("tray_util")
from winswitch.ui.tray_util import setup_tray
debug_import("config_applet")
from winswitch.ui.config_applet import ConfigureAppletWindow
debug_import("config_server")
from winswitch.ui.config_server import get_config_server_windows, edit_server_config
debug_import("session_start_dialog")
from winswitch.ui.session_start_dialog import SessionStartDialog
debug_import("session_options")
from winswitch.ui.session_options import SessionOptions
debug_import("session_customcommand_dialog")
from winswitch.ui.session_customcommand_dialog import SessionCustomCommandDialog
debug_import("user_page")
from winswitch.ui.user_page import close_all_user_info_windows, show_user_info_window
debug_import("session_info")
from winswitch.ui.session_info import close_all_session_info_windows, show_session_info_window
debug_import("quick_connect")
from winswitch.ui.quick_connect import show_quick_connect
debug_import("ui_util")
from winswitch.ui.ui_util import get_ui_util
debug_import("dialog_util")
from winswitch.ui.dialog_util import get_dialog_util
debug_import("notification_util")
from winswitch.ui.notification_util import get_notification_util
debug_import("window_util")
from winswitch.ui.window_util import get_wm_util
debug_import("session_start_options")
from winswitch.ui.session_start_options import SessionStartOptions
debug_import("icons")
from winswitch.ui import icons
debug_import("client")
from winswitch.client.client import ClientBase
debug_import("process_util")
from winswitch.util.process_util import dump_threads
debug_import("all imports done")

DEBUG_REFRESH = False

class WinSwitchApplet(ClientBase):

	def __init__(self):
		ClientBase.__init__(self, reactor, self._reactor_run, self._reactor_stop)

	def __str__(self):
		return	"WinSwitchApplet"



	def initialize(self):
		if not HAS_PIXBUFLOADER:
			raise Exception("Cannot continue without a gdk pixbuf loader")
		ClientBase.initialize(self)
		self.wm_util = None
		self.ui_util = None
		self.tray = None
		self.dialog_util = None
		self.notification_util = None
		self.on_tray_ready = []

		self.idle = False					# current idle state (screensaver / idle)
		self.idle_timer = None
		self.mouse_in_tray_menu = None		# so we know when to hide the menu (esp on win32)
		self.mouse_in_tray_menu_counter = 0
		# various windows (config, etc):
		self.config_applet_window = None

		self.last_menu_update = None
		self.menu = None
		self.menu_shown = None
		self.mac_menu = None
		self.preferences_menuitem = None
		self.on_menu_deactivated = None
		self.menu_refresh_pending = True	# block all menu updates until ready

		self._reactor_lock = False

	def exit_if_unclean(self, *args):
		self.sdebug(None, *args)
		"""
		tray implementations may call this method to request the application to terminate.
		Note: win32 statusicon may call this during cleanup(), if that's the case just ignore it
		as we are already in the process of terminating.
		"""
		if not self.cleanup_called:
			self.exit()

	def do_cleanup(self):
		self.sdebug("applet")
		if self.tray:
			#clear the menu
			menu = gtk.Menu()
			menu.set_title(APPLICATION_NAME)
			item = self.ui_util.make_menuitem("Closing..", "quit")
			item.set_sensitive(False)
			menu.append(item)
			self.tray.set_menu(menu)
		self.set_root_window_property(ROOT_WINDOW_UUID_PROPERTY, "")			# clear our uid from the root window (we're no longer there!)
		#close all windows and dialogs:
		if self.ui_util:
			self.ui_util.close()
		config_windows = get_config_server_windows().values()
		config_windows.append(self.config_applet_window)
		for config_window in config_windows:
			if config_window:
				config_window.destroy_window()
		close_all_user_info_windows()
		close_all_session_info_windows()
		self.sdebug("applet calling do_cleanup on ClientBase")
		ClientBase.do_cleanup(self)

	def stop(self):
		#close the tray last (may still be used for notifications during shutdown - even though we should try to avoid that)
		if self.tray:
			self.tray.close()
		ClientBase.stop(self)

	def do_threaded_setup(self):
		self.startup_progress("caching icons", 10)
		ClientBase.do_threaded_setup(self)
		populate_pixmap_lookup()
		self.startup_progress("prepare UI..", 90)

	def run(self):
		self.get_root_window()									# early check to ensure that the display is accessible
		self.wm_util = get_wm_util()
		self.ui_util = get_ui_util()
		self.dialog_util = get_dialog_util()
		self.notification_util = get_notification_util()
		set_gdk_debug(self.check_idle)							# optional gdk debug hook
		self.monitor_size_changes()
		ClientBase.run(self)

	def settings_assigned(self):
		""" prepare the system tray icon or osx dock (any notifications may need this) """
		def do_setup_tray():
			gtk.gdk.notify_startup_complete()
			root = gtk.gdk.get_default_root_window()
			xpra_server_version = root.property_get(gtk.gdk.atom_intern("XPRA_SERVER", False), gtk.gdk.atom_intern("STRING", False), False)
			winswitch_server_version = root.property_get(gtk.gdk.atom_intern("WINSWITCH_SERVER", False), gtk.gdk.atom_intern("STRING", False), False)
			if xpra_server_version:
				self.slog("detected Xpra server version=%s, using window mode" % str(xpra_server_version))
				self.settings.window_notray = True
			elif winswitch_server_version:
				self.slog("detected WinSwitch server version=%s, using window mode" % str(winswitch_server_version))
				self.settings.window_notray = True
			if self.settings.window_notray:
				self.settings.show_deep_menus = False
			self.notification_util.prefer_growl = self.settings.notifications_prefer_growl
			self.sdebug("before setup_tray")
			self.tray = setup_tray(self.settings.prefer_gtk_tray, self.settings.window_notray,
										self.popup_menu, self.activate_menu, self.exit_if_unclean, self.ask_quit, self.handle_session_event,
										self.get_default_tray_widget_icon_name())
			self.slog("tray=%s, now firing on_tray_ready=%s" % (self.tray, self.on_tray_ready))
			for x in self.on_tray_ready:
				self.slog("firing %s" % x)
				try:
					x()
				except Exception, e:
					self.serr(None, e)
			if not self.tray:
				self.notify("Unable to dock in system tray",
						"Window Switch was unable to dock itself in your system tray and is likely to be unusable!")
			else:
				if self.tray.getMacDock() is not None:
					self.create_application_menu()
					self.preferences_menuitem.set_sensitive(False)
				else:
					#give it a simple menu
					self.menu = gtk.Menu()
					self.menu.set_title(APPLICATION_NAME)
					item = self.ui_util.make_menuitem("Starting up..", "information")
					item.set_sensitive(False)
					self.menu.append(item)
					if WIN32:
						self.menu.append(self.ui_util.make_menuitem("Close", "close", None, self.close_menu))
					self.menu.show_all()
					self.menu.connect("deactivate", self.menu_deactivated)
					self.popup_menu_workaround(self.menu)
					self.tray.set_menu(self.menu)

		gobject.idle_add(do_setup_tray)
		gobject.idle_add(self.set_root_window_properties)
		ClientBase.settings_assigned(self)
		self.start_idle_timer()									# check periodically for screensaver/idle session

	def settings_ready(self):
		self.menu_refresh_pending = False
		if self.preferences_menuitem:
			self.preferences_menuitem.set_sensitive(True)
		self.schedule_menu_refresh()
		def tray_ready():
			self.sdebug()
			self.tray.ready()
			size = self.tray.get_size()
			self.sdebug("tray size=%s" % str(size))
			if size:
				# find the nearest menu size that is lower or equal
				sizes = MENU_SIZES.keys()[:]
				sizes.sort()
				sizes.reverse()
				best_size = size
				for s in sizes:
					if s>0:
						best_size = s
						if s<=size:
							break
				self.ui_util.default_size = best_size
				self.slog("set icon default_size=%s from size=%s" % (best_size, size))

		if self.tray:
			gobject.idle_add(tray_ready)
		else:
			self.on_tray_ready.append(tray_ready)
		ClientBase.settings_ready(self)

	def start_local_server_embedded(self):
		""" override so we can hook UI callbacks for embedded server: defines ask() and notify() """
		ClientBase.start_local_server_embedded(self)
		if self.local_server:
			self.local_server.ask = self.ask
			self.local_server.notify = self.notify
			self.local_server.selinux_check = self.selinux_enabled_check


	#**********************************************************************
	def ready(self):
		ClientBase.ready(self)
		if self.settings.is_new and WIN32:
			self.win32_initial_checks()
		if self.settings.version_update_check:
			callLater(60/2, version_update_check)
		self.selinux_enabled_check()
		return	False

	def selinux_enabled_check(self):
		self.slog("selinux_enabled_warning=%s" % self.settings.selinux_enabled_warning)
		if not LINUX or self.settings.selinux_enabled_warning<=0 or is_selinux_checked():
			return
		def selinux_callback(selinux_enabled, selinux_enforcing, warning_bool, info):
			self.sdebug(selinux_enabled, selinux_enforcing, warning_bool, info)
			if warning_bool:
				self.settings.selinux_enabled_warning -= 1
				modify_settings(self.settings, ["selinux_enabled_warning"])
				def selinux_show_info():
					self.open_url(SELINUX_FAQ_URL)
				self.notify("SELinux configuration problem", info, 20, callback=("Show SELinux Help page",selinux_show_info), notification_type=NOTIFY_ERROR)
		check_selinux(selinux_callback)


	def win32_initial_checks(self):
		self.win32_check_RDP()

	def win32_check_RDP(self):
		from winswitch.virt.rdp_common_util import get_WindowsEditionWarning, get_DenyTSConnections, enable_TS
		edition_warning = get_WindowsEditionWarning()
		if edition_warning:
			self.slog("windows edition %s does not support rdp servers" % edition_warning)
			if self.settings.windows_edition_warning>0:
				self.notify("Remote Desktop unavailable",
						"Your edition of Microsoft Windows '%s' does not provide a 'Terminal Services' server (aka RDP).\n" % edition_warning + \
						"Window Switch bundles TigerVNC which is free alternative.",
						notification_type=NOTIFY_ERROR)
				self.settings.windows_edition_warning -= 1
				modify_settings(self.settings, ["windows_edition_warning"])
				return
		deny_ts = get_DenyTSConnections()
		self.sdebug("DenyTSConnections=%s" % deny_ts)
		if deny_ts is None:
			#XP: http://www.microsoft.com/download/en/details.aspx?id=7208
			def download_ts_client(*args):
				self.open_url("http://www.microsoft.com/download/en/details.aspx?id=7208")
			self.notify("Remote Desktop client not found!",
					"This feature will be disabled until you install "
					"Microsoft Terminal Services server",
					callback=("Visit Microsoft's Download Page", download_ts_client), notification_type=NOTIFY_ERROR)
		elif deny_ts!=0:
			def enable_TS_cb():
				try:
					enable_TS()
					self.slog("after setting key, new value=%s" % get_DenyTSConnections())
				except Exception, e:
					self.serr(None, e)
			self.ask("Terminal Services currently disabled",
					"You must enable Terminal Services to allow remote access to this computer via Microsoft's RDesktop protocol (RDP).\n"
					"Click OK to enable it now.\n"
					"Note: you will need to restart the computer for this change to take effect.",
					None, enable_TS_cb)




	def _reactor_run(self, *args):
		if self._reactor_lock:
			self.slog("already running", *args)
		else:
			self._reactor_lock = True
			loop_run()
			self.slog("main loop ended", *args)
			self.exit()
			self.slog("exit() done", *args)
			dump_threads()
			self.slog("ended")

	def _reactor_stop(self):
		self.slog()
		loop_exit()
		self._reactor_lock = False
		self.slog("done")



	#****************************************************************
	#
	#	We override a few ClientBase methods to provide a UI for them.
	#
	#
	def open_url(self, url):
		webbrowser.open_new_tab(url)

	def notify(self, title, message, delay=None, callback=None, notification_type=None, from_server=None, from_uuid=None):
		def do_notify():
			self.do_notify(title, message, delay, callback, notification_type, from_server, from_uuid)
		if self.tray is None:
			self.sdebug("adding notification to delayed list", title, message, delay, callback, notification_type, from_server, from_uuid)
			self.on_tray_ready.append(do_notify)
		else:
			do_notify()

	def do_notify(self, title, message, delay=None, callback=None, notification_type=None, from_server=None, from_uuid=None):
		self.slog(None, title, message, delay, callback, notification_type, from_server, from_uuid)
		#add reply-to callback if none exists and we have a user id
		if callback is None:
			if from_uuid and from_server:
				user = from_server.get_user_by_uuid(from_uuid)
				if user:
					def reply_to(*args):
						self.sdebug(None, *args)
						self.user_action(None, from_server, user)
					callback = ("Send Message", reply_to)
			elif from_server: # and notification_type in [NOTIFY_ERROR, NOTIFY_AUTH_ERROR]:
				def show_config(*args):
					self.sdebug(None, *args)
					self.show_server_config(from_server)
				callback = ("Edit Server Configuration", show_config)
		if self.notification_util:
			self.notification_util.safe_notify(self.tray, title, message, delay=delay, callback=callback, notification_type=notification_type)
		gobject.idle_add(self.attention, title, delay)

	def show_server_config(self, server):
		self.sdebug(None, server)
		self.configure_server(None, server)

	def show_settings(self):
		self.configure_applet(None)

	def server_message(self, server, title, message, notification_type):
		self.sdebug(None, server, title, message, notification_type)
		self.notify(title, message, from_server=server, notification_type=notification_type)

	def attention(self, message=None, delay=5):
		self.slog(None, message, delay)
		if delay is None:
			delay = 5
		if self.tray:
			self.tray.request_attention(message, delay)

	def get_default_tray_widget_icon_name(self):
		from winswitch.objects.global_settings import TRAY_ICON_NAMES
		name = self.settings.tray_icon_name
		if not self.settings.tray_icon_name or self.settings.tray_icon_name not in TRAY_ICON_NAMES.values():
			name = "auto"
		if name == "auto":
			name = self.ui_util.get_best_colour_scheme_icon_name()
		self.sdebug("=%s" % name)
		return	name

	def set_default_tray_widget_icon(self):
		if self.tray:
			icon_name = self.get_default_tray_widget_icon_name()
			self.tray.default_icon_name = icon_name
			self.tray.set_icon(icon_name)



	def handle_session_event(self, event):
		"""
		Pass on the session event to all the client_utils
		See detect_win32_session_events
		"""
		name = SESSION_EVENT_NAMES.get(event)
		self.sdebug("%s" % name, event)
		#CONSOLE_CONNECT, CONSOLE_DISCONNECT, REMOTE_CONNECT, REMOTE_DISCONNECT, SESSION_LOGON, SESSION_LOGOFF, SESSION_LOCK, SESSION_UNLOCK, SESSION_REMOTE_CONTROL
		for util in self.client_utils.values():
			try:
				util.handle_session_event(self.servers, event, name)
			except Exception, e:
				self.serr("%s" % name, e, event)

	def set_root_window_properties(self):
		def set_prop(name, value):
			self.set_root_window_property(name, value)

		set_prop(ROOT_WINDOW_UUID_PROPERTY, self.settings.uuid)
		desktop_session = os.environ.get("XDG_CURRENT_DESKTOP") or os.environ.get("DESKTOP_SESSION")
		self.sdebug("desktop_session=%s" % desktop_session)
		if desktop_session:
			set_prop(ROOT_WINDOW_DESKTOPSESSION_PROPERTY, desktop_session)
		from winswitch.sound.sound_util import has_pa
		if has_pa():
			pulse_clone_device = self.settings.gst_sound_clone_options.get("device")
			if not pulse_clone_device or not self.settings.tunnel_clone:
				pulse_clone_device = ""
			set_prop(ROOT_WINDOW_PULSE_CLONE_DEVICE, pulse_clone_device)

	def set_root_window_property(self, name, value):
		root = self.get_root_window()
		root.property_change(gtk.gdk.atom_intern(name, False),
							gtk.gdk.atom_intern("STRING", False),
							8,
							gtk.gdk.PROP_MODE_REPLACE,
							value)
	def get_root_window(self):
		root = gtk.gdk.get_default_root_window()
		if not root:
			msg = "cannot find root window! check your DISPLAY environment"
			self.serror(msg)
			raise Exception(msg)
		return	root

	def monitor_size_changes(self):
		display = gtk.gdk.display_get_default()
		i=0
		while i<display.get_n_screens():
			screen = display.get_screen(i)
			screen.connect("size-changed", self.screen_size_changed, display, i)
			i += 1
	def screen_size_changed(self, screen, display, index):
		total_x = 0
		total_y = 0
		i=0
		n = display.get_n_screens()
		self.slog("%s screens" % n, screen, display, index)
		while i<n:
			s = display.get_screen(i)
			w = s.get_width()
			h = s.get_height()
			self.sdebug("screen(%s) %sx%s" % (i, w, h), screen, display, index)
			total_x += w
			total_y += h
			i += 1
		self.slog("new size: %sx%s" % (total_x, total_y))
		#now find the server which has this session and update it:
		for server in self.servers:
			session = server.get_session(self.settings.uuid)
			if session:
				client = self.get_link_client(server, "needed to update screen size")
				self.sdebug("updating server %s using %s" % (server, client), screen, display, index)
				if client:
					client.set_session_status(session)
				break

	def start_mdns_listener(self):
		"""
		Overrides start_mdns_listener on ClientBase to add an alert box telling the user about the missing feature and how to enable it.
		"""
		ClientBase.start_mdns_listener(self)
		if self.mdns_listener:
			return

		msg = "mDNS is disabled! Automatic discovery is OFF!"
		if self.settings.ignore_missing_mdns:
			self.slog(msg)
			return

		self.serr(msg)
		text = "In order to automatically detect local servers on your local network,\n"
		text += "%s uses Apple's 'Bonjour' protocol.\n" % APPLICATION_NAME
		text += "This component seems to be missing or it is not running."
		text += "\nInstall it first then click retry."
		mdns_url = None
		mdns_url_label = None
		install_action = None
		install_tooltip = None
		if WIN32:
			text += "\n\n(you only need 'bonjour' and not all the other optional\ncomponents Apple will try to make you install)"
			mdns_url = "http://apple.com/support/downloads/bonjourforwindows.html"
			mdns_url_label = "Download it from apple.com"
		else:
			if OSX:
				mdns_url = "http://developer.apple.com/networking/bonjour/index.html"
				mdns_url_label = "See apple.com for more information"
			else:
				from winswitch.util.distro_packaging_util import get_distro_helper
				install_action = get_distro_helper().get_install_action("avahi")
				install_tooltip = "Install now via %s" % get_distro_helper().manager
				if not install_action:
					text += "\nOn most *nix systems, this means installing and starting avahi"
					text += "\nand also installing its python bindings."

		dialog = gtk.Window()
		dialog.set_title('Automatic Discovery Feature Missing')
		dialog.set_destroy_with_parent(True)
		dialog.set_resizable(False)
		dialog.set_decorated(True)
		dialog.set_icon(icons.get(get_ui_util().get_best_colour_scheme_icon_name()))
		dialog.set_position(gtk.WIN_POS_CENTER)
		dialog.set_border_width(10)

		def cancel_dialog(*args):
			ignore = dismiss.get_active()
			self.slog("closed, ignore=%s" % ignore)
			if ignore:
				self.settings.ignore_missing_mdns = ignore
				self.slog("updating settings")
				modify_settings(self.settings, ["ignore_missing_mdns"])
			dialog.hide_all()
			dialog.destroy()
		def retry(*args):
			self.slog(None, *args)
			dialog.hide_all()
			ClientBase.start_mdns_listener(self)
			if self.mdns_listener:
				dialog.destroy()
			else:
				dialog.show_all()

		vbox = gtk.VBox(False, 0)
		vbox.set_spacing(10)
		dialog.add(vbox)
		dialog.set_geometry_hints(vbox)

		vbox.pack_start(self.ui_util.make_label('Automatic Discovery Feature Missing', "sans 14"))
		vbox.pack_start(gtk.Label(text))
		if install_action:
			def install_mdns(*args):
				self.slog(None, *args)
				install_action(retry)
			mdns_install_button = self.ui_util.make_imagebutton("install", "download", install_tooltip, install_mdns)
			vbox.pack_start(mdns_install_button)
		elif mdns_url:
			mdns_url_button = gtk.LinkButton(mdns_url, mdns_url_label)
			mdns_url_button.set_relief(gtk.RELIEF_NONE)
			def download_mdns(*args):
				self.sdebug(None, *args)
				self.open_url(mdns_url)
			mdns_url_button.connect("clicked", download_mdns)
			vbox.pack_start(mdns_url_button)
		dismiss = gtk.CheckButton("Do not show this message again")
		vbox.pack_start(dismiss)

		#buttons:
		hbox = gtk.HBox(True, 10)
		cancel_button = self.ui_util.make_imagebutton("Ignore", "remove", "Continue without mDNS", cancel_dialog, 24)
		hbox.pack_start(cancel_button)
		retry_button = self.ui_util.make_imagebutton("Retry", "retry", "Retry to start the mDNS feature", retry, 24)
		hbox.pack_start(retry_button)
		vbox.pack_start(hbox)

		#accel / close:
		dialog.connect('delete_event', cancel_dialog)
		self.ui_util.add_close_accel(dialog, cancel_dialog)

		dialog.show_all()



	def create_link(self, server, callback=None):
		"""
		We override create_link to ensure that we have all the settings we need before we try to connect to a new server.
		If not, we can ask the user to supply them.
		"""
		if server.is_new and not server.password and (not server.ssh_tunnel or not is_valid_file(server.ssh_keyfile)):
			#direct connection to a new server: we need a password
			#tunnelled connection: need password or key
			show_quick_connect(self, server, callback, "Password required for %s" % server.get_display_name())
			return
		ClientBase.create_link(self, server, callback)



	def start_idle_timer(self):
		if self.settings.idle_timeout<=0 or not self.wm_util.can_check_for_idle():
			self.sdebug("idle timer disabled")
			if self.idle_timer:
				self.sdebug("stopping existing timer: %s" % self.idle_timer)
				gobject.source_remove(self.idle_timer)
			self.idle_timer = None
		else:
			if not self.idle_timer:
				#not more often than once per minute, but at least every hour:
				how_often = min(60*60*1000, max(60*1000, (self.settings.idle_timeout/10)*1000))
				self.idle_timer = gobject.timeout_add(how_often, self.check_idle)
				self.sdebug("new idle_timer=%s" % self.idle_timer)

	def check_idle(self):
		new_state = self.wm_util.is_idle(self.settings.idle_timeout)
		self.sdebug("idle_timeout=%d, idle=%s" % (self.settings.idle_timeout, new_state))
		if new_state != self.idle:
			self.slog("at %s, new_state=%s" % (time.time(), new_state))
			if new_state:
				self.do_idle_connected_sessions()
			else:
				self.do_resume_idle_sessions()
				self.schedule_menu_refresh()
			self.idle = new_state
		return True

	#We override these methods so we can schedule a menu refresh whenever the server is modified
	def add_server(self, server):
		ClientBase.add_server(self, server)
		server.add_modified_callback(self.schedule_menu_refresh)
		self.schedule_menu_refresh(True)

	def remove_server(self, server):
		ClientBase.remove_server(self, server)
		self.schedule_menu_refresh(True)

	def schedule_menu_refresh(self, server_added_or_removed=False):
		if self.menu_refresh_pending:
			if DEBUG_REFRESH:
				self.sdebug("already pending - not scheduling it")
			return	True
		if DEBUG_REFRESH:
			self.sdebug("scheduling it")
		self.menu_refresh_pending = True
		gobject.timeout_add(100, self.batched_menu_refresh)
		return	True

	def batched_menu_refresh(self):
		if DEBUG_REFRESH:
			self.sdebug()
		self.menu_refresh_pending = False
		self.populate_menu()
		return	False





	#****************************************************************
	#
	#	Menu callbacks
	#
	#
	def get_connected_session_count(self):
		count = 0
		for server in self.servers:
			count += len(server.get_connected_sessions(self.settings.uuid))
		return count

	def quit(self, *args):
		count = self.get_connected_session_count()
		self.slog("%s connected sessions" % count, *args)
		if count>0:
			self.ask_quit()
		else:
			self.exit()

	def ask_quit(self, *args):
		count = self.get_connected_session_count()
		self.ask("Really quit?",
								"There are %d sessions still connected.\nDo you really want to quit?" % count,
								None, self.exit,
								buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_QUIT, gtk.RESPONSE_OK),
								UUID="QUIT")

	def stop_server(self, widget, server):
		self.sdebug(None, widget, server)
		count = len(server.get_live_sessions(self.settings.uuid))
		self.slog("%s live sessions" % count, widget, server)
		msg = "The server may need to be re-started to become available again."
		if count==0:
			msg += "\nThere are no active sessions."
		else:
			msg = "\nThere are %d active sessions.\nAll these sessions will be lost!" % count
		msg += "\nDo you really want to shutdown??"
		self.ask("Shutdown server %s" % server.get_display_name(),
						msg,
						None, lambda: self.do_stop_server(server))

	def do_stop_server(self, server):
		client = self.get_link_client(server)
		if client and client.handler:
			client.handler.send_stop_request("User request")

	def disconnect_server(self, widget, server):
		count = len(server.get_connected_sessions(self.settings.uuid))
		self.slog("%s connected sessions" % count, widget, server)
		if count>0:
			self.ask("Disconnect from %s" % server.get_display_name(),
							"There are %d sessions still connected.\nDo you really want to disconnect?" % count,
							None, lambda: self.do_disconnect_server(server),
							UUID="%s-DISCONNECT" % server.ID)
		else:
			self.do_disconnect_server(server)

	def configure_applet(self, widget):
		if self.config_applet_window:
			self.config_applet_window.destroy_window()
		try:
			self.config_applet_window = ConfigureAppletWindow(self)
			self.config_applet_window.show()
		except Exception, e:
			self.exc(e)

	def settings_modified(self):
		self.sdebug(None)
		self.start_idle_timer()
		#start / stop local server as required
		if self.local_server is not None and not self.settings.start_local_server:
			self.stop_local_server()
		elif self.local_server is None and self.settings.start_local_server:
			self.start_local_server()

		for server in self.servers:
			if server.is_connected():
				client = self.get_link_client(server)
				client.add_current_user()					#re-add ourselves (name, etc..)
				if self.settings.avatar_changed:
					self.sdebug("sending updated avatar to %s" % server)
					client.send_avatar()
			#update the avatar directly on our own user object (no need to get it from the server)
			if self.settings.avatar_changed:
				user = server.get_user_by_uuid(self.settings.uuid)
				if user:
					user.set_avatar_icon_data(self.settings.avatar_icon_data)
			#ensure that the flags are applied:
			mods = []
			if server.tunnel_fs and not self.settings.tunnel_fs:
				mods.append("tunnel_fs")
				server.tunnel_fs = False
			if server.tunnel_sink and not self.settings.tunnel_sink:
				mods.append("tunnel_sink")
				server.tunnel_sink = False
			if server.tunnel_source and not self.settings.tunnel_source:
				mods.append("tunnel_source")
				server.tunnel_source = False
			if server.tunnel_printer and not self.settings.tunnel_printer:
				mods.append("tunnel_printer")
				server.tunnel_printer = False
			if len(mods):
				server.touch()
				modify_server_config(server, mods)
		self.set_root_window_properties()
		self.init_client_utils()
		self.schedule_menu_refresh()
		self.set_default_tray_widget_icon()

	def quick_connect(self, *args):
		self.sdebug()
		show_quick_connect(self, None, lambda : self.schedule_menu_refresh(True), "Configure New Connection")

	def server_settings_modified(self, server):
		self.schedule_menu_refresh()
		get_config_server_windows()[server] = None
		if not server.enabled:
			self.disconnect_server(None, server)
			return
		if server.is_connecting():
			self.edit_callback_reconnect(server)
		elif server.is_connected():
			self.ask("Re-connect to server?",
					"To apply the new settings the connection to the server may need to be re-established.\n Do you want to do this now?",
					None, lambda : self.edit_callback_reconnect(server),
					UUID="%s-DISCONNECT" % server.ID)
		else:
			self.start_link(server)

	def edit_callback_reconnect(self, server):
		self.sdebug(None, server)
		#stop connection attempt
		link = server.link
		if link:
			link.stop()
			link.connect()
		else:
			self.start_link(server)

	def delete_callback(self, server):
		if server.link:
			server.link.stop()
		self.schedule_menu_refresh()

	def configure_server(self, widget, server):
		edit_server_config(self, server, lambda : self.server_settings_modified(server))
		return	False

	def disable_server(self, widget, server):
		server.enabled = False
		server.touch()
		modify_server_config(server, ["enabled"])

	def retry_server(self, widget, server):
		self.kick_server_link(server)

	def resume_all(self, widget, server):
		self.do_resume_all(server)

	def resume_session(self, widget, server, session):
		check_session = None
		if session.session_type==WINDOWS_TYPE:
			check_session = session
		elif session.session_type in [VNC_TYPE, NX_TYPE, OSX_TYPE] and session.shadowed_display:
			check_session = server.get_session_by_display(session.shadowed_display)
		if check_session:
			#check that this is not our own display
			self.sdebug("shadowed real_session=%s" % check_session)
			if check_session and (check_session.owner==self.settings.uuid or (check_session.session_type in [WINDOWS_TYPE, OSX_TYPE] and server.local)):
				self.ask("Resume a shadow of your own display?",
								"Session '%s' seems to be a shadow of your own display.\n"
								"Are you sure that you want to resume it?\n"
								"This may result in a very confused/confusing display!\n"
								"Or it may not work at all" % session.name,
								session.fire_touch_callbacks, lambda : self.do_resume_session(server, session), icon=icons.get("winswitch_warning"),
								UUID="%s-OWN-SHADOW" % session.ID)
				return
		self.do_resume_session(server, session)

	def close_all_sessions(self, widget, server):
		self.do_close_all_sessions(server)

	def close_session(self, widget, server, session):
		self.do_close_session(server, session)

	def raise_all_sessions(self):
		for server in self.servers:
			self.raise_sessions(server)

	def raise_sessions(self, server):
		for session in server.get_connected_sessions(self.settings.uuid):
			self.raise_session(None, server, session)

	def raise_session(self, widget, server, session):
		self.do_raise_session(server, session)

	def kill_session(self, widget, server, session):
		title = "Stop/Destroy session '%s'" % session.name
		text = "Stopping this session will forcibly shutdown the application,\nyou are very likely to lose all your unsaved data!"
		self.ask(title, text, None, lambda : self.do_kill_session(server, session), icon=icons.get("stop"), UUID="%s-KILL" % session.ID)

	def reconnect_session(self, widget, server, session):
		self.do_reconnect_session(server, session)

	def detach_session(self, widget, server, session):
		self.do_detach_session(server, session)

	def detach_all_sessions(self, widget, server):
		self.do_detach_all_sessions(server)

	def detach_all_servers(self):
		self.do_detach_all_servers()

	def shadow_session(self, widget, server, session, read_only, shadow_type):
		self.sdebug(None, widget, server, session, read_only, shadow_type)
		if session.is_local_display(self.settings.uuid, server.local):
			if read_only:
				icon = icons.get("shadow")
			else:
				icon = icons.get("copy")
			self.ask("Shadow your own display?",
							"Session '%s' seems to be your own local display.\nAre you sure that you want to shadow it?\nThis may result in a very confused/confusing display!" % session.name,
							session.fire_touch_callbacks, lambda : self.do_shadow_session(server, session, read_only, shadow_type, None, {}), icon=icon,
							UUID="%s-OWN-SHADOW" % session.ID)
			return
		self.do_shadow_session(server, session, read_only, shadow_type, None, {})

	def	do_shadow_session(self, server, session, read_only, shadow_type, screen_size, options):
		""" override the method so we can show an options dialog if modifier key is pressed """
		client_util = self.get_remote_util(shadow_type)
		if client_util.shadow_options_required or self.is_modifier_set():
			""" If modifier is set, show dialog to configure shadow """
			def shadow_configured(options):
				from winswitch.virt.options_common import SCREEN_SIZE
				screen_size = options.get(SCREEN_SIZE)
				ClientBase.do_shadow_session(self, server, session, read_only, shadow_type, screen_size, options)
			SessionOptions("Shadow %s using %s" % (session.name, PROTOCOL_NAMES.get(shadow_type)), session.get_window_icon(), server, client_util, ServerCommand.DESKTOP, session, shadow_configured)
		else:
			ClientBase.do_shadow_session(self, server, session, read_only, shadow_type, screen_size, options)
			session.touch()		#ensure the UI gets a quick refresh

	def open_local_file(self, widget, server):
		self.sdebug(None, widget, server)
		chooser = gtk.FileChooserDialog("Open on '%s'" % server.name,
									parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN,
									buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))

		chooser.set_select_multiple(True)
		chooser.set_default_response(gtk.RESPONSE_OK)
		response = chooser.run()
		filenames = chooser.get_filenames()
		chooser.hide()
		chooser.destroy()
		if response == gtk.RESPONSE_OK:
			self.slog("selected: %s" % filenames, widget, server)
			if len(filenames)>0:
				self.do_open_local_files(server, filenames)
		elif response == gtk.RESPONSE_CANCEL:
			self.sdebug("cancelled", widget, server)
		else:
			self.serror("unknown response: %d" % response, widget, server)


	def is_modifier_set(self):
		#alternative:
		#(_, _, current_mask) = gtk.gdk.get_default_root_window().get_pointer()
		mods = self.wm_util.get_modifiers_set()
		self.sdebug("modifiers=%s" % mods)
		if len(mods)==0:
			return	False
		""" Some modifiers are present, test them """
		keys = [self.settings.modifier_key]
		if self.settings.modifier_key==MODIFIER_KEY_ANY:
			keys = MODIFIER_KEY_ALL_LIST
		for key in keys:
			if key in mods:
				self.sdebug("modifiers=%s, matching modifier found: %s" % (mods, key))
				return	True
		return	False

	def default_start_session(self, server, command):
		""" We override this method to test for key modifiers set (Shift/Ctrl/Alt) """
		if self.is_modifier_set():
			""" The key modifier was held """
			self.slog("modifier set, action is: %s" % self.settings.wrapper_modifier_action, server, command)
			from winswitch.objects.global_settings import WRAPPER_MODIFIER_ENABLE, WRAPPER_MODIFIER_DISABLE, WRAPPER_MODIFIER_DIALOG
			if self.settings.wrapper_modifier_action==WRAPPER_MODIFIER_DISABLE:
				""" modifier action is to NOT launch the app from here (will be launched directly) """
				return	False
			elif self.settings.wrapper_modifier_action==WRAPPER_MODIFIER_ENABLE:
				""" modifier action is to launch the app here (will fall through) """
				pass
			elif self.settings.wrapper_modifier_action==WRAPPER_MODIFIER_DIALOG:
				""" modifier action is to ask the user: show a dialog """
				def session_start_options_start(session_type, screen_size, opts, bookmark):
					self.do_start_session(server, command, session_type, screen_size, opts, bookmark)
				ask_options = SessionStartOptions(server, self.client_utils, command, command.type, session_start_options_start, None, False)
				ask_options.show()
				return	True
		return ClientBase.default_start_session(self, server, command)



	def start_menu_callback(self, widget, server, server_command, session_type=None, screen_size=None, opts=None):
		"""
		Calls either SessionStartOptions() if session_type and preferred_session_type are missing or if "modifier key" was held,
		or do_start_session() otherwise.
		"""
		self.sdebug(None, widget, server, server_command)
		if not session_type:
			if server_command.type==ServerCommand.DESKTOP:
				session_type = server.preferred_desktop_type
			else:
				session_type = server.preferred_session_type
		if self.is_modifier_set() or not session_type:
			def session_start_options_start(session_type, screen_size, actual_opts, bookmark):
				self.do_start_session(server, server_command, session_type, screen_size, actual_opts, bookmark)
			ask_options = SessionStartOptions(server, self.client_utils, server_command, server_command.type, session_type, session_start_options_start, opts, True)
			ask_options.show()
		else:
			self.do_start_session(server, server_command, session_type, screen_size, opts, False)

	def do_start_menu_dialog(self, server, command_type):
		"""
		Show a dialog where the user can select the command to start.
		(as well as the protocol to use, as per start_menu_callback)
		"""
		def dialog_do_start(_server, command, session_type, screen_size, opts, bookmark):
			self.do_start_session(_server, command, session_type, screen_size, opts, bookmark)
		dialog = SessionStartDialog(self.servers, server, self.client_utils, command_type, dialog_do_start)
		self.sdebug("dialog=%s" % dialog, server, command_type)
		dialog.show()


	def send_info_message(self, server, user_uuid, sent_description, icon):
		client = self.get_link_client(server)
		if client:
			gobject.idle_add(self.do_send_info_message, client.handler, user_uuid, sent_description, icon)

	def do_send_info_message(self, net_handler, user_uuid, sent_description, icon):
		message_entry = gtk.Entry(max=64)
		info = gtk.Label("Sending %s" % sent_description)
		ok_to_send = lambda : self.ok_send_info_message(message_entry, net_handler, user_uuid, sent_description)
		self.ui_util.do_ask_dialog("Add a message?", [info, message_entry], None, ok_to_send, icon=icon)
		return	False

	def ok_send_info_message(self, message_entry, net_handler, user_uuid, sent_description):
		message = message_entry.get_text()
		title = "Sending you %s" % sent_description
		net_handler.send_plain_message(user_uuid, title, message, self.settings.uuid)


	def send(self, server, session, user_uuid, modifier_key=False):
		"""
		Send this session to another user
		"""
		client = self.get_link_client(server)
		self.slog("client=%s" % client, server,session,user_uuid,modifier_key)
		if client:
			if modifier_key:
				self.send_info_message(server, user_uuid, "session %s" % session.name, session.get_window_icon())
			self.do_detach_session(server, session)
			client.send_session_to_user(session, user_uuid)

	def send_all(self, server, unused, user_uuid, modifier_key=False):
		"""
		Send all the sessions we have on this server to another user
		"""
		self.slog(None, server, unused, user_uuid, modifier_key)
		if modifier_key:
			self.send_info_message(server, user_uuid, "all sessions", None)
		for session in server.get_live_sessions():
			self.send(server, session, user_uuid, False)

	def send_button_release_callback(self, widget, event, send_callback, server, session, user_uuid):
		"""
		Callback that calls send or send_all based on send_callback.
		Only here to allow us to catch the Shift modifier key.
		We have to use the 'button release' event to get it...
		"""
		self.sdebug("state=%s" % event.state, widget, event, send_callback, server, session, user_uuid)
		send_callback(server, session, user_uuid, self.is_modifier_set())

	def send_message_to_user(self, server, uuid, message):
		client = self.get_link_client(server)
		if client and client.handler:
			client.handler.send_plain_message(uuid, "Message from %s" % self.settings.name, message, self.settings.uuid)
		else:
			self.serror("cannot send message: not connected to server!", server, uuid, message)

	def user_action(self, widget, server, user):
		"""
		Clicked on a user's menu entry: we show the UserPage for that user.
		"""
		self.sdebug(None, widget, server, user)
		def send_message(message):
			self.send_message_to_user(server, user.uuid, message)
		show_user_info_window(user, send_message)

	def session_action(self, widget, server, session):
		"""
		Clicked on a session info menu entry
		"""
		self.sdebug(None, widget, server, session)
		show_session_info_window(self, server, session)



	#****************************************************************
	#	Menu activate / popup / position
	#
	def close_menu(self, *args):
		#self.sdebug("menu_shown=%s" % self.menu_shown, *args)
		self.sdebug(None, *args)
		if self.menu_shown:
			self.menu_shown.popdown()
			self.menu_shown.hide()
			self.menu_not_shown()

	def menu_deactivated(self, menu):
		#self.sdebug("menu_shown=%s, on_menu_deactivated=%s" % (self.menu_shown, self.on_menu_deactivated), menu)
		self.menu_not_shown()

	def menu_not_shown(self):
		self.menu_shown = None
		if self.on_menu_deactivated:
			self.on_menu_deactivated()
			self.on_menu_deactivated = None

	def activate_menu(self, widget, *args):
		#self.sdebug("menu_shown=%s" % self.menu_shown, widget, *args)
		if OSX and self.menu_shown:
			#with gtk-osx, we can click on the StatusIcon whilst it's down
			self.menu_deactivated(None)
		else:
			self.show_menu(1, 0)

	def popup_menu(self, widget, button, time, *args):
		#self.sdebug(None, widget, button, time, *args)
		self.show_menu(button, time)

	def show_menu(self, button, time):
		#self.sdebug(None, button, time)
		self.notification_util.close_notifications()
		self.close_menu()
		if self.menu and not self.cleanup_called:						#ensure menu is ready (not present during early startup)
			self.menu.show_all()
			self.menu.popup(None, None, self.tray.position_menu, button, time, self.tray.getStatusIcon())
			#another way to avoid the "scrolling menu" on win32 is to call popup with None:
			#menu.popup(None, None, None, button, activate_time)
			#instead we adjust the location in do_position_menu()
			self.menu_shown = self.menu








	def populate_menu(self):
		if DEBUG_REFRESH:
			self.sdebug("menu_shown=%s" % self.menu_shown)
		if self.menu_shown:
			self.on_menu_deactivated = self.populate_menu	#re-generate the menu when it is no longer in use
			return
		try:
			self.menu = self.create_application_menu()
		except KeyboardInterrupt, e:
			self.serr(None, e)
			raise e
		except Exception, e:
			self.serr(None, e)
		return	False

	def create_application_menu(self):
		now = time.time()
		if DEBUG_REFRESH:
			self.sdebug("last_menu_update=%s, now=%s" % (self.last_menu_update, now))
		self.last_menu_update = now
		if self.tray:
			macdock = self.tray.getMacDock()
			if macdock:
				return self.create_mac_menu(macdock)
			return self.create_tray_menu()

	def create_mac_menu(self, dock):
		self.sdebug(None, dock)
		if self.mac_menu is not None:
			""" re-populate it """
			self.mac_populate_menubar()
			return	self.mac_menu
		# the menu bar:
		menu = gtk.MenuBar()
		# We need to add it to a widget (otherwise it just does not work)
		self.hidden_window = gtk.Window()
		self.hidden_window.add(menu)

		def mac_menu(name):
			submenu = gtk.Menu()
			item = gtk.MenuItem(name)
			item.set_submenu(submenu)
			menu.add(item)
			return submenu

		self.mac_servers_menu	= mac_menu("Servers")
		self.mac_window_menu	= mac_menu("Windows")
		self.mac_desktop_menu	= mac_menu("Desktops")
		self.mac_shortcut_menu	= mac_menu("Bookmarks")

		# Quit (will be hidden - see below)
		quit_item = gtk.MenuItem("Quit")
		quit_item.connect("activate", self.exit)
		menu.add(quit_item)

		self.mac_populate_menubar()
		menu.show_all()
		self.mac_menu = menu
		dock.set_menu_bar(menu)
		quit_item.hide()

		# Add to the application menu:
		item = gtk.MenuItem("About")
		item.show()
		item.connect("activate", self.ui_util.about)
		dock.insert_app_menu_item(item, 0)
		dock.insert_app_menu_item(gtk.SeparatorMenuItem(), 1)

		#Preferences:
		item = gtk.MenuItem("Preferences")
		item.show()
		item.connect("activate", self.configure_applet)
		self.preferences_menuitem = item
		dock.insert_app_menu_item(item, 2)
		return	menu


	def mac_populate_menubar(self):
		self.sdebug(None)
		# Clear menus:
		for menu in [self.mac_desktop_menu, self.mac_window_menu, self.mac_servers_menu, self.mac_shortcut_menu]:
			for m in menu.get_children():
				menu.remove(m)

		# Always show this at the top of the server menu:
		self.mac_servers_menu.add(self.make_new_connection_menu())
		if len(self.servers)>0:
			self.mac_servers_menu.add(gtk.SeparatorMenuItem())
		# Generic Start button in window menu:
		self.mac_window_menu.add(self.make_start_session_menu(None, True))
		# Generic Start button in desktop menu:
		self.mac_desktop_menu.add(self.make_start_desktop_menu(None, True))
		# shortcuts
		global shortcut_menu_count
		shortcut_menu_count = 0

		def add_servers_to_menu(servers, add_sessions=True):
			window_menu_count = 0
			desktop_menu_count = 0
			global shortcut_menu_count
			for server in servers:
				server_menu = self.add_server_list_entry(server)
				self.mac_servers_menu.append(server_menu)
				if not add_sessions:
					continue
				# Sessions:
				for session in server.get_live_sessions(False, ignore=[]):
					session_menu_item = self.create_session_menuitem(server, session, " on %s" % server.name, True)
					if session.full_desktop:
						if desktop_menu_count==0:
							self.mac_desktop_menu.add(gtk.SeparatorMenuItem())
						desktop_menu_count += 1
						self.mac_desktop_menu.add(session_menu_item)
					else:
						if window_menu_count==0:
							self.mac_window_menu.add(gtk.SeparatorMenuItem())
						window_menu_count += 1
						self.mac_window_menu.add(session_menu_item)
				# Shortcuts:
				shortcuts = self.get_local_shortcuts(server).values()
				if shortcut_menu_count>0:
					self.mac_shortcut_menu.add(gtk.SeparatorMenuItem())
				self.populate_menu_with_actions(self.mac_shortcut_menu, shortcuts, add_separator=False, suffix=" on %s" % server.get_display_name())
				shortcut_menu_count += len(shortcuts)

		connected = [x for x in self.servers if x.is_connected()]
		not_connected = [x for x in self.servers if not x.is_connected()]
		# Servers we can connect to
		add_servers_to_menu(connected)
		if len(connected)>0 and len(not_connected)>0:
			self.mac_servers_menu.add(gtk.SeparatorMenuItem())
		add_servers_to_menu(not_connected, False)

		for menu in [self.mac_desktop_menu, self.mac_window_menu, self.mac_servers_menu]:
			menu.show_all()

	def popup_menu_workaround(self, menu):
		""" windows does not automatically close the popup menu when we click outside it
			so we workaround it by using a timer and closing the menu when the mouse
			has stayed outside it for more than 0.5s.
			This code must be added to all the sub-menus of the popup menu too!
		"""
		if not (WIN32 or OSX):
			return	#not needed
		def enter_menu(*args):
			self.mouse_in_tray_menu_counter += 1
			self.mouse_in_tray_menu = True
		def leave_menu(*args):
			self.mouse_in_tray_menu_counter += 1
			self.mouse_in_tray_menu = False
			def check_menu_left(expected_counter):
				#self.sdebug("counter=%s" % self.mouse_in_tray_menu_counter, expected_counter)
				if self.cleanup_called:
					return	False
				if self.mouse_in_tray_menu:
					return	False
				if expected_counter!=self.mouse_in_tray_menu_counter:
					return	False			#counter has changed
				self.close_menu()
			if not self.cleanup_called:
				gobject.timeout_add(500, check_menu_left, self.mouse_in_tray_menu_counter)
		menu.connect("enter-notify-event", enter_menu)
		menu.connect("leave-notify-event", leave_menu)

	def make_new_connection_menu(self):
		return	self.ui_util.make_menuitem("New Connection", "connect", None, self.quick_connect)

	def create_tray_menu(self):
		if not self.tray:
			return			#not used
		menu = gtk.Menu()
		menu.set_title("WindowSwitch")
		# About
		menu.append(self.ui_util.make_menuitem("About", "information", None, self.ui_util.about))
		# Configure Applet
		self.preferences_menuitem = self.ui_util.make_menuitem("Configuration", "configure", None, self.configure_applet)
		menu.append(self.preferences_menuitem)
		self.populate_menu_with_actions(menu, self.get_session_start_actions(), False, True)
		if self.settings.show_deep_menus:
			self.add_server_list_submenu(menu)

		menu.append(gtk.SeparatorMenuItem())
		# Servers we can connect to
		for server in self.servers:
			self.populate_server_menu(menu, server)

		# Connect
		menu.append(self.make_new_connection_menu())
		# Quit
		quit_cb = self.quit
		if self.settings.window_notray:
			quit_cb = self.ask_quit
		menu.append(self.ui_util.make_menuitem("Quit", "quit", None, quit_cb))

		# MS Windows does not hide the menu if we click outside of it... so we need to deal with it explicitly
		# Found the idea in: http://stackoverflow.com/questions/1138891/gtk-statusicon-and-gtk-menu-on-windows
		if WIN32:
			menu.append(self.ui_util.make_menuitem("Close", "close", None, self.close_menu))
		self.popup_menu_workaround(menu)
		menu.connect("deactivate", self.menu_deactivated)
		menu.show_all()
		self.tray.set_menu(menu)
		return	menu


	def populate_menu_with_actions(self, menu, actions, add_separator, grey_out=False, suffix=None):
		new_items = []
		for name,tooltip,icon,callback,enabled in actions:
			if enabled or grey_out:
				label = name
				if suffix:
					label = "%s%s" % (name,suffix)
				item = self.ui_util.menuitem(label, icon, tooltip, callback)
				if grey_out and not enabled:
					item.set_sensitive(False)
				new_items.append(item)
		if len(new_items)>0:
			if add_separator:
				menu.append(gtk.SeparatorMenuItem())
			for item in new_items:
				menu.append(item)

	def create_server_menuitem(self, server, pref_name=None):
		extra_text = ""
		tooltip = None
		if not server.is_connected():
			icon = icons.get("disconnected")
			if server.invalid_login:
				extra_text = "- Invalid Login"
				tooltip = "Please verify the configuration"
			elif server.is_connecting():
				extra_text = "- Connecting"
				tooltip = "Connecting to %s on port %d" % (server.command_host, server.command_port)
			elif not server.auto_connect:
				extra_text = ""
			else:
				extra_text = " - Not connected"
		else:
			icon = self.ui_util.get_server_type_icon(server.type)

		if not pref_name:
			pref_name = "%s@%s" % (server.username, server.get_display_name())
		return self.ui_util.menuitem(pref_name+extra_text, icon, tooltip)


	def add_server_list_submenu(self, menu):
		""" shows a list of other servers in a sub-menu for quick access (only created if there is something to show) """
		submenu = None
		for server in self.servers:
			if (not server.enabled or server.dynamic) and not (server.is_connected() or server.is_connecting()):
				if submenu is None:
					#create it on demand:
					submenu = gtk.Menu()
					server_list_item = self.ui_util.menuitem("Other Servers", icons.get("servers"), "List of other servers, currently disconnected or disabled")
					server_list_item.set_submenu(submenu)
					self.popup_menu_workaround(submenu)
					menu.append(server_list_item)
				submenu.append(self.add_server_list_entry(server))

	def add_server_list_entry(self, server):
		server_menu = self.create_server_menuitem(server)
		server_menu.connect('activate', lambda x : self.configure_server(x, server))
		return	server_menu

	def populate_server_menu(self, menu, server, section_separator=False, trailing_separator=True, pref_name=None):
		""" The menu entries for a single server """
		# server menu item
		if (not server.enabled or server.dynamic) and not (server.is_connected() or server.is_connecting()):
			return	False

		server_menu = self.create_server_menuitem(server)
		menu.append(server_menu)

		if not self.settings.show_deep_menus:
			#no sub-menu
			server_menu.connect('activate', lambda x : self.configure_server(x, server))
		else:
			# sub menu
			sm = gtk.Menu()
			server_menu.set_submenu(sm)
			self.popup_menu_workaround(sm)

			#config link:
			self.populate_menu_with_actions(sm, self.get_configure_actions(server), False)
			#disconnect/reconnect & disable
			self.populate_menu_with_actions(sm, self.get_connection_actions(server), True)
			#custom .desktop files and "Open File" button:
			self.populate_menu_with_actions(sm, self.get_server_actions(server), True)
			#resume all/disconnect all
			self.populate_menu_with_actions(sm, self.get_session_group_actions(server), True)

			(_, _, sendable) = self.get_connected_suspended_sendable(server)
			if server.is_connected() and sendable>0:
				# send all
				self.add_send_to_menu(server, None, self.send_all, sm, label="Send All to ")

			users = server.get_active_users()
			if len(users)>0:
				users_menu = self.ui_util.make_menuitem("%d Users" % len(users), "group")
				users_submenu = gtk.Menu()
				users_menu.set_submenu(users_submenu)
				self.popup_menu_workaround(users_submenu)
				for user in users:
					user_menu = self.get_user_action_menu(server, user)
					users_submenu.append(user_menu)
				sm.append(users_menu)


		# Sessions
		menuItems = []
		sessions = sorted(server.get_live_sessions(False, ignore=[]), key=lambda session: session.name.lower())
		for session in sessions:
			# all but preload/closed
			menuItem = self.get_session_menu(server, session)
			if menuItem:
				menuItems.append(menuItem)
		if len(menuItems)>0:
			if section_separator:
				menu.append(gtk.SeparatorMenuItem())
			if len(menuItems)>self.settings.max_session_menu_items:
				#create a sub-menu
				sessions_menu = self.ui_util.make_menuitem("%d Existing Sessions" % len(menuItems), "arrow_small_green")
				sessions_submenu = gtk.Menu()
				sessions_menu.set_submenu(sessions_submenu)
				self.popup_menu_workaround(sessions_submenu)
				addTo = sessions_submenu
				menu.append(sessions_menu)
			else:
				#add to main
				addTo = menu
			for menuItem in menuItems:
				addTo.append(menuItem)

		#Start desktop/server command
		shortcuts = self.get_local_shortcuts(server).values()
		if self.settings.show_deep_menus and server.is_connected() and \
			(len(shortcuts)>0 or len(server.server_commands)>0 or len(server.desktop_commands)>0):
			if section_separator:
				menu.append(gtk.SeparatorMenuItem())

			if len(shortcuts)>0:
				shortcut_menu = self.make_shortcut_menu(server, shortcuts)
				menu.append(shortcut_menu)

			# Start menu
			commands = sorted(server.server_commands)
			if server.is_connected() and len(commands)>0:
				from_list = self.settings.get_available_session_types(False, False)
				types = server.get_filtered_session_types(from_list, False, False)
				if server.local and self.settings.hide_suboptimal_protocols and SSH_TYPE in types:
					types.remove(SSH_TYPE)			#doing SSH to a local server isn't very useful at all!
				if len(types)>0:
					#add start application menu to main menu
					start_menu = self.make_start_menu(server, commands)
					menu.append(start_menu)

			commands = sorted(server.desktop_commands)
			if server.is_connected() and len(commands)>0:
				from_list = self.settings.get_available_session_types(True, False)
				types = server.get_filtered_session_types(from_list, True, False)
				if len(types)>0:
					#add start desktop menu to main menu
					menu.append(self.make_desktop_menu(server, commands))

		if trailing_separator:
			menu.append(gtk.SeparatorMenuItem())
		return True

	def make_start_desktop_menu(self, server, add_action):
		start_menu = self.ui_util.make_menuitem("Start Desktop", "desktop")
		if add_action:
			start_menu.connect('activate', lambda x : self.do_start_menu_dialog(server, ServerCommand.DESKTOP))
		return start_menu

	def	make_desktop_menu(self, server, commands):
		# Start menu
		start_menu = self.make_start_desktop_menu(server, not self.settings.show_deep_menus)
		if not self.settings.show_deep_menus:
			return start_menu
		sm = gtk.Menu()
		start_menu.set_submenu(sm)
		self.popup_menu_workaround(sm)
		self.add_commands_menu(sm, server, commands)
		return	start_menu

	def get_connected_suspended_sendable(self, server, compat_only=True):
		suspended = 0
		connected = 0
		sendable = 0
		sessions = server.get_live_sessions()
		for session in sessions:
			if compat_only and not self.is_session_compatible(session):
				continue
			if session.is_connected_to(self.settings.uuid):
				connected += 1
			else:
				suspended += 1
			if session.session_type!=SSH_TYPE:
				sendable += 1
		return	(connected, suspended, sendable)

	def is_session_compatible(self, session):
		from winswitch.objects.global_settings import get_settings
		settings = get_settings()
		st = session.session_type
		if st==VNC_TYPE or st==LIBVIRT_TYPE:
			return	settings.supports_vnc
		if st==NX_TYPE:
			return	settings.supports_nx
		if st==XPRA_TYPE:
			return	settings.supports_xpra
		if st==SSH_TYPE or st==SCREEN_TYPE:
			return	settings.supports_ssh
		if st==WINDOWS_TYPE:
			return	settings.supports_rdp
		return	False



	def get_server_action_command(self, action, server, session_type=None, screen_size=None, opts=None):
		description = action.comment or ""
		if session_type:
			description += " (via %s)" % session_type
		return (action.name, description, action.get_icon(),
							lambda x : self.start_menu_callback(x, server, action, session_type, screen_size, opts), server.is_connected())

	def get_server_actions(self, server):
		"""
		Returns the list of actions that are valid for this server in the form:
		(label, description, icon, callback, enabled)
		enabled will only be set if the state of the server makes it so.
		The actions include all the server actions and the "Open File" action.
		"""
		actions = []
		#ensure we have compatible protocols available:
		has_any_proto = (server.supports_vnc		and self.settings.supports_vnc) or \
							(server.supports_nx		and self.settings.supports_nx) or \
							(server.supports_xpra	and self.settings.supports_xpra) or \
							(server.supports_rdp	and self.settings.supports_rdp)

		#List of server actions:
		for action in server.action_commands:
			actions.append(self.get_server_action_command(action, server))

		if server.supports_file_open and (server.allow_file_transfers or server.local):
			# Open File
			tooltip = "Copy a file on this computer to '%s' and open it from there" % server.get_display_name()
			if not has_any_proto:
				tooltip = "No compatible protocols found, cannot open files remotely!"
			enabled = server.is_connected() and has_any_proto
			actions.append(("Open File", tooltip, icons.get("open_file"),
							lambda x : self.open_local_file(x, server), enabled))
		return	actions

	def get_local_shortcuts(self, server):
		"""
		Returns a dict of the matched shortcuts for the local server.
		key is the shortcut string (ie: vnc:screen=800x600:gedit)
		value is the tuple (name, comment, icon, callback, enabled_flag) which can be used to draw menus/icons.
		"""
		actions = {}
		for shortcut in server.local_shortcuts:
			if not shortcut:
				continue
			parts = shortcut.split(":", 3)				#ie: nx:screen_size=1024x768:IDOFCOMMAND
			if len(parts)<3:
				continue
			session_type = None
			for t in ALL_TYPES:
				if parts[0]==t:
					session_type = t
			screen_size = None
			opts = None
			if len(parts[1])>0:
				opts = {}
				options = parts[1].split(";")
				for option in options:
					if option.find("=")>0:
						k,v = option.split("=")
						opts[k] = v
				screen_size = opts.get("screen_size")
				if screen_size:
					del opts["screen_size"]
			for action in server.server_commands:
				if action.command==parts[2]:
					actions[shortcut] = self.get_server_action_command(action, server, session_type, screen_size, opts)
					break
		return	actions

	def get_session_start_actions(self, server=None):
		"""
		Returns the start actions for the given server (or for all servers if server is None)
		"""
		return [self.get_start_session_action(server), self.get_start_desktop_action(server)]

	def get_start_desktop_action(self, server):
		tooltip = "Start a new remote desktop"
		if server:
			has_desktop_proto = server.has_desktop_protocol()
		else:
			has_desktop_proto = False
			for s in self.servers:
				if s.status==ServerConfig.STATUS_CONNECTED and s.has_desktop_protocol():
					has_desktop_proto = True
					break
		if not has_desktop_proto:
			tooltip = "No compatible protocols found, cannot start a new remote desktop!"
		enabled = (server is None or server.is_connected()) and has_desktop_proto
		return ("Start Desktop Session", tooltip, icons.get("desktop"),
					lambda x : self.do_start_menu_dialog(server, ServerCommand.DESKTOP), enabled)

	def get_start_session_action(self, server):
		tooltip = "Start a new session"
		if server:
			has_proto = server.has_seamless_protocol()
		else:
			has_proto = False
			for s in self.servers:
				if s.is_connected() and s.has_seamless_protocol():
					has_proto = True
					break
		if not has_proto:
			tooltip = "No compatible protocols found, cannot start a new session!"
		enabled = (server is None or server.is_connected()) and has_proto
		return ("Start Application Session", tooltip, icons.get("run"),
					lambda x : self.do_start_menu_dialog(server, ServerCommand.COMMAND), enabled)



	def get_connection_actions(self, server):
		actions = []
		# Disconnect
		if not server.embedded_server:
			actions.append(("Disconnect", "Disconnect from this server", icons.get("disconnected"),
							lambda x : self.disconnect_server(x, server), server.is_connected()))
		# Retry
		if server.is_connecting():
			actions.append(("Abort", "Abort the connection attempt to this server", icons.get("close"),
							lambda x : self.disconnect_server(x, server), True))
		else:
			title = "Connect"
			icon = "connect"
			tip = "Connect to this server"
			if server.auto_connect:
				title = "Re-connect"
				icon = "retry"
				tip = "Try to re-connect to this server"
			actions.append((title, tip, icons.get(icon),
							lambda x : self.retry_server(x, server), not server.is_connected()))

		if not server.embedded_server and server.clients_can_stop:
			actions.append(("Shutdown", "Stop the server", icons.get("stop"),
							lambda x : self.stop_server(x, server), server.is_connected()))
		return actions

	def get_configure_actions(self, server):
		return [
				("Configure", "Configure access to this server", icons.get("hammer"), lambda x : self.configure_server(x, server), True),
				("Disable", "Remove this server from the main menu", icons.get("unticked"), lambda x : self.disable_server(x, server), True)
				]

	def get_session_group_actions(self, server):
		"""
		Returns the list of actions that can be done as a group: Resume All, Detach All, Close All
		May be empty if there are no sessions.
		"""
		actions = []
		#count sessions
		(connected, suspended, _) = self.get_connected_suspended_sendable(server)
		if suspended > 0:
			# resume all
			actions.append(('Resume All %d Sessions' % suspended, "Re-connect all the available sessions", icons.get("play"),
							lambda x : self.resume_all(x, server), server.is_connected()))

		if connected > 0:
			# detach all
			actions.append(('Detach All', "Detach all connected sessions", icons.get("pause"),
							lambda x : self.detach_all_sessions(x, server), server.is_connected()))
			if False:	#disabled until we can do this safely
				#Close
				actions.append(('Close All', "Close all the connected sessions", icons.get("close"),
							lambda x : self.close_all_sessions(x, server), server.is_connected()))
		return	actions


	def make_shortcut_menu(self, server, shortcuts):
		shortcut_menu = self.ui_util.make_menuitem("Shortcuts", "star")
		sm = gtk.Menu()
		shortcut_menu.set_submenu(sm)
		self.popup_menu_workaround(sm)
		self.populate_menu_with_actions(sm, shortcuts, True)
		return	shortcut_menu

	def make_start_session_menu(self, server, add_action):
		start_session = self.ui_util.make_menuitem("Start Application", "run")
		if add_action:
			start_session.connect('activate', lambda x : self.do_start_menu_dialog(server, ServerCommand.COMMAND))
		return start_session

	def	make_start_menu(self, server, commands):
		# Start menu
		start_menu = self.make_start_session_menu(server, not self.settings.show_deep_menus)
		if not self.settings.show_deep_menus:
			return start_menu
		sm = gtk.Menu()
		start_menu.set_submenu(sm)
		self.popup_menu_workaround(sm)
		menus = sorted(server.menu_directories)

		if server.allow_custom_commands:
			#custom command dialog
			def custom_command_dialog(*args):
				def custom_dialog_do_start(server,server_command,session_type,screen_size,opts,bookmark):
					self.do_start_session(server, server_command, session_type, screen_size, opts, bookmark)
				custom_dialog = SessionCustomCommandDialog(server, self.client_utils, server.preferred_session_type, custom_dialog_do_start)
				custom_dialog.show()
			custom_launch = self.ui_util.menuitem("Custom Command", icons.get("command_prompt"), "Enter a specific command to run", custom_command_dialog)
			sm.append(custom_launch)


		if len(menus)==0 or len(commands) <= self.settings.max_command_menu_items:
			self.add_commands_menu(sm, server, commands)		#add to top level
		else:
			category_commands = self.ui_util.group_commands_in_categories(server, commands)
			for (category, commands) in category_commands:
				menu_item = self.ui_util.get_command_menu_entry(server, category)
				# sub menu
				start_submenu = gtk.Menu()
				menu_item.set_submenu(start_submenu)
				self.popup_menu_workaround(start_submenu)
				self.add_commands_menu(start_submenu, server, sorted(commands))
				#add this menu to start menu
				sm.append(menu_item)
		return	start_menu

	def get_user_action_menu(self, server, user):
		label = self.ui_util.get_user_visible_name(user)
		tip = None
		if not self.settings.show_user_host:
			tip = "connected from %s" % user.host
		return	self.ui_util.menuitem(label, user.get_avatar_icon(), tip,
										lambda x : self.user_action(x, server, user))


	def add_commands_menu(self, menu, server, commands):
		for command in commands:
			entry = self.ui_util.get_command_menu_entry(server, command)
			entry.connect("activate", self.start_menu_callback, server, command)
			menu.append(entry)

	def get_session_visible_name(self, session):
		visible_name = session.name
		if visible_name.startswith(": ") and len(session.window_names)>0:
			visible_name = session.window_names[0]
			pos = visible_name.rfind("(on ")
			if pos>0:
				visible_name = visible_name[:pos]
			pos = visible_name.rfind(": /")
			if pos>0:
				cmd_name = visible_name[:pos]
				path_name = visible_name[pos:]
				pos = path_name.rfind("/")
				if pos>0:
					path_name = path_name[pos+1:]
				visible_name = "%s: %s" % (cmd_name, path_name)
		if len(visible_name) > self.settings.max_menu_entry_length:
			visible_name = visible_name[self.settings.max_menu_entry_length-3:]+"..."
		return	visible_name


	def get_session_actions(self, server, session):
		"""
		Returns the list of actions that are valid for this session in the form:
		(label, description, icon, callback, enabled)
		enabled will only be set if the state of the session/server makes it so.
		"""
		actions = []
		if session.status in [Session.STATUS_CLOSED, Session.STATUS_UNAVAILABLE] or session.timed_out:
			return	actions
		ssh = session.session_type == SSH_TYPE
		xpra = session.session_type == XPRA_TYPE
		nx = session.session_type == NX_TYPE
		vnc = session.session_type == VNC_TYPE
		gstvideo = session.session_type == GSTVIDEO_TYPE
		rdp = session.session_type == WINDOWS_TYPE
		osx = session.session_type == OSX_TYPE
		x11 = session.session_type == X11_TYPE
		screen = session.session_type == SCREEN_TYPE
		libvirt = session.session_type == LIBVIRT_TYPE
		vbox = session.session_type == VIRTUALBOX_TYPE
		shadow = session.is_shadow()

		connected = session.is_connected_to(self.settings.uuid)
		connecting = session.status == Session.STATUS_CONNECTING
		starting = session.status == Session.STATUS_STARTING
		suspending = session.status == Session.STATUS_SUSPENDING
		compatible = (ssh and self.settings.supports_ssh) \
				or (xpra and self.settings.supports_xpra) \
				or (nx and self.settings.supports_nx) \
				or ((vnc or libvirt or osx) and self.settings.supports_vnc) \
				or (screen and self.settings.supports_screen) \
				or (((rdp and server.supports_rdp) or vbox) and self.settings.supports_rdp)
		ALLOW_SHADOWING = True
		could_shadow = ALLOW_SHADOWING and not libvirt and not shadow
		could_nx_shadow = could_shadow and server.supports_nx and session.can_be_shadowed(NX_TYPE)
		could_vnc_shadow = could_shadow and server.supports_vncshadow and session.can_be_shadowed(VNC_TYPE)
		from winswitch.util.gstreamer_util import has_tcp_plugins
		could_gst_shadow = could_shadow and server.supports_gstvideo and session.can_be_shadowed(GSTVIDEO_TYPE) and has_tcp_plugins
		could_xpra_shadow = could_shadow and server.supports_xprashadow and session.can_be_shadowed(XPRA_TYPE)

		#self.sdebug("shadow=%s, server.supports_nx=%s, server.supports_vncshadow=%s, connected=%s, connecting=%s, starting=%s, suspending=%s, compatible=%s, could_nx_shadow=%s, could_vnc_shadow=%s, could_gst_shadow=%s" %
		#		(shadow, server.supports_nx, server.supports_vncshadow, connected, connecting, starting, suspending, compatible, could_nx_shadow, could_vnc_shadow, could_gst_shadow), server, session)

		if (not shadow or vnc) and not (osx or x11 or ssh):	#can never suspend/resume SSH or shadow or real displays (x11/win/osx):
			actions.append(("Resume", "Re-connect this session", icons.get("play"), lambda x : self.resume_session(x, server, session),
						compatible and server.is_connected() and not connected and not connecting and not starting and not suspending))
			#actions.append(("Raise", "Raise this application's windows", icons.get("raise"), lambda x : self.raise_windows(x, server, session),
			#			connected and self.wm_util.can_raise_windows()))
			if (not rdp or not server.local):
				actions.append(("Detach", "Detach this session", icons.get("pause"), lambda x : self.detach_session(x, server, session),
						connected))
				actions.append(("Re-connect", "Re-connect to this session (will refresh the display)", icons.get("retry"), lambda x : self.reconnect_session(x, server, session),
						compatible and connected and server.is_connected()))
			#actions.append(("Terminate", "Terminate this session", icons.get("close"), lambda x : self.close_session(x, server, session),
			#			connected))

		if could_xpra_shadow:
			#FIXME: only one shadow at a time for now..
			shadows = server.get_all_sessions_for_display(session.display, shadows=True)
			can_shadow = self.settings.supports_xpra and server.is_connected() and len(shadows)==0
			actions.append(("Xpra Copy", "Create a new view of this session using Xpra", icons.get("copy"), lambda x : self.shadow_session(x, server, session, False, XPRA_TYPE),
						can_shadow))
			actions.append(("Read-only Copy", "Create a new read-only view of this session using Xpra", icons.get("shadow"), lambda x : self.shadow_session(x, server, session, True, XPRA_TYPE),
						can_shadow))
		if could_nx_shadow:
			can_shadow = self.settings.supports_nx and server.is_connected()
			actions.append(("NX Copy", "Create a new view of this session using NX", icons.get("copy"), lambda x : self.shadow_session(x, server, session, False, NX_TYPE),
						can_shadow))
			actions.append(("Read-only Copy", "Create a new read-only view of this session using NX", icons.get("shadow"), lambda x : self.shadow_session(x, server, session, True, NX_TYPE),
						can_shadow))
		if could_vnc_shadow:
			can_shadow = self.settings.supports_vnc and server.is_connected()
			actions.append(("VNC Copy", "Create a new view of this session using VNC", icons.get("copy"), lambda x : self.shadow_session(x, server, session, False, VNC_TYPE),
						can_shadow))
		if could_gst_shadow:
			can_shadow = self.settings.supports_gstvideo and server.is_connected()
			actions.append(("GST Video Copy", "Stream this session using Gstreamer video", icons.get("copy"), lambda x : self.shadow_session(x, server, session, True, GSTVIDEO_TYPE),
						can_shadow))

		if shadow:
			if (nx or gstvideo) and connected:
				#detach nxproxy is enough to get nxagent to stop
				actions.append(("Close", "Close this copy", icons.get("stop"), lambda x : self.detach_session(x, server, session),
						True))
			elif vnc and connected:
				#must kill session to get x0vncserver/x11vnc to disconnect
				actions.append(("Close", "Close this copy", icons.get("stop"), lambda x : self.do_kill_session(server, session),
						server.is_connected()))
		if not (shadow or x11 or rdp or osx or libvirt):
			UUID = self.settings.uuid
			if (server.is_connected() or ssh) and (screen or connected or session.owner==UUID):
				actions.append(("Stop", "Force this session to stop - beware, you may lose your data!", icons.get("stop"), lambda x : self.kill_session(x, server, session), True))
		return	actions

	def get_session_user(self, server, session):
		if not session.actor:
			return	None
		return	server.get_user_by_uuid(session.actor)

	def create_session_menuitem(self, server, session, suffix, add_action):
		visible_name = self.get_session_visible_name(session)
		if suffix:
			visible_name += suffix
		icon = self.ui_util.get_session_window_icon(server, session)
		tip = session.get_info()
		session_menu_item = self.ui_util.menuitem(visible_name, icon, tip, None)
		if add_action:
			session_menu_item.connect('activate', lambda x : self.session_action(x, server, session))
		return	session_menu_item


	def get_session_menu(self, server, session):
		actor = self.get_session_user(server, session)
		unavailable = session.status == Session.STATUS_UNAVAILABLE
		session_menu_item = self.create_session_menuitem(server, session, "", (not self.settings.show_deep_menus) or unavailable)
		if (not self.settings.show_deep_menus) or unavailable:
			return	session_menu_item

		#Have sub-menu:
		tip = None
		if actor:
			tip = "In use by %s" % (self.ui_util.get_user_visible_name(actor))
		actor_actions = self.ui_util.make_menuitem("Information", "information", tip,
												lambda x : self.session_action(x, server, session))
		sm = [actor_actions]

		actions = self.get_session_actions(server, session)
		for name,tooltip,icon,callback,enabled in actions:
			if enabled:
				sm.append(self.ui_util.menuitem(name, icon, tooltip, callback))

		if server.is_connected() and session.session_type!=SSH_TYPE:
			#TODO: security checks here to match server rules -> abstract them to match!
			self.add_send_to_menu(server, session, self.send, sm)

		if len(sm)>0:
			submenu = gtk.Menu()
			session_menu_item.set_submenu(submenu)
			self.popup_menu_workaround(submenu)
			for item in sm:
				submenu.append(item)
		return	session_menu_item

	def add_send_to_menu(self, server, session, send_callback, menu, label="Send to "):
		"""
		Adds a 'Send to' menu to the menu passed in if there are users who can access this session.
		If there are too many, a sub-menu will be created.
		"""
		def get_other_server_users(server, excluding=None):
			new_list = []
			UUID = self.settings.uuid
			all_excluding = []
			if excluding and excluding!=UUID:
				all_excluding.append(excluding)
			if not self.settings.show_send_to_self:
				all_excluding.append(UUID)
			for user in server.get_active_users():
				if user.uuid not in all_excluding:
					new_list.append(user)
			return new_list

		# send all
		if session:
			users = get_other_server_users(server, session.actor)
		else:
			users = get_other_server_users(server)

		users_can_access = []
		for user in users:
			if session and user.can_access_session(session):
				users_can_access.append(user)

		if len(users_can_access)==0:
			return

		if len(users_can_access)>self.settings.max_user_menu_items:
			#create a sub-menu and add options there (without prefix)
			prefix = ""
			send_sm = self.ui_util.make_menuitem(label, "group")
			menu.append(send_sm)
			# sub menu
			send_submenu = gtk.Menu()
			send_sm.set_submenu(send_submenu)
			self.popup_menu_workaround(send_submenu)
			# add entries there
			add_to_menu = send_submenu
		else:
			#add all options inline with prefix supplied in label
			prefix = label
			add_to_menu = menu

		for user in users_can_access:
			label = prefix+self.ui_util.get_user_visible_name(user)
			send = self.ui_util.menuitem(label, self.ui_util.get_user_image(user), "%s on %s" % (user.name, user.host), None)
			send.connect('button-release-event', self.send_button_release_callback, send_callback, server, session, user.uuid)
			add_to_menu.append(send)








#****************************************************************
def main():
	logger.slog("main")
	exit_code = 0
	winswitch = None
	try:
		winswitch = WinSwitchApplet()
	except Exception, e:
		logger.exc(e)
		if winswitch:	#if we managed to load that class, display_error should have loaded too
			display_error("%s failed to start" % APPLICATION_NAME, str(e))
		exit_code = 1

	if exit_code==0 and not winswitch.contact_existing_client():
		try:
			winswitch.run()
			logger.slog("run() ended")
		except Exception, e:
			logger.serr("caught exception, closing applet", e)
			exit_code = 1
			try:
				winswitch.cleanup()
			except Exception, e:
				logger.serror("exception during cleanup()", e)
	dump_threads()
	logger.slog("main()=%s" % exit_code)
	rotate_log_file()
	return	exit_code

if __name__ == "__main__":
	sys.exit(main())
