Java swing: how to align menu items in rows and columns? - java

I'm have no trouble when creating menu bar and its item. But now, when I get a question how to make the menu items appeared as column & rows-like table shaped, I really don't know about that.
The goals is to create this kind of menu items using java.
Check this link.
And right now, I just think that I should use a jpanel as menu item, and then applying a flowlayout and then adding many jlabel(s) as I could as menuitem inside the grid. But wouldn't it worst?
What's the best deal to create the menu items such as the image preview on the above links?
I tried google, but found no related cases. CMIIW.

The popup menu of a JMenu instance is a standard container, so you can add to it whatever you want. It has a default layout, but you can change it.
Something like in your mockup is created by this code:
import javax.swing.*;
import java.awt.*;
public class Test {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("Menu test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(new Dimension(600, 400));
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("Test");
JPopupMenu popupMenu = menu.getPopupMenu();
popupMenu.setLayout(new GridLayout(5, 5));
for (int r = 0; r < 5; r++) {
for (int c = 0; c < 5; c++) {
popupMenu.add(new JMenuItem("(" + (r + 1) + ", " + (c + 1) + ")"));
}
}
menuBar.add(menu);
frame.setJMenuBar(menuBar);
frame.setVisible(true);
}
});
}
}

I haven't seen a ready made component for anything like this. So I think you are on your own.
I see two possibilities:
JMenuItem is a JComponent, so you can add other components to it. You probably want to use some kind of grid based layout and add buttons or labels for the numbers.
Implement you own JMenuItem that displays your grid component instead of the normal JPopupMenu
In any case have a look at the source code of JMenu(Item) in order to understand how these components work.

The simplest solution is just set the layout of JMenu's JPopupMenu and then add items like you normally would. There's no need to create a subclass.
Example:
import javax.swing.*;
import java.awt.*;
public class menu {
public static void main(String ... args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("A regular menu");
menu.add(new JMenuItem("Menu item"));
JMenu gridMenu = new JMenu("Menu with grid");
// This does the trick:
gridMenu.getPopupMenu().setLayout(new GridLayout(2, 2));
gridMenu.add("Top left");
gridMenu.add("Top right");
gridMenu.add("Bottom left");
gridMenu.add("Bottom right");
menu.add(gridMenu);
menuBar.add(menu);
JFrame frame = new JFrame("Menus");
frame.setJMenuBar(menuBar);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
});
}
}

Related

Support for COPY option

