diff --git a/client/mac/mac_main.cpp b/client/mac/mac_main.cpp new file mode 100755 index 0000000000..d004b86ee6 --- /dev/null +++ b/client/mac/mac_main.cpp @@ -0,0 +1,959 @@ +// The contents of this file are subject to the BOINC Public License +// Version 1.0 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at +// http://boinc.berkeley.edu/license_1.0.txt +// +// Software distributed under the License is distributed on an "AS IS" +// basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +// License for the specific language governing rights and limitations +// under the License. +// +// The Original Code is the Berkeley Open Infrastructure for Network Computing. +// +// The Initial Developer of the Original Code is the SETI@home project. +// Portions created by the SETI@home project are Copyright (C) 2002 +// University of California at Berkeley. All Rights Reserved. +// +// Contributor(s): +// + +#include +#include +#include + +// project includes --------------------------------------------------------- + +#include "mac_main.h" +#include "mac_join.h" +#include "mac_about.h" +#include "mac_prefs.h" +#include "account.h" +#include "client_state.h" +#include "log_flags.h" +#include "message.h" +#include "gui_titles.h" +#include "util.h" + +// statics/globals (internal only) ------------------------------------------ + +WindowRef boincWindow; +EventLoopTimerRef boincTimer,boincIdleTimer; +EventLoopTimerUPP boincTimerUPP,boincIdleUPP; +EventHandlerUPP appCommandProcessor,winCommandProcessor; +WindowPtr boincAboutWindow; +Point oldMouse; +KeyMap oldKeys; +UInt32 user_idle_since,user_idle_length; +vector msgs; +ControlRef tabControl; +int winXPos, winYPos, winWidth, winHeight; +int tabList[] = {5, 129, 130, 131, 132, 133}; // Tab UserPane IDs +int lastTabIndex = 1; +MenuRef dockMenu, fileMenu; +MenuItemIndex dockSuspendIndex, dockResumeIndex; +MenuItemIndex fileSuspendIndex, fileResumeIndex; +bool user_requested_exit = false; + +ControlRef boincDataBrowsers[MAX_LIST_ID]; + +DataBrowserPropertyType column_types[MAX_LIST_ID][MAX_COLS] = { + {kDataBrowserTextType, kDataBrowserTextType, kDataBrowserTextType, kDataBrowserTextType, kDataBrowserProgressBarType, NULL, NULL}, + {kDataBrowserTextType, kDataBrowserTextType, kDataBrowserTextType, kDataBrowserTextType, kDataBrowserProgressBarType, kDataBrowserTextType, kDataBrowserTextType}, + {kDataBrowserTextType, kDataBrowserTextType, kDataBrowserProgressBarType, kDataBrowserTextType, kDataBrowserTextType, kDataBrowserTextType, NULL}, + {kDataBrowserTextType, kDataBrowserDateTimeType, kDataBrowserTextType, NULL, NULL, NULL, NULL} +}; + +DataBrowserItemDataUPP carbonItemCallbacks[MAX_LIST_ID] = + {BOINCCarbonProjectCallback, BOINCCarbonWorkCallback, BOINCCarbonTransferCallback, BOINCCarbonMessageCallback}; + +void Syncronize(ControlRef prog, vector* vect); + +// -------------------------------------------------------------------------- + +OSStatus InitMainWindow(void) { + OSStatus err; + Handle boincMenuBar; + ControlID ctrlID; + IBNibRef boincNibRef; + int i,n; + DataBrowserCallbacks callbacks; + + InitCursor(); + + // Search for the "boinc" .nib file + err = CreateNibReference(CFSTR("boinc"), &boincNibRef); + if ( err != noErr ) { + fprintf(stderr, "Can't load boinc.nib. Err: %d\n", (int)err); + ExitToShell(); + } + + // Init Menus + err = CreateMenuBarFromNib(boincNibRef, CFSTR("MainMenu"), &boincMenuBar); + if ( err != noErr ) { + fprintf(stderr, "Can't load MenuBar. Err: %d\n", (int)err); + ExitToShell(); + } + + err = CreateWindowFromNib(boincNibRef, CFSTR("Client Window"), &boincWindow); + if (err != noErr) { + fprintf(stderr, "Can't load Window. Err: %d\n", (int)err); + ExitToShell(); + } + + ReadBOINCPreferences(); + + // Enable the preferences item + EnableMenuCommand(NULL, kHICommandPreferences); + + // Add the columns to the data browsers + for (i=0;i 0) { + if (user_idle_length > 60 * gstate.global_prefs.idle_time_to_run) { // Is there a defined constant we can use instead of 60? + return true; + } else { + return false; + } + } else { + return true; + } +} + +//////////////////////////////////////////////////////// +// BOINCPollLoopProcessor // +//////////////////////////////////////////////////////// +pascal void BOINCPollLoopProcessor(EventLoopTimerRef inTimer, void* timeData) +{ + gstate.user_idle = CheckIfIdle(); + + // While we still have something to do we keep processing, + // otherwise, we go back to sleep + while (gstate.do_something()) { + if (gstate.time_to_exit()) { + break; + } + } + + Syncronize( boincDataBrowsers[0], (vector*)(&gstate.projects) ); + Syncronize( boincDataBrowsers[1], (vector*)(&gstate.results) ); + Syncronize( boincDataBrowsers[2], (vector*)(&gstate.pers_xfers->pers_file_xfers) ); + Syncronize( boincDataBrowsers[3], (vector*)(&msgs) ); + + GUIRedraw(); + + fflush(stdout); +} + +///////////////////////////////////////////////////// +// BOINCIdleDetect // +///////////////////////////////////////////////////// +pascal void BOINCIdleDetect(EventLoopTimerRef inTimer, void* timeData) +{ + Point curMouse; + KeyMap curKeys; + int i; + UInt32 curTime; + + curTime = TickCount(); + + // See if the mouse has moved + GetMouse( &curMouse ); + if (curMouse.h != oldMouse.h || curMouse.v != oldMouse.v) { + user_idle_since = curTime; + } + oldMouse.h = curMouse.h; + oldMouse.v = curMouse.v; + + // See if user has pressed the mouse button + if (Button()) + user_idle_since = curTime; + + // See if any keys have been pressed + GetKeys( curKeys ); + for (i=0;i<4;i++) { + if (oldKeys[i] != curKeys[i]) { + user_idle_since = curTime; + } + oldKeys[i] = curKeys[i]; + } + + user_idle_length = curTime - user_idle_since; +} + +////////////////////////////////////////////////////////////////////////////////// +// SuspendBOINC // +////////////////////////////////////////////////////////////////////////////////// +void SuspendBOINC( bool suspend ) { + gstate.suspend_requested = suspend; + if (suspend) { + DisableMenuItem( dockMenu, dockSuspendIndex ); + DisableMenuItem( fileMenu, fileSuspendIndex ); + EnableMenuItem( dockMenu, dockResumeIndex ); + EnableMenuItem( fileMenu, fileResumeIndex ); + } else { + DisableMenuItem( dockMenu, dockResumeIndex ); + DisableMenuItem( fileMenu, fileResumeIndex ); + EnableMenuItem( dockMenu, dockSuspendIndex ); + EnableMenuItem( fileMenu, fileSuspendIndex ); + } +} + +////////////////////////////////////////////////////////////////////////////////// +// MainAppEventHandler // +////////////////////////////////////////////////////////////////////////////////// +pascal OSStatus MainAppEventHandler(EventHandlerCallRef appHandler, EventRef theEvent, void* appData) +{ +#pragma unused (appHandler, appData) + + HICommand aCommand; + OSStatus result; + + switch(GetEventClass(theEvent)) + { + case kEventClassCommand: + result = GetEventParameter(theEvent, kEventParamDirectObject, + typeHICommand, NULL, sizeof(HICommand), + NULL, &aCommand); + switch (aCommand.commandID) + { + case kHICommandQuit: + QuitApplicationEventLoop(); + result = noErr; + break; + case kHICommandPreferences: // 'pref' + // Open prefs dialog + //CreatePrefsDialog(); + result = noErr; + break; + case kBOINCCommandJoin: // 'join' + char new_master_url[256], new_auth[256]; + // Open join dialog + if (CreateJoinDialog( new_master_url, new_auth ) ) { + gstate.add_project( new_master_url, new_auth ); + } + result = noErr; + break; + case kBOINCCommandSuspend: // 'susp' + // Suspend processing + SuspendBOINC(true); + result = noErr; + break; + case kBOINCCommandResume: // 'resm' + // Resume processing + SuspendBOINC(false); + result = noErr; + break; + case kHICommandAbout: // 'abou' + // Open About window + CreateAboutWindow(); + result = noErr; + break; + default: + result = eventNotHandledErr; + break; + } + break; + default: + result = eventNotHandledErr; + break; + } + return result; +} + + +////////////////////////////////////////////////////////////////////////////////// +// MainWinEventHandler // +////////////////////////////////////////////////////////////////////////////////// +pascal OSStatus MainWinEventHandler(EventHandlerCallRef appHandler, EventRef theEvent, void* appData) +{ +#pragma unused (appHandler, appData) + OSStatus result; + UInt32 attributes; + Rect winRect; + + switch(GetEventClass(theEvent)) + { + case kEventClassWindow: + GetEventParameter(theEvent, kEventParamAttributes, typeUInt32, NULL, sizeof(UInt32), NULL, &attributes); + switch(GetEventKind(theEvent)) { + case kEventWindowBoundsChanged: + GetEventParameter(theEvent, kEventParamCurrentBounds, typeQDRectangle, NULL, sizeof(Rect), NULL, &winRect); + winXPos = winRect.left; + winYPos = winRect.top; + winWidth = winRect.right - winRect.left; + winHeight = winRect.bottom - winRect.top; + if (attributes & kWindowBoundsChangeSizeChanged) { + SizeControl( tabControl, winWidth, winHeight-12 ); + for (int i=0;i<4;i++ ) + SizeControl( boincDataBrowsers[i], winWidth-30-9, winHeight-49-30 ); + } + break; + default: + result = eventNotHandledErr; + break; + } + break; + default: + result = eventNotHandledErr; + break; + } + return result; +} + +// -------------------------------------------------------------------------- + +OSStatus mac_setup (void) +{ + OSStatus err; + + err = InitMainWindow(); + if (err != noErr) return err; + RunApplicationEventLoop(); + + return true; +} + +// -------------------------------------------------------------------------- + +void mac_cleanup (void) +{ + RemoveEventLoopTimer(boincTimer); + DisposeEventLoopTimerUPP(boincTimerUPP); + DisposeEventHandlerUPP(appCommandProcessor); +} + +// -------------------------------------------------------------------------- + +int main(int argc, char** argv) { + read_log_flags(); + gstate.parse_cmdline(argc, argv); + + if (gstate.init()) return -1; + + // mac_setup won't return until the main application loop has quit + if (!mac_setup()) return -1; + + // Afterwards, we clean up and exit + gstate.cleanup_and_exit(); + + SaveBOINCPreferences(); + + mac_cleanup(); +} + + // Check if we're running OS 8/9 or OS X + /*pm_features = PMFeatures(); + // If the we can get the battery info from this API, do it + if ((pm_features>>canGetBatteryTime)&0x01) { + err = BatteryStatus( &a, &b ); + if (err != noErr); // do something + } else + fprintf( stderr, "%d %d", a, b ); + exit(0);*/ + +// -------------------------------------------------------------------------- + +void show_message(PROJECT *p, char* message, int priority) { + MESSAGE *new_msg; + + new_msg = (MESSAGE*)malloc(sizeof(MESSAGE)); + GetDateTime( &new_msg->timestamp ); + safe_strncpy( new_msg->msg, message, sizeof(new_msg->msg) ); + if (p) + safe_strncpy( new_msg->project, p->get_project_name(), sizeof(new_msg->msg) ); + else + safe_strncpy( new_msg->project, "BOINC", sizeof(new_msg->msg) ); + msgs.push_back(new_msg); +} + +// +int add_new_project() { + return 0; +} + +// ---------------------------------------------------------------------- +// Show the selected pane, hide the others. + +void SelectItemOfTabControl(ControlRef myTabControl) { + ControlRef userPane; + ControlRef selectedPane = NULL; + ControlID controlID; + UInt16 i; + + SInt16 index = GetControlValue(myTabControl); + + lastTabIndex = index; + controlID.signature = TAB_SIGNATURE; + + for (i = 1; i < tabList[0] + 1; i++) { + controlID.id = tabList[i]; + GetControlByID(GetControlOwner(myTabControl), &controlID, &userPane); + + if (i == index) { + selectedPane = userPane; + SetControlVisibility(boincDataBrowsers[i-1],true,true); + } else { + SetControlVisibility(userPane,false,false); + SetControlVisibility(boincDataBrowsers[i-1],false,false); + DisableControl(userPane); + } + } + + if (selectedPane != NULL) { + EnableControl(selectedPane); + SetControlVisibility(selectedPane, true, true); + } + + SizeControl( tabControl, winWidth, winHeight-12 ); + + Draw1Control(myTabControl); + + for (int i=0;i<4;i++ ) + SizeControl( boincDataBrowsers[i], winWidth-30-9, winHeight-49-30 ); +} + +// ---------------------------------------------------------------------- +// Listen to events. Only switch if the tab value has changed. + +pascal OSStatus TabEventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void *inUserData) +{ + OSStatus result = eventNotHandledErr; + + ControlRef theControl; + ControlID controlID; + + GetEventParameter(inEvent, kEventParamDirectObject, typeControlRef, NULL, sizeof( ControlRef ), NULL, &theControl ); + + GetControlID(theControl, &controlID); + + // If this event didn't trigger a tab change, give somebody else a chance to handle it. + if (controlID.id == TAB_ID && GetControlValue(theControl) != lastTabIndex) { + result = noErr; + SelectItemOfTabControl(theControl); + } + + return result; +} + +// ---------------------------------------------------------------------- + +void InstallTabHandler(WindowRef window) +{ + EventTypeSpec controlSpec = { kEventClassControl, kEventControlHit }; // event class, event kind + ControlID controlID; + + // Find the tab control and install an event handler on it. + controlID.signature = TAB_SIGNATURE; + controlID.id = TAB_ID; + GetControlByID(window, &controlID, &tabControl); + + InstallEventHandler(GetControlEventTarget(tabControl), NewEventHandlerUPP( TabEventHandler ), + 1, &controlSpec, 0, NULL); + + //Select the active tab to start with. + lastTabIndex = GetControlValue(tabControl); + SelectItemOfTabControl(tabControl); +} + +// Force a redraw of the GUI elements +// +void GUIRedraw(void) { + DataBrowserItemID items; + + items = kDataBrowserNoItem; + for (int i=0;iget_project_name(), sizeof(buf) ); + break; + case 1: // Account name + safe_strncpy( buf, p->user_name, sizeof(buf) ); + break; + case 2: // Total credit + sprintf( buf, "%0.2f", p->user_total_credit); + break; + case 3: // Average credit + sprintf( buf, "%0.2f", p->user_expavg_credit); + break; + case 4: // Resource share + for(i = 0; i < gstate.projects.size(); i ++) { + totalres += gstate.projects[i]->resource_share; + } + if (totalres <= 0) + SetDataBrowserItemDataValue( itemData, 100 ); + else + SetDataBrowserItemDataValue( itemData, (int)((100 * p->resource_share)/totalres) ); + break; + default: + break; + } + err = SetDataBrowserItemDataText( itemData, CFStringCreateWithCString(CFAllocatorGetDefault(), buf, kCFStringEncodingMacRoman)); + } + + return noErr; +} + +OSStatus BOINCCarbonWorkCallback( ControlRef browser, DataBrowserItemID itemID, + DataBrowserPropertyID property, DataBrowserItemDataRef itemData, Boolean changeValue) { + + OSStatus err; + char buf[256]; + RESULT *re = (RESULT*)itemID; + ACTIVE_TASK* at; + int cpuhour, cpumin, cpusec; + + if (!changeValue) { + sprintf( buf, " " ); + switch (property) { + case 0: // Project name + safe_strncpy( buf, re->project->project_name, sizeof(buf) ); + break; + case 1: // App name + safe_strncpy( buf, re->app->name, sizeof(buf) ); + break; + case 2: // Workunit name + safe_strncpy( buf, re->name, sizeof(buf) ); + break; + case 3: // CPU time + double cur_cpu; + at = gstate.lookup_active_task_by_result(re); + if (at) { + cur_cpu = at->current_cpu_time; + } else { + cur_cpu = 0; + } + cpuhour = (int)(cur_cpu / (60 * 60)); + cpumin = (int)(cur_cpu / 60) % 60; + cpusec = (int)(cur_cpu) % 60; + sprintf( buf, "%02d:%02d:%02d", cpuhour, cpumin, cpusec); + break; + case 4: // Progress + at = gstate.lookup_active_task_by_result(re); + if(!at) { + SetDataBrowserItemDataValue(itemData, 0); + } else { + SetDataBrowserItemDataValue(itemData, (int)(at->fraction_done * 100)); + } + break; + case 5: // Time to completion + double tocomp; + if(!at || at->fraction_done == 0) { + tocomp = gstate.estimate_cpu_time(*re->wup); + } else { + tocomp = at->est_time_to_completion(); + } + cpuhour = (int)(tocomp / (60 * 60)); + cpumin = (int)(tocomp / 60) % 60; + cpusec = (int)(tocomp) % 60; + sprintf( buf, "%02d:%02d:%02d", cpuhour, cpumin, cpusec); + break; + case 6: // Status + switch(re->state) { + case RESULT_NEW: + safe_strncpy(buf, g_szMiscItems[0], sizeof(buf)); + break; + case RESULT_FILES_DOWNLOADING: + safe_strncpy(buf, g_szMiscItems[9], sizeof(buf)); + break; + case RESULT_FILES_DOWNLOADED: + if (at) safe_strncpy(buf, g_szMiscItems[1], sizeof(buf)); + else safe_strncpy(buf, g_szMiscItems[2], sizeof(buf)); + break; + case RESULT_COMPUTE_DONE: + safe_strncpy(buf, g_szMiscItems[3], sizeof(buf)); + break; + case RESULT_FILES_UPLOADING: + safe_strncpy(buf, g_szMiscItems[8], sizeof(buf)); + break; + default: + if (re->server_ack) { + safe_strncpy(buf, g_szMiscItems[5], sizeof(buf)); break; + } else if (re->ready_to_ack) { + safe_strncpy(buf, g_szMiscItems[4], sizeof(buf)); break; + } else { + safe_strncpy(buf, g_szMiscItems[6], sizeof(buf)); break; + } + } + break; + } + err = SetDataBrowserItemDataText( itemData, CFStringCreateWithCString(CFAllocatorGetDefault(), buf, kCFStringEncodingMacRoman)); + } + + return noErr; +} + +OSStatus BOINCCarbonTransferCallback( ControlRef browser, DataBrowserItemID itemID, + DataBrowserPropertyID property, DataBrowserItemDataRef itemData, Boolean changeValue) { + + OSStatus err; + char buf[256]; + PERS_FILE_XFER *fi = (PERS_FILE_XFER*)itemID; + double xSent,xtime; + int xhour, xmin, xsec; + + if (!changeValue) { + xSent = 0; + if( fi->fip->generated_locally ) { + xSent = fi->fip->upload_offset; + } else { + char pathname[256]; + get_pathname(fi->fip, pathname); + file_size(pathname, xSent); + } + sprintf( buf, " " ); + switch (property) { + case 0: // Project name + safe_strncpy( buf, fi->fip->project->project_name, sizeof(buf) ); + break; + case 1: // File name + safe_strncpy( buf, fi->fip->name, sizeof(buf) ); + break; + case 2: // Transfer progress + SetDataBrowserItemDataValue(itemData, (int)(100.0 * xSent / fi->fip->nbytes)); + break; + case 3: // Size + sprintf( buf,"%0.0f/%0.0fKB", xSent / 1024.0, fi->fip->nbytes / 1024.0); + break; + case 4: // Time + xtime = fi->time_so_far; + xhour = (int)(xtime / (60 * 60)); + xmin = (int)(xtime / 60) % 60; + xsec = (int)(xtime) % 60; + sprintf(buf,"%02d:%02d:%02d", xhour, xmin, xsec); + break; + case 5: // Status + if (fi->next_request_time > time(0)) { + xtime = fi->next_request_time-time(0); + xhour = (int)(xtime / (60 * 60)); + xmin = (int)(xtime / 60) % 60; + xsec = (int)(xtime) % 60; + sprintf(buf,"%s %02d:%02d:%02d", g_szMiscItems[10], xhour, xmin, xsec); + } else if (fi->fip->status == ERR_GIVEUP_DOWNLOAD) { + safe_strncpy( buf, g_szMiscItems[11], sizeof(buf) ); + } else if (fi->fip->status == ERR_GIVEUP_UPLOAD) { + safe_strncpy( buf, g_szMiscItems[12], sizeof(buf) ); + } else { + safe_strncpy( buf, fi->fip->generated_locally?g_szMiscItems[8]:g_szMiscItems[9], sizeof(buf) ); + } + break; + } + err = SetDataBrowserItemDataText( itemData, CFStringCreateWithCString(CFAllocatorGetDefault(), buf, kCFStringEncodingMacRoman)); + } + + return noErr; +} + +OSStatus BOINCCarbonMessageCallback( ControlRef browser, DataBrowserItemID itemID, + DataBrowserPropertyID property, DataBrowserItemDataRef itemData, Boolean changeValue) { + + OSStatus err; + char buf[256]; + MESSAGE *msg = (MESSAGE*)itemID; + + if (!changeValue) { + switch (property) { + case 0: // Project + safe_strncpy( buf, msg->project, sizeof(buf) ); + err = SetDataBrowserItemDataText( itemData, CFStringCreateWithCString(CFAllocatorGetDefault(), buf, kCFStringEncodingMacRoman)); + break; + case 1: // Time + SetDataBrowserItemDataDateTime( itemData, msg->timestamp ); + break; + case 2: // File name + safe_strncpy( buf, msg->msg, sizeof(buf) ); + err = SetDataBrowserItemDataText( itemData, CFStringCreateWithCString(CFAllocatorGetDefault(), buf, kCFStringEncodingMacRoman)); + break; + default: + safe_strncpy( buf, " ", sizeof(buf) ); + err = SetDataBrowserItemDataText( itemData, CFStringCreateWithCString(CFAllocatorGetDefault(), buf, kCFStringEncodingMacRoman)); + break; + } + } + + return noErr; +} + +////////// +// arguments: prog: pointer to a progress list control +// vect: pointer to a vector of pointers +// returns: void +// function: first, goes through the vector and adds items to the list +// control for any pointers it does not already contain, then +// goes through the list control and removes any pointers the +// vector does not contain. +void Syncronize(ControlRef prog, vector* vect) { + unsigned int i,j; + Handle items; + OSStatus err; + DataBrowserItemID duff[1]; + + items = NewHandle(0); + err = GetDataBrowserItems(prog, kDataBrowserNoItem, true, 0, items); + + // add items to list that are not already in it + for(i = 0; i < vect->size(); i ++) { + void* item = (*vect)[i]; + bool contained = false; + for(j = 0; j < (GetHandleSize(items) / sizeof(DataBrowserItemID)); j ++) { + if(item == items[j]) { + contained = true; + break; + } + } + if(!contained) { + duff[0] = (long unsigned int)item; + err = AddDataBrowserItems( prog, kDataBrowserNoItem, 1, duff, kDataBrowserItemNoProperty ); + } + } + + // remove items from list that are not in vector + // now just set the pointer to NULL but leave the item in the list + for(i = 0; i < (GetHandleSize(items) / sizeof(DataBrowserItemID)); i ++) { + void* item = items[i]; + bool contained = false; + for(j = 0; j < vect->size(); j ++) { + if(item == (*vect)[j]) { + contained = true; + break; + } + } + if(!contained) { + duff[0] = (long unsigned int)item; + err = RemoveDataBrowserItems( prog, kDataBrowserNoItem, 1, duff, kDataBrowserItemNoProperty ); + } + } +} + +OSStatus SaveBOINCPreferences( void ) { + unsigned int i, n; + char buf[256]; + UInt32 ncols; + + // Set up the preferences + CFPreferencesSetAppValue(CFSTR("mainWindowXPos"), CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &winXPos), + kCFPreferencesCurrentApplication); + CFPreferencesSetAppValue(CFSTR("mainWindowYPos"), CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &winYPos), + kCFPreferencesCurrentApplication); + CFPreferencesSetAppValue(CFSTR("mainWindowWidth"), CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &winWidth), + kCFPreferencesCurrentApplication); + CFPreferencesSetAppValue(CFSTR("mainWindowHeight"), CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &winHeight), + kCFPreferencesCurrentApplication); + CFPreferencesSetAppValue(CFSTR("mainWindowCurTab"), CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &lastTabIndex), + kCFPreferencesCurrentApplication); + + for (i=0;i<4;i++) { + GetDataBrowserTableViewColumnCount(boincDataBrowsers[i],&ncols); + for (n=0;n