1/*
2 * Copyright 2006-2010, Axel Dörfler, axeld@pinc-software.de.
3 * Distributed under the terms of the MIT License.
4 */
5
6
7#include "AttributeWindow.h"
8#include "FileTypes.h"
9#include "FileTypesWindow.h"
10
11#include <Box.h>
12#include <Button.h>
13#include <Catalog.h>
14#include <CheckBox.h>
15#include <ControlLook.h>
16#include <LayoutBuilder.h>
17#include <Locale.h>
18#include <MenuField.h>
19#include <MenuItem.h>
20#include <Mime.h>
21#include <PopUpMenu.h>
22#include <SpaceLayoutItem.h>
23#include <String.h>
24#include <TextControl.h>
25
26#include <ctype.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <strings.h>
30
31
32#undef B_TRANSLATION_CONTEXT
33#define B_TRANSLATION_CONTEXT "Attribute Window"
34
35
36const uint32 kMsgAttributeUpdated = 'atup';
37const uint32 kMsgTypeChosen = 'typc';
38const uint32 kMsgDisplayAsChosen = 'dach';
39const uint32 kMsgVisibilityChanged = 'vsch';
40const uint32 kMsgAlignmentChosen = 'alnc';
41const uint32 kMsgAccept = 'acpt';
42
43
44static bool
45compare_display_as(const char* a, const char* b)
46{
47	bool emptyA = a == NULL || !a[0];
48	bool emptyB = b == NULL || !b[0];
49
50	if (emptyA && emptyB)
51		return true;
52	if (emptyA || emptyB)
53		return false;
54
55	const char* end = strchr(a, ':');
56	int32 lengthA = end ? end - a : strlen(a);
57	end = strchr(b, ':');
58	int32 lengthB = end ? end - b : strlen(b);
59
60	if (lengthA != lengthB)
61		return false;
62
63	return !strncasecmp(a, b, lengthA);
64}
65
66
67static const char*
68display_as_parameter(const char* special)
69{
70	const char* parameter = strchr(special, ':');
71	if (parameter != NULL)
72		return parameter + 1;
73
74	return NULL;
75}
76
77
78//	#pragma mark -
79
80
81AttributeWindow::AttributeWindow(FileTypesWindow* target, BMimeType& mimeType,
82		AttributeItem* attributeItem)
83	:
84	BWindow(BRect(100, 100, 350, 200), B_TRANSLATE("Attribute"),
85		B_MODAL_WINDOW_LOOK, B_MODAL_SUBSET_WINDOW_FEEL,
86		B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS | B_ASYNCHRONOUS_CONTROLS),
87	fTarget(target),
88	fMimeType(mimeType.Type())
89{
90	float padding = be_control_look->DefaultItemSpacing();
91
92	if (attributeItem != NULL)
93		fAttribute = *attributeItem;
94
95	fPublicNameControl = new BTextControl(B_TRANSLATE("Attribute name:"),
96		fAttribute.PublicName(), NULL);
97	fPublicNameControl->SetModificationMessage(
98		new BMessage(kMsgAttributeUpdated));
99	fPublicNameControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
100
101	fAttributeControl = new BTextControl(B_TRANSLATE("Internal name:"),
102		fAttribute.Name(), NULL);
103	fAttributeControl->SetModificationMessage(
104		new BMessage(kMsgAttributeUpdated));
105	fAttributeControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
106
107	// filter out invalid characters that can't be part of an attribute
108	BTextView* textView = fAttributeControl->TextView();
109	const char* disallowedCharacters = "/";
110	for (int32 i = 0; disallowedCharacters[i]; i++) {
111		textView->DisallowChar(disallowedCharacters[i]);
112	}
113
114	fTypeMenu = new BPopUpMenu("type");
115	BMenuItem* item = NULL;
116	for (int32 i = 0; kTypeMap[i].name != NULL; i++) {
117		BMessage* message = new BMessage(kMsgTypeChosen);
118		message->AddInt32("type", kTypeMap[i].type);
119
120		item = new BMenuItem(kTypeMap[i].name, message);
121		fTypeMenu->AddItem(item);
122
123		if (kTypeMap[i].type == fAttribute.Type())
124			item->SetMarked(true);
125	}
126
127	BMenuField* typeMenuField = new BMenuField("types" , B_TRANSLATE("Type:"),
128		fTypeMenu);
129	typeMenuField->SetAlignment(B_ALIGN_RIGHT);
130	// we must set the color manually when adding a menuField directly
131	// into a window.
132	typeMenuField->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
133	typeMenuField->SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));
134
135	fVisibleCheckBox = new BCheckBox("visible", B_TRANSLATE("Visible"),
136		new BMessage(kMsgVisibilityChanged));
137	fVisibleCheckBox->SetValue(fAttribute.Visible());
138
139	BMenu* menu = new BPopUpMenu("display as");
140	for (int32 i = 0; kDisplayAsMap[i].name != NULL; i++) {
141		BMessage* message = new BMessage(kMsgDisplayAsChosen);
142		if (kDisplayAsMap[i].identifier != NULL) {
143			message->AddString("identifier", kDisplayAsMap[i].identifier);
144			for (int32 j = 0; kDisplayAsMap[i].supported[j]; j++) {
145				message->AddInt32("supports", kDisplayAsMap[i].supported[j]);
146			}
147		}
148
149		item = new BMenuItem(kDisplayAsMap[i].name, message);
150		menu->AddItem(item);
151
152		if (compare_display_as(kDisplayAsMap[i].identifier,
153				fAttribute.DisplayAs()))
154			item->SetMarked(true);
155	}
156
157	fDisplayAsMenuField = new BMenuField("display as",
158		B_TRANSLATE_COMMENT("Display as:",
159			"Tracker offers different display modes for attributes."), menu);
160	fDisplayAsMenuField->SetAlignment(B_ALIGN_RIGHT);
161
162	fEditableCheckBox = new BCheckBox("editable",
163		B_TRANSLATE_COMMENT("Editable",
164			"If Tracker allows to edit this attribute."),
165		new BMessage(kMsgAttributeUpdated));
166	fEditableCheckBox->SetValue(fAttribute.Editable());
167
168	fSpecialControl = new BTextControl(B_TRANSLATE("Special:"),
169		display_as_parameter(fAttribute.DisplayAs()), NULL);
170	fSpecialControl->SetModificationMessage(
171		new BMessage(kMsgAttributeUpdated));
172	fSpecialControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
173	fSpecialControl->SetEnabled(false);
174
175	char text[64];
176	snprintf(text, sizeof(text), "%" B_PRId32, fAttribute.Width());
177	fWidthControl = new BTextControl(B_TRANSLATE_COMMENT("Width:",
178		"Default column width in Tracker for this attribute."),
179		text, NULL);
180	fWidthControl->SetModificationMessage(
181		new BMessage(kMsgAttributeUpdated));
182	fWidthControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
183
184	// filter out invalid characters that can't be part of a width
185	textView = fWidthControl->TextView();
186	for (int32 i = 0; i < 256; i++) {
187		if (!isdigit(i))
188			textView->DisallowChar(i);
189	}
190	textView->SetMaxBytes(4);
191
192	const struct alignment_map {
193		int32		alignment;
194		const char*	name;
195	} kAlignmentMap[] = {
196		{B_ALIGN_LEFT, B_TRANSLATE_COMMENT("Left",
197			"Attribute column alignment in Tracker")},
198		{B_ALIGN_RIGHT, B_TRANSLATE_COMMENT("Right",
199			"Attribute column alignment in Tracker")},
200		{B_ALIGN_CENTER, B_TRANSLATE_COMMENT("Center",
201			"Attribute column alignment in Tracker")},
202		{0, NULL}
203	};
204
205	menu = new BPopUpMenu("alignment");
206	for (int32 i = 0; kAlignmentMap[i].name != NULL; i++) {
207		BMessage* message = new BMessage(kMsgAlignmentChosen);
208		message->AddInt32("alignment", kAlignmentMap[i].alignment);
209
210		item = new BMenuItem(kAlignmentMap[i].name, message);
211		menu->AddItem(item);
212
213		if (kAlignmentMap[i].alignment == fAttribute.Alignment())
214			item->SetMarked(true);
215	}
216
217	fAlignmentMenuField = new BMenuField("alignment",
218		B_TRANSLATE("Alignment:"), menu);
219	fAlignmentMenuField->SetAlignment(B_ALIGN_RIGHT);
220
221	fAcceptButton = new BButton("add",
222		item ? B_TRANSLATE("Done") : B_TRANSLATE("Add"),
223		new BMessage(kMsgAccept));
224	fAcceptButton->SetEnabled(false);
225
226	BButton* cancelButton = new BButton("cancel", B_TRANSLATE("Cancel"),
227		new BMessage(B_QUIT_REQUESTED));
228
229	BBox* visibleBox;
230	BLayoutBuilder::Group<>(this, B_VERTICAL, padding)
231		.SetInsets(padding, padding, padding, padding)
232		.AddGrid(padding, padding / 2)
233			.Add(fPublicNameControl->CreateLabelLayoutItem(), 0, 0)
234			.Add(fPublicNameControl->CreateTextViewLayoutItem(), 1, 0)
235			.Add(fAttributeControl->CreateLabelLayoutItem(), 0, 1)
236			.Add(fAttributeControl->CreateTextViewLayoutItem(), 1, 1)
237			.Add(typeMenuField->CreateLabelLayoutItem(), 0, 2)
238			.Add(typeMenuField->CreateMenuBarLayoutItem(), 1, 2)
239			.End()
240		.Add(visibleBox = new BBox(B_FANCY_BORDER,
241			BLayoutBuilder::Grid<>(padding, padding / 2)
242				.Add(fDisplayAsMenuField->CreateLabelLayoutItem(), 0, 0)
243				.Add(fDisplayAsMenuField->CreateMenuBarLayoutItem(), 1, 0)
244				.Add(fEditableCheckBox, 2, 0)
245				.Add(fSpecialControl->CreateLabelLayoutItem(), 0, 1)
246				.Add(fSpecialControl->CreateTextViewLayoutItem(), 1, 1, 2)
247				.Add(fWidthControl->CreateLabelLayoutItem(), 0, 2)
248				.Add(fWidthControl->CreateTextViewLayoutItem(), 1, 2, 2)
249				.Add(fAlignmentMenuField->CreateLabelLayoutItem(), 0, 3)
250				.Add(fAlignmentMenuField->CreateMenuBarLayoutItem(), 1, 3, 2)
251				.SetInsets(padding, padding, padding, padding)
252				.View())
253			)
254		.AddGroup(B_HORIZONTAL, padding)
255			.AddGlue()
256			.Add(cancelButton)
257			.Add(fAcceptButton);
258
259	visibleBox->SetLabel(fVisibleCheckBox);
260
261	fAcceptButton->MakeDefault(true);
262	fPublicNameControl->MakeFocus(true);
263
264	target->PlaceSubWindow(this);
265	AddToSubset(target);
266
267	_CheckDisplayAs();
268	_CheckAcceptable();
269}
270
271
272AttributeWindow::~AttributeWindow()
273{
274}
275
276
277type_code
278AttributeWindow::_CurrentType() const
279{
280	type_code type = B_STRING_TYPE;
281	BMenuItem* item = fTypeMenu->FindMarked();
282	if (item != NULL && item->Message() != NULL) {
283		int32 value;
284		if (item->Message()->FindInt32("type", &value) == B_OK)
285			type = value;
286	}
287
288	return type;
289}
290
291
292BMenuItem*
293AttributeWindow::_DefaultDisplayAs() const
294{
295	return fDisplayAsMenuField->Menu()->ItemAt(0);
296}
297
298
299void
300AttributeWindow::_CheckDisplayAs()
301{
302	// check display as suported types
303
304	type_code currentType = _CurrentType();
305
306	BMenu* menu = fDisplayAsMenuField->Menu();
307	for (int32 i = menu->CountItems(); i-- > 0;) {
308		BMenuItem* item = menu->ItemAt(i);
309		bool supported = item == _DefaultDisplayAs();
310			// the default type is always supported
311		type_code type;
312		for (int32 j = 0; item->Message()->FindInt32("supports",
313				j, (int32*)&type) == B_OK; j++) {
314			if (type == currentType) {
315				supported = true;
316				break;
317			}
318		}
319
320		item->SetEnabled(supported);
321		if (item->IsMarked() && !supported)
322			menu->ItemAt(0)->SetMarked(true);
323	}
324
325	fSpecialControl->SetEnabled(!_DefaultDisplayAs()->IsMarked());
326}
327
328
329void
330AttributeWindow::_CheckAcceptable()
331{
332	bool enabled = fAttributeControl->Text() != NULL
333		&& fAttributeControl->Text()[0] != '\0'
334		&& fPublicNameControl->Text() != NULL
335		&& fPublicNameControl->Text()[0] != '\0';
336
337	if (enabled) {
338		// check for equality
339		AttributeItem* item = _NewItemFromCurrent();
340		enabled = fAttribute != *item;
341		delete item;
342	}
343
344	// Update button
345
346	if (fAcceptButton->IsEnabled() != enabled)
347		fAcceptButton->SetEnabled(enabled);
348}
349
350
351AttributeItem*
352AttributeWindow::_NewItemFromCurrent()
353{
354	const char* newAttribute = fAttributeControl->Text();
355
356	type_code type = _CurrentType();
357
358	int32 alignment = B_ALIGN_LEFT;
359	BMenuItem* item = fAlignmentMenuField->Menu()->FindMarked();
360	if (item != NULL && item->Message() != NULL) {
361		int32 value;
362		if (item->Message()->FindInt32("alignment", &value) == B_OK)
363			alignment = value;
364	}
365
366	int32 width = atoi(fWidthControl->Text());
367	if (width < 0)
368		width = 0;
369
370	BString displayAs;
371	item = fDisplayAsMenuField->Menu()->FindMarked();
372	if (item != NULL) {
373		const char* identifier;
374		if (item->Message()->FindString("identifier", &identifier) == B_OK) {
375			displayAs = identifier;
376
377			if (fSpecialControl->Text() && fSpecialControl->Text()[0]) {
378				displayAs += ":";
379				displayAs += fSpecialControl->Text();
380			}
381		}
382	}
383
384	return new AttributeItem(newAttribute,
385		fPublicNameControl->Text(), type, displayAs.String(), alignment,
386		width, fVisibleCheckBox->Value() == B_CONTROL_ON,
387		fEditableCheckBox->Value() == B_CONTROL_ON);
388}
389
390
391void
392AttributeWindow::MessageReceived(BMessage* message)
393{
394	switch (message->what) {
395		case kMsgAttributeUpdated:
396		case kMsgAlignmentChosen:
397		case kMsgTypeChosen:
398			_CheckDisplayAs();
399			_CheckAcceptable();
400			break;
401
402		case kMsgDisplayAsChosen:
403			fSpecialControl->SetEnabled(!_DefaultDisplayAs()->IsMarked());
404			_CheckAcceptable();
405			break;
406
407		case kMsgVisibilityChanged:
408		{
409			bool enabled = fVisibleCheckBox->Value() != B_CONTROL_OFF;
410
411			fDisplayAsMenuField->SetEnabled(enabled);
412			fWidthControl->SetEnabled(enabled);
413			fAlignmentMenuField->SetEnabled(enabled);
414			fEditableCheckBox->SetEnabled(enabled);
415
416			_CheckDisplayAs();
417			_CheckAcceptable();
418			break;
419		}
420
421		case kMsgAccept:
422		{
423			BMessage attributes;
424			status_t status = fMimeType.GetAttrInfo(&attributes);
425			if (status == B_OK) {
426				// replace the entry, and remove any equivalent entries
427				BList list;
428
429				const char* newAttribute = fAttributeControl->Text();
430
431				const char* attribute;
432				for (int32 i = 0; attributes.FindString("attr:name", i,
433						&attribute) == B_OK; i++) {
434					if (!strcmp(fAttribute.Name(), attribute)
435						|| !strcmp(newAttribute, attribute)) {
436						// remove this item
437						continue;
438					}
439
440					AttributeItem* item = create_attribute_item(attributes, i);
441					if (item != NULL)
442						list.AddItem(item);
443				}
444
445				list.AddItem(_NewItemFromCurrent());
446
447				// Copy them to a new message (their memory is still part of the
448				// original BMessage)
449				BMessage newAttributes;
450				for (int32 i = 0; i < list.CountItems(); i++) {
451					AttributeItem* item = (AttributeItem*)list.ItemAt(i);
452
453					newAttributes.AddString("attr:name", item->Name());
454					newAttributes.AddString("attr:public_name", item->PublicName());
455					newAttributes.AddInt32("attr:type", (int32)item->Type());
456					newAttributes.AddString("attr:display_as", item->DisplayAs());
457					newAttributes.AddInt32("attr:alignment", item->Alignment());
458					newAttributes.AddInt32("attr:width", item->Width());
459					newAttributes.AddBool("attr:viewable", item->Visible());
460					newAttributes.AddBool("attr:editable", item->Editable());
461
462					delete item;
463				}
464
465				status = fMimeType.SetAttrInfo(&newAttributes);
466			}
467
468			if (status != B_OK) {
469				error_alert(B_TRANSLATE("Could not change attributes"),
470					status);
471			}
472
473			PostMessage(B_QUIT_REQUESTED);
474			break;
475		}
476
477		default:
478			BWindow::MessageReceived(message);
479			break;
480	}
481}
482
483
484bool
485AttributeWindow::QuitRequested()
486{
487	return true;
488}
489