I am coding Java Swing Calculator in NetBeans. I have JFrame, calculator buttons and JTextField called display. I need to support the copy (and also CTRL+C) option. Does anyone have an idea on how to do this?
If you want to add a right-click menu for cut/copy/paste, you can use the Cut/Copy/Paste actions that your components already have, although I prefer to rename them to give them simpler easier to read names, since it's easier to read "Cut" rather than "cut-to-clipboard".
For instance, if you call this method and pass in any text component, it should add a right-click pop-up menu for cut-copy-paste:
// allows default cut copy paste popup menu actions
private void addCutCopyPastePopUp(JTextComponent textComponent) {
ActionMap am = textComponent.getActionMap();
Action paste = am.get("paste-from-clipboard");
Action copy = am.get("copy-to-clipboard");
Action cut = am.get("cut-to-clipboard");
cut.putValue(Action.NAME, "Cut");
copy.putValue(Action.NAME, "Copy");
paste.putValue(Action.NAME, "Paste");
JPopupMenu popup = new JPopupMenu("My Popup");
textComponent.setComponentPopupMenu(popup);
popup.add(new JMenuItem(cut));
popup.add(new JMenuItem(copy));
popup.add(new JMenuItem(paste));
}
For example:
import java.awt.BorderLayout;
import javax.swing.*;
import javax.swing.text.JTextComponent;
public class AddCopyAndPaste extends JPanel {
private JTextField textField = new JTextField("Four score and seven years ago...");
private JTextArea textArea = new JTextArea(15, 30);
public AddCopyAndPaste() {
addCutCopyPastePopUp(textField);
addCutCopyPastePopUp(textArea);
setLayout(new BorderLayout());
add(textField, BorderLayout.PAGE_START);
add(new JScrollPane(textArea), BorderLayout.CENTER);
}
// allows default cut copy paste popup menu actions
private void addCutCopyPastePopUp(JTextComponent textComponent) {
ActionMap am = textComponent.getActionMap();
Action paste = am.get("paste-from-clipboard");
Action copy = am.get("copy-to-clipboard");
Action cut = am.get("cut-to-clipboard");
cut.putValue(Action.NAME, "Cut");
copy.putValue(Action.NAME, "Copy");
paste.putValue(Action.NAME, "Paste");
JPopupMenu popup = new JPopupMenu("My Popup");
textComponent.setComponentPopupMenu(popup);
popup.add(new JMenuItem(cut));
popup.add(new JMenuItem(copy));
popup.add(new JMenuItem(paste));
}
private static void createAndShowGui() {
AddCopyAndPaste mainPanel = new AddCopyAndPaste();
JFrame frame = new JFrame("Add Copy And Paste");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}

How to use DefaultEditorKit in an AbstractAction

My notepad program that I'm writing uses AbstractActions for each item in the JMenuBar, and I want to keep it consistent that way throughout my code. And now I'm implementing Cut, Copy, Paste into the program but I'm unsure of how to do that with Action.
import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.text.DefaultEditorKit;
public class Home {
static Action Cut = new AbstractAction("Cut-Action") {
public void actionPerformed(ActionEvent e) {
// Where I want to use cut
new DefaultEditorKit.CutAction();
}
};
static public JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("Edit");
menu.add(Cut); // Adds the cut action
// adds the non-action method
JMenuItem item = new JMenuItem(new DefaultEditorKit.CutAction());
item.setText("Cut-NonAction");
menu.add(item);
menuBar.add(menu);
return menuBar;
}
public static void main(String[] args) {
JFrame frame = new JFrame("Home");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar menuBar = createMenuBar();
frame.add(menuBar, BorderLayout.NORTH);
JTextPane txt = new JTextPane();
JScrollPane s = new JScrollPane(txt);
frame.add(s, BorderLayout.CENTER);
frame.setSize(400, 300);
frame.setVisible(true);
}
}
How would I be able to use the cut action in my abstract action??
I figured it out with a little bit of trial and error.
I changed this code:
public void actionPerformed(ActionEvent e) {
new DefaultEditorKit().CutAction();
}
to:
public void actionPerformed(ActionEvent e) {
Action cut = new DefaultEditorKit.CutAction();
cut.actionPerformed(e);
}
How would I be able to use the cut action in my abstract action??
Why are you trying to do this? The is not the way to use the Actions from the editor kit.
This is the proper way to use the actions:
JMenuItem item = new JMenuItem(new DefaultEditorKit.CutAction());
Or if you happen to need the CutAction on a menu and on a toolbar you would use code like:
Action cut = new DefaultEditorKit.CutAction();
cut.putValue(Action.NAME, "Cut");
JMenuItem cutMenuItem = new JMenuItem( cut );
JButton cutButton = new JButton( cut );
Now the same Action is shared which means you can enable/disable the Action and both components will be affected. Read the section from the Swing tutorial on How to Use Actions for more information and examples.

Adding a JScrollPane to a JMenu

