1823 lines
61 KiB
Python
1823 lines
61 KiB
Python
'''
|
|
Folding@Home Client Control (FAHControl)
|
|
Copyright (C) 2010-2016 Stanford University
|
|
|
|
This program 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.
|
|
|
|
This program 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 PARTIULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import sys
|
|
import os
|
|
import re
|
|
import traceback
|
|
import platform
|
|
import urllib
|
|
|
|
import gtk
|
|
import glib
|
|
import pygtk
|
|
pygtk.require("2.0")
|
|
import pango
|
|
import webbrowser
|
|
import shlex
|
|
import subprocess
|
|
from wraplabel import WrapLabel
|
|
|
|
# OSX integration
|
|
if sys.platform == 'darwin':
|
|
try:
|
|
from gtk_osxapplication import *
|
|
except:
|
|
import gtkosx_application
|
|
from gtkosx_application import Application as OSXApplication
|
|
from gtkosx_application import gtkosx_application_get_resource_path \
|
|
as quartz_application_get_resource_path
|
|
|
|
from fah import *
|
|
from fah.db import *
|
|
from fah.util import *
|
|
|
|
|
|
def set_tree_view_font(widget, font):
|
|
for widget in iterate_container(widget):
|
|
if isinstance(widget, gtk.TreeView):
|
|
widget.modify_font(font)
|
|
|
|
|
|
def append_tree_entry(model, path, iter, selection):
|
|
selection.append((path, iter))
|
|
|
|
|
|
def get_tree_selection(tree_view):
|
|
selection = []
|
|
tree_view.get_selection().selected_foreach(append_tree_entry, selection)
|
|
return selection
|
|
|
|
|
|
def remove_tree_selection(tree_view, callback = None):
|
|
for path, iter in get_tree_selection(tree_view):
|
|
if callback is not None: callback(path, iter)
|
|
tree_view.get_model().remove(iter)
|
|
|
|
|
|
def osx_version():
|
|
""" returns osx version as tuple of integers """
|
|
if sys.platform != 'darwin': return None
|
|
try:
|
|
ver = tuple([int(x) for x in platform.mac_ver()[0].split('.')])
|
|
except Exception, e:
|
|
print e
|
|
darwin_ver = platform.release().split('.')
|
|
ver = (10, int(darwin_ver[0]) - 4, int(darwin_ver[1]))
|
|
return ver
|
|
|
|
|
|
def osx_add_GtkApplicationDelegate_methods():
|
|
# GtkApplicationDelegate Category via PyObjC
|
|
def applicationShouldHandleReopen_hasVisibleWindows_(self, app, flag):
|
|
# reopen event or dock icon clicked
|
|
# restore windows if hidden
|
|
controller = FAHControl.instance
|
|
if controller is not None: controller.restore()
|
|
return True
|
|
try:
|
|
import objc
|
|
cls = objc.lookUpClass('GtkApplicationDelegate')
|
|
sig1 = '%s%s%s%s%s' % (objc._C_NSBOOL, objc._C_ID, objc._C_SEL,
|
|
objc._C_ID, objc._C_NSBOOL)
|
|
objc.classAddMethods(cls, [
|
|
objc.selector(
|
|
applicationShouldHandleReopen_hasVisibleWindows_,
|
|
signature = sig1)
|
|
])
|
|
except Exception, e: print e
|
|
|
|
|
|
def osx_accel_window_close(accel_group, acceleratable, keyval, modifier):
|
|
acceleratable.hide()
|
|
return True
|
|
|
|
|
|
def osx_accel_window_minimize(accel_group, acceleratable, keyval, modifier):
|
|
acceleratable.iconify()
|
|
return True
|
|
|
|
|
|
def load_fahcontrol_db():
|
|
db = Database(os.path.join(get_home_dir(), 'FAHControl.db'))
|
|
db.validate()
|
|
|
|
return db
|
|
|
|
|
|
class FAHControl(SingleAppServer):
|
|
client_cols = 'name status status_color address'.split()
|
|
|
|
# NOTE: These URLs are here rather than in the Glade file because the
|
|
# Glade editor strips the '&'s on save. Even if you use '&' the
|
|
# ampersands get striped when resaved.
|
|
team_stats_links = [
|
|
['Folding@home', 'http://fah-web.stanford.edu/cgi-bin/main.py'
|
|
'?qtype=teampage&teamnum=%(team)s'],
|
|
['Extreme Overclocking', 'http://folding.extremeoverclocking.com/'
|
|
'team_summary.php?t=%(team)s'],
|
|
['Kakao Stats', 'http://kakaostats.com/tsum.php?t=%(team)s'],
|
|
['[H]ard Folding', 'http://www.hardfolding.com/fh_stats/index.php'
|
|
'?pz=101&tnum=%(team)s'],
|
|
['Custom', ''],
|
|
]
|
|
donor_stats_links = [
|
|
['Folding@home', 'http://fah-web.stanford.edu/cgi-bin/main.py'
|
|
'?qtype=userpage&username=%(donor)s'],
|
|
['Custom', ''],
|
|
]
|
|
|
|
folding_power_levels = ['Light', 'Medium', 'Full']
|
|
|
|
instance = None
|
|
|
|
def __init__(self, glade = 'FAHControl.glade'):
|
|
SingleAppServer.__init__(self)
|
|
|
|
self.__class__.instance = self
|
|
|
|
# Vars
|
|
self.clients = {}
|
|
self.clientsByAddress = {}
|
|
self.active_client = None
|
|
self.client_is_online = False
|
|
self.selected_clients = set()
|
|
self.status_clear_time = None
|
|
self.window_visible = False
|
|
self.viewer = None
|
|
self.last_db_flush = 0
|
|
self.last_clients_update = 0
|
|
self.error_dialog = None
|
|
self.restore_dialogs = []
|
|
self.last_clock = None
|
|
self.timer_id = None
|
|
self.folding_power_changing = False
|
|
|
|
# Open database
|
|
try:
|
|
self.db = load_fahcontrol_db()
|
|
|
|
except Exception, e:
|
|
print e
|
|
sys.exit(1)
|
|
|
|
# OSX integration
|
|
if sys.platform == 'darwin':
|
|
self.osx_app = OSXApplication()
|
|
self.osx_app.set_use_quartz_accelerators(True)
|
|
self.osx_version = osx_version()
|
|
self.is_old_gtk = gtk.gtk_version < (2,24)
|
|
osx_add_GtkApplicationDelegate_methods()
|
|
|
|
# URI hook
|
|
gtk.link_button_set_uri_hook(self.on_uri_hook, None)
|
|
|
|
# Style
|
|
settings = gtk.settings_get_default()
|
|
self.system_theme = settings.get_property('gtk-theme-name')
|
|
if sys.platform == 'darwin':
|
|
# Load standard key bindings for Mac and disable mnemonics
|
|
resources = quartz_application_get_resource_path()
|
|
rcfile = os.path.join(resources, 'themes/Mac/gtk-2.0-key/gtkrc')
|
|
if os.path.exists(rcfile): gtk.rc_parse(rcfile)
|
|
rcfile = os.path.join(os.path.expanduser("~"), '.FAHClient/gtkrc')
|
|
if os.path.exists(rcfile): gtk.rc_parse(rcfile)
|
|
self.mono_font = pango.FontDescription('Monospace')
|
|
small_font = pango.FontDescription('Sans 8')
|
|
|
|
# Default icon
|
|
gtk.window_set_default_icon(get_icon('small'))
|
|
|
|
# Filter glade
|
|
if len(glade) < 1024: glade = open(glade, 'r').read()
|
|
glade = re.subn('class="GtkLabel" id="wlabel',
|
|
'class="WrapLabel" id="wlabel', glade)[0]
|
|
if sys.platform == 'darwin':
|
|
# glade editor strips accel modifiers. add if missing
|
|
glade = re.subn('accelerator *key="comma" *signal',
|
|
'accelerator key="comma" modifiers="GDK_META_MASK" signal',
|
|
glade)[0]
|
|
|
|
# Build GUI
|
|
self.builder = builder = gtk.Builder()
|
|
try:
|
|
builder.add_from_string(glade)
|
|
except:
|
|
self.error('Failed to load UI file: %s' % glade)
|
|
sys.exit(1)
|
|
|
|
# Main window
|
|
self.window = builder.get_object('window')
|
|
self.window.set_geometry_hints(None, 440, 256, -1, -1, 800, 512)
|
|
set_tree_view_font(self.window, self.mono_font)
|
|
self.status_bar = builder.get_object('status_bar')
|
|
self.ppd_label = builder.get_object('ppd_label')
|
|
self.time_label = builder.get_object('time_label')
|
|
|
|
# Panes
|
|
self.panes = WidgetMap(self.window, 'paned')
|
|
for name, pane in self.panes.items():
|
|
prop = name + '_position'
|
|
|
|
# Load current value
|
|
if self.db.has(prop):
|
|
value = int(self.db.get(prop))
|
|
if value and value < 100: value = 100 # mimimum if not hidden
|
|
pane.set_position(value)
|
|
|
|
pane.connect('notify::position', self.store_property, prop)
|
|
|
|
# Dialogs
|
|
self.preferences_dialog = builder.get_object('preferences_dialog')
|
|
self.client_dialog = builder.get_object('client_dialog')
|
|
self.options_dialog = builder.get_object('options_dialog')
|
|
self.core_options_dialog = builder.get_object('core_options_dialog')
|
|
self.slot_dialog = builder.get_object('slot_dialog')
|
|
self.about_dialog = builder.get_object('about_dialog')
|
|
self.configure_dialog = builder.get_object('configure_dialog')
|
|
# Note: The order of these dialogs is important since they are restored
|
|
# in this order. Child dialogs cannot be restored before their
|
|
# parents so they must be last. See restore() below.
|
|
self.dialogs = [
|
|
self.about_dialog, self.preferences_dialog, self.client_dialog,
|
|
self.slot_dialog, self.options_dialog, self.core_options_dialog,
|
|
self.configure_dialog]
|
|
|
|
# Dialog & window sizes
|
|
self.windows = {
|
|
'preferences': self.preferences_dialog,
|
|
'client': self.client_dialog,
|
|
'options': self.options_dialog,
|
|
'core_options': self.core_options_dialog,
|
|
'slot': self.slot_dialog,
|
|
'about': self.about_dialog,
|
|
'main': self.window,
|
|
}
|
|
for name, win in self.windows.items():
|
|
if self.db.has(name + '_width'):
|
|
width = int(self.db.get(name + '_width'))
|
|
else: width = -1
|
|
|
|
if self.db.has(name + '_height'):
|
|
height = int(self.db.get(name + '_height'))
|
|
else: height = -1
|
|
|
|
if name == 'main':
|
|
if 0 < width and width < 600: width = 600
|
|
if 0 < height and height < 400: height = 400
|
|
|
|
if 100 <= width and 100 <= height: win.resize(width, height)
|
|
|
|
win.connect('configure_event', self.store_dimensions, name)
|
|
|
|
# Tool bar
|
|
builder.get_object('toolbar1').modify_font(small_font)
|
|
button = builder.get_object('viewer_button')
|
|
button.get_image().set_from_pixbuf(get_viewer_icon('small'))
|
|
|
|
# About Dialog
|
|
icon = builder.get_object('about_icon')
|
|
icon.set_from_pixbuf(get_icon('medium'))
|
|
about_version = builder.get_object('about_version')
|
|
about_version.set_markup('<b>Version: %s</b>' % version)
|
|
|
|
# Preferences
|
|
self.theme_list = self.load_themes()
|
|
widget = builder.get_object('theme_list')
|
|
for theme in self.theme_list: widget.append(theme)
|
|
|
|
# Client list
|
|
self.client_hpane = builder.get_object('client_hpane')
|
|
self.client_notebook = builder.get_object('client_notebook')
|
|
self.client_config_notebook = \
|
|
builder.get_object('client_config_notebook')
|
|
self.client_config_notebook.set_current_page(1)
|
|
self.client_tree = builder.get_object('client_tree_view')
|
|
self.client_list = builder.get_object('client_list')
|
|
self.client_label = builder.get_object('client_label')
|
|
self.client_config_label = builder.get_object('client_config_label')
|
|
self.client_tree.grab_focus()
|
|
selection = self.client_tree.get_selection()
|
|
selection.connect('changed', self.on_client_selection_changed)
|
|
|
|
# Option lists
|
|
self.option_tree, self.option_list = self.connect_option_view('')
|
|
self.slot_option_tree, self.slot_option_list =\
|
|
self.connect_option_view('slot_')
|
|
self.core_option_tree = builder.get_object('core_option_tree_view')
|
|
self.core_option_list = builder.get_object('core_option_list')
|
|
self.option_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
|
self.slot_option_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
|
self.core_option_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
|
|
|
# Option dialog
|
|
self.option_name_entry = builder.get_object('option_name_entry')
|
|
self.option_value_entry = builder.get_object('option_value_entry')
|
|
|
|
# Core option dialog
|
|
self.core_option_entry = builder.get_object('core_option_entry')
|
|
|
|
# Folding power
|
|
self.folding_power_label = builder.get_object('folding_power_label')
|
|
self.folding_power = builder.get_object('folding_power_hscale')
|
|
for i in range(len(self.folding_power_levels)):
|
|
level = self.folding_power_levels[i]
|
|
markup = '<span font_size="small" weight="bold">%s</span>' % level
|
|
self.folding_power.add_mark(i, gtk.POS_BOTTOM, markup)
|
|
|
|
# User info
|
|
self.donor_info = builder.get_object('donor_info')
|
|
self.team_info = builder.get_object('team_info')
|
|
|
|
# Client stats
|
|
self.client_ppd = builder.get_object('client_ppd')
|
|
|
|
# Client config
|
|
self.core_priority_low = builder.get_object('core_priority_low')
|
|
|
|
# Proxy
|
|
self.proxy_frame = builder.get_object('proxy_frame')
|
|
self.proxy_auth_frame = builder.get_object('proxy_auth_frame')
|
|
self.proxy_port = builder.get_object('proxy_port_entry')
|
|
|
|
# Project
|
|
self.project_frame = builder.get_object('project_frame')
|
|
self.project_text = builder.get_object('project_text')
|
|
self.project_label = builder.get_object('project_label')
|
|
|
|
# Slot lists
|
|
self.slot_status_tree = builder.get_object('slot_status_tree_view')
|
|
self.slot_status_tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
|
|
self.slot_status_list = builder.get_object('slot_status_list')
|
|
self.slot_tree = builder.get_object('slot_tree_view')
|
|
self.slot_list = builder.get_object('slot_list')
|
|
self.slot_menu = builder.get_object('slot_menu')
|
|
self.idle_slot_item = builder.get_object('idle_slot_item')
|
|
view_slot_item = builder.get_object('view_slot_item')
|
|
view_slot_item.get_image().set_from_pixbuf(get_viewer_icon('tiny'))
|
|
|
|
# Slot dialog
|
|
self.slot_type_cpu = builder.get_object('slot_type_cpu')
|
|
self.slot_type_gpu = builder.get_object('slot_type_gpu')
|
|
|
|
# Queue list
|
|
self.queue_tree = builder.get_object('queue_tree')
|
|
self.queue_list = builder.get_object('queue_list')
|
|
|
|
# Info
|
|
self.info = builder.get_object('info_alignment')
|
|
|
|
# Log
|
|
self.log_view = builder.get_object('log_text_view')
|
|
self.log_view.modify_font(self.mono_font)
|
|
self.log = builder.get_object('log_buffer')
|
|
self.log.create_mark('end', self.log.get_end_iter())
|
|
self.log_severity = builder.get_object('log_severity')
|
|
self.log_slot_enable = builder.get_object('log_slot_enable')
|
|
self.log_slot = builder.get_object('log_slot')
|
|
self.log_unit_enable = builder.get_object('log_unit_enable')
|
|
self.log_unit = builder.get_object('log_unit')
|
|
self.log_follow = builder.get_object('log_follow')
|
|
|
|
# Widget maps
|
|
self.client_entries = WidgetMap(self.client_dialog, '_entry')
|
|
self.client_option_widgets = \
|
|
WidgetMap(self.client_config_notebook, '_option')
|
|
self.client_config_tabs = WidgetMap(self.client_config_notebook, '_tab')
|
|
self.slot_option_widgets = WidgetMap(self.slot_dialog, '_option')
|
|
roots = [self.window, self.client_dialog]
|
|
self.preference_widgets = WidgetMap(self.preferences_dialog, '_pref')
|
|
widget = builder.get_object('advanced_unit_frame')
|
|
self.queue_widgets = WidgetMap(widget, None, 'queue_')
|
|
|
|
# Stats prefs
|
|
self.donor_stats_pref = self.preference_widgets['donor_stats']
|
|
self.team_stats_pref = self.preference_widgets['team_stats']
|
|
self.donor_stats_list = builder.get_object('donor_stats_links_list')
|
|
map(self.donor_stats_list.append, self.donor_stats_links)
|
|
self.team_stats_list = builder.get_object('team_stats_links_list')
|
|
map(self.team_stats_list.append, self.team_stats_links)
|
|
|
|
# OSX integration
|
|
if sys.platform == 'darwin':
|
|
# Setup dock menu
|
|
self.osx_menu = builder.get_object('osx_tray_menu')
|
|
if self.is_old_gtk:
|
|
self.osx_app.set_dock_menu(self.osx_menu)
|
|
else:
|
|
self.osx_create_dock_menu(self.osx_menu)
|
|
|
|
# Create application menu
|
|
self.osx_menubar = gtk.MenuBar()
|
|
self.osx_menubar.show_all()
|
|
self.osx_app.set_menu_bar(self.osx_menubar)
|
|
if self.is_old_gtk:
|
|
self.osx_group = self.osx_app.add_app_menu_group()
|
|
self.osx_menu.foreach(self.osx_add_to_menu)
|
|
else:
|
|
self.osx_create_app_menu(self.osx_menu)
|
|
|
|
# Hide some widgets in OSX
|
|
for name in ['ui_pref_frame', 'theme_pref', 'theme_label']:
|
|
widget = builder.get_object(name)
|
|
widget.set_property('visible', False)
|
|
|
|
if self.osx_version >= (10,7):
|
|
# remove broken window resize grip
|
|
self.status_bar.set_property('has-resize-grip', False)
|
|
self.time_label.set_property('xpad', 6)
|
|
else:
|
|
self.time_label.set_property('xpad', 2)
|
|
|
|
# Validators
|
|
EntryValidator(self, self.client_option_widgets['user'], r'^[!-~]+$',
|
|
'User name must be a non-empty string containing only '
|
|
'alphanumeric characters, standard punctuation and '
|
|
'no white-space.')
|
|
|
|
self.passkey_validator = \
|
|
PasswordValidator(self, builder.get_object('passkey_option'),
|
|
builder.get_object('passkey_reenter'),
|
|
builder.get_object('passkey_valid_image'),
|
|
builder.get_object('passkey_valid_text'),
|
|
r'^[0-9a-fA-F]{0,32}$', 'The passkey must be a '
|
|
'32 character hexadecimal string.')
|
|
|
|
self.password_validator = \
|
|
PasswordValidator(self, builder.get_object('password_option'),
|
|
builder.get_object('password_reenter'),
|
|
builder.get_object('password_valid_image'),
|
|
builder.get_object('password_valid_text'))
|
|
|
|
self.proxy_pass_validator = \
|
|
PasswordValidator(self, builder.get_object('proxy_pass_option'),
|
|
builder.get_object('proxy_pass_reenter'),
|
|
builder.get_object('proxy_pass_valid_image'),
|
|
builder.get_object('proxy_pass_valid_text'))
|
|
|
|
# Fix client port default
|
|
port = builder.get_object('port_adjustment')
|
|
port.set_value(36330)
|
|
|
|
# Connect signals
|
|
builder.connect_signals(self)
|
|
self.builder = builder = None # Discard builder
|
|
|
|
self.client_dialog.client = None
|
|
|
|
# Load
|
|
self.preferences_load()
|
|
self.load_clients()
|
|
|
|
# If we don't have any clients add the default client
|
|
if not len(self.clients):
|
|
client = Client(self, 'local', '127.0.0.1', 36330, '')
|
|
self.add_client(client)
|
|
self.save_clients()
|
|
|
|
# Select first client
|
|
self.select_first_client()
|
|
|
|
# Start client notebook deactivated
|
|
self.deactivate_client()
|
|
|
|
self.window.connect('notify::is-active', self.on_window_is_active)
|
|
|
|
|
|
# Main loop
|
|
def run(self):
|
|
self.quitting = False
|
|
|
|
if sys.platform != 'darwin':
|
|
self.check_clients() # Slightly faster load?
|
|
|
|
self.restore()
|
|
|
|
self.set_update_timer_interval(100)
|
|
|
|
if sys.platform == 'darwin':
|
|
# reduce updates to 2Hz after 30 seconds
|
|
gobject.timeout_add(30000, self.set_update_timer_interval, 500)
|
|
|
|
# OSX signals
|
|
self.osx_app.connect('NSApplicationDidBecomeActive',
|
|
self.app_did_become_active)
|
|
self.osx_app.connect('NSApplicationWillTerminate',
|
|
self.app_will_terminate)
|
|
self.osx_app.connect('NSApplicationBlockTermination',
|
|
self.app_should_block_terminate)
|
|
|
|
self.osx_app.ready()
|
|
|
|
if self.osx_version >= (10,7) and self.osx_version < (10,9):
|
|
self.osx_window_focus_workaround()
|
|
|
|
try:
|
|
# add cmd-w and cmd-m to window
|
|
# cmd-w would need to be same as cancel for dialogs
|
|
ag = gtk.AccelGroup()
|
|
self.window_accel_group = ag
|
|
key, mod = gtk.accelerator_parse("<meta>w")
|
|
ag.connect_group(key, mod, gtk.ACCEL_VISIBLE,
|
|
osx_accel_window_close)
|
|
key, mod = gtk.accelerator_parse("<meta>m")
|
|
ag.connect_group(key, mod, gtk.ACCEL_VISIBLE,
|
|
osx_accel_window_minimize)
|
|
self.window.add_accel_group(ag)
|
|
except Exception, e: print e
|
|
|
|
gtk.main()
|
|
|
|
|
|
# Util
|
|
def osx_add_to_menu(self, widget):
|
|
if isinstance(widget, gtk.SeparatorMenuItem):
|
|
self.osx_group = self.osx_app.add_app_menu_group()
|
|
|
|
elif isinstance(widget, gtk.MenuItem):
|
|
name = widget.child.get_text()
|
|
|
|
def activate_item(widget, target):
|
|
target.emit('activate')
|
|
|
|
item = gtk.MenuItem(name)
|
|
item.show()
|
|
item.connect('activate', activate_item, widget)
|
|
self.osx_app.add_app_menu_item(self.osx_group, item)
|
|
|
|
|
|
def osx_create_app_menu(self, widgets):
|
|
i = 0
|
|
for widget in widgets:
|
|
if not isinstance(widget, gtk.SeparatorMenuItem):
|
|
def activate_item(widget, target):
|
|
target.emit('activate')
|
|
label = widget.get_label()
|
|
item = gtk.MenuItem(label)
|
|
item.show()
|
|
item.connect('activate', activate_item, widget)
|
|
self.osx_app.insert_app_menu_item(widget, i)
|
|
i += 1
|
|
|
|
|
|
def osx_create_dock_menu(self, widgets):
|
|
menu = gtk.Menu()
|
|
for widget in widgets:
|
|
if isinstance(widget, gtk.SeparatorMenuItem):
|
|
item = gtk.SeparatorMenuItem()
|
|
else:
|
|
def activate_item(widget, target):
|
|
target.emit('activate')
|
|
label = widget.get_label()
|
|
item = gtk.MenuItem(label)
|
|
item.connect('activate', activate_item, widget)
|
|
menu.append(item)
|
|
menu.show_all()
|
|
self.osx_app.set_dock_menu(menu)
|
|
# retain menu, or it won't work
|
|
self.osx_dock_menu = menu
|
|
|
|
|
|
def osx_window_focus_workaround(self):
|
|
# osx 10.7+, part of Trac #793, not resolved by gtk 2.24.10
|
|
# only thing that works is clicking FAHControl icon in Dock
|
|
# this is the equivalent
|
|
# must be backgrounded so app can process reopen event
|
|
# and not deadlock waiting for osascript
|
|
cmd = ['/usr/bin/osascript', '-e', 'tell app "FAHControl" to reopen']
|
|
try:
|
|
subprocess.Popen(cmd)
|
|
except Exception, e:
|
|
print e, ':', ' '.join(cmd)
|
|
|
|
|
|
def connect_option_cell(self, name, model, col):
|
|
cell = self.builder.get_object(name)
|
|
cell.connect('edited', self.on_option_edit, model, col)
|
|
|
|
|
|
def connect_option_view(self, prefix):
|
|
tree = self.builder.get_object(prefix + 'option_tree_view')
|
|
model = self.builder.get_object(prefix + 'option_list')
|
|
|
|
self.connect_option_cell(prefix + 'option_name_cell', model, 0)
|
|
self.connect_option_cell(prefix + 'option_value_cell', model, 1)
|
|
|
|
return tree, model
|
|
|
|
# Timer functions
|
|
def set_update_timer_interval(self, interval = None):
|
|
if self.timer_id is not None:
|
|
glib.source_remove(self.timer_id)
|
|
self.timer_id = None
|
|
if interval and int(interval) > 0:
|
|
self.timer_id = gobject.timeout_add(interval, self.on_timer)
|
|
return False # stop if timer callback
|
|
|
|
|
|
def check_clients(self):
|
|
# Make sure there is a selected client
|
|
if not len(self.selected_clients): self.select_first_client()
|
|
|
|
# Update clients
|
|
for client in self.clients.values(): client.update(self)
|
|
|
|
# (De)activate client
|
|
if self.active_client:
|
|
if not self.active_client.is_updated():
|
|
self.deactivate_client()
|
|
|
|
else: self.activate_client() # Try to activate
|
|
|
|
# Check if active and online
|
|
if self.active_client and self.active_client.is_online():
|
|
self.client_notebook.set_sensitive(True)
|
|
else: self.client_notebook.set_sensitive(False)
|
|
|
|
# Update status bar
|
|
if self.status_clear_time and self.status_clear_time < time.time():
|
|
self.status_bar.pop(0)
|
|
self.status_clear_time = None
|
|
|
|
|
|
def on_timer(self):
|
|
try:
|
|
# Update clock
|
|
now = time.time()
|
|
if self.last_clock and self.last_clock != now:
|
|
s = time.strftime('UTC: %Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
|
self.time_label.set_text(s)
|
|
|
|
# Update ppd
|
|
ppd = 0
|
|
for client in self.clients.values(): ppd += client.ppd
|
|
label = 'Total Estimated Points Per Day: '
|
|
if int(ppd): label += '%d' % int(ppd)
|
|
else: label += 'Unknown'
|
|
self.ppd_label.set_text(label)
|
|
|
|
self.last_clock = now
|
|
|
|
self.check_clients()
|
|
self.viewer_check()
|
|
|
|
if self.exit_requested.isSet():
|
|
self.quit()
|
|
|
|
if self.ping.isSet():
|
|
self.ping.clear()
|
|
self.restore()
|
|
|
|
if 2.5 < now - self.last_db_flush:
|
|
self.last_db_flush = time.time()
|
|
self.db.flush_queued()
|
|
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
return True # Keep running
|
|
|
|
|
|
# Actions
|
|
def quit(self):
|
|
if self.quitting: return
|
|
self.quitting = True
|
|
|
|
gtk.main_quit()
|
|
|
|
self.viewer_close()
|
|
|
|
self.set_update_timer_interval(0)
|
|
|
|
for client in self.clients.values(): client.close()
|
|
|
|
try:
|
|
self.db.flush_queued()
|
|
except Exception, e: print e
|
|
|
|
self.shutdown() # Shutdown SingleAppServer
|
|
|
|
|
|
def set_status(self, text):
|
|
self.status_bar.pop(0)
|
|
self.status_bar.push(0, text)
|
|
self.status_clear_time = time.time() + 10
|
|
|
|
|
|
# OSX signals
|
|
def app_did_become_active(self, app):
|
|
self.restore()
|
|
|
|
|
|
def app_will_terminate(self, app):
|
|
# Calxalot: Probably don't need both this and should_block, but I can
|
|
# imagine the app quitting without asking. Note that if we reach this,
|
|
# we exit after quit() and never return to run()
|
|
self.quit()
|
|
|
|
|
|
def app_should_block_terminate(self, app):
|
|
self.quit()
|
|
return False
|
|
|
|
|
|
# Preference methods
|
|
def load_theme(self, theme):
|
|
for name, rc in self.theme_list:
|
|
if theme == name:
|
|
print 'Loading theme', theme
|
|
|
|
settings = gtk.settings_get_default()
|
|
|
|
if rc is None:
|
|
settings.set_property('gtk-theme-name', self.system_theme)
|
|
gtk.rc_set_default_files([])
|
|
else:
|
|
settings.set_property('gtk-theme-name', theme)
|
|
gtk.rc_set_default_files([rc])
|
|
|
|
gtk.rc_reparse_all_for_settings(settings, True)
|
|
gtk.rc_reset_styles(settings)
|
|
|
|
break
|
|
|
|
|
|
def load_themes(self):
|
|
paths = get_theme_dirs()
|
|
unique = set()
|
|
list = []
|
|
default_rc = None
|
|
|
|
for path in paths:
|
|
if not os.path.exists(path): continue
|
|
for name in os.listdir(path):
|
|
if name in unique: continue
|
|
rc = path + '/' + name + '/gtk-2.0/gtkrc'
|
|
if os.path.exists(rc):
|
|
unique.add(name)
|
|
if sys.platform == 'win32' and \
|
|
name == 'Windows-Default' and default_rc is None:
|
|
default_rc = rc
|
|
else: list.append([name, rc])
|
|
|
|
list.sort(key = lambda x: x[0])
|
|
|
|
return [['Default', default_rc]] + list
|
|
|
|
|
|
def get_pref(self, name):
|
|
widget = self.preference_widgets[name]
|
|
return get_widget_str_value(widget)
|
|
|
|
|
|
def get_viz_render_mode(self):
|
|
value = self.get_pref('viz_render_mode')
|
|
|
|
if value == 'Space Filling': return 1
|
|
if value == 'Ball And Stick': return 2
|
|
if value == 'Stick': return 3
|
|
if value == 'Advanced Space Filling': return 4
|
|
if value == 'Advanced Ball And Stick': return 5
|
|
if value == 'Advanced Stick': return 6
|
|
if value == 'Cartoon Space Filling': return 7
|
|
if value == 'Cartoon Ball And Stick': return 8
|
|
|
|
return 1
|
|
|
|
|
|
def preferences_set(self):
|
|
# URLs
|
|
for pref in ['donor', 'team']:
|
|
entry = self.preference_widgets[pref + '_stats']
|
|
combo = self.preference_widgets[pref + '_stats_link']
|
|
button = getattr(self, pref + '_info')
|
|
|
|
link = self.get_pref(pref + '_stats_link')
|
|
custom_uri = self.get_pref(pref + '_stats')
|
|
entry.set_text(custom_uri)
|
|
|
|
if link == 'Custom': button.set_uri(custom_uri)
|
|
else:
|
|
model = combo.get_model()
|
|
iter = model.get_iter_first()
|
|
while iter is not None:
|
|
if model.get_value(iter, 0) == link:
|
|
combo.set_active_iter(iter)
|
|
button.set_uri(model.get_value(iter, 1))
|
|
break
|
|
iter = model.iter_next(iter)
|
|
|
|
|
|
def preferences_load(self):
|
|
# Preferences dialog
|
|
for name, widget in self.preference_widgets.items():
|
|
value = None
|
|
|
|
if self.db.has(name):
|
|
value = self.db.get(name)
|
|
if name == 'theme': self.load_theme(value)
|
|
|
|
elif name == 'theme': value = 'Default'
|
|
elif name == 'viz_command': value = 'FAHViewer'
|
|
elif name == 'viz_fullscreen': value = 'False'
|
|
elif name == 'viz_width': value = '800'
|
|
elif name == 'viz_height': value = '600'
|
|
elif name == 'viz_render_mode': value = 'Advanced Space Filling'
|
|
elif name == 'viz_cycle_snapshots': value = 'True'
|
|
elif name == 'donor_stats': value = ''
|
|
elif name == 'team_stats': value = ''
|
|
elif name == 'donor_stats_link': value = 'Folding@home'
|
|
elif name == 'team_stats_link': value = 'Folding@home'
|
|
else: raise Exception, 'Unknown preference widget "%s"' % name
|
|
|
|
if value is not None: set_widget_str_value(widget, value)
|
|
|
|
# Update
|
|
self.preferences_set()
|
|
|
|
|
|
def preferences_dialog_init(self):
|
|
for name, widget in self.preference_widgets.items():
|
|
if self.db.has(name):
|
|
value = self.db.get(name)
|
|
set_widget_str_value(widget, value)
|
|
|
|
for pref in ['donor', 'team']:
|
|
entry = self.preference_widgets[pref + '_stats']
|
|
combo = self.preference_widgets[pref + '_stats_link']
|
|
entry.set_sensitive(combo.get_active_text() == 'Custom')
|
|
|
|
|
|
def preferences_save(self):
|
|
for name, widget in self.preference_widgets.items():
|
|
value = get_widget_str_value(widget)
|
|
if value is None: self.db.clear(name, False)
|
|
else: self.db.set(name, value, False)
|
|
|
|
self.db.commit()
|
|
|
|
|
|
# Client methods
|
|
def select_first_client(self):
|
|
iter = self.client_list.get_iter_first()
|
|
if iter: self.client_tree.get_selection().select_iter(iter)
|
|
|
|
|
|
def activate_client(self):
|
|
if self.active_client: return
|
|
if not len(self.selected_clients): return
|
|
|
|
# Check that all selected clients are active
|
|
for client in self.selected_clients:
|
|
if not client.is_updated(): return
|
|
|
|
# Activate client(s)
|
|
for client in self.selected_clients:
|
|
self.active_client = client
|
|
self.active_client.update_status_ui(self)
|
|
break # TODO only supporting one active client right now
|
|
|
|
self.last_clients_update = 0
|
|
self.update_client_status()
|
|
|
|
|
|
def deactivate_client(self):
|
|
if self.active_client:
|
|
self.active_client.reset_status_ui(self)
|
|
self.active_client = None
|
|
|
|
self.client_label.set_markup('<b>Client: inactive</b>')
|
|
self.update_client_status()
|
|
|
|
|
|
def update_client_status(self):
|
|
if len(self.selected_clients):
|
|
client = list(self.selected_clients)[0]
|
|
|
|
text = '<b>Client: %s' % client.name
|
|
status = client.get_status()
|
|
color = status_to_color(status)
|
|
text += ' <span background="%s">%s</span></b>' % (color, status)
|
|
|
|
if self.active_client and self.active_client.config.get_running():
|
|
text += ' Running'
|
|
else: text += ' Inactive'
|
|
|
|
else: text = '<b>Client: no client selected</b>'
|
|
|
|
self.client_label.set_markup(text)
|
|
if self.client_dialog.client is not None:
|
|
self.client_config_label.set_markup(text)
|
|
|
|
|
|
def save_clients(self):
|
|
self.db.delete('clients')
|
|
|
|
for client in self.clients.values():
|
|
client.save(self.db)
|
|
|
|
self.db.commit()
|
|
|
|
|
|
def update_client_list(self):
|
|
# update all rows, whether selected/active or not
|
|
try:
|
|
iter = self.client_list.get_iter_first()
|
|
while iter is not None:
|
|
name = self.client_list.get_value(iter, 0)
|
|
client = self.clients.get(name)
|
|
if client is not None:
|
|
path = self.client_list.get_path(iter)
|
|
row = client.get_row(self)
|
|
for i in range(len(row)):
|
|
self.client_list.set(iter, i, row[i])
|
|
self.client_list.row_changed(path, iter)
|
|
iter = self.client_list.iter_next(iter)
|
|
except Exception, e: print e
|
|
return False # no timer repeat
|
|
|
|
|
|
def resort_client_list(self):
|
|
ibyname_old = {}
|
|
iter = self.client_list.get_iter_first()
|
|
i = 0
|
|
while iter is not None:
|
|
name = self.client_list.get_value(iter, 0)
|
|
ibyname_old[name] = i
|
|
iter = self.client_list.iter_next(iter)
|
|
i += 1
|
|
new_order = []
|
|
for client in self.sorted_clients():
|
|
name = client.name
|
|
i = ibyname_old.get(name)
|
|
if i is None:
|
|
print 'unable to resort client list: unknown name %s' % name
|
|
return
|
|
new_order.append(i)
|
|
self.client_list.reorder(new_order)
|
|
return False # don't repeat if timer callback
|
|
|
|
|
|
def sorted_clients(self, unsorted_clients = None):
|
|
if unsorted_clients is None:
|
|
unsorted_clients = self.clients.values()
|
|
|
|
# pre-sort by client.name
|
|
clients = sorted(unsorted_clients, key=lambda c: c.name)
|
|
|
|
# sort local clients first
|
|
group0 = [] # client "local" (should only be one, currently)
|
|
group1 = [] # other is_local clients (should not be any, currently)
|
|
group2 = [] # localhost clients starting with "local"
|
|
group3 = [] # other localhost clients
|
|
group4 = [] # remote clients (and any local referenced by host name)
|
|
|
|
for client in clients:
|
|
is_local = client.is_local()
|
|
is_local_addr = client.address in ['localhost','127.0.0.1']
|
|
|
|
if is_local and client.name == 'local':
|
|
group0.append(client)
|
|
elif is_local:
|
|
group1.append(client)
|
|
elif is_local_addr and client.name.startswith('local'):
|
|
group2.append(client)
|
|
elif is_local_addr:
|
|
group3.append(client)
|
|
else:
|
|
group4.append(client)
|
|
|
|
return group0 + group1 + group2 + group3 + group4
|
|
|
|
|
|
def load_clients(self):
|
|
clients = []
|
|
for row in self.db.select('clients', orderby = 'name'):
|
|
client = Client(self,
|
|
row['name'], row['address'], int(row['port']), row['password'])
|
|
clients.append(client)
|
|
|
|
for client in self.sorted_clients(clients):
|
|
self.add_client(client)
|
|
|
|
|
|
def clear_clients(self):
|
|
for client in self.clients: client.close()
|
|
self.clients.clear()
|
|
self.clientsByName.clear()
|
|
self.client_list.clear()
|
|
|
|
|
|
def save_client_config(self, client):
|
|
try:
|
|
options, slots = client.config.get_changes(self)
|
|
if not (options or slots != ([], [], [])): return True
|
|
|
|
# Validate passkey
|
|
if not self.passkey_validator.is_good():
|
|
raise Exception, 'Passkey is invalid'
|
|
|
|
# Validate password
|
|
if not self.password_validator.is_good():
|
|
raise Exception, 'Client password is invalid'
|
|
|
|
# Validate proxy password
|
|
if not self.proxy_pass_validator.is_good():
|
|
raise Exception, 'Proxy password is invalid'
|
|
|
|
self.deactivate_client()
|
|
|
|
self.set_status('Saving...')
|
|
|
|
for client in self.selected_clients:
|
|
# TODO check returned error count
|
|
client.save_config(options, slots)
|
|
client.update(self) # Slightly faster save
|
|
|
|
# Update password if changed on client
|
|
if 'password' in options:
|
|
client.set_password(options['password'])
|
|
if 'password!' in options: client.set_password('')
|
|
|
|
return True
|
|
|
|
except Exception, msg:
|
|
self.set_status('Save Failed')
|
|
self.error(msg)
|
|
return False
|
|
|
|
|
|
def check_duplicate_client_name(self, name):
|
|
if name in self.clients:
|
|
self.error('Client with name "%s" is already in client list' % name)
|
|
return True
|
|
return False
|
|
|
|
|
|
def check_duplicate_client_address(self, address):
|
|
if address in self.clientsByAddress:
|
|
self.error('Client with address "%s" is already in client list' %
|
|
address)
|
|
return True
|
|
return False
|
|
|
|
|
|
def update_client(self, client, name, address, port, password):
|
|
reload = False
|
|
old_name = client.name
|
|
|
|
# Check for duplicates
|
|
if client.name != name:
|
|
if self.check_duplicate_client_name(name): return False
|
|
|
|
new_address = Client.make_address(address, port)
|
|
if client.get_address() != new_address:
|
|
if self.check_duplicate_client_address(new_address): return False
|
|
|
|
# Update
|
|
if client.name != name:
|
|
del self.clients[client.name]
|
|
client.name = name
|
|
self.clients[name] = client
|
|
|
|
if client.get_address() != new_address:
|
|
del self.clientsByAddress[client.get_address()]
|
|
client.set_address(address, port)
|
|
self.clientsByAddress[new_address] = client
|
|
reload = True
|
|
|
|
if client.get_password() != password:
|
|
client.set_password(password)
|
|
reload = True
|
|
|
|
# Update client row
|
|
row = client.get_row(self)
|
|
|
|
# Find client in client_list
|
|
iter = self.client_list.get_iter_first()
|
|
while iter is not None:
|
|
if old_name == self.client_list.get_value(iter, 0):
|
|
path = self.client_list.get_path(iter)
|
|
|
|
for i in range(len(row)):
|
|
self.client_list.set(iter, i, row[i])
|
|
self.client_list.row_changed(path, iter)
|
|
|
|
break
|
|
|
|
iter = self.client_list.iter_next(iter)
|
|
|
|
# Reload
|
|
if reload:
|
|
client.reconnect()
|
|
self.deactivate_client()
|
|
|
|
return True
|
|
|
|
|
|
def add_client(self, client):
|
|
name = client.name
|
|
address = client.get_address()
|
|
|
|
# Check for duplicates
|
|
if self.check_duplicate_client_name(name): return False
|
|
if self.check_duplicate_client_address(address): return False
|
|
|
|
# Add it
|
|
self.clients[name] = client
|
|
self.clientsByAddress[address] = client
|
|
self.client_list.append(client.get_row(self))
|
|
|
|
return True
|
|
|
|
|
|
def remove_client(self, client):
|
|
client.close()
|
|
del self.clients[client.name]
|
|
del self.clientsByAddress[client.get_address()]
|
|
if client is self.active_client: self.active_client = None
|
|
|
|
|
|
def remove_client_path(self, path, iter):
|
|
name = self.client_list.get(iter, 0)[0]
|
|
self.remove_client(self.clients[name])
|
|
|
|
|
|
def get_selected_clients(self):
|
|
selection = get_tree_selection(self.client_tree)
|
|
names = map(lambda item: self.client_list.get(item[1], 0)[0], selection)
|
|
return set(map(self.clients.get, names))
|
|
|
|
|
|
def edit_client(self, client):
|
|
self.client_dialog.client = client
|
|
self.update_client_status()
|
|
client.load_dialog(self)
|
|
|
|
if self.active_client and self.active_client.is_online():
|
|
for name, widget in self.client_config_tabs.items(): widget.show()
|
|
self.client_dialog.config_hidden = False
|
|
|
|
else:
|
|
for name, widget in self.client_config_tabs.items():
|
|
if name != 'connection': widget.hide()
|
|
self.client_dialog.config_hidden = True
|
|
|
|
self.open_dialog(self.client_dialog)
|
|
|
|
|
|
def open_dialog(self, dialog):
|
|
# Hack to make WrapLabel work
|
|
dims = dialog.get_size()
|
|
dialog.resize(dims[0] + 1, dims[1] + 1)
|
|
dialog.present()
|
|
dialog.resize(*dims)
|
|
|
|
|
|
# Slot methods
|
|
def get_selected_slot_ids(self):
|
|
selection = get_tree_selection(self.slot_status_tree)
|
|
ids = map(lambda x: int(self.slot_status_list.get(x[1], 0)[0]),
|
|
selection)
|
|
return ids
|
|
|
|
|
|
# Window methods
|
|
def get_visible_dialogs(self):
|
|
dialogs = []
|
|
for dialog in self.dialogs:
|
|
if dialog.flags() & gtk.MAPPED:
|
|
dialogs.append(dialog)
|
|
|
|
return dialogs
|
|
|
|
|
|
def hide_all_windows(self):
|
|
self.restore_dialogs = self.get_visible_dialogs()
|
|
for dialog in self.restore_dialogs: dialog.hide()
|
|
self.window.hide()
|
|
self.window_visible = False
|
|
|
|
|
|
def restore(self):
|
|
self.window.present()
|
|
self.window.deiconify()
|
|
self.window_visible = True
|
|
|
|
if sys.platform == 'darwin':
|
|
# restore osx minimized dialogs
|
|
for dialog in self.get_visible_dialogs():
|
|
dialog.present()
|
|
|
|
# Restore dialogs
|
|
for dialog in self.restore_dialogs: dialog.present()
|
|
self.restore_dialogs = []
|
|
|
|
|
|
# Messages
|
|
def close_error_dialog(self, dialog, id = None, data = None):
|
|
dialog.destroy()
|
|
self.error_dialog = None
|
|
|
|
|
|
def error(self, message, buttons = gtk.BUTTONS_OK, on_response = None,
|
|
on_response_data = None):
|
|
message = str(message)
|
|
|
|
# log to terminal window
|
|
if sys.exc_info()[2]: traceback.print_exc()
|
|
print 'ERROR:', message
|
|
|
|
# Don't open more than one
|
|
if self.error_dialog is not None: return False
|
|
|
|
if sys.exc_info()[1]: message += '\n%s' % sys.exc_info()[1]
|
|
|
|
# create an error message dialog and display modally to the user
|
|
dialog = \
|
|
gtk.MessageDialog(None,
|
|
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
gtk.MESSAGE_ERROR, buttons, message)
|
|
|
|
dialog.connect('close', self.close_error_dialog)
|
|
if on_response is not None:
|
|
dialog.connect('response', on_response, on_response_data)
|
|
dialog.connect('response', self.close_error_dialog)
|
|
dialog.set_transient_for(self.window)
|
|
|
|
self.error_dialog = dialog
|
|
dialog.show()
|
|
|
|
return True
|
|
|
|
|
|
# Property signals
|
|
def store_property(self, widget, property, name):
|
|
self.db.set(name, widget.get_property(property.name), queue = True)
|
|
|
|
|
|
def store_dimensions(self, widget, event, name):
|
|
x, y, width, height = widget.get_allocation()
|
|
if 0 <= width and 0 <= height:
|
|
self.db.set(name + '_width', width, queue = True);
|
|
self.db.set(name + '_height', height, queue = True);
|
|
|
|
|
|
# Action signals
|
|
def on_quit(self, widget, data = None):
|
|
self.quit()
|
|
|
|
|
|
def on_preferences(self, widget, data = None):
|
|
# OSX crashes with out this, but it's a good idea anyway
|
|
if not self.window_visible: self.restore()
|
|
|
|
if self.get_visible_dialogs(): return
|
|
|
|
self.preferences_dialog_init()
|
|
self.preferences_dialog.present()
|
|
|
|
|
|
def viewer_check(self):
|
|
if self.viewer is not None and self.viewer.poll() is not None:
|
|
self.viewer = None # Viewer exited
|
|
|
|
|
|
def viewer_close(self):
|
|
if self.viewer is not None:
|
|
try:
|
|
self.viewer.kill()
|
|
self.viewer.wait() # Note: This could cause a hang kill() fails
|
|
except: pass
|
|
|
|
self.viewer = None
|
|
|
|
|
|
def on_viewer(self, widget, data = None):
|
|
self.viewer_close()
|
|
|
|
# Get preferences
|
|
command = self.get_pref('viz_command')
|
|
fullscreen = parse_bool(self.get_pref('viz_fullscreen'))
|
|
width = self.get_pref('viz_width')
|
|
height = self.get_pref('viz_height')
|
|
mode = self.get_viz_render_mode()
|
|
cycle_snapshots = self.get_pref('viz_cycle_snapshots')
|
|
|
|
# Create command line
|
|
cmd = shlex.split(command)
|
|
|
|
if not (len(cmd) and len(cmd[0])): cmd = ['FAHViewer']
|
|
|
|
if fullscreen: cmd.append('--fullscreen')
|
|
cmd.append('--width=' + width)
|
|
cmd.append('--height=' + height)
|
|
cmd.append('--mode=%d' % mode)
|
|
cmd.append('--cycle-snapshots=' + cycle_snapshots)
|
|
|
|
if self.active_client and self.active_client.is_online():
|
|
address = self.active_client.address
|
|
port = self.active_client.port
|
|
cmd.append('--connect=%s:%d' % (address, port))
|
|
|
|
password = self.active_client.password
|
|
if password: cmd.append('--password="%s"' % password)
|
|
|
|
slot = self.active_client.get_selected_slot(self)
|
|
if slot is not None: cmd.append('--slot=%d' % slot.id)
|
|
|
|
debug = True
|
|
if debug: print cmd
|
|
|
|
try:
|
|
self.viewer = subprocess.Popen(cmd, cwd = get_home_dir())
|
|
except Exception, e:
|
|
self.error('Failed to launch viewer with command:\n\n' +
|
|
' '.join(cmd))
|
|
|
|
|
|
def on_about(self, widget, data = None):
|
|
# OSX crashes with out this, but it's a good idea anyway
|
|
if not self.window_visible: self.restore()
|
|
|
|
if self.get_visible_dialogs(): return False
|
|
|
|
self.open_dialog(self.about_dialog)
|
|
|
|
|
|
def on_about_close(self, widget, data = None):
|
|
self.about_dialog.hide()
|
|
return True # Cancel event
|
|
|
|
|
|
# Window signals
|
|
def on_window_destroy(self, widget, data = None):
|
|
if sys.platform == 'darwin':
|
|
self.hide_all_windows()
|
|
return True # prevent destroy
|
|
self.quit()
|
|
|
|
|
|
def on_window_delete(self, widget, event, data = None):
|
|
return self.on_window_destroy(widget)
|
|
|
|
|
|
def on_window_is_active(self, window, *args):
|
|
try:
|
|
if window.is_active(): self.update_client_list()
|
|
except Exception, e: print e
|
|
|
|
|
|
# Preferences signals
|
|
def on_preferences_ok(self, widget, data = None):
|
|
self.preferences_set()
|
|
self.preferences_save()
|
|
self.preferences_dialog.hide()
|
|
|
|
|
|
def on_preferences_cancel(self, widget, data = None):
|
|
# Reset theme
|
|
if self.db.has('theme'): current_theme = self.db.get('theme')
|
|
else: current_theme = 'Default'
|
|
if self.get_pref('theme') != current_theme:
|
|
self.load_theme(current_theme)
|
|
|
|
# FIXME workaround for defect: get_pref takes from dialog widgets
|
|
self.preferences_dialog_init()
|
|
self.preferences_dialog.hide()
|
|
return True # Cancel event
|
|
|
|
|
|
def on_theme_pref_changed(self, widget, data = None):
|
|
iter = widget.get_active_iter()
|
|
theme = widget.get_model().get_value(iter, 0)
|
|
self.load_theme(theme)
|
|
|
|
|
|
# Proxy signals
|
|
def on_proxy_enable_toggled(self, widget, data = None):
|
|
self.proxy_frame.set_sensitive(widget.get_active())
|
|
self.proxy_auth_frame.set_sensitive(widget.get_active())
|
|
|
|
|
|
# Client list signals
|
|
def on_client_add_button_clicked(self, widget, data = None):
|
|
if self.get_visible_dialogs(): return
|
|
|
|
# Make client name
|
|
for i in xrange(sys.maxint):
|
|
name = 'client%d' % i
|
|
if not name in self.clients: break
|
|
|
|
self.client_entries['name'].set_text(name)
|
|
|
|
self.client_dialog.client = None
|
|
text = 'Configure New Client Connection'
|
|
self.client_config_label.set_markup(text)
|
|
|
|
# Reset dialog
|
|
self.client_entries['name'].set_sensitive(True)
|
|
self.client_entries['address'].set_sensitive(True)
|
|
for name, widget in self.client_config_tabs.items():
|
|
if name != 'connection': widget.hide()
|
|
|
|
self.open_dialog(self.client_dialog)
|
|
|
|
|
|
def on_client_remove_button_clicked(self, widget, data = None):
|
|
remove_tree_selection(self.client_tree, self.remove_client_path)
|
|
self.save_clients()
|
|
|
|
|
|
def on_configure(self, widget, data = None):
|
|
if self.get_visible_dialogs(): return
|
|
|
|
selection = get_tree_selection(self.client_tree)
|
|
if not len(selection): return
|
|
name = self.client_list.get(selection[0][1], 0)[0]
|
|
client = self.clients[name]
|
|
self.edit_client(client)
|
|
|
|
|
|
def on_client_tree_view_row_activated(self, widget, path, col, data = None):
|
|
if self.get_visible_dialogs(): return
|
|
|
|
iter = self.client_list.get_iter(path)
|
|
name = self.client_list.get(iter, 0)[0]
|
|
client = self.clients[name]
|
|
self.edit_client(client)
|
|
|
|
|
|
def on_client_selection_changed(self, widget, data = None):
|
|
self.deactivate_client()
|
|
|
|
self.selected_clients = self.get_selected_clients()
|
|
|
|
# Modify selection
|
|
for client in self.clients.values():
|
|
client.set_selected(client in self.selected_clients)
|
|
|
|
self.update_client_status()
|
|
|
|
if len(self.clients): self.activate_client()
|
|
|
|
self.update_client_list()
|
|
gobject.timeout_add(5000, self.update_client_list)
|
|
|
|
# temporarily increase update rate for faster switch
|
|
if sys.platform == 'darwin':
|
|
self.set_update_timer_interval(100)
|
|
gobject.timeout_add(10000, self.set_update_timer_interval, 500)
|
|
|
|
|
|
# Client options list signals
|
|
def on_client_options_add_button_clicked(self, widget, data = None):
|
|
self.option_present(self.option_list, self.client_dialog)
|
|
|
|
|
|
def on_client_options_remove_button_clicked(self, widget, data = None):
|
|
remove_tree_selection(self.option_tree)
|
|
|
|
|
|
# Core options list signals
|
|
def on_core_options_add_button_clicked(self, widget, data = None):
|
|
self.core_option_entry.set_text('')
|
|
self.core_options_dialog.set_transient_for(self.client_dialog)
|
|
self.core_options_dialog.present()
|
|
|
|
|
|
def on_core_options_remove_button_clicked(self, widget, data = None):
|
|
remove_tree_selection(self.core_option_tree)
|
|
|
|
|
|
def on_core_options_ok(self, widget, data = None):
|
|
option = self.core_option_entry.get_text().strip()
|
|
if not option:
|
|
self.error('Invalid option')
|
|
return
|
|
|
|
self.core_option_list.append([option])
|
|
self.core_options_dialog.hide()
|
|
|
|
|
|
def on_core_options_cancel(self, widget, data = None):
|
|
self.core_options_dialog.hide()
|
|
return True # Cancel event
|
|
|
|
|
|
# Client dialog signals
|
|
def on_client_ok(self, widget, data = None):
|
|
name = self.client_entries['name'].get_text()
|
|
address = self.client_entries['address'].get_text()
|
|
port = self.client_entries['port'].get_text()
|
|
password = self.client_entries['password'].get_text()
|
|
|
|
if not name:
|
|
self.error('Invalid name')
|
|
return
|
|
|
|
if not address:
|
|
self.error('Invalid address')
|
|
return
|
|
|
|
if not port:
|
|
self.error('Invalid port')
|
|
return
|
|
|
|
port = int(port)
|
|
|
|
if self.client_dialog.client: # Existing client
|
|
client = self.client_dialog.client
|
|
config_hidden = self.client_dialog.config_hidden
|
|
|
|
# Save client options
|
|
if config_hidden or self.save_client_config(client):
|
|
# Save client connection
|
|
if self.update_client(client, name, address, port, password):
|
|
self.save_clients()
|
|
self.client_dialog.hide()
|
|
self.resort_client_list()
|
|
|
|
else: # New client
|
|
client = Client(self, name, address, port, password)
|
|
if self.add_client(client):
|
|
self.save_clients()
|
|
self.client_dialog.hide()
|
|
self.resort_client_list()
|
|
|
|
|
|
def on_client_cancel(self, widget, data = None):
|
|
self.client_dialog.hide()
|
|
return True # Cancel event
|
|
|
|
|
|
# Folding power signals
|
|
def on_fold_button_clicked(self, widget, data = None):
|
|
self.active_client.unpause()
|
|
|
|
|
|
def on_pause_button_clicked(self, widget, data = None):
|
|
self.active_client.pause()
|
|
|
|
|
|
def on_finish_button_clicked(self, widget, data = None):
|
|
self.active_client.finish()
|
|
|
|
|
|
def on_folding_power_change_value(self, widget, scroll, value, data = None):
|
|
# Clamp slider to integer increments
|
|
value = int(round(value))
|
|
if self.folding_power.get_value() != value:
|
|
self.folding_power.set_value(value)
|
|
return True
|
|
|
|
|
|
def on_folding_power_value_changed(self, widget, data = None):
|
|
if not self.folding_power_changing and self.active_client:
|
|
power = self.folding_power_levels[int(widget.get_value())]
|
|
self.active_client.set_power(power)
|
|
|
|
|
|
def on_folding_power_button_press(self, widget, data = None):
|
|
self.folding_power_changing = True
|
|
|
|
|
|
def on_folding_power_button_release(self, widget, data = None):
|
|
if self.active_client:
|
|
power = self.folding_power_levels[int(widget.get_value())]
|
|
self.active_client.set_power(power)
|
|
|
|
self.folding_power_changing = False
|
|
|
|
|
|
# User status signals
|
|
def on_team_stats_link_changed(self, widget, data = None):
|
|
iter = widget.get_active_iter()
|
|
model = widget.get_model()
|
|
text = model.get_value(iter, 0)
|
|
self.team_stats_pref.set_sensitive(text == 'Custom')
|
|
|
|
|
|
def on_donor_stats_link_changed(self, widget, data = None):
|
|
iter = widget.get_active_iter()
|
|
model = widget.get_model()
|
|
text = model.get_value(iter, 0)
|
|
self.donor_stats_pref.set_sensitive(text == 'Custom')
|
|
|
|
|
|
# Queue tree signals
|
|
def on_queue_tree_cursor_changed(self, widget, data = None):
|
|
if self.active_client:
|
|
self.active_client.config.select_queue_slot(self)
|
|
|
|
|
|
# Slot list signals
|
|
def on_slot_add_button_clicked(self, widget, data = None):
|
|
SlotConfig.clear_dialog(self)
|
|
self.slot_dialog.slot_iter = None
|
|
self.open_dialog(self.slot_dialog)
|
|
|
|
|
|
def on_slot_remove_button_clicked(self, widget, data = None):
|
|
remove_tree_selection(self.slot_tree)
|
|
|
|
|
|
def on_slot_edit_button_clicked(self, widget, data = None):
|
|
selection = get_tree_selection(self.slot_tree)
|
|
|
|
if selection:
|
|
iter = selection[0][1]
|
|
slot = self.slot_list.get(iter, 2)[0].slot
|
|
slot.load_dialog(self)
|
|
self.slot_dialog.slot_iter = iter
|
|
self.open_dialog(self.slot_dialog)
|
|
|
|
|
|
def on_slot_tree_view_row_activated(self, widget, path, col, data = None):
|
|
iter = self.slot_list.get_iter(path)
|
|
slot = self.slot_list.get(iter, 2)[0].slot
|
|
slot.load_dialog(self)
|
|
self.slot_dialog.slot_iter = iter
|
|
self.open_dialog(self.slot_dialog)
|
|
|
|
|
|
|
|
# Slot dialog signals
|
|
def on_slot_ok(self, widget, data = None):
|
|
if self.slot_dialog.slot_iter is None:
|
|
slot = SlotConfig()
|
|
slot.save_dialog(self)
|
|
slot.add_to_ui(self) # Add to list
|
|
|
|
else:
|
|
iter = self.slot_dialog.slot_iter
|
|
id, type, wrapper = self.slot_list.get(iter, 0, 1, 2)
|
|
slot = wrapper.slot
|
|
slot.save_dialog(self)
|
|
if slot.type != type: self.slot_list.set(iter, 1, slot.type)
|
|
|
|
self.slot_dialog.hide()
|
|
|
|
|
|
def on_slot_cancel(self, widget, data = None):
|
|
self.slot_dialog.hide()
|
|
return True # Cancel event
|
|
|
|
|
|
# Slot options list signals
|
|
def on_slot_options_add_button_clicked(self, widget, data = None):
|
|
self.option_present(self.slot_option_list, self.slot_dialog)
|
|
|
|
|
|
def on_slot_options_remove_button_clicked(self, widget, data = None):
|
|
remove_tree_selection(self.slot_option_tree)
|
|
|
|
|
|
# Options edit
|
|
def check_option_name(self, name):
|
|
if not re.match(r'^[a-zA-Z][\w-]*$', name):
|
|
self.error('Invalid option name.')
|
|
return False
|
|
return True
|
|
|
|
|
|
def on_option_edit(self, cell, path, text, model, column):
|
|
if column == 0 and not self.check_option_name(text): return
|
|
if model[path][column] != text: model[path][column] = text
|
|
|
|
|
|
def on_core_option_cell_edited(self, cell, path, text, data = None):
|
|
if not text:
|
|
self.error('Invalid option')
|
|
return
|
|
if self.core_option_list[path][0] != text:
|
|
self.core_option_list[path][0] = text
|
|
|
|
|
|
# Options dialog signals
|
|
def option_present(self, model, parent):
|
|
self.option_name_entry.set_text('')
|
|
self.option_value_entry.set_text('')
|
|
self.options_dialog.option_model = model
|
|
self.options_dialog.set_transient_for(parent)
|
|
self.options_dialog.present()
|
|
|
|
|
|
def on_options_ok(self, widget, data = None):
|
|
name = self.option_name_entry.get_text().strip()
|
|
value = self.option_value_entry.get_text()
|
|
|
|
if not self.check_option_name(name): return
|
|
|
|
self.options_dialog.option_model.append([name, value])
|
|
self.options_dialog.hide()
|
|
|
|
|
|
def on_options_cancel(self, widget, data = None):
|
|
self.options_dialog.hide()
|
|
return True # Cancel event
|
|
|
|
|
|
# Configure dialog signals
|
|
def on_configure_ok(self, widget, data = None):
|
|
self.configure_dialog.hide()
|
|
if self.active_client:
|
|
# Select identity tab
|
|
self.client_config_notebook.set_current_page(1)
|
|
self.edit_client(self.active_client)
|
|
return True # Cancel event
|
|
|
|
|
|
def on_configure_cancel(self, widget, data = None):
|
|
self.configure_dialog.hide()
|
|
if self.active_client:
|
|
# Fold anonymously
|
|
self.active_client.conn.queue_command('save')
|
|
return True # Cancel event
|
|
|
|
|
|
# Slot status tree signals
|
|
def on_slot_status_tree_cursor_changed(self, widget, data = None):
|
|
if self.active_client:
|
|
self.active_client.config.select_slot(self)
|
|
|
|
|
|
def on_slot_status_tree_view_button_release_event(self, widget, event,
|
|
data = None):
|
|
if event.button != 3: return
|
|
idle = self.active_client.get_selected_slot(self).idle
|
|
self.idle_slot_item.set_active(idle)
|
|
self.slot_menu.popup(None, None, None, button = event.button,
|
|
activate_time = event.time, data = data)
|
|
|
|
|
|
def on_unpause_slot_item_activate(self, widget, data = None):
|
|
for id in self.get_selected_slot_ids():
|
|
self.active_client.unpause(id)
|
|
|
|
|
|
def on_pause_slot_item_activate(self, widget, data = None):
|
|
for id in self.get_selected_slot_ids():
|
|
self.active_client.pause(id)
|
|
|
|
|
|
def on_idle_slot_item_toggled(self, widget, data = None):
|
|
for id in self.get_selected_slot_ids():
|
|
if widget.get_active(): self.active_client.on_idle(id)
|
|
else: self.active_client.always_on(id)
|
|
|
|
|
|
def on_finish_slot_item_activate(self, widget, data = None):
|
|
for id in self.get_selected_slot_ids():
|
|
self.active_client.finish(id)
|
|
|
|
|
|
# Log signals
|
|
def on_download_log_clicked(self, widget, data = None):
|
|
self.active_client.refresh_log()
|
|
self.log.set_text('')
|
|
|
|
|
|
def on_copy_log_clicked(self, widget, data = None):
|
|
log = self.log
|
|
text = log.get_text(log.get_start_iter(), log.get_end_iter())
|
|
gtk.Clipboard().set_text(text)
|
|
|
|
|
|
def on_clear_log_clicked(self, widget, data = None):
|
|
if self.active_client: self.active_client.config.log_clear(self)
|
|
|
|
|
|
def on_update_log(self, widget, data = None):
|
|
if self.active_client: self.active_client.config.update_log(self)
|
|
|
|
|
|
# Scale value formatting
|
|
def on_cpu_usage_scale_format_value(self, widget, value, data = None):
|
|
return '%d%%' % value
|
|
|
|
|
|
def on_checkpoint_scale_format_value(self, widget, value, data = None):
|
|
return '%d min.' % value
|
|
|
|
|
|
def on_uri_hook(self, widget, url, data = None):
|
|
keys = {'donor': urllib.quote(self.donor_info.get_label()),
|
|
'team': urllib.quote(self.team_info.get_label())}
|
|
webbrowser.open(url % keys)
|