4 * Construct and manage the controller configuration pane
6 * Copyright (c) 2008 Nathan Keynes.
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
23 #include "maple/maple.h"
24 #include "vmu/vmulist.h"
26 #include <glib/gstrfuncs.h>
28 #define FIRST_SECONDARY_DEVICE MAPLE_PORTS
30 #define FIRST_VMU_TAG 0x1000
31 #define LOAD_VMU_TAG -1
32 #define CREATE_VMU_TAG -2
34 #define KEYBINDING_SIZE 110
36 static void cocoa_config_keysym_hook(void *data, const gchar *keysym);
37 static gboolean cocoa_config_vmulist_hook(vmulist_change_type_t type, int idx, void *data);
39 @interface KeyBindingEditor (Private)
40 - (void)updateKeysym: (const gchar *)sym;
43 @implementation KeyBindingEditor
49 [self setFieldEditor: YES];
50 [self setEditable: FALSE];
55 if( lastValue != nil ) {
61 - (void)setPrimed: (BOOL)primed
63 if( primed != isPrimed ) {
66 lastValue = [[NSString stringWithString: [self string]] retain];
67 [self setString: @"<press key>"];
68 input_set_keysym_hook(cocoa_config_keysym_hook, self);
72 input_set_keysym_hook(NULL,NULL);
76 - (void)resignFirstResponder
79 [self setString: lastValue];
82 [super resignFirstResponder];
84 - (void)fireBindingChanged
86 id delegate = [self delegate];
87 if( delegate != nil && [delegate respondsToSelector:@selector(textDidChange:)] ) {
88 [delegate textDidChange: [NSNotification notificationWithName: NSTextDidChangeNotification object: self]];
92 - (void)updateKeysym: (const gchar *)sym
95 [self setString: [NSString stringWithCString: sym]];
97 [self fireBindingChanged];
100 - (void)updateMousesym: (int)button
102 gchar *keysym = input_keycode_to_keysym( &system_mouse_driver, (button+1) );
103 if( keysym != NULL ) {
104 [self updateKeysym: keysym ];
108 - (void)keyPressed: (int)keycode
110 gchar *keysym = input_keycode_to_keysym(NULL, keycode);
111 if( keysym != NULL ) {
112 [self updateKeysym: keysym];
116 - (void)insertText:(id)string
120 - (void)mouseDown: (NSEvent *)event
123 [self updateMousesym: 0];
125 [self setPrimed: YES];
126 [super mouseDown: event];
129 - (void)rightMouseDown: (NSEvent *)event
132 [self updateMousesym: 1];
135 - (void)otherMouseDown: (NSEvent *)event
138 [self updateMousesym: [event buttonNumber]];
141 - (void)keyDown: (NSEvent *) event
143 NSString *chars = [event characters];
145 if( chars != NULL && [chars length] == 1 && [chars characterAtIndex: 0] == 27 ) {
146 // Escape char = abort change
147 [self setString: lastValue];
148 [self setPrimed: NO];
150 [self keyPressed: ([event keyCode]+1)];
153 if( chars != NULL && [chars length] == 1 ) {
154 int ch = [chars characterAtIndex: 0];
157 [self setString: @""];
158 [self fireBindingChanged];
161 [self setPrimed: YES];
164 [super keyDown: event];
168 [super keyDown: event];
172 - (void)flagsChanged: (NSEvent *) event
175 [self keyPressed: ([event keyCode]+1)];
177 [super flagsChanged: event];
181 static void cocoa_config_keysym_hook(void *data, const gchar *keysym)
183 KeyBindingEditor *editor = (KeyBindingEditor *)data;
184 [editor updateKeysym: keysym];
188 @implementation KeyBindingField
191 /*************************** Key-binding sub-view ***********************/
193 #define MAX_KEY_BINDINGS 32
195 @interface ControllerKeyBindingView : NSView
197 maple_device_t device;
198 NSTextField *field[MAX_KEY_BINDINGS][2];
200 - (id)initWithFrame: (NSRect)frameRect;
201 - (void)setDevice: (maple_device_t)device;
204 @implementation ControllerKeyBindingView
205 - (id)initWithFrame: (NSRect)frameRect
207 if( [super initWithFrame: frameRect] == nil ) {
218 - (void)removeSubviews
220 [[self subviews] makeObjectsPerformSelector: @selector(removeFromSuperview)];
222 - (void)controlTextDidChange: (NSNotification *)notify
224 const gchar *p = NULL;
225 int binding = [[notify object] tag];
226 NSString *val1 = [field[binding][0] stringValue];
227 if( field[binding][1] == NULL ) {
228 p = [val1 UTF8String];
230 NSString *val2 = [field[binding][1] stringValue];
231 char buf[ [val1 length] + [val2 length] + 2 ];
233 if( [val1 length] == 0 ) {
234 if( [val2 length] != 0 ) {
235 p = [val2 UTF8String];
237 } else if( [val2 length] == 0 ) {
238 p = [val1 UTF8String];
240 sprintf( buf, "%s,%s", [val1 UTF8String], [val2 UTF8String] );
244 maple_set_device_config_value( device, binding, p );
245 lxdream_save_config();
247 - (void)setDevice: (maple_device_t)newDevice
250 [self removeSubviews];
251 if( device != NULL && !MAPLE_IS_VMU(device) ) {
252 lxdream_config_entry_t config = maple_get_device_config(device);
253 if( config != NULL ) {
256 for( count=0; config[count].key != NULL; count++ );
258 NSSize size = NSMakeSize(85+KEYBINDING_SIZE*2+TEXT_GAP*4, count*(TEXT_HEIGHT+TEXT_GAP)+TEXT_GAP);
259 [self setFrameSize: size];
260 [self scrollRectToVisible: NSMakeRect(0,0,1,1)];
262 for( i=0; config[i].key != NULL; i++ ) {
264 NSRect frame = NSMakeRect(x, y + 2, 85, LABEL_HEIGHT);
265 NSTextField *label = cocoa_gui_add_label(self, NS_(config[i].label), frame);
266 [label setAlignment: NSRightTextAlignment];
268 switch(config[i].type) {
269 case CONFIG_TYPE_KEY:
270 frame = NSMakeRect( x + 85 + TEXT_GAP, y, KEYBINDING_SIZE, TEXT_HEIGHT);
271 field[i][0] = [[KeyBindingField alloc] initWithFrame: frame];
272 [field[i][0] setAutoresizingMask: (NSViewMinYMargin|NSViewMaxXMargin)];
273 [field[i][0] setTag: i];
274 [field[i][0] setDelegate: self];
275 [self addSubview: field[i][0]];
277 frame = NSMakeRect( x + 85 + KEYBINDING_SIZE + (TEXT_GAP*2), y, KEYBINDING_SIZE, TEXT_HEIGHT);
278 field[i][1] = [[KeyBindingField alloc] initWithFrame: frame];
279 [field[i][1] setAutoresizingMask: (NSViewMinYMargin|NSViewMaxXMargin)];
280 [field[i][1] setTag: i];
281 [field[i][1] setDelegate: self];
282 [self addSubview: field[i][1]];
284 if( config[i].value != NULL ) {
285 gchar **parts = g_strsplit(config[i].value,",",3);
286 if( parts[0] != NULL ) {
287 [field[i][0] setStringValue: [NSString stringWithCString: parts[0]]];
288 if( parts[1] != NULL ) {
289 [field[i][1] setStringValue: [NSString stringWithCString: parts[1]]];
295 case CONFIG_TYPE_FILE:
296 case CONFIG_TYPE_PATH:
297 frame = NSMakeRect( x + 85 + TEXT_GAP, y, KEYBINDING_SIZE*2+TEXT_GAP, TEXT_HEIGHT);
298 field[i][0] = [[NSTextField alloc] initWithFrame: frame];
299 [field[i][0] setAutoresizingMask: (NSViewMinYMargin|NSViewMaxXMargin)];
300 [field[i][0] setTag: i];
301 [field[i][0] setDelegate: self];
302 [self addSubview: field[i][0]];
303 if( config[i].value != NULL ) {
304 [field[i][0] setStringValue: [NSString stringWithCString: config[i].value]];
308 y += (TEXT_HEIGHT + TEXT_GAP);
311 [self setFrameSize: NSMakeSize(100,TEXT_HEIGHT+TEXT_GAP) ];
314 [self setFrameSize: NSMakeSize(100,TEXT_HEIGHT+TEXT_GAP) ];
319 /*************************** Top-level controller pane ***********************/
320 static NSButton *addRadioButton( int port, int sub, int x, int y, id parent )
325 snprintf( buf, sizeof(buf), _("Port %c."), 'A'+port );
327 snprintf( buf, sizeof(buf), _("VMU %d."), sub );
330 NSButton *radio = [[NSButton alloc] initWithFrame: NSMakeRect( x, y, 60, TEXT_HEIGHT )];
331 [radio setTitle: [NSString stringWithUTF8String: buf]];
332 [radio setTag: MAPLE_DEVID(port,sub) ];
333 [radio setButtonType: NSRadioButton];
334 [radio setAlignment: NSRightTextAlignment];
335 [radio setTarget: parent];
336 [radio setAction: @selector(radioChanged:)];
337 [radio setAutoresizingMask: (NSViewMinYMargin|NSViewMaxXMargin)];
338 [parent addSubview: radio];
342 static void setDevicePopupSelection( NSPopUpButton *popup, maple_device_t device )
344 if( device == NULL ) {
345 [popup selectItemAtIndex: 0];
346 } else if( MAPLE_IS_VMU(device) ) {
347 int idx = vmulist_get_index_by_filename( MAPLE_VMU_NAME(device) );
349 [popup selectItemAtIndex: 0];
351 [popup selectItemWithTag: FIRST_VMU_TAG + idx];
354 const struct maple_device_class **devices = maple_get_device_classes();
356 for( i=0; devices[i] != NULL; i++ ) {
357 if( devices[i] == device->device_class ) {
358 [popup selectItemWithTag: i+1];
362 // Should never get here, but if so...
363 [popup selectItemAtIndex: 0];
367 static void buildDevicePopupMenu( NSPopUpButton *popup, maple_device_t device, BOOL primary )
370 const struct maple_device_class **devices = maple_get_device_classes();
372 [popup removeAllItems];
373 [popup addItemWithTitle: NS_("<empty>")];
374 [[popup itemAtIndex: 0] setTag: 0];
375 for( j=0; devices[j] != NULL; j++ ) {
376 int isPrimaryDevice = devices[j]->flags & MAPLE_TYPE_PRIMARY;
377 if( primary ? isPrimaryDevice : (!isPrimaryDevice && !MAPLE_IS_VMU_CLASS(devices[j])) ) {
378 [popup addItemWithTitle: [NSString stringWithUTF8String: devices[j]->name]];
379 if( device != NULL && device->device_class == devices[j] ) {
380 [popup selectItemAtIndex: ([popup numberOfItems]-1)];
382 [[popup itemAtIndex: ([popup numberOfItems]-1)] setTag: (j+1)];
387 BOOL vmu_selected = NO;
388 const char *vmu_name;
389 if( device != NULL && MAPLE_IS_VMU(device) ) {
391 vmu_name = MAPLE_VMU_NAME(device);
393 if( [popup numberOfItems] > 0 ) {
394 [[popup menu] addItem: [NSMenuItem separatorItem]];
397 unsigned int vmu_count = vmulist_get_size();
398 for( j=0; j<vmu_count; j++ ) {
399 const char *name = vmulist_get_name(j);
400 [popup addItemWithTitle: [NSString stringWithUTF8String: name]];
401 if( vmu_selected && strcmp(vmu_name, vmulist_get_filename(j)) == 0 ) {
402 [popup selectItemAtIndex: ([popup numberOfItems]-1)];
404 [[popup itemAtIndex: ([popup numberOfItems]-1)] setTag: FIRST_VMU_TAG + j];
407 [popup addItemWithTitle: NS_("Load VMU...")];
408 [[popup itemAtIndex: ([popup numberOfItems]-1)] setTag: LOAD_VMU_TAG];
409 [popup addItemWithTitle: NS_("Create VMU...")];
410 [[popup itemAtIndex: ([popup numberOfItems]-1)] setTag: CREATE_VMU_TAG];
415 static NSPopUpButton *addDevicePopup( int port, int sub, int x, int y, maple_device_t device, BOOL primary, id parent )
417 NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame: NSMakeRect(x,y,150,TEXT_HEIGHT)
419 [popup setAutoresizingMask: (NSViewMinYMargin|NSViewMaxXMargin)];
420 buildDevicePopupMenu(popup,device,primary);
422 [popup setTarget: parent];
423 [popup setAction: @selector(deviceChanged:)];
424 [popup setTag: MAPLE_DEVID(port,sub) ];
425 [parent addSubview: popup];
429 @interface VMULoadValidator : NSObject
432 - (BOOL)panel:(id) sender isValidFilename: (NSString *)filename;
435 @implementation VMULoadValidator
436 - (BOOL)panel:(id) sender isValidFilename: (NSString *)filename
438 const char *c_fn = [filename UTF8String];
439 vmu_volume_t vol = vmu_volume_load( c_fn );
441 vmulist_add_vmu(c_fn, vol);
444 ERROR( "Unable to load VMU file (not a valid VMU)" );
451 @interface VMUCreateValidator : NSObject
454 - (BOOL)panel:(id) sender isValidFilename: (NSString *)filename;
457 @implementation VMUCreateValidator
458 - (BOOL)panel:(id) sender isValidFilename: (NSString *)filename
460 const char *vmu_filename = [filename UTF8String];
461 int idx = vmulist_create_vmu(vmu_filename, FALSE);
463 ERROR( "Unable to create file: %s\n", strerror(errno) );
472 @interface LxdreamPrefsControllerPane: LxdreamPrefsPane
474 struct maple_device *save_controller[MAPLE_MAX_DEVICES];
475 NSButton *radio[MAPLE_MAX_DEVICES];
476 NSPopUpButton *popup[MAPLE_MAX_DEVICES];
477 ControllerKeyBindingView *key_bindings;
479 + (LxdreamPrefsControllerPane *)new;
480 - (void)vmulistChanged: (id)sender;
483 static gboolean cocoa_config_vmulist_hook(vmulist_change_type_t type, int idx, void *data)
485 LxdreamPrefsControllerPane *pane = (LxdreamPrefsControllerPane *)data;
486 [pane vmulistChanged: nil];
490 @implementation LxdreamPrefsControllerPane
491 + (LxdreamPrefsControllerPane *)new
493 return [[LxdreamPrefsControllerPane alloc] initWithFrame: NSMakeRect(0,0,600,400)];
495 - (id)initWithFrame: (NSRect)frameRect
497 if( [super initWithFrame: frameRect title: NS_("Controllers")] == nil ) {
501 int y = [self contentHeight] - TEXT_HEIGHT - TEXT_GAP;
503 memset( radio, 0, sizeof(radio) );
504 memset( save_controller, 0, sizeof(save_controller) );
505 NSBox *rule = [[NSBox alloc] initWithFrame:
506 NSMakeRect(210+(TEXT_GAP*3), 1, 1, [self contentHeight] + TEXT_GAP - 2)];
507 [rule setAutoresizingMask: (NSViewMaxXMargin|NSViewHeightSizable)];
508 [rule setBoxType: NSBoxSeparator];
509 [self addSubview: rule];
511 NSRect bindingFrame = NSMakeRect(210+(TEXT_GAP*4), 0,
512 frameRect.size.width - (210+(TEXT_GAP*4)), [self contentHeight] + TEXT_GAP );
513 NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame: bindingFrame];
514 key_bindings = [[ControllerKeyBindingView alloc] initWithFrame: bindingFrame ];
515 [scrollView setAutoresizingMask: (NSViewWidthSizable|NSViewHeightSizable)];
516 [scrollView setDocumentView: key_bindings];
517 [scrollView setDrawsBackground: NO];
518 [scrollView setHasVerticalScroller: YES];
519 [scrollView setAutohidesScrollers: YES];
521 [self addSubview: scrollView];
522 [key_bindings setDevice: maple_get_device(0,0)];
524 for( i=0; i<MAPLE_PORTS; i++ ) {
525 maple_device_t device = maple_get_device(i,0);
527 radio[i] = addRadioButton(i,0,TEXT_GAP,y,self);
528 popup[i] = addDevicePopup(i,0,60 + (TEXT_GAP*2),y,device, YES,self);
529 y -= (TEXT_HEIGHT+TEXT_GAP);
531 int j,max = device == NULL ? 0 : MAPLE_SLOTS(device->device_class);
532 for( j=1; j<=MAPLE_USER_SLOTS; j++ ) {
533 radio[MAPLE_DEVID(i,j)] = addRadioButton(i, j, TEXT_GAP*2, y, self);
534 popup[MAPLE_DEVID(i,j)] = addDevicePopup(i, j, 60 + TEXT_GAP*2, y, maple_get_device(i,j), NO, self);
535 y -= (TEXT_HEIGHT+TEXT_GAP);
537 [radio[MAPLE_DEVID(i,j)] setEnabled: NO];
538 [popup[MAPLE_DEVID(i,j)] setEnabled: NO];
543 [radio[0] setState: NSOnState];
545 register_vmulist_change_hook(cocoa_config_vmulist_hook, self);
551 unregister_vmulist_change_hook(cocoa_config_vmulist_hook,self);
554 - (void)vmulistChanged: (id)sender
557 for( i=FIRST_SECONDARY_DEVICE; i<MAPLE_MAX_DEVICES; i++ ) {
558 if( popup[i] != NULL ) {
559 buildDevicePopupMenu(popup[i], maple_get_device(MAPLE_DEVID_PORT(i), MAPLE_DEVID_SLOT(i)), NO );
563 - (void)radioChanged: (id)sender
565 int tag = [sender tag];
567 for( i=0; i<MAPLE_MAX_DEVICES; i++ ) {
568 if( i != tag && radio[i] != NULL ) {
569 [radio[i] setState: NSOffState];
572 [key_bindings setDevice: maple_get_device(MAPLE_DEVID_PORT(tag),MAPLE_DEVID_SLOT(tag))];
574 - (void)deviceChanged: (id)sender
576 int tag = [sender tag];
577 int port = MAPLE_DEVID_PORT(tag);
578 int slot = MAPLE_DEVID_SLOT(tag);
579 int new_device_idx = [[sender selectedItem] tag], i;
580 maple_device_class_t new_device_class = NULL;
581 const gchar *vmu_filename = NULL;
583 for( i=0; i<MAPLE_MAX_DEVICES; i++ ) {
584 if( radio[i] != NULL ) {
586 [radio[i] setState: NSOnState];
588 [radio[i] setState: NSOffState];
593 maple_device_t current = maple_get_device(port,slot);
594 maple_device_t new_device = NULL;
595 if( new_device_idx == LOAD_VMU_TAG ) {
596 NSArray *array = [NSArray arrayWithObjects: @"vmu", nil];
597 NSOpenPanel *panel = [NSOpenPanel openPanel];
598 VMULoadValidator *valid = [[VMULoadValidator alloc] autorelease];
599 [panel setDelegate: valid];
600 int result = [panel runModalForDirectory: [NSString stringWithUTF8String: get_gui_path(CONFIG_VMU_PATH)]
601 file: nil types: array];
602 if( result == NSOKButton ) {
603 vmu_filename = [[panel filename] UTF8String];
604 int idx = vmulist_get_index_by_filename(vmu_filename);
605 [sender selectItemWithTag: (FIRST_VMU_TAG+idx)];
606 new_device_class = &vmu_class;
607 set_gui_path(CONFIG_VMU_PATH, [[panel directory] UTF8String]);
609 /* Cancelled - restore previous value */
610 setDevicePopupSelection( sender, current );
613 } else if( new_device_idx == CREATE_VMU_TAG ) {
614 NSSavePanel *panel = [NSSavePanel savePanel];
615 [panel setTitle: NS_("Create VMU")];
616 [panel setCanCreateDirectories: YES];
617 [panel setRequiredFileType: @"vmu"];
618 VMUCreateValidator *valid = [[VMUCreateValidator alloc] autorelease];
619 [panel setDelegate: valid];
620 int result = [panel runModalForDirectory: [NSString stringWithUTF8String: get_gui_path(CONFIG_VMU_PATH)]
622 if( result == NSFileHandlingPanelOKButton ) {
623 /* Validator has already created the file by now */
624 vmu_filename = [[panel filename] UTF8String];
625 int idx = vmulist_get_index_by_filename(vmu_filename);
626 [sender selectItemWithTag: (FIRST_VMU_TAG+idx)];
627 new_device_class = &vmu_class;
628 set_gui_path(CONFIG_VMU_PATH, [[panel directory] UTF8String]);
630 setDevicePopupSelection( sender, current );
633 } else if( new_device_idx >= FIRST_VMU_TAG ) {
634 vmu_filename = vmulist_get_filename( new_device_idx - FIRST_VMU_TAG );
635 new_device_class = &vmu_class;
636 } else if( new_device_idx > 0) {
637 new_device_class = maple_get_device_classes()[new_device_idx-1];
640 if( current == NULL ? new_device_class == NULL :
641 (current->device_class == new_device_class &&
642 (!MAPLE_IS_VMU(current) || MAPLE_VMU_HAS_NAME(current, vmu_filename))) ) {
644 [key_bindings setDevice: current];
647 if( current != NULL && current->device_class == &controller_class ) {
648 save_controller[tag] = current->clone(current);
650 if( new_device_class == NULL ) {
651 maple_detach_device(port,slot);
653 /* If we detached the top-level dev, any children are automatically detached */
654 for( i=1; i<=MAPLE_USER_SLOTS; i++ ) {
655 [popup[MAPLE_DEVID(port,i)] selectItemWithTag: 0];
659 if( new_device_class == &controller_class && save_controller[tag] != NULL ) {
660 new_device = save_controller[tag];
661 save_controller[tag] = NULL;
663 new_device = maple_new_device( new_device_class->name );
665 if( MAPLE_IS_VMU(new_device) ) {
666 /* Remove the VMU from any other attachment point */
667 for( i=0; i<MAPLE_MAX_DEVICES; i++ ) {
668 maple_device_t dev = maple_get_device(MAPLE_DEVID_PORT(i),MAPLE_DEVID_SLOT(i));
669 if( dev != NULL && MAPLE_IS_VMU(dev) && MAPLE_VMU_HAS_NAME(dev,vmu_filename) ) {
670 maple_detach_device(MAPLE_DEVID_PORT(i),MAPLE_DEVID_SLOT(i));
671 [popup[i] selectItemWithTag: 0];
674 MAPLE_SET_VMU_NAME(new_device,vmu_filename);
676 maple_attach_device(new_device,port,slot);
678 [key_bindings setDevice: maple_get_device(port,slot)];
680 if( slot == 0 ) { /* Change primary */
681 int max = new_device_class == NULL ? 0 : MAPLE_SLOTS(new_device_class);
682 for( i=1; i<=MAPLE_USER_SLOTS; i++ ) {
684 [radio[MAPLE_DEVID(port,i)] setEnabled: YES];
685 [popup[MAPLE_DEVID(port,i)] setEnabled: YES];
687 [radio[MAPLE_DEVID(port,i)] setEnabled: NO];
688 [popup[MAPLE_DEVID(port,i)] setEnabled: NO];
692 lxdream_save_config();
696 NSView *cocoa_gui_create_prefs_controller_pane()
698 return [LxdreamPrefsControllerPane new];
.