I have a JMenu which will include JMenuItems that are generated on start-up from a database. As such, it's quite likely that the menu will be too large and run off the screen.
As such, I am trying to add a JScrollPane to the JMenu.
Example, to the effect of;
JMenu employeesMenu = new JMenu("Employees");
JScrollPane emScroll = new JScrollPane();
JList contents = new JList();
contents.add(new JRadioButton("1"));
contents.add(new JRadioButton("2"));
contents.add(new JRadioButton("3"));
// ... etc
emScroll.add(contents);
employeesMenu.add(emScroll);
Now, my understanding is that a JMenu's contents are stored in a JList inside a JPopupMenu. So my question now is, what is a way of forcing that JList into a JScrollPane? Alternatively, is it possible to use a JScrollBar instead? Any input appreciated.
Maybe you can use Darryl's Menu Scroller approach. It adds arrow buttons at the top/bottom of the menu when required.
The below shows how to add JScrollPane to a JMenu. JLabel components are used as menu items below, as JMenuItem seems that can't be used in a JScrollPane. A mouseListener is added to each JLabel to mimic the JMenuItem behaviour, i.e., changing colors on mouse entering/exiting, as well as taking action on clicking an item (in this case, the text of the label is printed out, which can be used to decide what follows next). One may need to adjust the scrollPane.setPreferredSize, as well as the colours when the mouse enters/exits an item (even though, for convenience, the default colours used by the current LookAndFeel for the JMenuItem are used), as required.
The reason for using <html> tags in the text of the JLabel is to allow the background colour (when you move your mouse over the items) to fill the width of each item in the JScrollPane, rather than applying background colour only up to where the text ends. The <html> tags are removed when reading the text of the selected item/label.
MenuExample.java
import javax.swing.*;
import java.awt.*;
import java.util.Random;
public class MenuExample {
Random rand = new Random();
Color menuBackCol;
Color mItemBackCol;
Color mItemForegCol;
Color mItmSelBackCol;
Color mItmSelForegCol;
MenuExample() {
menuBackCol = UIManager.getColor("Menu.background");
mItemBackCol = UIManager.getColor("MenuItem.background");
mItemForegCol = UIManager.getColor("MenuItem.foreground");
mItmSelBackCol = UIManager.getColor("MenuItem.selectionBackground");
mItmSelForegCol = UIManager.getColor("MenuItem.selectionForeground");
Box box = new Box(BoxLayout.Y_AXIS);
for (int i = 0; i < 250; i++) {
box.add(Box.createRigidArea(new Dimension(0, 2))); // creates space between the components
JLabel lbl = new JLabel("<html>  " + i + ": " + rand.nextInt(10000) + "</html>");
lbl.setOpaque(true);
lbl.setBackground(mItemBackCol);
lbl.addMouseListener(
new LabelController(lbl, mItemBackCol, mItemForegCol, mItmSelBackCol, mItmSelForegCol));
box.add(lbl);
}
JScrollPane scrollPane = new JScrollPane(box);
scrollPane.getVerticalScrollBar().setUnitIncrement(20); // adjusts scrolling speed
scrollPane.setPreferredSize(new Dimension(100, 300));
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.getViewport().setBackground(menuBackCol);
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("Menu");
JMenu submenu = new JMenu("Sub Menu");
submenu.add(scrollPane);
menu.add(new JMenuItem("Item 1"));
menu.add(new JMenuItem("Item 2"));
menu.add(new JMenuItem("Item 3"));
menu.add(submenu);
mb.add(menu);
JFrame f = new JFrame("Menu with ScrollBar Example");
f.setJMenuBar(mb);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(640, 480);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String args[]) {
new MenuExample();
}
}
LabelController.java
import java.awt.event.MouseEvent;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class LabelController implements MouseListener {
JLabel lbl;
Color mItemBackCol;
Color mItemForegCol;
Color mItmSelBackCol;
Color mItmSelForegCol;
public LabelController(JLabel lbl, Color mItemBackCol, Color mItemForegCol, Color mItmSelBackCol,
Color mItmSelForegCol) {
this.lbl = lbl;
this.mItemBackCol = mItemBackCol;
this.mItemForegCol = mItemForegCol;
this.mItmSelBackCol = mItmSelBackCol;
this.mItmSelForegCol = mItmSelForegCol;
}
#Override
public void mouseClicked(MouseEvent e) {
String selectedText = lbl.getText().replaceAll("<[^>]*>", "").replace(" ","").trim();
System.out.println(selectedText);
javax.swing.MenuSelectionManager.defaultManager().clearSelectedPath(); // close the menu
lbl.setBackground(mItemBackCol);
lbl.setForeground(mItemForegCol);
}
#Override
public void mouseEntered(MouseEvent e) {
lbl.setBackground(mItmSelBackCol);
lbl.setForeground(mItmSelForegCol);
}
#Override
public void mouseExited(MouseEvent e) {
lbl.setBackground(mItemBackCol);
lbl.setForeground(mItemForegCol);
}
#Override
public void mousePressed(MouseEvent e) {
}
#Override
public void mouseReleased(MouseEvent e) {
}
}

JFrame's getFocusOwner() not being helpful

