1111import inspect
1212import json
1313from collections .abc import Callable
14+ from pathlib import Path
1415from typing import Any
1516
16- from PySide6 .QtCore import Qt
17+ from PySide6 .QtCore import QSettings , Qt
18+ from PySide6 .QtGui import QDragEnterEvent , QDropEvent , QKeySequence , QShortcut
1719from PySide6 .QtWidgets import (
1820 QCheckBox ,
1921 QComboBox ,
4547
4648_PATH_HINT_SUBSTRINGS = ("path" , "_dir" , "_file" , "directory" , "filename" , "target" )
4749_SECRET_HINT_SUBSTRINGS = ("password" , "secret" , "token" , "credential" )
50+ _SETTINGS_ORG = "automation_file"
51+ _SETTINGS_APP = "ui"
52+ _LAST_JSON_DIR_KEY = "json_editor/last_dir"
4853
4954
5055def _is_path_like (name : str ) -> bool :
@@ -237,6 +242,8 @@ def __init__(self, log, pool) -> None:
237242 self ._current_form : _ActionForm | None = None
238243 self ._current_form_row : int = - 1
239244 self ._suppress_sync = False
245+ self ._settings = QSettings (_SETTINGS_ORG , _SETTINGS_APP )
246+ self .setAcceptDrops (True )
240247
241248 self ._action_list = QListWidget ()
242249 self ._action_list .currentRowChanged .connect (self ._on_row_changed )
@@ -261,6 +268,8 @@ def __init__(self, log, pool) -> None:
261268 root .addWidget (splitter )
262269 root .addWidget (self ._build_run_bar ())
263270
271+ self ._register_shortcuts ()
272+
264273 def _build_toolbar (self ) -> QWidget :
265274 toolbar = QWidget ()
266275 row = QHBoxLayout (toolbar )
@@ -431,9 +440,15 @@ def _unpack_action(action: list[Any]) -> tuple[str, dict[str, Any]]:
431440 return name , {}
432441
433442 def _on_load (self ) -> None :
434- path , _ = QFileDialog .getOpenFileName (self , "Load action JSON" , filter = "JSON (*.json)" )
443+ start_dir = str (self ._settings .value (_LAST_JSON_DIR_KEY , "" ))
444+ path , _ = QFileDialog .getOpenFileName (
445+ self , "Load action JSON" , start_dir , filter = "JSON (*.json)"
446+ )
435447 if not path :
436448 return
449+ self ._load_path (path )
450+
451+ def _load_path (self , path : str ) -> None :
437452 try :
438453 with open (path , encoding = "utf-8" ) as fp :
439454 data = json .load (fp )
@@ -444,12 +459,18 @@ def _on_load(self) -> None:
444459 self ._log .append_line ("load error: top-level JSON must be an array" )
445460 return
446461 self ._actions = data
462+ self ._settings .setValue (_LAST_JSON_DIR_KEY , str (Path (path ).parent ))
463+ self ._clear_current_form ()
447464 self ._refresh_list (select = 0 if data else None )
448465 self ._sync_raw_from_model ()
466+ self ._log .append_line (f"loaded { len (data )} actions from { path } " )
449467
450468 def _on_save (self ) -> None :
451469 self ._commit_current_form ()
452- path , _ = QFileDialog .getSaveFileName (self , "Save action JSON" , filter = "JSON (*.json)" )
470+ start_dir = str (self ._settings .value (_LAST_JSON_DIR_KEY , "" ))
471+ path , _ = QFileDialog .getSaveFileName (
472+ self , "Save action JSON" , start_dir , filter = "JSON (*.json)"
473+ )
453474 if not path :
454475 return
455476 try :
@@ -458,6 +479,7 @@ def _on_save(self) -> None:
458479 except OSError as error :
459480 self ._log .append_line (f"save error: { error } " )
460481 return
482+ self ._settings .setValue (_LAST_JSON_DIR_KEY , str (Path (path ).parent ))
461483 self ._log .append_line (f"saved { len (self ._actions )} actions to { path } " )
462484
463485 def _on_clear (self ) -> None :
@@ -536,3 +558,38 @@ def _on_validate(self) -> None:
536558 f"validate_action({ len (actions )} )" ,
537559 kwargs = {"action_list" : actions },
538560 )
561+
562+ def _register_shortcuts (self ) -> None :
563+ for keys , handler in (
564+ ("Ctrl+O" , self ._on_load ),
565+ ("Ctrl+S" , self ._on_save ),
566+ ("Ctrl+R" , self ._on_run ),
567+ ):
568+ shortcut = QShortcut (QKeySequence (keys ), self )
569+ shortcut .setContext (Qt .ShortcutContext .WidgetWithChildrenShortcut )
570+ shortcut .activated .connect (handler )
571+
572+ def dragEnterEvent (self , event : QDragEnterEvent ) -> None : # noqa: N802 — Qt override
573+ if self ._is_json_drop (event ):
574+ event .acceptProposedAction ()
575+ return
576+ event .ignore ()
577+
578+ def dropEvent (self , event : QDropEvent ) -> None : # noqa: N802 — Qt override
579+ if not self ._is_json_drop (event ):
580+ event .ignore ()
581+ return
582+ url = event .mimeData ().urls ()[0 ]
583+ self ._load_path (url .toLocalFile ())
584+ event .acceptProposedAction ()
585+
586+ @staticmethod
587+ def _is_json_drop (event : QDragEnterEvent | QDropEvent ) -> bool :
588+ mime = event .mimeData ()
589+ if not mime .hasUrls ():
590+ return False
591+ urls = mime .urls ()
592+ if not urls :
593+ return False
594+ local = urls [0 ].toLocalFile ()
595+ return bool (local ) and local .lower ().endswith (".json" )
0 commit comments