JobListCtrl.cpp. #1

  • //
  • guest/
  • YourUncleBob/
  • p4win/
  • main/
  • gui/
  • JobListCtrl.cpp.
  • View
  • Commits
  • Open Download .zip Download (36 KB)
//
// Copyright 1997 Nicholas J. Irias.  All rights reserved.
//
//

// JobListCtrl.cpp : implementation file
//

#include "stdafx.h"
#include "p4win.h"
#include "JobListCtrl.h"
#include "JobsConfigure.h"
#include "MainFrm.h"
#include "TokenString.h"
#include "JobDescribe.h"
#include "SpecDescDlg.h"
#include "cmd_editspec.h"
#include "cmd_jobs.h"
#include "cmd_delete.h"
#include "catchalldlg.h"
#include "RegKeyEx.h"
#include "ImageList.h"

static LPCTSTR sRegKey = _T("Software\\Perforce\\P4Win\\Layout\\Job List");
static LPCTSTR sRegValue_ColumnWidths = _T("Column Widths");
static LPCTSTR sRegValue_SortColumns = _T("Sort Columns");

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

#define IMG_INDEX(x) (x-IDB_PERFORCE)

/////////////////////////////////////////////////////////////////////////////
// CJobListCtrl

IMPLEMENT_DYNCREATE(CJobListCtrl, CP4ListCtrl)

BEGIN_MESSAGE_MAP(CJobListCtrl, CP4ListCtrl)
	ON_WM_CREATE()
	ON_UPDATE_COMMAND_UI(ID_VIEW_UPDATE_RIGHT, OnUpdateViewUpdate)
	ON_WM_CONTEXTMENU()
	ON_UPDATE_COMMAND_UI(ID_JOB_DELETE, OnUpdateJobDelete)
	ON_COMMAND(ID_JOB_DELETE, OnJobDelete)
	ON_UPDATE_COMMAND_UI(ID_JOB_EDITSPEC, OnUpdateJobEditspec)
	ON_COMMAND(ID_JOB_EDITSPEC, OnJobEditspec)
	ON_NOTIFY_REFLECT(LVN_DELETEITEM, OnDeleteitem)
	ON_UPDATE_COMMAND_UI(ID_JOB_DESCRIBE, OnUpdateJobDescribe)
	ON_WM_LBUTTONDBLCLK()

	ON_COMMAND(ID_SETFILTER_JOBS, OnJobSetFilter)
	ON_COMMAND(ID_CLEARFILTER_JOBS, OnJobRemovefilter)

	ON_UPDATE_COMMAND_UI(ID_JOB_SETFILTER, OnUpdateJobSetFilter)
	ON_COMMAND(ID_JOB_SETFILTER, OnJobSetFilter)
	ON_UPDATE_COMMAND_UI(ID_JOB_REMOVEFILTER, OnUpdateJobRemovefilter)
	ON_COMMAND(ID_JOB_REMOVEFILTER, OnJobRemovefilter)

	ON_UPDATE_COMMAND_UI(ID_JOB_SETFILEFILTER, OnUpdateJobSetFileFilter)
	ON_COMMAND(ID_JOB_SETFILEFILTER, OnJobSetFileFilter)
	ON_UPDATE_COMMAND_UI(ID_JOB_SETFILEFILTERINTEG, OnUpdateJobSetFileFilter)
	ON_COMMAND(ID_JOB_SETFILEFILTERINTEG, OnJobSetFileFilterInteg)
	ON_UPDATE_COMMAND_UI(ID_JOB_REMOVEFILEFILTER, OnUpdateJobRemoveFileFilter)
	ON_COMMAND(ID_JOB_REMOVEFILEFILTER, OnJobRemoveFileFilter)

	ON_UPDATE_COMMAND_UI(ID_JOB_NEW, OnUpdateJobNew)
	ON_COMMAND(ID_JOB_NEW, OnJobNew)
	ON_COMMAND(ID_JOB_DESCRIBE, OnDescribe)
	ON_COMMAND(ID_VIEW_UPDATE_RIGHT, OnViewUpdate)
	ON_UPDATE_COMMAND_UI(ID_JOB_CONFIGURE, OnUpdateJobConfigure)
	ON_COMMAND(ID_JOB_CONFIGURE, OnJobConfigure)
	ON_COMMAND(ID_PERFORCE_OPTIONS, OnPerforceOptions)
	ON_MESSAGE(WM_P4JOBS, OnP4JobList )
	ON_MESSAGE(WM_P4EDITSPEC, OnP4JobSpec )
	ON_MESSAGE(WM_P4ENDSPECEDIT, OnP4EndSpecEdit )
	ON_MESSAGE(WM_P4DELETE, OnP4Delete )
	ON_MESSAGE(WM_P4JOBSPEC, OnP4JobSpecColumnNames )
	ON_MESSAGE(WM_P4DESCRIBE, OnP4Describe )
    ON_MESSAGE(WM_P4ENDDESCRIBE, OnP4EndDescribe )
    ON_MESSAGE(WM_FETCHJOBS, OnFetchJobs )
    ON_MESSAGE(WM_QUERYJOBS, OnQueryJobs )
    ON_MESSAGE(WM_QUERYJOBSPEC, OnQueryJobSpec )
    ON_MESSAGE(WM_QUERYJOBFIELDS, OnQueryJobFields )
    ON_MESSAGE(WM_QUERYJOBCOLS, OnQueryJobColumns )
    ON_MESSAGE(	WM_QUERYJOBSELECTION, OnQueryJobSelection )
	ON_MESSAGE(WM_JOB_FILTER, OnJobFilter2)
	ON_MESSAGE(WM_CLEARLIST, OnClear)
END_MESSAGE_MAP()

CJobListCtrl::CJobListCtrl()
{
	m_SortAscending = m_FilterIncIntegs = FALSE;
	m_LastSortCol=0;
	m_viewType = P4JOB_SPEC;
	m_bAlreadyGotColumns = m_Need2CallOnJobConfigure = FALSE;
    m_PostListToChangeNum= 0;
	m_PostListToChangeWnd= 0;
	m_Need2DoNew = FALSE;
	m_NewJob = FALSE;
    m_ColCodes.RemoveAll();
    m_captionplain = LoadStringResource(IDS_PERFORCE_JOBS);

	m_FastJobs = GetSavedColumnNames(m_DesiredCols, _T("Job List"));
	m_CF_JOB   = static_cast<CLIPFORMAT>(RegisterClipboardFormat(LoadStringResource(IDS_DRAGFROMJOB)));
	m_CF_DEPOT = static_cast<CLIPFORMAT>(RegisterClipboardFormat(LoadStringResource(IDS_DRAGFROMDEPOT)));
}