I am working with a Swing program and having a little trouble. The program has two windows (both are JFrames). The main window is just fine and should not be relevant to this issue.
The window I am having issues with contains a JScrollPane with a JPanel in it, and has a JMenuBar. The JPanel has a bunch of JTextComponents (some JTextFields, some JTextAreas) on it.
What I want to do is have an ActionListener attached to a JMenuItem find the JTextComponent that has focus.
I have seen the previous posts at focused component reference and How to find out which object currently has focus. My issue is that calling the particular window's getFocusOwner() method merely returns the JFrame's JRootPane, which is utterly unhelpful. Both the JScrollPane and the JPanel in question are focusable according to their isFocusable() methods. This happens even if I actually enter text into one of the JTextComponents before clicking the menu item. The cursor still blinks in the text field while I open the menu, and everything. For what it's worth, getMostRecentFocusOwner() also simply returns the JRootPane.
If you use TextActions, the action knows which JTextComponent has the focus. I've modified some code that I found here to show that even if the TextActions come from one JTextArea, they still will automatically work on any and all text components that have focus:
import java.awt.GridLayout;
import javax.swing.*;
public class TextActionExample {
public static void main(String[] args) {
// Create a text area.
JTextArea ta = new JTextArea(15, 30);
ta.setLineWrap(true);
// Add all actions to the menu (split into two menus to make it more
// usable).
Action[] actions = ta.getActions();
JMenuBar menubar = new JMenuBar();
JMenu actionmenu = new JMenu("Actions");
menubar.add(actionmenu);
JMenu firstHalf = new JMenu("1st Half");
JMenu secondHalf = new JMenu("2nd Half");
actionmenu.add(firstHalf);
actionmenu.add(secondHalf);
int mid = actions.length / 2;
for (int i = 0; i < mid; i++) {
firstHalf.add(actions[i]);
}
for (int i = mid; i < actions.length; i++) {
secondHalf.add(actions[i]);
}
JTextField textField = new JTextField(20);
JPanel textFieldPanel = new JPanel();
textFieldPanel.add(textField);
JPanel mainPanel = new JPanel(new GridLayout(1, 0, 5, 5));
mainPanel.add(new JScrollPane(ta));
mainPanel.add(new JScrollPane(new JTextArea(15, 30)));
mainPanel.add(textFieldPanel);
// Show it . . .
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(mainPanel);
f.setJMenuBar(menubar);
f.pack();
f.setVisible(true);
}
}
This is very interesting stuff that I have to learn more about.
I think I have solved this, because of the fact that you lose focus when you click on a menu item, we simply have to wait for focus to return to the component before we check who has focus, I have done this using a swing timer that waits 100ms and then executes its method to check which component has focus:
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.TimerTask;
import javax.swing.*;
public class JavaApplication180 extends JFrame {
private JTextField[] JTextFields;
private JMenuBar menuBar;
private JMenu menu;
private JMenuItem item;
public JavaApplication180() {
initComponents();
createAndShowUI();
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new JavaApplication180();
}
});
}
private void createAndShowUI() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().setLayout(new GridLayout(2, 2, 10, 10));
setJMenuBar(menuBar);
addComponentsToPane();
pack();
setVisible(true);
}
private void initComponents() {
JTextFields = new JTextField[4];
menuBar = new JMenuBar();
item = new JMenuItem("Who has focus?");
item.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent ae) {
TimerTask tt = new TimerTask() {
#Override
public void run() {
JOptionPane.showMessageDialog(null, getMostRecentFocusOwner().getName());
}
};
new java.util.Timer().schedule(tt, 100);
}
});
menu = new JMenu("File");
menu.add(item);
menuBar.add(menu);
}
private void addComponentsToPane() {
for (int i = 0; i < JTextFields.length; i++) {
JTextFields[i] = new JTextField();
JTextFields[i].setText(String.valueOf(i));
JTextFields[i].setName(String.valueOf(i));
getContentPane().add(JTextFields[i]);
}
}
}
I'm probably too late, but I just did the following in my JFrame constructor.
this.rootPane.setFocusable(false);
Now
getFocusOwner()
will return the current JTextComponent instead of the rootPane.
I then used this code in an ActionListener attached to my JMenuItem to select the text within it.
if (getFocusOwner() instanceof JTextField)
{
((JTextField) getMostRecentFocusOwner()).selectAll();
}
It should be noted that If you have a JScrollPane etc, you may have to use setFocusable(false) on all the components between the rootPane and the textfields.
Hope this helps anyone else with the same issue!
Source: Personal Experience
When I've needed this, I wrote a focus listener. I had a JPanel with two columns of JTextFields and the focus listener kept track of which column was last used by the user. I enablesd the user to enter some text into that last focused column with a button click. You would use just one instance of your FocusListener for all text fields and have a field referencing the most recently focused component. Your menu action can then query that field to determine which text field to use.
See http://docs.oracle.com/javase/tutorial/uiswing/events/focuslistener.html

Can't Add JMenuItem to JMenu in JPopupMenu

