(iOS) Major refactoring and simplification of UI logic

This commit is contained in:
meancoot 2013-03-05 00:14:26 -05:00
parent 30209b3ca9
commit 735bbdd013
10 changed files with 228 additions and 309 deletions

View File

@ -16,102 +16,157 @@
#include "general.h"
#include "rarch_wrapper.h"
static float screen_scale;
static int frame_skips = 4;
static bool is_syncing = true;
static const float ALMOST_INVISIBLE = .021f;
static float g_screen_scale;
static int g_frame_skips = 4;
static bool g_is_syncing = true;
static RAGameView* g_instance;
@implementation RAGameView
{
EAGLContext* _glContext;
UIView* _pauseView;
UIView* _pauseIndicatorView;
}
+ (RAGameView*)get
{
if (!g_instance)
g_instance = [RAGameView new];
return g_instance;
}
- (id)init
{
self = [super init];
screen_scale = [[UIScreen mainScreen] scale];
_glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:_glContext];
UINib* xib = [UINib nibWithNibName:@"PauseView" bundle:nil];
_pauseView = [[xib instantiateWithOwner:[RetroArch_iOS get] options:nil] lastObject];
xib = [UINib nibWithNibName:@"PauseIndicatorView" bundle:nil];
_pauseIndicatorView = [[xib instantiateWithOwner:[RetroArch_iOS get] options:nil] lastObject];
self.view = [GLKView new];
((GLKView*)self.view).context = _glContext;
self.view.multipleTouchEnabled = YES;
[self.view addSubview:_pauseView];
[self.view addSubview:_pauseIndicatorView];
return self;
}
- (void)needsToDie
// Driver
- (void)driverInit
{
g_screen_scale = [[UIScreen mainScreen] scale];
_glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:_glContext];
((GLKView*)self.view).context = _glContext;
// Show pause button for a few seconds, so people know it's there
_pauseIndicatorView.alpha = 1.0f;
[self performSelector:@selector(hidePauseButton) withObject:self afterDelay:3.0f];
}
- (void)driverQuit
{
glFinish();
GLKView* glview = (GLKView*)self.view;
glview.context = nil;
_glContext = nil;
((GLKView*)self.view).context = nil;
[EAGLContext setCurrentContext:nil];
_glContext = nil;
}
- (void)flip
{
if (--g_frame_skips < 0)
{
[self.view setNeedsDisplay];
[(GLKView*)self.view bindDrawable];
g_frame_skips = g_is_syncing ? 0 : 3;
}
}
// Pause Menus
- (void)viewWillLayoutSubviews
{
UIInterfaceOrientation orientation = self.interfaceOrientation;
CGRect screenSize = [[UIScreen mainScreen] bounds];
const float width = ((int)orientation < 3) ? CGRectGetWidth(screenSize) : CGRectGetHeight(screenSize);
const float height = ((int)orientation < 3) ? CGRectGetHeight(screenSize) : CGRectGetWidth(screenSize);
float tenpctw = width / 10.0f;
float tenpcth = height / 10.0f;
_pauseView.frame = CGRectMake(width / 2.0f - 150.0f, height / 2.0f - 150.0f, 300.0f, 300.0f);
_pauseIndicatorView.frame = CGRectMake(tenpctw * 4.0f, 0.0f, tenpctw * 2.0f, tenpcth);
}
- (void)openPauseMenu
{
// Setup save state selector
UISegmentedControl* stateSelect = (UISegmentedControl*)[_pauseView viewWithTag:1];
stateSelect.selectedSegmentIndex = (g_extern.state_slot < 10) ? g_extern.state_slot : -1;
//
[UIView animateWithDuration:0.2
animations:^ { _pauseView.alpha = 1.0f; }
completion:^(BOOL finished){}];
}
- (void)closePauseMenu
{
[UIView animateWithDuration:0.2
animations:^ { _pauseView.alpha = 0.0f; }
completion:^(BOOL finished) { }
];
}
- (void)hidePauseButton
{
[UIView animateWithDuration:0.2
animations:^ { _pauseIndicatorView.alpha = ALMOST_INVISIBLE; }
completion:^(BOOL finished) { }
];
}
@end
static RAGameView* gameViewer;
bool ios_init_game_view()
{
if (!gameViewer)
{
gameViewer = [RAGameView new];
[[RetroArch_iOS get] pushViewController:gameViewer isGame:YES];
}
[RAGameView.get driverInit];
return true;
}
void ios_destroy_game_view()
{
if (gameViewer)
{
[gameViewer needsToDie];
[[RetroArch_iOS get] popViewController];
gameViewer = nil;
}
[RAGameView.get driverQuit];
}
void ios_flip_game_view()
{
if (gameViewer)
{
GLKView* gl_view = (GLKView*)gameViewer.view;
if (--frame_skips < 0)
{
[gl_view setNeedsDisplay];
[gl_view bindDrawable];
frame_skips = is_syncing ? 0 : 3;
}
}
[RAGameView.get flip];
}
void ios_set_game_view_sync(bool on)
{
is_syncing = on;
frame_skips = on ? 0 : 3;
g_is_syncing = on;
g_frame_skips = on ? 0 : 3;
}
void ios_get_game_view_size(unsigned *width, unsigned *height)
{
if (gameViewer)
{
GLKView* gl_view = (GLKView*)gameViewer.view;
*width = RAGameView.get.view.bounds.size.width * g_screen_scale;
*width = *width ? *width : 640;
*width = gl_view.bounds.size.width * screen_scale;
*height = gl_view.bounds.size.height * screen_scale;
}
*height = RAGameView.get.view.bounds.size.height * g_screen_scale;
*height = *height ? *height : 480;
}
void ios_bind_game_view_fbo()
{
if (gameViewer)
{
GLKView* gl_view = (GLKView*)gameViewer.view;
[gl_view bindDrawable];
}
[(GLKView*)RAGameView.get.view bindDrawable];
}