CJobListCtrl::~CJobListCtrl()
{

}

/////////////////////////////////////////////////////////////////////////////
// CJobListCtrl diagnostics

#ifdef _DEBUG
void CJobListCtrl::AssertValid() const
{
	CP4ListCtrl::AssertValid();
}

void CJobListCtrl::Dump(CDumpContext& dc) const
{
	CP4ListCtrl::Dump(dc);
}
#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////
// CJobListCtrl message handlers

void CJobListCtrl::OnLButtonDblClk(UINT nFlags, CPoint point) 
{
	int index = GetHitItem ( point );
	if(index != -1)
	{
		SetItemState( index, LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED );
		OnDescribe();
	}
	else
		CP4ListCtrl::OnLButtonDblClk(nFlags, point);
}

LRESULT CJobListCtrl::OnClear( WPARAM wParam, LPARAM lParam )
{
	m_sFilter.Empty( );
	PersistentJobFilter( KEY_READ );
	Clear();
	return 0;
}

void CJobListCtrl::Clear()
{
    SetRedraw(FALSE);
	DeleteAllItems();
    SetRedraw(TRUE);

	CP4ListCtrl::Clear();
}

/////////////////////////////////
// Three functions to allow the changelist pane to fetch the job list
// and the current job codes list
/////////////////////////////////

LRESULT CJobListCtrl::OnFetchJobs( WPARAM wParam, LPARAM lParam )
{
    m_PostListToChangeNum= wParam;
	m_PostListToChangeWnd= (HWND)lParam;

    if( GetItemCount() > 0 )
    {
        ::SendMessage(m_PostListToChangeWnd, WM_P4JOBS, m_PostListToChangeNum, 0);
        m_PostListToChangeNum=0;
		m_PostListToChangeWnd=0;
    }
    else
        OnViewUpdate();

    return 0;
}

LRESULT CJobListCtrl::OnQueryJobs( WPARAM wParam, LPARAM lParam )
{
    CObList *m_pJobList= new CObList;

    // Make a copy of the joblist, so the consumer doesnt need
    // to worry about any refresh that happens here at a later
    // time.  (They just need to delete the list when done)
    for( int i = 0; i < GetItemCount(); i++ )
	{
        CP4Job *job= (CP4Job *) GetItemData(i);
		CP4Job *newJob= new CP4Job;
        newJob->Create(job);
        m_pJobList->AddHead(newJob);
	}  

    return (LRESULT) m_pJobList;
}

LRESULT CJobListCtrl::OnQueryJobSpec( WPARAM wParam, LPARAM lParam )
{
	return (LRESULT)&m_Spec;
}

LRESULT CJobListCtrl::OnQueryJobFields( WPARAM wParam, LPARAM lParam )
{
    // The caller has provided an array.  Just transpose our array
    // into that array, to give the caller a snapshot.
    CArray<int,int> *array= ( CArray<int,int> *) wParam;

    array->RemoveAll();
    for(int i=0; i < m_ColCodes.GetSize(); i++)
        array->Add( m_ColCodes[i] );

    return (LRESULT) array;
}

LRESULT CJobListCtrl::OnQueryJobColumns( WPARAM wParam, LPARAM lParam )
{
	return (LRESULT)&m_ColNames;
}

LRESULT CJobListCtrl::OnQueryJobSelection( WPARAM wParam, LPARAM lParam )
{
	m_Active = GetSelectedItemText();
	return (LRESULT)&m_Active;
}

void CJobListCtrl::OnUpdateJobNew(CCmdUI* pCmdUI) 
{
	pCmdUI->SetText ( LoadStringResource( IDS_NEW ) );
	pCmdUI->Enable(!SERVER_BUSY() && !m_EditInProgress);		
	m_Need2DoNew = FALSE;
}

/*
	_________________________________________________________________

	since users can now define their own types in the job tracker, we
	have to get the names from the server. 
	_________________________________________________________________
*/

LRESULT CJobListCtrl::OnP4JobSpecColumnNames( WPARAM wParam, LPARAM lParam )
{
	CCmd_JobSpec *pCmd = ( CCmd_JobSpec *) wParam;
	ASSERT_KINDOF(CCmd_JobSpec, pCmd);

	if(!pCmd->GetError())
	{
        // Delete the old column names
		int i;
        for( i= m_ColCodes.GetUpperBound(); i>=0; i-- )
            DeleteColumn(i);

		pCmd->GetSpec( m_Spec );
		m_FieldNames.SetSize(0);
		GetFldNames( m_FieldNames, m_Spec );

		//	Default column widths based on all 5 default columns being present
		//
		int width[MAX_JOBS_COLUMNS] = { 90,80,60,90,200 };

		RestoreSavedWidths( width, max(MAX_JOBS_COLUMNS, m_ColCodes.GetSize()), _T("Job List") );
		for (i = -1; ++i < MAX_JOBS_COLUMNS; )
		{
			if (width[i] > 5000)
				width[i] = 30;
		}
		InsertColumnHeaders( m_ColNames, width );
		m_bAlreadyGotColumns = TRUE;

		//	okay, we got all the job column names.
		//	now get the job list or start the configure dialog
		//
		if (m_Need2CallOnJobConfigure)
			OnJobConfigure();
		else
			GetJobs( );
	}
    else if(m_PostListToChangeNum != 0)
    {
        ::SendMessage(m_PostListToChangeWnd, WM_P4JOBS, m_PostListToChangeNum, 0);
        m_PostListToChangeNum=0;
		m_PostListToChangeWnd=0;
    }
        
	
	delete pCmd;
	MainFrame()->ClearStatus();
	return 0;
}


/*
	_________________________________________________________________

	Simple parse code to check jobspec for spec codes 101-105, 
    which are reserved codes for default fields that may be present in the
    output of p4 jobs.  Record the column headings for these codes.
	_________________________________________________________________
*/