I've got a new UI I'm working on implementing in Java and I'm having trouble implementing a JPopupMenu containing a JMenu (as well as several JMenuItems), which itself contains several JMenuItems. The JPopupMenu appears where I click the RMB, and it looks good, but the "Connect" JMenu doesn't seem to have any children when I mouse-over, despite my best efforts to .add() them.
Having looked at several examples online, I haven't seen any that specifically implement a listener for mouseEntered() to roll out the sub-items. I'm of a mind that I'm messing something up in my menu initialization method.
I've attached the pertinent code for your perusal.
//Elsewhere...
private JPopupMenu _clickMenu;
//End Elsehwere...
private void initializeMenu()
{
_clickMenu = new JPopupMenu();
_clickMenu.setVisible(false);
_clickMenu.add(generateConnectionMenu());
JMenuItem menuItem;
menuItem = new JMenuItem("Configure");
addMenuItemListeners(menuItem);
_clickMenu.add(menuItem);
menuItem = new JMenuItem("Status");
addMenuItemListeners(menuItem);
_clickMenu.add(menuItem);
}
private JMenu generateConnectionMenu()
{
JMenu menu = new JMenu("Connect");
List<Port> portList = _database.getAllPortsInCard(_cardId);
for(int i = 0; i < portList.size(); i++)
{
menu.add(new JMenuItem(portList.get(i).getName()));
}
return menu;
}
The code is certainly not the prettiest, but go easy on me as it's been altered too many times today as time permitted while I tried to figure out why this wasn't working. I'm thinking it may be a question of scope, but I've tried a few different code configurations to no avail. Feel free to ask any followup questions or smack me for an obvious oversight (it's happened before...). Thanks all!
Edit:
Chalk this one up to a lack of experience with Java and Swing... I was manually positioning and making the JPopupMenu visible instead of using the JComponent.setComponentPopupMenu(menu) method. After doing this for the card module in the above image (itself a JButton), the submenu displays correctly. A different, functional version of the initialization code is included below.
private void initializeMenu()
{
_cardMenu = new JPopupMenu();
JMenu menu = new JMenu("Connect");
JMenuItem menuItem;
menuItem = new JMenuItem("1");
menu.add(menuItem);
menuItem = new JMenuItem("2");
menu.add(menuItem);
_cardMenu.add(menu);
_cardMenu.add(new JMenuItem("Configure"));
_cardMenu.add(new JMenuItem("Status"));
_mainButton.setComponentPopupMenu(_cardMenu); //Important, apparently!
}
So, lesson learned. Thanks for the help guys!
This is common Bug or Swing property that in one moment can be visible only one Lightweight popup window, same issue is e.g. with popup from JComboBox added into JPopupMenu,
change Lightweight property to the Heavyweight
better would be
use un_decorated JDialog or JOptionPane with JComponents
EDIT #trashgod
everything works as I excepted, all JMenus, JMenuItems are visible and repeatly fired correct evets
code
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class ContextMenu implements ActionListener, MenuListener, MenuKeyListener {
private JTextArea textArea = new JTextArea();
public ContextMenu() {
final JPopupMenu contextMenu = new JPopupMenu("Edit");
JMenu menu = new JMenu("Sub Menu");
menu.add(makeMenuItem("Sub Menu Save"));
menu.add(makeMenuItem("Sub Menu Save As"));
menu.add(makeMenuItem("Sub Menu Close"));
menu.addMenuListener(this);
JMenu menu1 = new JMenu("Sub Menu");
menu1.add(makeMenuItem("Deepest Sub Menu Save"));
menu1.add(makeMenuItem("Deepest Sub Menu Save As"));
menu1.add(makeMenuItem("Deepest Sub Menu Close"));
menu.add(menu1);
menu1.addMenuListener(this);
contextMenu.add(menu);
contextMenu.add(makeMenuItem("Plain Save"));
contextMenu.add(makeMenuItem("Plain Save As"));
contextMenu.add(makeMenuItem("Plain Close"));
contextMenu.addMenuKeyListener(this);
JFrame frame = new JFrame();
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
frame.add(panel);
panel.setComponentPopupMenu(contextMenu);
textArea.setInheritsPopupMenu(true);
panel.add(BorderLayout.CENTER, textArea);
JTextField textField = new JTextField();
textField.setInheritsPopupMenu(true);
panel.add(BorderLayout.SOUTH, textField);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 200);
frame.setVisible(true);
}
#Override
public void actionPerformed(ActionEvent e) {
textArea.append(e.getActionCommand() + "\n");
}
private JMenuItem makeMenuItem(String label) {
JMenuItem item = new JMenuItem(label);
item.addActionListener(this);
return item;
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
ContextMenu contextMenu = new ContextMenu();
}
});
}
public void menuSelected(MenuEvent e) {
textArea.append("menuSelected" + "\n");
}
public void menuDeselected(MenuEvent e) {
textArea.append("menuDeselected" + "\n");
}
public void menuCanceled(MenuEvent e) {
textArea.append("menuCanceled" + "\n");
}
public void menuKeyTyped(MenuKeyEvent e) {
textArea.append("menuKeyTyped" + "\n");
}
public void menuKeyPressed(MenuKeyEvent e) {
textArea.append("menuKeyPressed" + "\n");
}
public void menuKeyReleased(MenuKeyEvent e) {
textArea.append("menuKeyReleased" + "\n");
}
}
I don't see an obvious problem in the code shown, although #mKorbel's point may apply. For reference, this ControlPanel adds a subMenu with several items.

Categories