From c494b5426f71c58443de49def8ec9678c606ae5f Mon Sep 17 00:00:00 2001
From: Aarni Koskela <akx@iki.fi>
Date: Wed, 14 Aug 2024 00:03:22 +0300
Subject: [PATCH 1/5] Reorder toolbar semantically

---
 labelme/app.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/labelme/app.py b/labelme/app.py
index 7bbce4936..ee010b526 100644
--- a/labelme/app.py
+++ b/labelme/app.py
@@ -826,13 +826,14 @@ def __init__(
             save,
             deleteFile,
             None,
+            undo,
+            None,
             createMode,
             editMode,
             duplicate,
             delete,
-            undo,
-            brightnessContrast,
             None,
+            brightnessContrast,
             fitWindow,
             zoom,
             None,

From e46cdc44d972f50e39e63452de1ddb28145f9068 Mon Sep 17 00:00:00 2001
From: Aarni Koskela <akx@iki.fi>
Date: Wed, 14 Aug 2024 00:06:59 +0300
Subject: [PATCH 2/5] Move action collections out of `actions` namespace

Let's have the `actions` object just hold single actions, not lists of them.
---
 labelme/app.py | 187 +++++++++++++++++++++++--------------------------
 1 file changed, 86 insertions(+), 101 deletions(-)

diff --git a/labelme/app.py b/labelme/app.py
index ee010b526..c186dbc02 100644
--- a/labelme/app.py
+++ b/labelme/app.py
@@ -576,15 +576,6 @@ def __init__(
             self.tr("Adjust brightness and contrast"),
             enabled=False,
         )
-        # Group zoom controls into a list for easier toggling.
-        zoomActions = (
-            self.zoomWidget,
-            zoomIn,
-            zoomOut,
-            zoomOrg,
-            fitWindow,
-            fitWidth,
-        )
         self.zoomMode = self.FIT_WINDOW
         fitWindow.setChecked(Qt.Checked)
         self.scalers = {
@@ -657,60 +648,78 @@ def __init__(
             fitWindow=fitWindow,
             fitWidth=fitWidth,
             brightnessContrast=brightnessContrast,
-            zoomActions=zoomActions,
             openNextImg=openNextImg,
             openPrevImg=openPrevImg,
-            fileMenuActions=(open_, opendir, save, saveAs, close, quit),
-            tool=(),
-            # XXX: need to add some actions here to activate the shortcut
-            editMenu=(
-                edit,
-                duplicate,
-                copy,
-                paste,
-                delete,
-                None,
-                undo,
-                undoLastPoint,
-                None,
-                removePoint,
-                None,
-                toggle_keep_prev_mode,
-            ),
-            # menu shown at right click
-            menu=(
-                createMode,
-                createRectangleMode,
-                createCircleMode,
-                createLineMode,
-                createPointMode,
-                createLineStripMode,
-                createAiPolygonMode,
-                createAiMaskMode,
-                editMode,
-                edit,
-                duplicate,
-                copy,
-                paste,
-                delete,
-                undo,
-                undoLastPoint,
-                removePoint,
-            ),
-            onLoadActive=(
-                close,
-                createMode,
-                createRectangleMode,
-                createCircleMode,
-                createLineMode,
-                createPointMode,
-                createLineStripMode,
-                createAiPolygonMode,
-                createAiMaskMode,
-                editMode,
-                brightnessContrast,
-            ),
-            onShapesPresent=(saveAs, hideAll, showAll, toggleAll),
+        )
+        self.on_shapes_present_actions = (saveAs, hideAll, showAll, toggleAll)
+
+        self.draw_actions = {
+            "polygon": createMode,
+            "rectangle": createRectangleMode,
+            "circle": createCircleMode,
+            "point": createPointMode,
+            "line": createLineMode,
+            "linestrip": createLineStripMode,
+            "ai_polygon": createAiPolygonMode,
+            "ai_mask": createAiMaskMode,
+        }
+
+        # Group zoom controls into a list for easier toggling.
+        self.zoom_actions = (
+            self.zoomWidget,
+            zoomIn,
+            zoomOut,
+            zoomOrg,
+            fitWindow,
+            fitWidth,
+        )
+        self.on_load_active_actions = (
+            close,
+            createMode,
+            createRectangleMode,
+            createCircleMode,
+            createLineMode,
+            createPointMode,
+            createLineStripMode,
+            createAiPolygonMode,
+            createAiMaskMode,
+            editMode,
+            brightnessContrast,
+        )
+        # menu shown at right click
+        self.context_menu_actions = (
+            createMode,
+            createRectangleMode,
+            createCircleMode,
+            createLineMode,
+            createPointMode,
+            createLineStripMode,
+            createAiPolygonMode,
+            createAiMaskMode,
+            editMode,
+            edit,
+            duplicate,
+            copy,
+            paste,
+            delete,
+            undo,
+            undoLastPoint,
+            removePoint,
+        )
+        # XXX: need to add some actions here to activate the shortcut
+        self.edit_menu_actions = (
+            edit,
+            duplicate,
+            copy,
+            paste,
+            delete,
+            None,
+            undo,
+            undoLastPoint,
+            None,
+            removePoint,
+            None,
+            toggle_keep_prev_mode,
         )
 
         self.canvas.vertexSelected.connect(self.actions.removePoint.setEnabled)
@@ -773,7 +782,7 @@ def __init__(
         self.menus.file.aboutToShow.connect(self.updateFileMenu)
 
         # Custom context menu for the canvas widget:
-        utils.addActions(self.canvas.menus[0], self.actions.menu)
+        utils.addActions(self.canvas.menus[0], self.context_menu_actions)
         utils.addActions(
             self.canvas.menus[1],
             (
@@ -818,7 +827,7 @@ def __init__(
         ai_prompt_action.setDefaultWidget(self._ai_prompt_widget)
 
         self.tools = self.toolbar("Tools")
-        self.actions.tool = (
+        self.toolbar_actions = (
             open_,
             opendir,
             openPrevImg,
@@ -929,24 +938,17 @@ def noShapes(self):
         return not len(self.labelList)
 
     def populateModeActions(self):
-        tool, menu = self.actions.tool, self.actions.menu
         self.tools.clear()
-        utils.addActions(self.tools, tool)
+        utils.addActions(self.tools, self.toolbar_actions)
         self.canvas.menus[0].clear()
-        utils.addActions(self.canvas.menus[0], menu)
+        utils.addActions(self.canvas.menus[0], self.context_menu_actions)
         self.menus.edit.clear()
         actions = (
-            self.actions.createMode,
-            self.actions.createRectangleMode,
-            self.actions.createCircleMode,
-            self.actions.createLineMode,
-            self.actions.createPointMode,
-            self.actions.createLineStripMode,
-            self.actions.createAiPolygonMode,
-            self.actions.createAiMaskMode,
+            *self.draw_actions.values(),
             self.actions.editMode,
+            *self.edit_menu_actions,
         )
-        utils.addActions(self.menus.edit, actions + self.actions.editMenu)
+        utils.addActions(self.menus.edit, actions)
 
     def setDirty(self):
         # Even if we autosave the file, we keep the ability to undo
@@ -969,14 +971,8 @@ def setDirty(self):
     def setClean(self):
         self.dirty = False
         self.actions.save.setEnabled(False)
-        self.actions.createMode.setEnabled(True)
-        self.actions.createRectangleMode.setEnabled(True)
-        self.actions.createCircleMode.setEnabled(True)
-        self.actions.createLineMode.setEnabled(True)
-        self.actions.createPointMode.setEnabled(True)
-        self.actions.createLineStripMode.setEnabled(True)
-        self.actions.createAiPolygonMode.setEnabled(True)
-        self.actions.createAiMaskMode.setEnabled(True)
+        for action in self.draw_actions.values():
+            action.setEnabled(True)
         title = __appname__
         if self.filename is not None:
             title = "{} - {}".format(title, self.filename)
@@ -989,9 +985,9 @@ def setClean(self):
 
     def toggleActions(self, value=True):
         """Enable/Disable widgets which depend on an opened image."""
-        for z in self.actions.zoomActions:
+        for z in self.zoom_actions:
             z.setEnabled(value)
-        for action in self.actions.onLoadActive:
+        for action in self.on_load_active_actions:
             action.setEnabled(value)
 
     def queueEvent(self, function):
@@ -1105,24 +1101,13 @@ def toggleDrawingSensitive(self, drawing=True):
         self.actions.delete.setEnabled(not drawing)
 
     def toggleDrawMode(self, edit=True, createMode="polygon"):
-        draw_actions = {
-            "polygon": self.actions.createMode,
-            "rectangle": self.actions.createRectangleMode,
-            "circle": self.actions.createCircleMode,
-            "point": self.actions.createPointMode,
-            "line": self.actions.createLineMode,
-            "linestrip": self.actions.createLineStripMode,
-            "ai_polygon": self.actions.createAiPolygonMode,
-            "ai_mask": self.actions.createAiMaskMode,
-        }
-
         self.canvas.setEditing(edit)
         self.canvas.createMode = createMode
         if edit:
-            for draw_action in draw_actions.values():
+            for draw_action in self.draw_actions.values():
                 draw_action.setEnabled(True)
         else:
-            for draw_mode, draw_action in draw_actions.items():
+            for draw_mode, draw_action in self.draw_actions.items():
                 draw_action.setEnabled(createMode != draw_mode)
         self.actions.editMode.setEnabled(not edit)
 
@@ -1323,7 +1308,7 @@ def addLabel(self, shape):
             rgb = self._get_rgb_by_label(shape.label)
             self.uniqLabelList.setItemLabel(item, shape.label, rgb)
         self.labelDialog.addLabelHistory(shape.label)
-        for action in self.actions.onShapesPresent:
+        for action in self.on_shapes_present_actions:
             action.setEnabled(True)
 
         self._update_shape_color(shape)
@@ -2109,7 +2094,7 @@ def removeSelectedPoint(self):
             self.canvas.deleteShape(self.canvas.hShape)
             self.remLabels([self.canvas.hShape])
             if self.noShapes():
-                for action in self.actions.onShapesPresent:
+                for action in self.on_shapes_present_actions:
                     action.setEnabled(False)
         self.setDirty()
 
@@ -2124,7 +2109,7 @@ def deleteSelectedShape(self):
             self.remLabels(self.canvas.deleteSelected())
             self.setDirty()
             if self.noShapes():
-                for action in self.actions.onShapesPresent:
+                for action in self.on_shapes_present_actions:
                     action.setEnabled(False)
 
     def copyShape(self):

From 1eecec1dae2f78528710d624e0891a937a95b644 Mon Sep 17 00:00:00 2001
From: Aarni Koskela <akx@iki.fi>
Date: Wed, 14 Aug 2024 00:22:39 +0300
Subject: [PATCH 3/5] Show all draw modes in toolbar

---
 labelme/app.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/labelme/app.py b/labelme/app.py
index c186dbc02..4d41808eb 100644
--- a/labelme/app.py
+++ b/labelme/app.py
@@ -837,7 +837,8 @@ def __init__(
             None,
             undo,
             None,
-            createMode,
+            *self.draw_actions.values(),
+            None,
             editMode,
             duplicate,
             delete,

From 9e9f849ac9ce350e6a753cb8ecf2345e454b4e53 Mon Sep 17 00:00:00 2001
From: Aarni Koskela <akx@iki.fi>
Date: Wed, 14 Aug 2024 00:41:30 +0300
Subject: [PATCH 4/5] Improve dock feature configuration

* Don't leak `features` in loop
* Don't crash if configuration is missing
---
 labelme/app.py | 33 ++++++++++++++++++++++-----------
 1 file changed, 22 insertions(+), 11 deletions(-)

diff --git a/labelme/app.py b/labelme/app.py
index 4d41808eb..26b5cd8ad 100644
--- a/labelme/app.py
+++ b/labelme/app.py
@@ -191,17 +191,10 @@ def __init__(
 
         self.setCentralWidget(scrollArea)
 
-        features = QtWidgets.QDockWidget.DockWidgetFeatures()
-        for dock in ["flag_dock", "label_dock", "shape_dock", "file_dock"]:
-            if self._config[dock]["closable"]:
-                features = features | QtWidgets.QDockWidget.DockWidgetClosable
-            if self._config[dock]["floatable"]:
-                features = features | QtWidgets.QDockWidget.DockWidgetFloatable
-            if self._config[dock]["movable"]:
-                features = features | QtWidgets.QDockWidget.DockWidgetMovable
-            getattr(self, dock).setFeatures(features)
-            if self._config[dock]["show"] is False:
-                getattr(self, dock).setVisible(False)
+        self._configure_dock("flag_dock", self.flag_dock)
+        self._configure_dock("label_dock", self.label_dock)
+        self._configure_dock("shape_dock", self.shape_dock)
+        self._configure_dock("file_dock", self.file_dock)
 
         self.addDockWidget(Qt.RightDockWidgetArea, self.flag_dock)
         self.addDockWidget(Qt.RightDockWidgetArea, self.label_dock)
@@ -917,6 +910,24 @@ def __init__(
         # if self.firstStart:
         #    QWhatsThis.enterWhatsThisMode()
 
+    def _configure_dock(self, dock_name: str, dock_obj: QtWidgets.QDockWidget) -> None:
+        dock_config = self._config.get(dock_name, None)
+        if dock_config is None:
+            # Missing configuration? Never mind then...
+            return
+
+        features = QtWidgets.QDockWidget.DockWidgetFeatures()
+
+        if dock_config.get("closable"):
+            features = features | QtWidgets.QDockWidget.DockWidgetClosable
+        if dock_config.get("floatable"):
+            features = features | QtWidgets.QDockWidget.DockWidgetFloatable
+        if dock_config.get("movable"):
+            features = features | QtWidgets.QDockWidget.DockWidgetMovable
+        dock_obj.setFeatures(features)
+        if dock_config.get("show") is False:
+            dock_obj.setVisible(False)
+
     def menu(self, title, actions=None):
         menu = self.menuBar().addMenu(title)
         if actions:

From b70c54e4ac459865250d1534866d035d2c7a05af Mon Sep 17 00:00:00 2001
From: Aarni Koskela <akx@iki.fi>
Date: Wed, 14 Aug 2024 00:32:12 +0300
Subject: [PATCH 5/5] Move AI configuration to dock

---
 labelme/app.py                     | 96 +++++++++++++-----------------
 labelme/config/default_config.yaml |  5 ++
 2 files changed, 48 insertions(+), 53 deletions(-)

diff --git a/labelme/app.py b/labelme/app.py
index 26b5cd8ad..13f5a627e 100644
--- a/labelme/app.py
+++ b/labelme/app.py
@@ -161,6 +161,10 @@ def __init__(
         fileListWidget.setLayout(fileListLayout)
         self.file_dock.setWidget(fileListWidget)
 
+        self.ai_dock = QtWidgets.QDockWidget(self.tr("AI"), self)
+        self.ai_dock.setWidget(self.configure_ai_dock())
+        self.ai_dock.setObjectName("AI")
+
         self.zoomWidget = ZoomWidget()
         self.setAcceptDrops(True)
 
@@ -200,6 +204,7 @@ def __init__(
         self.addDockWidget(Qt.RightDockWidgetArea, self.label_dock)
         self.addDockWidget(Qt.RightDockWidgetArea, self.shape_dock)
         self.addDockWidget(Qt.RightDockWidgetArea, self.file_dock)
+        self.addDockWidget(Qt.RightDockWidgetArea, self.ai_dock)
 
         # Actions
         action = functools.partial(utils.newAction, self)
@@ -367,13 +372,7 @@ def __init__(
             self.tr("Start drawing ai_polygon. Ctrl+LeftClick ends creation."),
             enabled=False,
         )
-        createAiPolygonMode.changed.connect(
-            lambda: self.canvas.initializeAiModel(
-                name=self._selectAiModelComboBox.currentText()
-            )
-            if self.canvas.createMode == "ai_polygon"
-            else None
-        )
+        createAiPolygonMode.changed.connect(self._initialize_ai_model_if_needed)
         createAiMaskMode = action(
             self.tr("Create AI-Mask"),
             lambda: self.toggleDrawMode(False, createMode="ai_mask"),
@@ -382,13 +381,7 @@ def __init__(
             self.tr("Start drawing ai_mask. Ctrl+LeftClick ends creation."),
             enabled=False,
         )
-        createAiMaskMode.changed.connect(
-            lambda: self.canvas.initializeAiModel(
-                name=self._selectAiModelComboBox.currentText()
-            )
-            if self.canvas.createMode == "ai_mask"
-            else None
-        )
+        createAiMaskMode.changed.connect(self._initialize_ai_model_if_needed)
         editMode = action(
             self.tr("Edit Polygons"),
             self.setEditMode,
@@ -784,41 +777,6 @@ def __init__(
             ),
         )
 
-        selectAiModel = QtWidgets.QWidgetAction(self)
-        selectAiModel.setDefaultWidget(QtWidgets.QWidget())
-        selectAiModel.defaultWidget().setLayout(QtWidgets.QVBoxLayout())
-        #
-        selectAiModelLabel = QtWidgets.QLabel(self.tr("AI Mask Model"))
-        selectAiModelLabel.setAlignment(QtCore.Qt.AlignCenter)
-        selectAiModel.defaultWidget().layout().addWidget(selectAiModelLabel)
-        #
-        self._selectAiModelComboBox = QtWidgets.QComboBox()
-        selectAiModel.defaultWidget().layout().addWidget(self._selectAiModelComboBox)
-        model_names = [model.name for model in MODELS]
-        self._selectAiModelComboBox.addItems(model_names)
-        if self._config["ai"]["default"] in model_names:
-            model_index = model_names.index(self._config["ai"]["default"])
-        else:
-            logger.warning(
-                "Default AI model is not found: %r",
-                self._config["ai"]["default"],
-            )
-            model_index = 0
-        self._selectAiModelComboBox.setCurrentIndex(model_index)
-        self._selectAiModelComboBox.currentIndexChanged.connect(
-            lambda: self.canvas.initializeAiModel(
-                name=self._selectAiModelComboBox.currentText()
-            )
-            if self.canvas.createMode in ["ai_polygon", "ai_mask"]
-            else None
-        )
-
-        self._ai_prompt_widget: QtWidgets.QWidget = AiPromptWidget(
-            on_submit=self._submit_ai_prompt, parent=self
-        )
-        ai_prompt_action = QtWidgets.QWidgetAction(self)
-        ai_prompt_action.setDefaultWidget(self._ai_prompt_widget)
-
         self.tools = self.toolbar("Tools")
         self.toolbar_actions = (
             open_,
@@ -839,10 +797,6 @@ def __init__(
             brightnessContrast,
             fitWindow,
             zoom,
-            None,
-            selectAiModel,
-            None,
-            ai_prompt_action,
         )
 
         self.statusBar().showMessage(str(self.tr("%s started.")) % __appname__)
@@ -928,6 +882,42 @@ def _configure_dock(self, dock_name: str, dock_obj: QtWidgets.QDockWidget) -> No
         if dock_config.get("show") is False:
             dock_obj.setVisible(False)
 
+    def configure_ai_dock(self) -> QtWidgets.QWidget:
+        select_ai_model_combo_box = QtWidgets.QComboBox(self)
+        model_names = [model.name for model in MODELS]
+        select_ai_model_combo_box.addItems(model_names)
+        default_ai_model = self._config["ai"]["default"]
+        if default_ai_model in model_names:
+            model_index = model_names.index(default_ai_model)
+        else:
+            logger.warning(
+                "Default AI model is not found: %r",
+                default_ai_model,
+            )
+            model_index = 0
+        select_ai_model_combo_box.setCurrentIndex(model_index)
+        select_ai_model_combo_box.currentIndexChanged.connect(
+            self._initialize_ai_model_if_needed,
+        )
+        self._selectAiModelComboBox = select_ai_model_combo_box
+        self._ai_prompt_widget: QtWidgets.QWidget = AiPromptWidget(
+            on_submit=self._submit_ai_prompt, parent=self
+        )
+
+        ai_dock_layout = QtWidgets.QVBoxLayout()
+        ai_dock_widget = QtWidgets.QWidget()
+        ai_dock_widget.setLayout(ai_dock_layout)
+        ai_dock_layout.addWidget(QtWidgets.QLabel(self.tr("AI Mask Model")))
+        ai_dock_layout.addWidget(select_ai_model_combo_box)
+        ai_dock_layout.addWidget(self._ai_prompt_widget)
+        return ai_dock_widget
+
+    def _initialize_ai_model_if_needed(self) -> None:
+        if self.canvas.createMode in ("ai_polygon", "ai_mask"):
+            self.canvas.initializeAiModel(
+                name=self._selectAiModelComboBox.currentText()
+            )
+
     def menu(self, title, actions=None):
         menu = self.menuBar().addMenu(title)
         if actions:
diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml
index 128dc6d6a..cee853c78 100644
--- a/labelme/config/default_config.yaml
+++ b/labelme/config/default_config.yaml
@@ -54,6 +54,11 @@ file_dock:
   closable: true
   movable: true
   floatable: true
+ai_dock:
+  show: true
+  closable: true
+  movable: true
+  floatable: true
 
 # label_dialog
 show_label_text_field: true