BOOL CJobListCtrl::GetFldNames( CStringArray &fldNames, const CString &spec )
{
	int i;
    CString errorText;
    CString fields;

	m_SpecNames.RemoveAll();
	m_ColNames.RemoveAll();
	m_ColNames.SetSize(0);
	m_ColNames.Copy(m_DesiredCols);

	//  Get the field names from the spec. they are between the word 
    //  "Fields:" and the word "Required" strip the separator.  Don't
    //  crash if the jobspec is not recognized.
    
	int start= spec.Find(_T("Fields:")) + lstrlen(_T("Fields:"));
    int end= spec.Find(_T("Required:"));
    if( start == -1 || end == -1 || end < start || end >= spec.GetLength())
        errorText=LoadStringResource(IDS_UNABLE_TO_FIND_FIELD_DELIMITERS);
    
    if( errorText.IsEmpty() )
    {
	    fields = spec.Mid(start, end - start);
	    start= fields.Find( 0x09 );
        if( start == -1 )
            errorText=LoadStringResource(IDS_UNABLE_TO_FIND_INITIAL_FIELD_DELIMITER);
    }

    if( errorText.IsEmpty() )
    {
        m_ColCodes.RemoveAll();        
	    fields = fields.Mid( start + 1 );

	    // Pick out the requested field names if present
	    
	    int code;
	    CString field;
		int max = m_ColNames.GetSize();
    	do  
	    {
		    //		get the field spec (e.g., "101 Job word 32")
		    //		and truncate the fields.
		    //
			int delim = fields.Find( 0x0a);
			if(delim < 9)	// need at least "n Job x n" or something's wrong
				break;
		    field = fields.Left ( delim + 1 );
		    fields = fields.Right ( fields.GetLength( ) - (delim + 1) );

		    //		get the "101" part of the spec in code
		    //
			delim = field.Find(_T(' '));
			if(delim < 1)
				break;
		    code = _ttoi ( field.Left ( delim ) ) ;
		    field = field.Right ( field.GetLength ( ) - (delim + 1) );
		    //		get the "Job" part in field.
			//
			delim = field.Find(_T(' '));
			if(delim < 1)
				break;
		    field = field.Left ( delim );

			m_SpecNames.Add(field);	// save all field names from the spec here.

			if (code == 101)	// if this is the jobname field, capture its name for 1st column
				m_ColNames.SetAt(0, field);
			else if (code == 103)
				m_ReportedByTitle = field;

			for (i = -1; ++i < max; )
            {
				CString colname = m_ColNames.GetAt(i);
				if (colname == field)
				{
					fldNames.Add( field );
	                m_ColCodes.Add( code );
					break;
				}
				else if ((colname.GetAt(0) == _T(':')) && (colname.GetAt(1) == _T('1')) && (colname.GetAt(2) == _T('0')))
				{
					TCHAR digit = colname.GetAt(3);
					int colcode = 100 + (digit & 0x0F);
					if (colcode == code)
					{
						m_ColNames.SetAt(i, field);
						fldNames.Add( field );
						m_ColCodes.Add( code );
						break;
					}
				}
            }
	    }
	    while ( !field.IsEmpty( ) );
    }

    if( !errorText.IsEmpty() )
    {
        // Very unlikely that we will ever report this error, but an error
        // message is always preferable to an inexplicable crash
		AddToStatus(CString(_T("JobView::GetFldNames()") + errorText), SV_ERROR);
        return FALSE;
    }
    else
	    return TRUE;
}


/*
	_________________________________________________________________
*/

void CJobListCtrl::InsertJob(CP4Job *job, int index)
{
	LV_ITEM lvItem;
	int iActualItem = -1;
	CString txt;
	CString colName;
	
	ASSERT(job != NULL);
	m_iImage = CP4ViewImageList::VI_JOB;

	int maxcols = m_ColNames.GetSize();
	for(int subItem=0; subItem < maxcols; subItem++)
	{
		lvItem.mask=LVIF_TEXT | 
					((subItem==0) ? LVIF_IMAGE : 0) |
					((subItem==0) ? LVIF_PARAM : 0);

		lvItem.iItem= (subItem==0) ? index : iActualItem;
        ASSERT(lvItem.iItem != -1);
		lvItem.iSubItem= subItem;
		lvItem.iImage = CP4ViewImageList::VI_JOB;
		lvItem.lParam=(LPARAM) job;

		txt=PadCRs(job->GetJobField(subItem));
		lvItem.pszText = const_cast<LPTSTR>( (LPCTSTR ) txt);

		if(subItem==0)
	    	iActualItem=InsertItem(&lvItem);
		else
		    SetItem(&lvItem);
        
	}
}


/*	_________________________________________________________________
	
	After a spec edit, update the appropriate tree item
	_________________________________________________________________
*/

void CJobListCtrl::UpdateJob(CP4Job *job, int index)
{
	// First, switch the user data
	CP4Job *oldJob= (CP4Job *) GetItemData(index);
	delete oldJob;
	SetItemData(index, (LPARAM) job);

    CString txt;
	CString colName;

    // Then update the text for any fields that are present
	int maxCols = m_ColNames.GetSize();
    for( int subItem=0; subItem < maxCols; subItem++ )
    {
		txt=PadCRs(job->GetJobField(subItem));
		SetItemText(index, subItem, const_cast<LPTSTR>((LPCTSTR)txt));
    }
}


/*
	_________________________________________________________________
*/

void CJobListCtrl::OnUpdateJobDescribe(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable( OnUpdateShowMenuItem( pCmdUI, IDS_DESCRIBEIT_s ) );	
}


/*
	_________________________________________________________________
*/