View File

@ -15,23 +15,18 @@
#import <UIKit/UIKit.h>
@interface RetroArch_iOS : UIResponder <UIApplicationDelegate>
@interface RetroArch_iOS : UINavigationController<UIApplicationDelegate, UINavigationControllerDelegate>
+ (void)displayErrorMessage:(NSString*)message;
+ (RetroArch_iOS*)get;
- (void)runGame:(NSString*)path;
- (void)pushViewController:(UIViewController*)theView isGame:(BOOL)game;
- (UIViewController*)popViewController;
- (IBAction)showSettings;
- (IBAction)showWiiRemoteConfig;
@property (strong, nonatomic) RAModuleInfo* moduleInfo;
@property (strong, nonatomic) NSString* system_directory;
@property (strong, nonatomic) UIImage* file_icon;
@property (strong, nonatomic) UIImage* folder_icon;
@end

View File

@ -23,75 +23,14 @@
#import "input/BTStack/WiiMoteHelper.h"
#endif
#define ALMOST_INVISIBLE .021f
@interface RANavigator : UINavigationController<UINavigationControllerDelegate>
@end
@implementation RANavigator
{
RetroArch_iOS* _delegate;
}
- (id)initWithAppDelegate:(RetroArch_iOS*)delegate
{
self = [super init];
self.delegate = self;
assert(delegate);
_delegate = delegate;
return self;
}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
return [_delegate popViewController];
}
- (UIViewController*)reallyPopViewControllerAnimated:(BOOL)animated
{
return [super popViewControllerAnimated:animated];
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[_delegate performSelector:@selector(screenDidRotate) withObject:nil afterDelay:.01f];
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
#ifdef WIIMOTE
navigationController.topViewController.navigationItem.rightBarButtonItem = (![WiiMoteHelper isBluetoothRunning]) ? nil :
[[UIBarButtonItem alloc]
initWithTitle:@"Stop Bluetooth"
style:UIBarButtonItemStyleBordered
target:[RetroArch_iOS get]
action:@selector(stopBluetooth)];
#endif
}
@end
@implementation RetroArch_iOS
{
UIWindow* _window;
RANavigator* _navigator;
NSTimer* _gameTimer;
UIView* _pauseView;
UIView* _pauseIndicatorView;
RAGameView* _game;
bool _isGameTop;
bool _isPaused;
bool _isRunning;
// 0 if no RAGameView is in the navigator
// 1 if a RAGameView is the top
// 2+ if there are views pushed ontop of the RAGameView
unsigned _gameAndAbove;
}
+ (void)displayErrorMessage:(NSString*)message
@ -109,178 +48,22 @@
return (RetroArch_iOS*)[[UIApplication sharedApplication] delegate];
}
// UIApplicationDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
// TODO: Relocate this!
self.system_directory = @"/var/mobile/Library/RetroArch/";
mkdir([self.system_directory UTF8String], 0755);
// Load icons
self.file_icon = [UIImage imageNamed:@"ic_file"];
self.folder_icon = [UIImage imageNamed:@"ic_dir"];
// Load pause menu
UINib* xib = [UINib nibWithNibName:@"PauseView" bundle:nil];
_pauseView = [[xib instantiateWithOwner:self options:nil] lastObject];
xib = [UINib nibWithNibName:@"PauseIndicatorView" bundle:nil];
_pauseIndicatorView = [[xib instantiateWithOwner:self options:nil] lastObject];
// Show status bar
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
// Setup window
_navigator = [[RANavigator alloc] initWithAppDelegate:self];
[_navigator pushViewController: [RADirectoryList directoryListOrGridWithPath:nil] animated:YES];
self.delegate = self;
[self pushViewController:[RADirectoryList directoryListOrGridWithPath:nil] animated:YES];
_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
_window.rootViewController = _navigator;
_window.rootViewController = self;
[_window makeKeyAndVisible];
}
#pragma mark VIEW MANAGEMENT
- (void)screenDidRotate
{
UIInterfaceOrientation orientation = _navigator.interfaceOrientation;
CGRect screenSize = [[UIScreen mainScreen] bounds];
const float width = ((int)orientation < 3) ? CGRectGetWidth(screenSize) : CGRectGetHeight(screenSize);
const float height = ((int)orientation < 3) ? CGRectGetHeight(screenSize) : CGRectGetWidth(screenSize);
float tenpctw = width / 10.0f;
float tenpcth = height / 10.0f;
_pauseView.frame = CGRectMake(width / 2.0f - 150.0f, height / 2.0f - 150.0f, 300.0f, 300.0f);
_pauseIndicatorView.frame = CGRectMake(tenpctw * 4.0f, 0.0f, tenpctw * 2.0f, tenpcth);
_pauseIndicatorView.hidden = NO;
[self performSelector:@selector(hidePauseButton) withObject:self afterDelay:3.0f];
}
- (void)hidePauseButton
{
[UIView animateWithDuration:0.2
animations:^ { _pauseIndicatorView.alpha = ALMOST_INVISIBLE; }
completion:^(BOOL finished) { }
];
}
- (void)pushViewController:(UIViewController*)theView isGame:(BOOL)game
{
assert(!game || _gameAndAbove == 0);
_gameAndAbove += (game || _gameAndAbove) ? 1 : 0;
// Update status and navigation bars
[[UIApplication sharedApplication] setStatusBarHidden:game withAnimation:UIStatusBarAnimationNone];
_navigator.navigationBarHidden = game;
//
[_navigator pushViewController:theView animated:!(_gameAndAbove == 1 || _gameAndAbove == 2)];
if (game)
{
_game = (RAGameView*)theView;
_pauseIndicatorView.alpha = 1.0f;
_pauseIndicatorView.hidden = YES;
[theView.view addSubview:_pauseView];
[theView.view addSubview:_pauseIndicatorView];
[self startTimer];
[self performSelector:@selector(screenDidRotate) withObject:nil afterDelay:.01f];
}
}
- (UIViewController*)popViewController
{
const bool poppingFromGame = _gameAndAbove == 1;
const bool poppingToGame = _gameAndAbove == 2;
_gameAndAbove -= (_gameAndAbove) ? 1 : 0;
if (poppingToGame)
[self startTimer];
// Update status and navigation bar
[[UIApplication sharedApplication] setStatusBarHidden:poppingToGame withAnimation:UIStatusBarAnimationNone];
_navigator.navigationBarHidden = poppingToGame;
//
if (poppingFromGame)
{
[_pauseView removeFromSuperview];
[_pauseIndicatorView removeFromSuperview];
}
return [_navigator reallyPopViewControllerAnimated:!poppingToGame && !poppingFromGame];
}
#pragma mark EMULATION
- (void)runGame:(NSString*)path
{
[RASettingsList refreshConfigFile];
const char* const sd = [[RetroArch_iOS get].system_directory UTF8String];
const char* const cf =[[RetroArch_iOS get].moduleInfo.configPath UTF8String];
const char* const libretro = [[RetroArch_iOS get].moduleInfo.path UTF8String];
struct rarch_main_wrap main_wrapper = {[path UTF8String], sd, sd, cf, libretro};
if (rarch_main_init_wrap(&main_wrapper) == 0)
{
_isRunning = true;
rarch_init_msg_queue();
[self startTimer];
}
else
{
_isRunning = false;
[RetroArch_iOS displayErrorMessage:@"Failed to load game."];
}
}
- (void)closeGame
{
if (_isRunning)
{
rarch_main_deinit();
rarch_deinit_msg_queue();
#ifdef PERF_TEST
rarch_perf_log();
#endif
rarch_main_clear_state();
}
[self stopTimer];
_isRunning = false;
}
- (void)iterate
{
if (_isPaused || !_isRunning || _gameAndAbove != 1)
[self stopTimer];
else if (_isRunning && !rarch_main_iterate())
[self closeGame];
}
- (void)startTimer
{
if (!_gameTimer)
_gameTimer = [NSTimer scheduledTimerWithTimeInterval:0.001f target:self selector:@selector(iterate) userInfo:nil repeats:YES];
}
- (void)stopTimer
{
if (_gameTimer)
[_gameTimer invalidate];
_gameTimer = nil;
}
#pragma mark LIFE CYCLE
- (void)applicationDidBecomeActive:(UIApplication*)application
{
[self startTimer];
@ -303,19 +86,105 @@
uninit_drivers();
}
// UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
_isGameTop = [viewController isKindOfClass:[RAGameView class]];
[[UIApplication sharedApplication] setStatusBarHidden:_isGameTop withAnimation:UIStatusBarAnimationNone];
self.navigationBarHidden = _isGameTop;
if (_isGameTop)
[self startTimer];
#ifdef WIIMOTE
navigationController.topViewController.navigationItem.rightBarButtonItem = (![WiiMoteHelper isBluetoothRunning]) ? nil :
[[UIBarButtonItem alloc]
initWithTitle:@"Stop Bluetooth"
style:UIBarButtonItemStyleBordered
target:[RetroArch_iOS get]
action:@selector(stopBluetooth)];
#endif
}
// UINavigationController: Never animate when pushing onto, or popping, an RAGameView
- (void)pushViewController:(UIViewController*)theView animated:(BOOL)animated
{
[super pushViewController:theView animated:animated && !_isGameTop];
}
- (UIViewController*)popViewControllerAnimated:(BOOL)animated
{
return [super popViewControllerAnimated:animated && !_isGameTop];
}
#pragma mark EMULATION
- (void)runGame:(NSString*)path
{
[RASettingsList refreshConfigFile];
const char* const sd = [[RetroArch_iOS get].system_directory UTF8String];
const char* const cf =[[RetroArch_iOS get].moduleInfo.configPath UTF8String];
const char* const libretro = [[RetroArch_iOS get].moduleInfo.path UTF8String];
struct rarch_main_wrap main_wrapper = {[path UTF8String], sd, sd, cf, libretro};
if (rarch_main_init_wrap(&main_wrapper) == 0)
{
rarch_init_msg_queue();
[self pushViewController:RAGameView.get animated:NO];
_isRunning = true;
}
else
{
_isRunning = false;
[RetroArch_iOS displayErrorMessage:@"Failed to load game."];
}
}
- (void)closeGame
{
if (_isRunning)
{
_isRunning = false;
rarch_main_deinit();
rarch_deinit_msg_queue();
rarch_main_clear_state();
[self popToViewController:[RAGameView get] animated:NO];
[self popViewControllerAnimated:NO];
}
}
- (void)iterate
{
if (_isPaused || !_isRunning || !_isGameTop)
[self stopTimer];
else if (_isRunning && !rarch_main_iterate())
[self closeGame];
}
- (void)startTimer
{
if (!_gameTimer)
_gameTimer = [NSTimer scheduledTimerWithTimeInterval:0.001f target:self selector:@selector(iterate) userInfo:nil repeats:YES];
}
- (void)stopTimer
{
if (_gameTimer)
[_gameTimer invalidate];
_gameTimer = nil;
}
#pragma mark PAUSE MENU
- (IBAction)showPauseMenu:(id)sender
{
if (_isRunning && !_isPaused && _gameAndAbove == 1)
if (_isRunning && !_isPaused && _isGameTop)
{
_isPaused = true;
UISegmentedControl* stateSelect = (UISegmentedControl*)[_pauseView viewWithTag:1];
stateSelect.selectedSegmentIndex = (g_extern.state_slot < 10) ? g_extern.state_slot : -1;
[UIView animateWithDuration:0.2
animations:^ { _pauseView.alpha = 1.0f; }
completion:^(BOOL finished){}];
[[RAGameView get] openPauseMenu];
}
}
@ -344,15 +213,13 @@
- (IBAction)closePauseMenu:(id)sender
{
[[RAGameView get] closePauseMenu];
if (_isPaused)
[UIView animateWithDuration:0.2
animations:^ { _pauseView.alpha = 0.0f; }
completion:^(BOOL finished)
{
_isPaused = false;
[self startTimer];
}
];
{
_isPaused = false;
[self startTimer];
}
}
- (IBAction)closeGamePressed:(id)sender
@ -363,7 +230,7 @@
- (IBAction)showSettings
{
[self pushViewController:[RASettingsList new] isGame:NO];
[self pushViewController:[RASettingsList new] animated:YES];
}
- (IBAction)showWiiRemoteConfig
@ -377,7 +244,7 @@
{
#ifdef WIIMOTE
[WiiMoteHelper stopBluetooth];
[_navigator.topViewController.navigationItem setRightBarButtonItem:nil animated:YES];
[self.topViewController.navigationItem setRightBarButtonItem:nil animated:YES];
#endif
}

