From d21447d096a320a08b3efb2b8768fad0dcdcfd64 Mon Sep 17 00:00:00 2001 From: Vincent Sanders Date: Thu, 5 May 2016 22:28:51 +0100 Subject: move frontends into sub directory --- .../cocoa/PSMTabBarControl/PSMTabBarController.m | 643 +++++++++++++++++++++ 1 file changed, 643 insertions(+) create mode 100644 frontends/cocoa/PSMTabBarControl/PSMTabBarController.m (limited to 'frontends/cocoa/PSMTabBarControl/PSMTabBarController.m') diff --git a/frontends/cocoa/PSMTabBarControl/PSMTabBarController.m b/frontends/cocoa/PSMTabBarControl/PSMTabBarController.m new file mode 100644 index 000000000..68e1bc498 --- /dev/null +++ b/frontends/cocoa/PSMTabBarControl/PSMTabBarController.m @@ -0,0 +1,643 @@ +// +// PSMTabBarController.m +// PSMTabBarControl +// +// Created by Kent Sutherland on 11/24/06. +// Copyright 2006 Kent Sutherland. All rights reserved. +// + +#import "PSMTabBarController.h" +#import "PSMTabBarControl.h" +#import "PSMTabBarCell.h" +#import "PSMTabStyle.h" +#import "NSString_AITruncation.h" + +#define MAX_OVERFLOW_MENUITEM_TITLE_LENGTH 60 + +@interface PSMTabBarController (Private) +- (NSArray *)_generateWidthsFromCells:(NSArray *)cells; +- (void)_setupCells:(NSArray *)cells withWidths:(NSArray *)widths; +@end + +@implementation PSMTabBarController + +/*! + @method initWithTabBarControl: + @abstract Creates a new PSMTabBarController instance. + @discussion Creates a new PSMTabBarController for controlling a PSMTabBarControl. Should only be called by + PSMTabBarControl. + @param A PSMTabBarControl. + @returns A newly created PSMTabBarController instance. + */ + +- (id)initWithTabBarControl:(PSMTabBarControl *)control { + if((self = [super init])) { + _control = control; + _cellTrackingRects = [[NSMutableArray alloc] init]; + _closeButtonTrackingRects = [[NSMutableArray alloc] init]; + _cellFrames = [[NSMutableArray alloc] init]; + _addButtonRect = NSZeroRect; + } + return self; +} + +- (void)dealloc { + [_cellTrackingRects release]; + [_closeButtonTrackingRects release]; + [_cellFrames release]; + [super dealloc]; +} + +/*! + @method addButtonRect + @abstract Returns the position for the add tab button. + @discussion Returns the position for the add tab button. + @returns The rect for the add button rect. + */ + +- (NSRect)addButtonRect { + return _addButtonRect; +} + +/*! + @method overflowMenu + @abstract Returns current overflow menu or nil if there is none. + @discussion Returns current overflow menu or nil if there is none. + @returns The current overflow menu. + */ + +- (NSMenu *)overflowMenu { + return _overflowMenu; +} + +/*! + @method cellTrackingRectAtIndex: + @abstract Returns the rect for the tracking rect at the requested index. + @discussion Returns the rect for the tracking rect at the requested index. + @param Index of a cell. + @returns The tracking rect of the cell at the requested index. + */ + +- (NSRect)cellTrackingRectAtIndex:(NSUInteger)index { + NSRect rect; + if(index < [_cellTrackingRects count]) { + rect = [[_cellTrackingRects objectAtIndex:index] rectValue]; + } else { + NSLog(@"cellTrackingRectAtIndex: Invalid index (%ld)", (long)index); + rect = NSZeroRect; + } + return rect; +} + +/*! + @method closeButtonTrackingRectAtIndex: + @abstract Returns the tracking rect for the close button at the requested index. + @discussion Returns the tracking rect for the close button at the requested index. + @param Index of a cell. + @returns The close button tracking rect of the cell at the requested index. + */ + +- (NSRect)closeButtonTrackingRectAtIndex:(NSUInteger)index { + NSRect rect; + if(index < [_closeButtonTrackingRects count]) { + rect = [[_closeButtonTrackingRects objectAtIndex:index] rectValue]; + } else { + NSLog(@"closeButtonTrackingRectAtIndex: Invalid index (%ld)", (long)index); + rect = NSZeroRect; + } + return rect; +} + +/*! + @method cellFrameAtIndex: + @abstract Returns the frame for the cell at the requested index. + @discussion Returns the frame for the cell at the requested index. + @param Index of a cell. + @returns The frame of the cell at the requested index. + */ + +- (NSRect)cellFrameAtIndex:(NSUInteger)index { + NSRect rect; + + if(index < [_cellFrames count]) { + rect = [[_cellFrames objectAtIndex:index] rectValue]; + } else { + NSLog(@"cellFrameAtIndex: Invalid index (%ld)", (long)index); + rect = NSZeroRect; + } + return rect; +} + +/*! + @method setSelectedCell: + @abstract Changes the cell states so the given cell is the currently selected cell. + @discussion Makes the given cell the active cell and properly recalculates the tab states for surrounding cells. + @param An instance of PSMTabBarCell to make active. + */ + +- (void)setSelectedCell:(PSMTabBarCell *)cell { + NSArray *cells = [_control cells]; + NSEnumerator *enumerator = [cells objectEnumerator]; + PSMTabBarCell *lastCell = nil, *nextCell; + + //deselect the previously selected tab + while((nextCell = [enumerator nextObject]) && ([nextCell state] == NSOffState)) { + lastCell = nextCell; + } + + [nextCell setState:NSOffState]; + [nextCell setTabState:PSMTab_PositionMiddleMask]; + + if(lastCell && lastCell != [_control lastVisibleTab]) { + [lastCell setTabState:~[lastCell tabState] & PSMTab_RightIsSelectedMask]; + } + + if((nextCell = [enumerator nextObject])) { + [nextCell setTabState:~[lastCell tabState] & PSMTab_LeftIsSelectedMask]; + } + + [cell setState:NSOnState]; + [cell setTabState:PSMTab_SelectedMask]; + + if(![cell isInOverflowMenu]) { + NSUInteger cellIndex = [cells indexOfObject:cell]; + + if(cellIndex > 0) { + nextCell = [cells objectAtIndex:cellIndex - 1]; + [nextCell setTabState:[nextCell tabState] | PSMTab_RightIsSelectedMask]; + } + + if(cellIndex < [cells count] - 1) { + nextCell = [cells objectAtIndex:cellIndex + 1]; + [nextCell setTabState:[nextCell tabState] | PSMTab_LeftIsSelectedMask]; + } + } +} + +/*! + @method layoutCells + @abstract Recalculates cell positions and states. + @discussion This method calculates the proper frame, tabState and overflow menu status for all cells in the + tab bar control. + */ + +- (void)layoutCells { + NSArray *cells = [_control cells]; + NSInteger cellCount = [cells count]; + + // make sure all of our tabs are accounted for before updating + if([[_control tabView] numberOfTabViewItems] != cellCount) { + return; + } + + [_cellTrackingRects removeAllObjects]; + [_closeButtonTrackingRects removeAllObjects]; + [_cellFrames removeAllObjects]; + + NSArray *cellWidths = [self _generateWidthsFromCells:cells]; + [self _setupCells:cells withWidths:cellWidths]; + + //set up the rect from the add tab button + _addButtonRect = [_control genericCellRect]; + _addButtonRect.size = [[_control addTabButton] frame].size; + if([_control orientation] == PSMTabBarHorizontalOrientation) { + _addButtonRect.origin.y = MARGIN_Y; + _addButtonRect.origin.x += [[cellWidths valueForKeyPath:@"@sum.floatValue"] doubleValue] + 2; + } else { + _addButtonRect.origin.x = 0; + _addButtonRect.origin.y = [[cellWidths lastObject] doubleValue]; + } +} + +/*! + * @method _shrinkWidths:towardMinimum:withAvailableWidth: + * @abstract Decreases widths in an array toward a minimum until they fit within availableWidth, if possible + * @param An array of NSNumbers + * @param The target minimum + * @param The maximum available width + * @returns The amount by which the total array width was shrunk + */ +- (NSInteger)_shrinkWidths:(NSMutableArray *)newWidths towardMinimum:(NSInteger)minimum withAvailableWidth:(CGFloat)availableWidth { + BOOL changed = NO; + NSInteger count = [newWidths count]; + NSInteger totalWidths = [[newWidths valueForKeyPath:@"@sum.intValue"] integerValue]; + NSInteger originalTotalWidths = totalWidths; + + do { + changed = NO; + + for(NSInteger q = (count - 1); q >= 0; q--) { + CGFloat cellWidth = [[newWidths objectAtIndex:q] doubleValue]; + if(cellWidth - 1 >= minimum) { + cellWidth--; + totalWidths--; + + [newWidths replaceObjectAtIndex:q + withObject:[NSNumber numberWithDouble:cellWidth]]; + + changed = YES; + } + } + } while(changed && (totalWidths > availableWidth)); + + return(originalTotalWidths - totalWidths); +} + +/*! + * @function potentialMinimumForArray() + * @abstract Calculate the minimum total for a given array of widths + * @discussion The array is summed using, for each item, the minimum between the current value and the passed minimum value. + * This is useful for getting a sum if the array has size-to-fit widths which will be allowed to be less than the + * specified minimum. + * @param An array of widths + * @param The minimum + * @returns The smallest possible sum for the array + */ +static NSInteger potentialMinimumForArray(NSArray *array, NSInteger minimum){ + NSInteger runningTotal = 0; + NSInteger count = [array count]; + + for(NSInteger i = 0; i < count; i++) { + NSInteger currentValue = [[array objectAtIndex:i] integerValue]; + runningTotal += MIN(currentValue, minimum); + } + + return runningTotal; +} + +/*! + @method _generateWidthsFromCells: + @abstract Calculates the width of cells that would be visible. + @discussion Calculates the width of cells in the tab bar and returns an array of widths for the cells that would be + visible. Uses large blocks of code that were previously in PSMTabBarControl's update method. + @param An array of PSMTabBarCells. + @returns An array of numbers representing the widths of cells that would be visible. + */ + +- (NSArray *)_generateWidthsFromCells:(NSArray *)cells { + NSInteger cellCount = [cells count], i, numberOfVisibleCells = ([_control orientation] == PSMTabBarHorizontalOrientation) ? 1 : 0; + NSMutableArray *newWidths = [NSMutableArray arrayWithCapacity:cellCount]; + id style = [_control style]; + CGFloat availableWidth = [_control availableCellWidth], currentOrigin = 0, totalOccupiedWidth = 0.0, width; + NSRect cellRect = [_control genericCellRect], controlRect = [_control frame]; + PSMTabBarCell *currentCell; + + if([_control orientation] == PSMTabBarVerticalOrientation) { + currentOrigin = [style topMarginForTabBarControl]; + } + + //Don't let cells overlap the add tab button if it is visible + if([_control showAddTabButton]) { + availableWidth -= [self addButtonRect].size.width; + } + + for(i = 0; i < cellCount; i++) { + currentCell = [cells objectAtIndex:i]; + + // supress close button? + [currentCell setCloseButtonSuppressed:((cellCount == 1 && [_control canCloseOnlyTab] == NO) || + [_control disableTabClose] || + ([[_control delegate] respondsToSelector:@selector(tabView:disableTabCloseForTabViewItem:)] && + [[_control delegate] tabView:[_control tabView] disableTabCloseForTabViewItem:[currentCell representedObject]]))]; + + if([_control orientation] == PSMTabBarHorizontalOrientation) { + // Determine cell width + if([_control sizeCellsToFit]) { + width = [currentCell desiredWidthOfCell]; + if(width > [_control cellMaxWidth]) { + width = [_control cellMaxWidth]; + } + } else { + width = [_control cellOptimumWidth]; + } + + width = ceil(width); + + //check to see if there is not enough space to place all tabs as preferred + if(totalOccupiedWidth + width >= availableWidth) { + //There's not enough space to add currentCell at its preferred width! + + //If we're not going to use the overflow menu, cram all the tab cells into the bar regardless of minimum width + if(![_control useOverflowMenu]) { + NSInteger j, averageWidth = (availableWidth / cellCount); + + numberOfVisibleCells = cellCount; + [newWidths removeAllObjects]; + + for(j = 0; j < cellCount; j++) { + CGFloat desiredWidth = [[cells objectAtIndex:j] desiredWidthOfCell]; + [newWidths addObject:[NSNumber numberWithDouble:(desiredWidth < averageWidth && [_control sizeCellsToFit]) ? desiredWidth : averageWidth]]; + } + + totalOccupiedWidth = [[newWidths valueForKeyPath:@"@sum.intValue"] integerValue]; + break; + } + + //We'll be using the overflow menu if needed. + numberOfVisibleCells = i; + if([_control sizeCellsToFit]) { + BOOL remainingCellsMustGoToOverflow = NO; + + totalOccupiedWidth = [[newWidths valueForKeyPath:@"@sum.intValue"] integerValue]; + + /* Can I squeeze it in without violating min cell width? This is the width we would take up + * if every cell so far were at the control minimum size (or their current size if that is less than the control minimum). + */ + if((potentialMinimumForArray(newWidths, [_control cellMinWidth]) + MIN(width, [_control cellMinWidth])) <= availableWidth) { + /* It's definitely possible for cells so far to be visible. + * Shrink other cells to allow this one to fit + */ + NSInteger cellMinWidth = [_control cellMinWidth]; + + /* Start off adding it to the array; we know that it will eventually fit because + * (the potential minimum <= availableWidth) + * + * This allows average and minimum aggregates on the NSArray to work. + */ + [newWidths addObject:[NSNumber numberWithDouble:width]]; + numberOfVisibleCells++; + + totalOccupiedWidth += width; + + //First, try to shrink tabs toward the average. Tabs smaller than average won't change + totalOccupiedWidth -= [self _shrinkWidths:newWidths + towardMinimum:[[newWidths valueForKeyPath:@"@avg.intValue"] integerValue] + withAvailableWidth:availableWidth]; + + + + if(totalOccupiedWidth > availableWidth) { + //Next, shrink tabs toward the smallest of the existing tabs. The smallest tab won't change. + NSInteger smallestTabWidth = [[newWidths valueForKeyPath:@"@min.intValue"] integerValue]; + if(smallestTabWidth > cellMinWidth) { + totalOccupiedWidth -= [self _shrinkWidths:newWidths + towardMinimum:smallestTabWidth + withAvailableWidth:availableWidth]; + } + } + + if(totalOccupiedWidth > availableWidth) { + //Finally, shrink tabs toward the imposed minimum size. All tabs larger than the minimum wll change. + totalOccupiedWidth -= [self _shrinkWidths:newWidths + towardMinimum:cellMinWidth + withAvailableWidth:availableWidth]; + } + + if(totalOccupiedWidth > availableWidth) { + NSLog(@"**** -[PSMTabBarController generateWidthsFromCells:] This is a failure (available %f, total %f, width is %f)", + availableWidth, totalOccupiedWidth, width); + remainingCellsMustGoToOverflow = YES; + } + + if(totalOccupiedWidth < availableWidth) { + /* We're not using all available space not but exceeded available width before; + * stretch all cells to fully fit the bar + */ + NSInteger leftoverWidth = availableWidth - totalOccupiedWidth; + if(leftoverWidth > 0) { + NSInteger q; + for(q = numberOfVisibleCells - 1; q >= 0; q--) { + NSInteger desiredAddition = (NSInteger)leftoverWidth / (q + 1); + NSInteger newCellWidth = (NSInteger)[[newWidths objectAtIndex:q] doubleValue] + desiredAddition; + [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithDouble:newCellWidth]]; + leftoverWidth -= desiredAddition; + totalOccupiedWidth += desiredAddition; + } + } + } + } else { + // stretch - distribute leftover room among cells, since we can't add this cell + NSInteger leftoverWidth = availableWidth - totalOccupiedWidth; + NSInteger q; + for(q = i - 1; q >= 0; q--) { + NSInteger desiredAddition = (NSInteger)leftoverWidth / (q + 1); + NSInteger newCellWidth = (NSInteger)[[newWidths objectAtIndex:q] doubleValue] + desiredAddition; + [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithDouble:newCellWidth]]; + leftoverWidth -= desiredAddition; + } + + remainingCellsMustGoToOverflow = YES; + } + + // done assigning widths; remaining cells go in overflow menu + if(remainingCellsMustGoToOverflow) { + break; + } + } else { + //We're not using size-to-fit + NSInteger revisedWidth = availableWidth / (i + 1); + if(revisedWidth >= [_control cellMinWidth]) { + NSUInteger q; + totalOccupiedWidth = 0; + + for(q = 0; q < [newWidths count]; q++) { + [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithDouble:revisedWidth]]; + totalOccupiedWidth += revisedWidth; + } + // just squeezed this one in... + [newWidths addObject:[NSNumber numberWithDouble:revisedWidth]]; + totalOccupiedWidth += revisedWidth; + numberOfVisibleCells++; + } else { + // couldn't fit that last one... + break; + } + } + } else { + //(totalOccupiedWidth < availableWidth) + numberOfVisibleCells = cellCount; + [newWidths addObject:[NSNumber numberWithDouble:width]]; + totalOccupiedWidth += width; + } + } else { + //lay out vertical tabs + if(currentOrigin + cellRect.size.height <= controlRect.size.height) { + [newWidths addObject:[NSNumber numberWithDouble:currentOrigin]]; + numberOfVisibleCells++; + currentOrigin += cellRect.size.height; + } else { + //out of room, the remaining tabs go into overflow + if([newWidths count] > 0 && controlRect.size.height - currentOrigin < 17) { + [newWidths removeLastObject]; + numberOfVisibleCells--; + } + break; + } + } + } + + //make sure there are at least two items in the horizontal tab bar + if([_control orientation] == PSMTabBarHorizontalOrientation) { + if(numberOfVisibleCells < 2 && [cells count] > 1) { + PSMTabBarCell *cell1 = [cells objectAtIndex:0], *cell2 = [cells objectAtIndex:1]; + NSNumber *cellWidth; + + [newWidths removeAllObjects]; + totalOccupiedWidth = 0; + + cellWidth = [NSNumber numberWithDouble:[cell1 desiredWidthOfCell] < availableWidth * 0.5f ?[cell1 desiredWidthOfCell] : availableWidth * 0.5f]; + [newWidths addObject:cellWidth]; + totalOccupiedWidth += [cellWidth doubleValue]; + + cellWidth = [NSNumber numberWithDouble:[cell2 desiredWidthOfCell] < (availableWidth - totalOccupiedWidth) ?[cell2 desiredWidthOfCell] : (availableWidth - totalOccupiedWidth)]; + [newWidths addObject:cellWidth]; + totalOccupiedWidth += [cellWidth doubleValue]; + + if(totalOccupiedWidth < availableWidth) { + [newWidths replaceObjectAtIndex:0 withObject:[NSNumber numberWithDouble:availableWidth - [cellWidth doubleValue]]]; + } + + numberOfVisibleCells = 2; + } + } + + return newWidths; +} + +/*! + @method _setupCells:withWidths + @abstract Creates tracking rect arrays and sets the frames of the visible cells. + @discussion Creates tracking rect arrays and sets the cells given in the widths array. + */ + +- (void)_setupCells:(NSArray *)cells withWidths:(NSArray *)widths { + NSUInteger i, tabState, cellCount = [cells count]; + NSRect cellRect = [_control genericCellRect]; + PSMTabBarCell *cell; + NSTabViewItem *selectedTabViewItem = [[_control tabView] selectedTabViewItem]; + NSMenuItem *menuItem; + + [_overflowMenu release], _overflowMenu = nil; + + for(i = 0; i < cellCount; i++) { + cell = [cells objectAtIndex:i]; + + if(i < [widths count]) { + tabState = 0; + + // set cell frame + if([_control orientation] == PSMTabBarHorizontalOrientation) { + cellRect.size.width = [[widths objectAtIndex:i] doubleValue]; + } else { + cellRect.size.width = [_control frame].size.width; + cellRect.origin.y = [[widths objectAtIndex:i] doubleValue]; + cellRect.origin.x = 0; + } + + [_cellFrames addObject:[NSValue valueWithRect:cellRect]]; + + //add tracking rects to arrays + [_closeButtonTrackingRects addObject:[NSValue valueWithRect:[cell closeButtonRectForFrame:cellRect]]]; + [_cellTrackingRects addObject:[NSValue valueWithRect:cellRect]]; + + if([[cell representedObject] isEqualTo:selectedTabViewItem]) { + [cell setState:NSOnState]; + tabState |= PSMTab_SelectedMask; + // previous cell + if(i > 0) { + [[cells objectAtIndex:i - 1] setTabState:([(PSMTabBarCell *)[cells objectAtIndex:i - 1] tabState] | PSMTab_RightIsSelectedMask)]; + } + // next cell - see below + } else { + [cell setState:NSOffState]; + // see if prev cell was selected + if((i > 0) && ([[cells objectAtIndex:i - 1] state] == NSOnState)) { + tabState |= PSMTab_LeftIsSelectedMask; + } + } + + // more tab states + if([widths count] == 1) { + tabState |= PSMTab_PositionLeftMask | PSMTab_PositionRightMask | PSMTab_PositionSingleMask; + } else if(i == 0) { + tabState |= PSMTab_PositionLeftMask; + } else if(i == [widths count] - 1) { + tabState |= PSMTab_PositionRightMask; + } + + [cell setTabState:tabState]; + [cell setIsInOverflowMenu:NO]; + + // indicator + if(![[cell indicator] isHidden] && ![_control isTabBarHidden]) { + if(![[_control subviews] containsObject:[cell indicator]]) { + [_control addSubview:[cell indicator]]; + [[cell indicator] startAnimation:self]; + } + } + + // next... + cellRect.origin.x += [[widths objectAtIndex:i] doubleValue]; + } else { + [cell setState:NSOffState]; + [cell setIsInOverflowMenu:YES]; + [[cell indicator] removeFromSuperview]; + + //position the cell well offscreen + if([_control orientation] == PSMTabBarHorizontalOrientation) { + cellRect.origin.x += [[_control style] rightMarginForTabBarControl] + 20; + } else { + cellRect.origin.y = [_control frame].size.height + 2; + } + + [_cellFrames addObject:[NSValue valueWithRect:cellRect]]; + + if(_overflowMenu == nil) { + _overflowMenu = [[NSMenu alloc] init]; + [_overflowMenu insertItemWithTitle:@"" action:nil keyEquivalent:@"" atIndex:0]; // Because the overflowPupUpButton is a pull down menu + [_overflowMenu setDelegate:self]; + } + + // Each item's title is limited to 60 characters. If more than 60 characters, use an ellipsis to indicate that more exists. + menuItem = [_overflowMenu addItemWithTitle:[[[cell attributedStringValue] string] stringWithEllipsisByTruncatingToLength:MAX_OVERFLOW_MENUITEM_TITLE_LENGTH] + action:@selector(overflowMenuAction:) + keyEquivalent:@""]; + [menuItem setTarget:_control]; + [menuItem setRepresentedObject:[cell representedObject]]; + + if([cell count] > 0) { + [menuItem setTitle:[[menuItem title] stringByAppendingFormat:@" (%lu)", (unsigned long)[cell count]]]; + } + } + } +} + +- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)menuItem atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel { + if(menu == _overflowMenu) { + if([[[menuItem representedObject] identifier] respondsToSelector:@selector(icon)]) { + [menuItem setImage:[[[menuItem representedObject] identifier] valueForKey:@"icon"]]; + } + } + + return TRUE; +} + +- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu { + if(menu == _overflowMenu) { + return [_overflowMenu numberOfItems]; + } else { + NSLog(@"Warning: Unexpected menu delegate call for menu %@", menu); + return 0; + } +} + +@end + +/* + PSMTabBarController will store what the current tab frame state should be like based off the last layout. PSMTabBarControl + has to handle fetching the new frame and then changing the tab cell frame. + Tab states will probably be changed immediately. + + Tabs that aren't going to be visible need to have their frame set offscreen. Treat them as if they were visible. + + The overflow menu is rebuilt and stored by the controller. + + Arrays of tracking rects will be created here, but not applied. + Tracking rects are removed and added by PSMTabBarControl at the end of an animate/display cycle. + + The add tab button frame is handled by this controller. Visibility and location are set by the control. + + isInOverflowMenu should probably be removed in favor of a call that returns yes/no to if a cell is in overflow. (Not yet implemented) + + Still need to rewrite most of the code in PSMTabDragAssistant. + */ -- cgit v1.2.3