LRESULT CJobListCtrl::OnP4JobList(WPARAM wParam, LPARAM lParam)
{
	POSITION pos;
	CP4Job *job;
	int index;
	CObList *jobs;

	CCmd_Jobs *pCmd= (CCmd_Jobs *) wParam;

	if(!pCmd->GetError())
	{
		SET_BUSYCURSOR();
		jobs= pCmd->GetList();

        SetRedraw(FALSE);
		CString bigjob = _T("");
		for(pos= jobs->GetHeadPosition(), index=0; pos != NULL; index++)
		{
			job=(CP4Job *) jobs->GetNext(pos);
			if (m_FilterView.GetCount() > 1)	// check for duplicates
			{									// if filtering on more than 1 file
				CString jobname = job->GetJobName();
				if (jobname <= bigjob)
				{
					if (jobname == bigjob || FindInList(jobname) != -1)
					{
						delete job;
						continue;
					}
				}
				else
					bigjob = jobname;
			}
			InsertJob(job, index);
		}
        SetRedraw(TRUE);

		CString msg;
		msg.FormatMessage(IDS_NUMBER_OF_JOBS_n, index );
		AddToStatus( msg, SV_COMPLETION );

		////jobs->RemoveAll();

		ReSort();
	
		if(jobs->GetCount() > 0)
		{
			int i = FindInList(m_Active);
			if (i < 0)	i=0;
			SetItemState(i, LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED);
			EnsureVisible(i, FALSE);
		}

		CP4ListCtrl::SetUpdateDone();
		if (m_Need2DoNew)
			OnJobNew();
		::SetCursor(::LoadCursor(NULL, IDC_ARROW));
	}
	else
	{
		CP4ListCtrl::SetUpdateFailed();
		m_Need2DoNew = FALSE;
	}

    if(m_PostListToChangeNum != 0)
    {
        ::SendMessage(m_PostListToChangeWnd, WM_P4JOBS, m_PostListToChangeNum, 0);
        m_PostListToChangeNum= 0;
		m_PostListToChangeWnd= 0;
    }
	
    delete pCmd;
	MainFrame()->ClearStatus();

	// Notify the mainframe that we have finished getting the jobs,
	// hence the entire set of async command have finished.
	MainFrame()->ExpandDepotIfNeedBe();

	return 0;
}

/*
	_________________________________________________________________
*/

void CJobListCtrl::OnUpdateViewUpdate(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable(!SERVER_BUSY() && !MainFrame()->IsModlessUp());
}

void CJobListCtrl::OnViewUpdate( ) 
{ 
	MainFrame()->SetJobUpdateTime(GetTickCount());
	m_Active = GetSelectedItemText();
	SetCaption( );
	if( m_ReadSavedWidths )
		SaveColumnWidths();


	if( GET_SERVERLEVEL() > 3 )
		GetJobSpec( ) ;		//which also calls GetJobs( )
    else
    {
        // Against 97.3 server, the column names are always the same
       	m_ColNames.RemoveAll();
        m_ColNames.Add(LoadStringResource(IDS_P4JOB));
        m_ColNames.Add(LoadStringResource(IDS_P4STATUS));
        m_ColNames.Add(LoadStringResource(IDS_REPORTEDBY));
        m_ColNames.Add(LoadStringResource(IDS_REPORTEDDATE));
        m_ColNames.Add(LoadStringResource(IDS_DESCRIPTION));

        m_ColCodes.RemoveAll();

		int i;
        for(i=101; i <= 105; i++)
            m_ColCodes.Add(i);
        

		//		there is no default for the jobs now that the fields 
		//		are user-defined.
		//
		int width[ MAX_JOBS_COLUMNS ] = { 90,80,60,90,200 };

		RestoreSavedWidths( width, m_ColCodes.GetSize(), _T("Job List") );
		for (i = -1; ++i < MAX_JOBS_COLUMNS; )
		{
			if (width[i] > 5000)
				width[i] = 30;
		}
		InsertColumnHeaders( m_ColNames, width );
		m_bAlreadyGotColumns = TRUE;
        GetJobs();
    }
}


void CJobListCtrl::GetJobSpec( )
{
    CCmd_JobSpec *pCmd = new CCmd_JobSpec;
	pCmd->Init( m_hWnd, RUN_ASYNC );
	
    if( !pCmd->Run( ) )
	   	delete pCmd;
}


void CJobListCtrl::GetJobs( )
{
	CCmd_Jobs *pCmd = new CCmd_Jobs;
	pCmd->Init( m_hWnd, RUN_ASYNC);
	
    if( GET_SERVERLEVEL() == 3)
    {
        m_sFilter.Empty();
       	pCmd->SetFilter( FALSE );
    }
    else
    	pCmd->SetFilter( !m_sFilter.IsEmpty( ) );

	ASSERT(m_bAlreadyGotColumns);
	int maxFlds = m_FieldNames.GetSize();
	int maxCols = m_ColNames.GetSize();
	int i;
	for (i = -1; ++i < maxCols; )	// add the fields desired in column order
	{
		CString colName = m_ColNames.GetAt(i);
		int j;
		for (j = -1; ++j < maxFlds; )
		{
			if (m_FieldNames.GetAt(j) == colName)
			{
				pCmd->GetFieldNames().Add(colName);
				pCmd->GetFieldCodes().Add(m_ColCodes.GetAt(j));
				break;
			}
		}
		if (j >= maxFlds)
		{
			pCmd->GetFieldNames().Add(_T(""));
			pCmd->GetFieldCodes().Add(0);
		}
	}

	// Make a copy of the filter view, because CCmd_Jobs will
	// destroy that copy
	POSITION pos=m_FilterView.GetHeadPosition();
	m_StrList.RemoveAll();
	while(pos != NULL)
		m_StrList.AddTail(m_FilterView.GetNext(pos));

	if( pCmd->Run( m_sFilter, m_FastJobs, &m_StrList, m_FilterIncIntegs ) )
	{
		Clear();
		CP4ListCtrl::OnViewUpdate();
		MainFrame()->UpdateStatus( LoadStringResource(IDS_REQUESTING_JOBS_LISTING) );
	}
	else
		delete pCmd;
}


/*
	_________________________________________________________________
*/