View File

@ -58,9 +58,9 @@
RADirectoryItem* path = [_list objectAtIndex: indexPath.row];
if(path.isDirectory)
[[RetroArch_iOS get] pushViewController:[RADirectoryList directoryListOrGridWithPath:path.path] isGame:NO];
[[RetroArch_iOS get] pushViewController:[RADirectoryList directoryListOrGridWithPath:path.path] animated:YES];
else
[[RetroArch_iOS get] pushViewController:[[RAModuleList alloc] initWithGame:path.path] isGame:NO];
[[RetroArch_iOS get] pushViewController:[[RAModuleList alloc] initWithGame:path.path] animated:YES];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
@ -74,7 +74,7 @@
if (!cell.backgroundView)
{
cell.backgroundView = [[UIImageView alloc] initWithImage:[RetroArch_iOS get].folder_icon];
cell.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_dir"]];
((UIImageView*)cell.backgroundView).contentMode = UIViewContentModeScaleAspectFit;
}
}

View File

@ -55,9 +55,9 @@
RADirectoryItem* path = [_list objectAtIndex: indexPath.row];
if(path.isDirectory)
[[RetroArch_iOS get] pushViewController:[RADirectoryList directoryListOrGridWithPath:path.path] isGame:NO];
[[RetroArch_iOS get] pushViewController:[RADirectoryList directoryListOrGridWithPath:path.path] animated:YES];
else
[[RetroArch_iOS get] pushViewController:[[RAModuleList alloc] initWithGame:path.path] isGame:NO];
[[RetroArch_iOS get] pushViewController:[[RAModuleList alloc] initWithGame:path.path] animated:YES];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
@ -73,7 +73,7 @@
cell = (cell != nil) ? cell : [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"path"];
cell.textLabel.text = [path.path lastPathComponent];
cell.accessoryType = (path.isDirectory) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
cell.imageView.image = (path.isDirectory) ? [RetroArch_iOS get].folder_icon : [RetroArch_iOS get].file_icon;
cell.imageView.image = [UIImage imageNamed:(path.isDirectory) ? @"ic_dir" : @"ic_file"];
return cell;
}

