You've already forked lazarus-ccr
fpspreadsheet: Implement writing and reading of ods formulas. Some issues with date/time formulas.
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3510 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
@ -33,6 +33,9 @@
|
|||||||
- add boolean constants "TRUE" and "FALSE".
|
- add boolean constants "TRUE" and "FALSE".
|
||||||
- add property RPNFormula to interface the parser to RPN formulas of xls files.
|
- add property RPNFormula to interface the parser to RPN formulas of xls files.
|
||||||
- accept funtions with zero parameters
|
- accept funtions with zero parameters
|
||||||
|
- generalize scanner and parser to allow localized decimal and list separators
|
||||||
|
- add to spreadsheet format to parser to take account of formula "dialect"
|
||||||
|
(see OpenDocument using [] around cell addresses)
|
||||||
|
|
||||||
******************************************************************************}
|
******************************************************************************}
|
||||||
|
|
||||||
@ -87,6 +90,8 @@ type
|
|||||||
TsExpressionParser = class;
|
TsExpressionParser = class;
|
||||||
TsBuiltInExpressionManager = class;
|
TsBuiltInExpressionManager = class;
|
||||||
|
|
||||||
|
TsFormulaDialect = (fdExcel, fdOpenDocument);
|
||||||
|
|
||||||
TsResultType = (rtEmpty, rtBoolean, rtInteger, rtFloat, rtDateTime, rtString,
|
TsResultType = (rtEmpty, rtBoolean, rtInteger, rtFloat, rtDateTime, rtString,
|
||||||
rtCell, rtCellRange, rtError, rtAny);
|
rtCell, rtCellRange, rtError, rtAny);
|
||||||
TsResultTypes = set of TsResultType;
|
TsResultTypes = set of TsResultType;
|
||||||
@ -651,6 +656,7 @@ type
|
|||||||
function DoIdentifier: TsTokenType;
|
function DoIdentifier: TsTokenType;
|
||||||
function DoNumber: TsTokenType;
|
function DoNumber: TsTokenType;
|
||||||
function DoDelimiter: TsTokenType;
|
function DoDelimiter: TsTokenType;
|
||||||
|
function DoSquareBracket: TsTokenType;
|
||||||
function DoString: TsTokenType;
|
function DoString: TsTokenType;
|
||||||
function NextPos: Char; // inline;
|
function NextPos: Char; // inline;
|
||||||
procedure SkipWhiteSpace; // inline;
|
procedure SkipWhiteSpace; // inline;
|
||||||
@ -681,6 +687,7 @@ type
|
|||||||
FHashList: TFPHashObjectlist;
|
FHashList: TFPHashObjectlist;
|
||||||
FDirty: Boolean;
|
FDirty: Boolean;
|
||||||
FWorksheet: TsWorksheet;
|
FWorksheet: TsWorksheet;
|
||||||
|
FDialect: TsFormulaDialect;
|
||||||
procedure CheckEOF;
|
procedure CheckEOF;
|
||||||
procedure CheckNodes(var ALeft, ARight: TsExprNode);
|
procedure CheckNodes(var ALeft, ARight: TsExprNode);
|
||||||
function ConvertNode(Todo: TsExprNode; ToType: TsResultType): TsExprNode;
|
function ConvertNode(Todo: TsExprNode; ToType: TsResultType): TsExprNode;
|
||||||
@ -743,6 +750,7 @@ type
|
|||||||
property Identifiers: TsExprIdentifierDefs read FIdentifiers write SetIdentifiers;
|
property Identifiers: TsExprIdentifierDefs read FIdentifiers write SetIdentifiers;
|
||||||
property BuiltIns: TsBuiltInExprCategories read FBuiltIns write SetBuiltIns;
|
property BuiltIns: TsBuiltInExprCategories read FBuiltIns write SetBuiltIns;
|
||||||
property Worksheet: TsWorksheet read FWorksheet;
|
property Worksheet: TsWorksheet read FWorksheet;
|
||||||
|
property Dialect: TsFormulaDialect read FDialect write FDialect;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TsSpreadsheetParser = class(TsExpressionParser)
|
TsSpreadsheetParser = class(TsExpressionParser)
|
||||||
@ -855,6 +863,8 @@ resourcestring
|
|||||||
SErrCommaExpected = 'Expected comma (,) at position %d, but got %s';
|
SErrCommaExpected = 'Expected comma (,) at position %d, but got %s';
|
||||||
SErrInvalidNumberChar = 'Unexpected character in number : %s';
|
SErrInvalidNumberChar = 'Unexpected character in number : %s';
|
||||||
SErrInvalidNumber = 'Invalid numerical value : %s';
|
SErrInvalidNumber = 'Invalid numerical value : %s';
|
||||||
|
SErrInvalidCell = 'No valid cell address specification : %s';
|
||||||
|
SErrInvalidCellRange = 'No valid cell range specification : %s';
|
||||||
SErrNoOperand = 'No operand for unary operation %s';
|
SErrNoOperand = 'No operand for unary operation %s';
|
||||||
SErrNoLeftOperand = 'No left operand for binary operation %s';
|
SErrNoLeftOperand = 'No left operand for binary operation %s';
|
||||||
SErrNoRightOperand = 'No left operand for binary operation %s';
|
SErrNoRightOperand = 'No left operand for binary operation %s';
|
||||||
@ -1039,6 +1049,40 @@ begin
|
|||||||
Result := ttNumber;
|
Result := ttNumber;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ Scans until closing square bracket is reached. In OpenDocument, this is
|
||||||
|
a cell or cell range identifier. }
|
||||||
|
function TsExpressionScanner.DoSquareBracket: TsTokenType;
|
||||||
|
var
|
||||||
|
C: Char;
|
||||||
|
p: Integer;
|
||||||
|
r1,c1,r2,c2: Cardinal;
|
||||||
|
flags: TsRelFlags;
|
||||||
|
begin
|
||||||
|
FToken := '';
|
||||||
|
C := NextPos;
|
||||||
|
while (C <> ']') do
|
||||||
|
begin
|
||||||
|
if C = cNull then
|
||||||
|
ScanError(SErrUnexpectedEndOfExpression);
|
||||||
|
FToken := FToken + C;
|
||||||
|
C := NextPos;
|
||||||
|
end;
|
||||||
|
FToken := Copy(FToken, 2, Length(FToken) - 2); // Delete "[" and "]"
|
||||||
|
p := system.pos('.', FToken); // Delete up tp "." (--> to be considered later!)
|
||||||
|
if p <> 0 then Delete(FToken, 1, p);
|
||||||
|
if system.pos(':', FToken) > 0 then
|
||||||
|
begin
|
||||||
|
if ParseCellRangeString(FToken, r1, c1, r2, c2, flags) then
|
||||||
|
Result := ttCellRange
|
||||||
|
else
|
||||||
|
ScanError(Format(SErrInvalidCellRange, [FToken]));
|
||||||
|
end else
|
||||||
|
if ParseCellString(FToken, r1, c1, flags) then
|
||||||
|
Result := ttCell
|
||||||
|
else
|
||||||
|
ScanError(Format(SErrInvalidCell, [FToken]));
|
||||||
|
end;
|
||||||
|
|
||||||
function TsExpressionScanner.DoString: TsTokenType;
|
function TsExpressionScanner.DoString: TsTokenType;
|
||||||
|
|
||||||
function TerminatingChar(C: Char): boolean;
|
function TerminatingChar(C: Char): boolean;
|
||||||
@ -1055,7 +1099,7 @@ begin
|
|||||||
C := NextPos;
|
C := NextPos;
|
||||||
while not TerminatingChar(C) do
|
while not TerminatingChar(C) do
|
||||||
begin
|
begin
|
||||||
FToken := FToken+C;
|
FToken := FToken + C;
|
||||||
if C = cDoubleQuote then
|
if C = cDoubleQuote then
|
||||||
NextPos;
|
NextPos;
|
||||||
C := NextPos;
|
C := NextPos;
|
||||||
@ -1082,7 +1126,9 @@ begin
|
|||||||
FToken := '';
|
FToken := '';
|
||||||
SkipWhiteSpace;
|
SkipWhiteSpace;
|
||||||
C := FChar^;
|
C := FChar^;
|
||||||
if c = cNull then
|
if (FParser.Dialect = fdOpenDocument) and (C = '[') then
|
||||||
|
Result := DoSquareBracket
|
||||||
|
else if C = cNull then
|
||||||
Result := ttEOF
|
Result := ttEOF
|
||||||
else if IsDelim(C) then
|
else if IsDelim(C) then
|
||||||
Result := DoDelimiter
|
Result := DoDelimiter
|
||||||
@ -1156,6 +1202,7 @@ end;
|
|||||||
constructor TsExpressionParser.Create(AWorksheet: TsWorksheet);
|
constructor TsExpressionParser.Create(AWorksheet: TsWorksheet);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
|
FDialect := fdExcel;
|
||||||
FWorksheet := AWorksheet;
|
FWorksheet := AWorksheet;
|
||||||
FIdentifiers := TsExprIdentifierDefs.Create(TsExprIdentifierDef);
|
FIdentifiers := TsExprIdentifierDefs.Create(TsExprIdentifierDef);
|
||||||
FIdentifiers.FParser := Self;
|
FIdentifiers.FParser := Self;
|
||||||
@ -3723,6 +3770,8 @@ end;
|
|||||||
function TsCellExprNode.AsString: string;
|
function TsCellExprNode.AsString: string;
|
||||||
begin
|
begin
|
||||||
Result := GetCellString(FRow, FCol, FFlags);
|
Result := GetCellString(FRow, FCol, FFlags);
|
||||||
|
if FParser.Dialect = fdOpenDocument then
|
||||||
|
Result := '[.' + Result + ']';
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TsCellExprNode.GetNodeValue(var Result: TsExpressionResult);
|
procedure TsCellExprNode.GetNodeValue(var Result: TsExpressionResult);
|
||||||
|
@ -193,7 +193,7 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
StrUtils, Variants, fpsStreams;
|
StrUtils, Variants, fpsStreams, fpsExprParser;
|
||||||
|
|
||||||
const
|
const
|
||||||
{ OpenDocument general XML constants }
|
{ OpenDocument general XML constants }
|
||||||
@ -685,6 +685,7 @@ begin
|
|||||||
inherited Create(AWorkbook);
|
inherited Create(AWorkbook);
|
||||||
FPointSeparatorSettings := DefaultFormatSettings;
|
FPointSeparatorSettings := DefaultFormatSettings;
|
||||||
FPointSeparatorSettings.DecimalSeparator := '.';
|
FPointSeparatorSettings.DecimalSeparator := '.';
|
||||||
|
FPointSeparatorSettings.ListSeparator := ';'; // for formulas
|
||||||
|
|
||||||
FCellStyleList := TFPList.Create;
|
FCellStyleList := TFPList.Create;
|
||||||
FColumnStyleList := TFPList.Create;
|
FColumnStyleList := TFPList.Create;
|
||||||
@ -1257,10 +1258,12 @@ var
|
|||||||
valueType: String;
|
valueType: String;
|
||||||
valueStr: String;
|
valueStr: String;
|
||||||
node: TDOMNode;
|
node: TDOMNode;
|
||||||
|
parser: TsSpreadsheetParser;
|
||||||
|
p: Integer;
|
||||||
begin
|
begin
|
||||||
|
|
||||||
// Create cell and apply format
|
// Create cell and apply format
|
||||||
if FIsVirtualMode then begin
|
if FIsVirtualMode then
|
||||||
|
begin
|
||||||
InitCell(ARow, ACol, FVirtualCell);
|
InitCell(ARow, ACol, FVirtualCell);
|
||||||
cell := @FVirtualCell;
|
cell := @FVirtualCell;
|
||||||
end else
|
end else
|
||||||
@ -1269,10 +1272,28 @@ begin
|
|||||||
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
styleName := GetAttrValue(ACellNode, 'table:style-name');
|
||||||
ApplyStyleToCell(cell, stylename);
|
ApplyStyleToCell(cell, stylename);
|
||||||
|
|
||||||
// Read formula, store in the cell's FormulaValue.FormulaStr
|
if (boReadFormulas in FWorkbook.Options) then begin
|
||||||
formula := GetAttrValue(ACellNode, 'table:formula');
|
// Read formula, trim it, ...
|
||||||
if formula <> '' then Delete(formula, 1, 3); // delete "of:"
|
formula := GetAttrValue(ACellNode, 'table:formula');
|
||||||
cell^.FormulaValue := formula;
|
if formula <> '' then
|
||||||
|
begin
|
||||||
|
// formulas written by Spread begin with 'of:=', our's with '=' --> remove that
|
||||||
|
p := pos('=', formula);
|
||||||
|
Delete(formula, 1, p);
|
||||||
|
end;
|
||||||
|
// ... convert to Excel dialect used by fps by defailt
|
||||||
|
parser := TsSpreadsheetParser.Create(FWorksheet);
|
||||||
|
try
|
||||||
|
parser.Dialect := fdOpenDocument;
|
||||||
|
parser.Expression := formula;
|
||||||
|
parser.Dialect := fdExcel;
|
||||||
|
formula := parser.Expression;
|
||||||
|
finally
|
||||||
|
parser.Free;
|
||||||
|
end;
|
||||||
|
// ... and store in cell's FormulaValue field.
|
||||||
|
cell^.FormulaValue := formula;
|
||||||
|
end;
|
||||||
|
|
||||||
// Read formula results
|
// Read formula results
|
||||||
// ... number value
|
// ... number value
|
||||||
@ -2685,6 +2706,8 @@ end;
|
|||||||
procedure TsSpreadOpenDocWriter.WriteWorksheet(AStream: TStream;
|
procedure TsSpreadOpenDocWriter.WriteWorksheet(AStream: TStream;
|
||||||
CurSheet: TsWorksheet);
|
CurSheet: TsWorksheet);
|
||||||
begin
|
begin
|
||||||
|
FWorksheet := CurSheet;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
AppendToStream(AStream,
|
AppendToStream(AStream,
|
||||||
'<table:table table:name="' + CurSheet.Name + '" table:style-name="ta1">');
|
'<table:table table:name="' + CurSheet.Name + '" table:style-name="ta1">');
|
||||||
@ -3069,6 +3092,7 @@ begin
|
|||||||
|
|
||||||
FPointSeparatorSettings := SysUtils.DefaultFormatSettings;
|
FPointSeparatorSettings := SysUtils.DefaultFormatSettings;
|
||||||
FPointSeparatorSettings.DecimalSeparator:='.';
|
FPointSeparatorSettings.DecimalSeparator:='.';
|
||||||
|
FPointSeparatorSettings.ListSeparator := ';'; // for formulas
|
||||||
|
|
||||||
// http://en.wikipedia.org/wiki/List_of_spreadsheet_software#Specifications
|
// http://en.wikipedia.org/wiki/List_of_spreadsheet_software#Specifications
|
||||||
FLimitations.MaxColCount := 1024;
|
FLimitations.MaxColCount := 1024;
|
||||||
@ -3576,6 +3600,10 @@ procedure TsSpreadOpenDocWriter.WriteFormula(AStream: TStream; const ARow,
|
|||||||
var
|
var
|
||||||
lStyle: String = '';
|
lStyle: String = '';
|
||||||
lIndex: Integer;
|
lIndex: Integer;
|
||||||
|
parser: TsExpressionParser;
|
||||||
|
formula: String;
|
||||||
|
valuetype: String;
|
||||||
|
value: string;
|
||||||
begin
|
begin
|
||||||
Unused(AStream, ARow, ACol);
|
Unused(AStream, ARow, ACol);
|
||||||
|
|
||||||
@ -3585,15 +3613,65 @@ begin
|
|||||||
end else
|
end else
|
||||||
lStyle := '';
|
lStyle := '';
|
||||||
|
|
||||||
|
// Convert string formula to the format needed by ods: semicolon list separators!
|
||||||
|
parser := TsSpreadsheetParser.Create(FWorksheet);
|
||||||
|
try
|
||||||
|
parser.Dialect := fdOpenDocument;
|
||||||
|
parser.Expression := ACell^.FormulaValue;
|
||||||
|
formula := Parser.LocalizedExpression[FPointSeparatorSettings];
|
||||||
|
finally
|
||||||
|
parser.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
case ACell^.ContentType of
|
||||||
|
cctNumber:
|
||||||
|
begin
|
||||||
|
valuetype := 'float';
|
||||||
|
value := FormatFloat('%g', ACell^.NumberValue, FPointSeparatorSettings);
|
||||||
|
end;
|
||||||
|
cctDateTime:
|
||||||
|
begin
|
||||||
|
valuetype := 'float';
|
||||||
|
value := FormatFloat('%g', ACell^.DateTimeValue, FPointSeparatorSettings);
|
||||||
|
end;
|
||||||
|
cctUTF8String:
|
||||||
|
begin
|
||||||
|
valuetype := 'string';
|
||||||
|
value := ACell^.UTF8StringValue;
|
||||||
|
end;
|
||||||
|
cctBool:
|
||||||
|
begin
|
||||||
|
valuetype := 'boolean';
|
||||||
|
value := BoolToStr(ACell^.BoolValue, 'true', 'false');
|
||||||
|
end;
|
||||||
|
cctError:
|
||||||
|
begin
|
||||||
|
valuetype := 'error';
|
||||||
|
value := GetErrorValueStr(ACell^.ErrorValue);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ Fix special xml characters }
|
||||||
|
formula := UTF8TextToXMLText(formula);
|
||||||
|
|
||||||
{ We are writing a very rudimentary formula here without result and result
|
{ We are writing a very rudimentary formula here without result and result
|
||||||
data type. Seems to work... }
|
data type. Seems to work... }
|
||||||
AppendToStream(AStream, Format(
|
if ACell^.CalcState=csCalculated then
|
||||||
'<table:table-cell table:formula="%s" %s>' +
|
AppendToStream(AStream, Format(
|
||||||
'</table:table-cell>', [
|
'<table:table-cell table:formula="=%s" office:value-type="%s" office-value="%s" %s>' +
|
||||||
ACell^.FormulaValue, lStyle
|
'<text:p>%s</text:p>'+
|
||||||
]));
|
'</table:table-cell>', [
|
||||||
|
formula, valuetype, value, lStyle,
|
||||||
|
value
|
||||||
|
]))
|
||||||
|
else
|
||||||
|
AppendToStream(AStream, Format(
|
||||||
|
'<table:table-cell table:formula="=%s" %s/>', [
|
||||||
|
formula, lStyle
|
||||||
|
]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
Writes a cell with text content
|
Writes a cell with text content
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ type
|
|||||||
procedure Test_Write_Read_FormulaStrings_BIFF8;
|
procedure Test_Write_Read_FormulaStrings_BIFF8;
|
||||||
{ OOXML Tests }
|
{ OOXML Tests }
|
||||||
procedure Test_Write_Read_FormulaStrings_OOXML;
|
procedure Test_Write_Read_FormulaStrings_OOXML;
|
||||||
|
{ ODS Tests }
|
||||||
|
procedure Test_Write_Read_FormulaStrings_ODS;
|
||||||
|
|
||||||
// Writes out and calculates rpn formulas, read back
|
// Writes out and calculates rpn formulas, read back
|
||||||
{ BIFF2 Tests }
|
{ BIFF2 Tests }
|
||||||
@ -185,6 +187,11 @@ begin
|
|||||||
TestWriteReadFormulaStrings(sfOOXML, true);
|
TestWriteReadFormulaStrings(sfOOXML, true);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSpreadWriteReadFormulaTests.Test_Write_Read_FormulaStrings_ODS;
|
||||||
|
begin
|
||||||
|
TestWriteReadFormulaStrings(sfOpenDocument, true);
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
{ Test calculation of formulas }
|
{ Test calculation of formulas }
|
||||||
|
|
||||||
|
@ -48,10 +48,12 @@
|
|||||||
<Unit1>
|
<Unit1>
|
||||||
<Filename Value="datetests.pas"/>
|
<Filename Value="datetests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="datetests"/>
|
||||||
</Unit1>
|
</Unit1>
|
||||||
<Unit2>
|
<Unit2>
|
||||||
<Filename Value="stringtests.pas"/>
|
<Filename Value="stringtests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="stringtests"/>
|
||||||
</Unit2>
|
</Unit2>
|
||||||
<Unit3>
|
<Unit3>
|
||||||
<Filename Value="numberstests.pas"/>
|
<Filename Value="numberstests.pas"/>
|
||||||
@ -61,7 +63,6 @@
|
|||||||
<Unit4>
|
<Unit4>
|
||||||
<Filename Value="manualtests.pas"/>
|
<Filename Value="manualtests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
<UnitName Value="manualtests"/>
|
|
||||||
</Unit4>
|
</Unit4>
|
||||||
<Unit5>
|
<Unit5>
|
||||||
<Filename Value="testsutility.pas"/>
|
<Filename Value="testsutility.pas"/>
|
||||||
@ -71,6 +72,7 @@
|
|||||||
<Unit6>
|
<Unit6>
|
||||||
<Filename Value="internaltests.pas"/>
|
<Filename Value="internaltests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="internaltests"/>
|
||||||
</Unit6>
|
</Unit6>
|
||||||
<Unit7>
|
<Unit7>
|
||||||
<Filename Value="formattests.pas"/>
|
<Filename Value="formattests.pas"/>
|
||||||
@ -80,6 +82,7 @@
|
|||||||
<Unit8>
|
<Unit8>
|
||||||
<Filename Value="colortests.pas"/>
|
<Filename Value="colortests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="colortests"/>
|
||||||
</Unit8>
|
</Unit8>
|
||||||
<Unit9>
|
<Unit9>
|
||||||
<Filename Value="fonttests.pas"/>
|
<Filename Value="fonttests.pas"/>
|
||||||
@ -96,7 +99,6 @@
|
|||||||
<Unit12>
|
<Unit12>
|
||||||
<Filename Value="rpnformulaunit.pas"/>
|
<Filename Value="rpnformulaunit.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
<UnitName Value="rpnFormulaUnit"/>
|
|
||||||
</Unit12>
|
</Unit12>
|
||||||
<Unit13>
|
<Unit13>
|
||||||
<Filename Value="formulatests.pas"/>
|
<Filename Value="formulatests.pas"/>
|
||||||
@ -110,10 +112,12 @@
|
|||||||
<Unit15>
|
<Unit15>
|
||||||
<Filename Value="errortests.pas"/>
|
<Filename Value="errortests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="errortests"/>
|
||||||
</Unit15>
|
</Unit15>
|
||||||
<Unit16>
|
<Unit16>
|
||||||
<Filename Value="virtualmodetests.pas"/>
|
<Filename Value="virtualmodetests.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
<UnitName Value="virtualmodetests"/>
|
||||||
</Unit16>
|
</Unit16>
|
||||||
</Units>
|
</Units>
|
||||||
</ProjectOptions>
|
</ProjectOptions>
|
||||||
|
Reference in New Issue
Block a user