CString CJobListCtrl::SetCaption()
{
	if( (m_sFilter.IsEmpty() && m_FilterView.IsEmpty()) || GET_SERVERLEVEL() < 4)
		m_caption = LoadStringResource(IDS_PERFORCE_JOBS);
	else
    {
        CString filter;
		if (!m_sFilter.IsEmpty() && !m_FilterView.IsEmpty())
			filter.FormatMessage(IDS_FILTERED_BOTH, m_sFilter);
		else if (!m_FilterView.IsEmpty())
		{
			CString filelist=m_FilterView.GetHead();
			if (m_FilterView.GetCount() > 1)
			{
				POSITION pos= m_FilterView.GetHeadPosition();
				m_FilterView.GetNext(pos);
				while( pos != NULL )
					filelist += _T(", ") + m_FilterView.GetNext(pos);
			}
			filter.FormatMessage(IDS_FILTERED_BY_FILES, filelist);
		}
		else
			filter.FormatMessage(IDS_FILTERED, m_sFilter);
		m_caption = LoadStringResource(IDS_PERFORCE_JOBS) + filter;
    }

	CP4PaneContent::GetView()->SetCaption();

	return m_caption;
}


/*
	_________________________________________________________________
*/

void CJobListCtrl::OnContextMenu(CWnd* pWnd, CPoint point) 
{
	// make sure window is active
	GetParentFrame()->ActivateFrame();

	///////////////////////////////
	// See ContextMenuRules.txt for order of menu commands!

	// create an empty context menu
	CP4Menu popMenu;
	popMenu.CreatePopupMenu();

	int	index;
    SetIndexAndPoint( index, point );

	// Can always create new job
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_NEW );

	if(index != -1)
	{
		popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_EDITSPEC );
		popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_DESCRIBE );
		popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_DELETE );
	}
	popMenu.AppendMenu(MF_SEPARATOR);
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_SETFILTER );
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_REMOVEFILTER );
	popMenu.AppendMenu(MF_SEPARATOR);
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_SETFILEFILTER, LoadStringResource(IDS_FILTER_JOBVIEW) );
	if (GET_P4REGPTR()->GetEnableSubChgIntegFilter())
		popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_SETFILEFILTERINTEG, LoadStringResource(IDS_FILTERINTEG_JOBVIEW) );
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_REMOVEFILEFILTER, LoadStringResource(IDS_JOB_REMOVEFILEFILTER) );
	popMenu.AppendMenu(MF_SEPARATOR);
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_JOB_CONFIGURE, LoadStringResource(IDS_JOB_CONFIGURE) );
	popMenu.AppendMenu(MF_ENABLED | MF_STRING, ID_VIEW_UPDATE, LoadStringResource(IDS_REFRESH));

	MainFrame()->AddToolsToContextMenu(&popMenu);

	popMenu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,	point.x, point.y, AfxGetMainWnd());
}


/*
	_________________________________________________________________
*/

void CJobListCtrl::OnUpdateJobDelete(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable( OnUpdateShowMenuItem( pCmdUI, IDS_DELETE_s ) );	
}

void CJobListCtrl::OnJobDelete() 
{
	OnDelete( P4JOB_DEL );
}


/*
	_________________________________________________________________
*/

void CJobListCtrl::OnUpdateJobEditspec( CCmdUI* pCmdUI ) 
{
	pCmdUI->Enable( OnUpdateShowMenuItem(pCmdUI, IDS_EDITSPEC_s) && !m_EditInProgress );	
}

void CJobListCtrl::EditTheSpec(CString *name)
{
	OnJobEditspec(name);
}

void CJobListCtrl::OnJobEditspec() 
{
	CString str = GetSelectedItemText();
	OnJobEditspec(&str);
}

void CJobListCtrl::OnJobEditspec(CString *jobname) 
{
	m_Active = *jobname;
	m_NewJob = FALSE;
	EditSpec( ) ;
}


void CJobListCtrl::OnJobNew() 
{
	MainFrame()->ViewJobs();
	if (SERVER_BUSY())
		m_Need2DoNew = TRUE;
	else
	{
		m_Need2DoNew = FALSE;
		m_Active.Empty();
		m_NewJob = TRUE;
		EditSpec( ) ;
	}
}


void CJobListCtrl::EditSpec()
{
	if (m_EditInProgress)
	{
		CantEditRightNow(IDS_JOB);
		return;
	}

	m_pNewSpec = new CP4Job;
	
	CCmd_EditSpec *pCmd = new CCmd_EditSpec;
	pCmd->Init( m_hWnd, RUN_ASYNC, HOLD_LOCK );
	if( pCmd->Run( P4JOB_SPEC, m_Active, m_pNewSpec ) )//fanny:is this okay for edit and new?
		MainFrame()->UpdateStatus( LoadStringResource(IDS_EDITING_JOB_SPEC) );
	else
	{
		delete pCmd;
		delete m_pNewSpec;
	}
}


LRESULT CJobListCtrl::OnP4JobSpec(WPARAM wParam, LPARAM lParam)
{
	CCmd_EditSpec *pCmd= (CCmd_EditSpec *) wParam;

	// let the dialogbox know whether this is a new job or an edit of an existing one
	pCmd->SetIsRequestingNew(m_NewJob);
	pCmd->SetCaller(DYNAMIC_DOWNCAST(CView, GetParent()));
	if(!pCmd->GetError() && !m_EditInProgress && pCmd->DoSpecDlg(this))
	{
		m_EditInProgress = TRUE;
		m_EditInProgressWnd = pCmd->GetSpecSheet();
	}
	else
	{
		delete m_pNewSpec;
		if (pCmd->HaveServerLock())
			pCmd->ReleaseServerLock();
		delete pCmd;
	}
	MainFrame()->ClearStatus();
	return 0;
}