View File

@ -107,7 +107,7 @@ static bool btOK;
[[BTstackManager sharedInstance] addListener:discoveryView];
}
[[RetroArch_iOS get] pushViewController:discoveryView isGame:NO];
[[RetroArch_iOS get] pushViewController:discoveryView animated:YES];
}
// BTStackManagerListener

View File

@ -57,7 +57,7 @@
_value.value = (indexPath.section == 1) ? [_value.subValues objectAtIndex:indexPath.row] : @"";
[_view reloadData];
[[RetroArch_iOS get] popViewController];
[[RetroArch_iOS get] popViewControllerAnimated:YES];
}
@end

View File

@ -100,7 +100,7 @@ static RASettingData* custom_action(NSString* action)
[NSArray arrayWithObjects:@"General",
custom_action(@"Module Info"),
#ifdef WIIMOTE
custom_action(@"Connect WiiMotes");
custom_action(@"Connect WiiMotes"),
#endif
nil],
@ -157,7 +157,6 @@ static RASettingData* custom_action(NSString* action)
nil]),
nil],
[NSArray arrayWithObjects:@"Save States",
boolean_setting(config, @"rewind_enable", @"Enable Rewinding", @"false"),
boolean_setting(config, @"block_sram_overwrite", @"Disable SRAM on Load", @"false"),
@ -179,7 +178,7 @@ static RASettingData* custom_action(NSString* action)
- (void)handleCustomAction:(NSString*)action
{
if ([@"Module Info" isEqualToString:action])
[[RetroArch_iOS get] pushViewController:[[RAModuleInfoList alloc] initWithModuleInfo:[RetroArch_iOS get].moduleInfo] isGame:NO];
[[RetroArch_iOS get] pushViewController:[[RAModuleInfoList alloc] initWithModuleInfo:[RetroArch_iOS get].moduleInfo] animated:YES];
else if([@"Connect WiiMotes" isEqualToString:action])
[[RetroArch_iOS get] showWiiRemoteConfig];
}

View File

@ -88,7 +88,7 @@ static const char* const SETTINGID = "SETTING";
{
case EnumerationSetting:
case FileListSetting:
[[RetroArch_iOS get] pushViewController:[[RASettingEnumerationList alloc] initWithSetting:setting fromTable:(UITableView*)self.view] isGame:NO];
[[RetroArch_iOS get] pushViewController:[[RASettingEnumerationList alloc] initWithSetting:setting fromTable:(UITableView*)self.view] animated:YES];
break;
case ButtonSetting:
@ -96,7 +96,7 @@ static const char* const SETTINGID = "SETTING";
break;
case GroupSetting:
[[RetroArch_iOS get] pushViewController:[[RASettingsSubList alloc] initWithSettings:setting.subValues title:setting.label] isGame:NO];
[[RetroArch_iOS get] pushViewController:[[RASettingsSubList alloc] initWithSettings:setting.subValues title:setting.label] animated:YES];
break;
case CustomAction:

View File

@ -19,6 +19,9 @@
#import "RAConfig.h"
@interface RAGameView : UIViewController
+ (RAGameView*)get;
- (void)openPauseMenu;
- (void)closePauseMenu;
@end
@interface RAModuleInfo : NSObject