LRESULT CJobListCtrl::OnP4EndSpecEdit( WPARAM wParam, LPARAM lParam )
{
	CCmd_EditSpec *pCmd= (CCmd_EditSpec *) wParam;
	int index;

	if (lParam != IDCANCEL && lParam != IDABORT && lParam != IDRETRY)
	{
		m_pNewSpec->SetJobName(pCmd->GetNewJobName());

		if(m_NewJob)
		{
			LPCTSTR statustext = m_pNewSpec->GetStatusText();
			if (!*statustext)
				m_pNewSpec->SetJobStatus(JOB_OPEN);
		}

		m_pNewSpec->ConvertToColumns(m_ColCodes, m_ColNames, m_FieldNames);
		if(m_NewJob & (FindInList(pCmd->GetNewJobName()) == -1) )
		{
			InsertJob(m_pNewSpec, GetItemCount());
			ReSort();
			index= FindInList(pCmd->GetNewJobName());
		}
		else
		{
			index= FindInList(pCmd->GetNewJobName());
			if (index != -1)
				UpdateJob(m_pNewSpec, index);
			else if (m_ColNames.GetSize())	// Has job pane been initialized?
			{
				InsertJob(m_pNewSpec, GetItemCount());
				ReSort();
				index= FindInList(pCmd->GetNewJobName());
			}
			else
				delete m_pNewSpec;
		}
		EnsureVisible( index, TRUE );
		SetItemState( index, LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED );
	}
	else
		delete m_pNewSpec;

	if (lParam != IDABORT)
	{
		MainFrame()->ClearStatus();
		if (pCmd->HaveServerLock())
			pCmd->ReleaseServerLock();
		CDialog *dlg = (CDialog *)pCmd->GetSpecSheet();
		dlg->DestroyWindow();
	}
	delete pCmd;
	m_EditInProgress = FALSE;
	return 0;
}

//////////////////////////////////////////////////////////////////////////
// Sort callback, not in class


int CJobListCtrl::OnCompareItems(LPARAM lParam1, LPARAM lParam2, int subItem)
{
    ASSERT(lParam1 && lParam2);
	CString txt1= ((CP4Job const *)lParam1)->GetJobField(subItem);
	CString txt2= ((CP4Job const *)lParam2)->GetJobField(subItem);

	int rc;

	if(m_SortAscending)
		rc = txt1.Compare(txt2);
	else
		rc = txt2.Compare(txt1);

	return rc;
}

/*
	_________________________________________________________________
*/

void CJobListCtrl::OnDeleteitem(NMHDR* pNMHDR, LRESULT* pResult) 
{
	NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
	
	delete (CP4Job *) GetItemData(pNMListView->iItem);
			
	*pResult = 0;
}


/*
	_________________________________________________________________
*/

void CJobListCtrl::OnUpdateJobSetFilter(CCmdUI* pCmdUI) 
{
	pCmdUI->SetText ( LoadStringResource( IDS_FILTER ) );
	pCmdUI->Enable(MainFrame()->SetMenuIcon(pCmdUI, GET_SERVERLEVEL() > 3 && !SERVER_BUSY()));
}

LRESULT CJobListCtrl::OnJobFilter2(WPARAM, LPARAM) 
{
	OnJobSetFilter();
	return 0;
}

void CJobListCtrl::OnJobSetFilter() 
{
	RECT	rect;

	CJobFilter dlg;
	dlg.SetFilterString ( m_sFilter );
	GetWindowRect(&rect);										// we want to position filter dialog box
	dlg.m_top  = rect.top + GetSystemMetrics(SM_CYCAPTION);		// at the top left of this pane, so pass
	dlg.m_left = rect.left + 2;									// it our current screen location
	dlg.m_right= rect.right;
	MainFrame()->DoNotAutoPoll();
	dlg.DoModal( );
	MainFrame()->ResumeAutoPoll();
	if ( m_sFilter != dlg.GetFilterString ( ) )
	{
		m_sFilter = dlg.GetFilterString ( );
		PersistentJobFilter( KEY_WRITE );
		OnViewUpdate( );	
	}
}

void CJobListCtrl::OnUpdateJobRemovefilter(CCmdUI* pCmdUI) 
{
	pCmdUI->SetText ( LoadStringResource( IDS_CLEARFILTER ) );
	pCmdUI->Enable(MainFrame()->SetMenuIcon(pCmdUI, GET_SERVERLEVEL() > 3 
		&& !SERVER_BUSY() && !m_sFilter.IsEmpty()));
}

void CJobListCtrl::OnJobRemovefilter() 
{
	m_sFilter.Empty ( );
	PersistentJobFilter( KEY_WRITE );
	OnViewUpdate( );
}


void CJobListCtrl::PersistentJobFilter( REGSAM accessmask )
{
	HKEY hKey = NULL;
	CString sKey = _T("Software\\Perforce\\P4Win\\");
	CString sEntry = _T("JobFilter");
	DWORD disposition;

	if ( RegCreateKeyEx( HKEY_CURRENT_USER, sKey,
							0, NULL,
							REG_OPTION_NON_VOLATILE,
							accessmask, NULL,
							&hKey, &disposition ) == ERROR_SUCCESS )
	{
		const DWORD lenFilter = 512;

		if ( accessmask == KEY_WRITE )
		{
			RegSetValueEx( hKey, sEntry, NULL, REG_SZ
				, (LPBYTE)(LPCTSTR) m_sFilter, m_sFilter.GetLength( ) * sizeof(TCHAR) + 1);
		}
		else
		{
			TCHAR buf[ lenFilter ];
			DWORD cbData = sizeof(buf);
			if ( (RegQueryValueEx( hKey, sEntry, NULL, NULL, 
									(LPBYTE)buf, &cbData ) == ERROR_SUCCESS) && cbData )
			{
				if(!cbData)
					cbData = 1;
				buf[cbData-1] = _T('\0');
				m_sFilter = buf;
			}
		}

		RegCloseKey( hKey );
	}
}

void CJobListCtrl::OnUpdateJobConfigure(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable(MainFrame()->SetMenuIcon(pCmdUI, !SERVER_BUSY()));
}

int CJobListCtrl::GetFieldNbr( CString str, const CString &spec )
{
	//  Get the field names from the spec. they are between the word 
    //  "Fields:" and the word "Required" strip the separator.  Don't
    //  crash if the jobspec is not recognized.

    CString errorText;
    CString fields;
    
	int start= spec.Find(_T("Fields:")) + lstrlen(_T("Fields:"));
    int end= spec.Find(_T("Required:"));
    if( start == -1 || end == -1 || end < start || end >= spec.GetLength())
        errorText=LoadStringResource(IDS_UNABLE_TO_FIND_FIELD_DELIMITERS);
    
    if( errorText.IsEmpty() )
    {
	    fields = spec.Mid(start, end - start);
	    start= fields.Find( _T('\t') );
        if( start == -1 )
            errorText=LoadStringResource(IDS_UNABLE_TO_FIND_INITIAL_FIELD_DELIMITER);
    }

    if( errorText.IsEmpty() )
    {
	    fields = fields.Mid( start + 1 );

	    // Pick out the requested field names if present
	    
	    int code;
	    CString field;
    	do  
	    {
		    //		get the field spec (e.g., "101 Job word 32")
		    //		and truncate the fields.
		    //
		    field = fields.Left ( fields.Find( _T('\n') ) + 1 );
		    fields = fields.Right ( fields.GetLength( ) - field.Find ( _T('\n') ) - 1 );

		    //		get the "101" part of the spec in code
		    //		and the "Job" part in field.
		    //
			int i = field.Find( _T(' ') );
			if (i == -1)
				return 0x7FFF;
		    code = _ttoi ( field.Left ( i ) ) ;
		    field = field.Right ( field.GetLength ( ) - field.Find( _T(' ') ) - 1 );
		    field = field.Left ( field.Find ( _T(' ') ) );

			if (str == field)
                return code;
	    }
	    while ( !field.IsEmpty( ) );
    }

    if( !errorText.IsEmpty() )
    {
		// Very unlikely that we will ever report this error, but an error
		// message is always preferable to an inexplicable crash
		AddToStatus(CString(_T("JobView::GetFldNbr()") + errorText), SV_ERROR);
	}
    return 0x7FFF;
}

void CJobListCtrl::OnJobConfigure() 
{
	int rc;
	CJobsConfigure dlg;
	int maxcols = m_ColNames.GetSize();
	if (!maxcols && !m_Need2CallOnJobConfigure)
	{
		m_Need2CallOnJobConfigure = TRUE;
		GetJobSpec( );
		return;
	}
	m_Need2CallOnJobConfigure = FALSE;

	CStringArray colNames;
	colNames.SetSize(maxcols);
    CArray<int, int> colWidths;
	colWidths.SetSize(maxcols);
	int subItem;
	for(subItem=0; subItem < maxcols; subItem++)
	{
		dlg.m_ColNames += m_ColNames.GetAt(subItem) + _T(' ');
		colWidths.SetAt(subItem, GetColumnWidth(subItem));
		colNames.SetAt(subItem, m_ColNames.GetAt(subItem));
	}
	int maxflds = m_SpecNames.GetSize();
	for(subItem=0; subItem < maxflds; subItem++)
		dlg.m_SpecNames += m_SpecNames.GetAt(subItem) + _T(' ');
	if ((rc = dlg.DoModal( )) != IDCANCEL)
	{
		BOOL need2save = FALSE;
		CString newWidths;
		CString newSorts;
		if (rc == IDOK)
		{
			int i, j;
			CString num;
			CString newNames;
			int newSortCols[MAX_SORT_COLUMNS] = {0,0,0,0};
			int tokenctr = 1;
			CTokenString tokstr;
			CString token;
			tokstr.Create(dlg.m_ColNames);
			tokstr.PrepareParse( );
			token=tokstr.GetToken();  
			while(!token.IsEmpty())
			{
				num = _T("50");
				for (i = -1; ++i < maxcols; )
				{
					if (m_ColNames.GetAt(i) == token)
					{
						int k;
						for (k=-1; ++k < maxcols; )
						{
							if (colNames.GetAt(k) == token)
								break;
						}
						if (k < maxcols)
							num.Format(_T("%d"), colWidths.GetAt(k));
						for (j=-1, ++i; ++j < MAX_SORT_COLUMNS; )
						{
							if (abs(m_SortColumns[j]) == i)
							{
								newSortCols[j] = m_SortColumns[j] < 0 ? -tokenctr : tokenctr;
								break;
							}
						}
						break;
					}
				}
			    if(!newWidths.IsEmpty())
				    newWidths += _T(",");
				newWidths += num;
				int n = GetFieldNbr(token, m_Spec);
				if (n < 106)
					token.Format(_T(":%d"), n);
				newNames += token + _T(' ');
				token=tokstr.GetToken();
				tokenctr++;
			}
			SaveColumnNames(newNames, _T("Job List"));
			for (i = -1; ++i < MAX_SORT_COLUMNS; )
			{
				num.Format(_T("%d"), newSortCols[i]);
				if(i)
					newSorts+=_T(",");
				newSorts+=num;
			}
			need2save = TRUE;
		}
		else if (rc == IDIGNORE)
		{
			DeleteColumnNames(_T("Job List"));
			newWidths = _T("90,80,60,90,200");
			newSorts  = _T("-1,0,0,0");
			need2save = TRUE;
		}
		if (need2save)
		{
			// Save the column widths
			CString theKey = sRegKey;
			CRegKeyEx key;
			if(ERROR_SUCCESS == key.Create(HKEY_CURRENT_USER, theKey))
			{
				key.SetValueString(newWidths, sRegValue_ColumnWidths);
	            key.SetValueString(newSorts, sRegValue_SortColumns);
				m_ReadSavedWidths = FALSE;	
			}
		}
		m_DesiredCols.RemoveAll();
		m_DesiredCols.SetSize(0);
		m_FastJobs = GetSavedColumnNames(m_DesiredCols, _T("Job List"));
		m_FieldNames.SetSize(0);
		GetFldNames( m_FieldNames, m_Spec );
		OnViewUpdate( );
	}
}

void CJobListCtrl::OnDescribeJob() 
{
	//		let user type in the job name. if it's blank the user bailed.
	//
	CJobDescribe dlg;
	dlg.m_JobStr = m_Describing;
	if( dlg.DoModal( ) == IDCANCEL )
		return;

	m_Describing = dlg.GetJobStr( ) ;
	if ( m_Describing.IsEmpty( ) )
		return;

	m_Active = GetSelectedItemText();	// so we can see whether to show the Edit button or not

	CCmd_Describe *pCmd= new CCmd_Describe;
	pCmd->Init( m_hWnd, RUN_ASYNC);
	if( pCmd->Run( P4JOB_SPEC, m_Describing) )
		MainFrame()->UpdateStatus(LoadStringResource(IDS_FETCHING_JOB_SPEC));
	else
		delete pCmd;
}

BOOL CJobListCtrl::TryDragDrop( )
{
	// Store the job this is from
	m_DragFromItemName = GetSelectedItemText();

	m_OLESource.DelayRenderData( (unsigned short) m_CF_JOB);

	return m_OLESource.DoDragDrop(DROPEFFECT_COPY, &m_DragSourceRect, NULL)
			== DROPEFFECT_NONE ? FALSE : TRUE;
}

/////////////////////////////////////////////////////////////////////
// OLE drag-drop support, to accept depot files or folders
// or accept user or client names which will
// define a view to be used to filter the submitted
// changes that this window displays.
// Also can drop Jobs to be Fixed.
/////////////////////////////////////////////////////////////////////

DROPEFFECT CJobListCtrl::OnDragEnter(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) 
{
	m_DropEffect=DROPEFFECT_NONE;
	m_DragDataFormat=0;

	// Dont allow a drop if the server is busy, since a drop immediately attempts to
	// invoke a server command
	if(SERVER_BUSY())
		return DROPEFFECT_NONE;
		
	if(pDataObject->IsDataAvailable( (unsigned short) m_CF_DEPOT))
	{
		m_DropEffect=DROPEFFECT_COPY;
		m_DragDataFormat=m_CF_DEPOT;
	}
#ifdef UNICODE
	else if(pDataObject->IsDataAvailable( (unsigned short) CF_UNICODETEXT))
	{
		m_DropEffect=DROPEFFECT_COPY;
		m_DragDataFormat=CF_UNICODETEXT;
	}
#else
	else if(pDataObject->IsDataAvailable( (unsigned short) CF_TEXT))
	{
		m_DropEffect=DROPEFFECT_COPY;
		m_DragDataFormat=CF_TEXT;
	}
#endif
	return m_DropEffect;
}

DROPEFFECT CJobListCtrl::OnDragOver(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) 
{
	// Dont allow a drop if the server is busy, since a drop immediately attempts to
	// invoke a server command
	if(SERVER_BUSY())
		m_DropEffect= DROPEFFECT_NONE;
		
	return m_DropEffect;
}

BOOL CJobListCtrl::OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point) 
{
	CString fname;
	
	if(SERVER_BUSY())
	{
		// OnDragEnter() and OnDragOver() should avoid a drop at 
		// the wrong time!
		ASSERT(0);
		return FALSE;
	}
	
	if(m_DragDataFormat == m_CF_DEPOT)
	{
		ClientToScreen(&point);
		::SendMessage(m_depotWnd, WM_DROPTARGET, JOBVIEW, MAKELPARAM(point.x,point.y));
		return TRUE;
	}
#ifdef UNICODE
	if(m_DragDataFormat == CF_UNICODETEXT)
	{
		HGLOBAL hGlob = pDataObject->GetGlobalData(CF_UNICODETEXT);
#else
	if(m_DragDataFormat == CF_TEXT)
	{
		HGLOBAL hGlob = pDataObject->GetGlobalData(CF_TEXT);
#endif
		LPCTSTR p;

		if ((hGlob != NULL)	&& ((p = (LPCTSTR)::GlobalLock(hGlob)) != NULL))
		{
			CString itemStr = p;
			::GlobalUnlock(hGlob);
			OnEditPaste( itemStr );
		}
		return TRUE;
	}
	// Return false, so depot window doesnt start a file-open operation
	return FALSE;
}

int CJobListCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
	if (CP4ListCtrl::OnCreate(lpCreateStruct) == -1)
		return -1;
	
	m_sFilter.Empty( );
	PersistentJobFilter( KEY_READ );

	return 0;
}


void CJobListCtrl::OnJobSetFileFilter() 
{
    if( !SERVER_BUSY() )
    {
	    ::SendMessage(m_depotWnd, WM_GETSELLIST, (WPARAM) &m_FilterView, 0);
		m_FilterIncIntegs = FALSE;
	    OnViewUpdate();
    }
}

void CJobListCtrl::OnJobSetFileFilterInteg() 
{
    if( !SERVER_BUSY() )
    {
	    ::SendMessage(m_depotWnd, WM_GETSELLIST, (WPARAM) &m_FilterView, 0);
		m_FilterIncIntegs = GET_P4REGPTR()->GetEnableSubChgIntegFilter();
	    OnViewUpdate();
    }
}

void CJobListCtrl::OnUpdateJobSetFileFilter(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable( !SERVER_BUSY() 
			&& (pCmdUI->m_nID != ID_JOB_SETFILEFILTERINTEG 
			 || GET_P4REGPTR()->GetEnableSubChgIntegFilter())
			&& ::SendMessage(m_depotWnd, WM_GETSELCOUNT, 0, 0) > 0 );
}

void CJobListCtrl::OnUpdateJobRemoveFileFilter(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable(!SERVER_BUSY() && !m_FilterView.IsEmpty());
}

void CJobListCtrl::OnJobRemoveFileFilter() 
{
	m_FilterView.RemoveAll(); 
	m_FilterIncIntegs = FALSE;
	OnViewUpdate( );
}

void CJobListCtrl::OnUpdateSetFilterJobs(CCmdUI* pCmdUI)
{
	pCmdUI->Enable( !SERVER_BUSY() && 
		// can do expression filtering
		(GET_SERVERLEVEL() > 3 ||	
		// or files selected for filter by files
		 ::SendMessage(m_depotWnd, WM_GETSELCOUNT, 0, 0) > 0 ));
}

void CJobListCtrl::OnUpdateClearFilterJobs(CCmdUI* pCmdUI)
{
	pCmdUI->Enable(!SERVER_BUSY() && 
		(!m_FilterView.IsEmpty() || !m_sFilter.IsEmpty()));
}

void CJobListCtrl::OnPerforceOptions()
{
	MainFrame()->OnPerforceOptions(TRUE, FALSE, IDS_PAGE_JOB);
}
# Change User Description Committed
#1 19924 YourUncleBob Populate -o //guest/perforce_software/p4win/...
//guest/YourUncleBob/p4win/.....
//guest/perforce_software/p4win/main/gui/JobListCtrl.cpp
#1 16169 perforce_software Move files to follow new path scheme for branches.
//guest/perforce_software/p4win/gui/JobListCtrl.cpp
#1 8562 Matt Attaway These feet never stop running.

Initial commit of the P4Win source code.  To the best of our knowledge this
compiles and runs using the 2013.3 P4 API and VS 2010. Expect a few changes
as we refine the build process. Please post any build issues to the forums.