PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 All 35 releases
← All changes | includes/framework/class-app.php +151 -1 1.1.91.1.11 View file →
@@ -113,8 +113,16 @@
113 113 */
114 114 private $defaults = array();
115 115
116 116 /**
117 + * The admin menu this window answers for, and the tabs that ARE
118 + * that menu's pages. See {@see App::menu()}.
119 + *
120 + * @var array<string,mixed>|null
121 + */
122 + private $menu = null;
123 +
124 + /**
117 125 * @var callable|null
118 126 */
119 127 private $mount = null;
120 128
@@ -503,8 +511,110 @@
503 511 return $this;
504 512 }
505 513
506 514 /**
515 + * Declare the admin menu this window answers for, and the tabs
516 + * that ARE that menu's pages.
517 + *
518 + * A window that replaces an admin screen replaces its menu too:
519 + * whatever the window offers as a tab, the dock offers as a row,
520 + * with the same label and in the same order, and picking a row
521 + * opens the window on that tab. One declaration drives all three
522 + * halves of that:
523 + *
524 + * - the dock's submenu for `$slug` becomes these tabs (the first
525 + * is the menu's own page, so it becomes the tile's label);
526 + * - each row's URL is the menu's own tagged `os_tab=<id>`, which
527 + * the shell's remap reads back as the tab to open on;
528 + * - `tab` becomes declared state, written on mount and reopen;
529 + * - the tabs reach the client view as `menuTabs`, which its strip
530 + * renders from, so the two lists cannot drift.
531 + *
532 + * A tab's value is its label, or `array( 'label' => …, 'page' => … )`
533 + * naming the submenu slug it replaces (`user-new.php`). That page
534 + * is which of wp-admin's own rows this window answers for — the
535 + * rest are kept, so a plugin's page under this menu stays
536 + * reachable — and what the shell routes here from anywhere else.
537 + *
538 + * @param string $slug Admin menu slug, e.g. `users.php`.
539 + * @param array<string,mixed>|callable $tabs Ordered `id => label|array`, or a
540 + * callable returning one (for tabs
541 + * that depend on capabilities).
542 + * @param callable|null $enabled Optional gate — the opt-in that
543 + * decides whether this window answers
544 + * for the menu at all. Default: always.
545 + * @return self
546 + */
547 + public function menu( $slug, $tabs, $enabled = null ) {
548 + $this->menu = array(
549 + 'slug' => (string) $slug,
550 + 'tabs' => $tabs,
551 + 'enabled' => $enabled,
552 + );
553 + return $this;
554 + }
555 +
556 + /**
557 + * The declared menu's tabs for the CURRENT user, as an ordered
558 + * list of `array( 'id', 'label' )`. Empty when no menu is declared.
559 + *
560 + * Not gated: these are the window's tabs whoever opened it and
561 + * whatever the opt-in says, and the client view renders its strip
562 + * from them. The gate decides only whether the DOCK's submenu
563 + * becomes this list — see {@see App::menu_owns_dock()}.
564 + *
565 + * @return array<int,array<string,string>>
566 + */
567 + public function menu_tabs() {
568 + if ( ! $this->menu ) {
569 + return array();
570 + }
571 + $tabs = is_callable( $this->menu['tabs'] )
572 + ? (array) call_user_func( $this->menu['tabs'] )
573 + : (array) $this->menu['tabs'];
574 + $out = array();
575 + foreach ( $tabs as $id => $tab ) {
576 + $id = strtolower( (string) preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $id ) );
577 + $label = is_array( $tab ) ? (string) ( $tab['label'] ?? '' ) : (string) $tab;
578 + if ( '' === $id || '' === $label ) {
579 + continue;
580 + }
581 + $out[] = array(
582 + 'id' => $id,
583 + 'label' => $label,
584 + // The wp-admin submenu slug this tab stands in for,
585 + // '' for a tab wp-admin has no page for.
586 + 'page' => is_array( $tab ) ? (string) ( $tab['page'] ?? '' ) : '',
587 + );
588 + }
589 + return $out;
590 + }
591 +
592 + /**
593 + * The admin menu slug this window answers for, `''` when none.
594 + *
595 + * @return string
596 + */
597 + public function menu_slug() {
598 + return $this->menu ? (string) $this->menu['slug'] : '';
599 + }
600 +
601 + /**
602 + * Whether this window is the one answering for its menu right
603 + * now — the per-user opt-in that chooses between it and the
604 + * classic screen. Only then does the dock's submenu become the
605 + * window's tabs.
606 + *
607 + * @return bool
608 + */
609 + public function menu_owns_dock() {
610 + if ( ! $this->menu ) {
611 + return false;
612 + }
613 + return ! is_callable( $this->menu['enabled'] ) || (bool) call_user_func( $this->menu['enabled'] );
614 + }
615 +
616 + /**
507 617 * Runs once, before the first render, with the fresh state.
508 618 *
509 619 * @param callable $mount `function ( State $state, Os $os )`.
510 620 * @return self
@@ -781,8 +891,15 @@
781 891 $config = $this->config;
782 892 foreach ( $this->config_lazy as $callable ) {
783 893 $config = array_merge( $config, (array) call_user_func( $callable, $this ) );
784 894 }
895 + // The declared menu's tabs, for a client view that renders its
896 + // strip from them — the only way the strip and the dock's
897 + // submenu cannot drift, since both read this list.
898 + $tabs = $this->menu_tabs();
899 + if ( $tabs ) {
900 + $config['menuTabs'] = $tabs;
901 + }
785 902 return $config;
786 903 }
787 904
788 905 // ----------------------------------------------------------- readers
@@ -801,9 +918,19 @@
801 918 *
802 919 * @return array<string,mixed>
803 920 */
804 921 public function defaults() {
805 - return $this->defaults;
922 + if ( ! $this->menu || array_key_exists( 'tab', $this->defaults ) ) {
923 + return $this->defaults;
924 + }
925 + // Declared here rather than by every app that declares a menu:
926 + // the runtime writes this key from the `tab` open-time param,
927 + // and a key the schema does not carry would be dropped.
928 + $tabs = $this->menu_tabs();
929 + return array_merge(
930 + array( 'tab' => $tabs ? $tabs[0]['id'] : '' ),
931 + $this->defaults
932 + );
806 933 }
807 934
808 935 /**
809 936 * Whether an action is declared.
@@ -811,8 +938,9 @@
811 938 * @param string $name Action name.
812 939 * @return bool
813 940 */
814 941 public function has_action( $name ) {
942 + $this->ensure_menu_reopen();
815 943 return isset( $this->actions[ (string) $name ] );
816 944 }
817 945
818 946 /**
@@ -820,12 +948,32 @@
820 948 *
821 949 * @return string[]
822 950 */
823 951 public function action_names() {
952 + $this->ensure_menu_reopen();
824 953 return array_keys( $this->actions );
825 954 }
826 955
827 956 /**
957 + * A window that declares a menu answers `reopen`, whether or not
958 + * it wrote a handler for one.
959 + *
960 + * The client dispatches a lifecycle action only when the manifest
961 + * says the app declared it, so without this the runtime's own
962 + * "land on the tab the opener named" never runs on a window that
963 + * is already open — the case a dock row for another tab IS.
964 + * Registered here rather than in {@see App::menu()} so an app's
965 + * own actions keep the order they were declared in.
966 + *
967 + * @return void
968 + */
969 + private function ensure_menu_reopen() {
970 + if ( $this->menu && ! isset( $this->actions['reopen'] ) ) {
971 + $this->actions['reopen'] = static function () {};
972 + }
973 + }
974 +
975 + /**
828 976 * Run the mount hook, if any.
829 977 *
830 978 * @param State $state State.
831 979 * @param Os $os Host handle.
@@ -1017,8 +1165,10 @@
1017 1165 'title_bar_buttons' => $this->title_bar_buttons,
1018 1166 'window_actions' => $this->window_actions,
1019 1167 'appearance' => $this->appearance,
1020 1168 'config' => $this->resolved_config(),
1169 + 'menu' => $this->menu_slug(),
1170 + 'menu_tabs' => $this->menu_tabs(),
1021 1171 'tabs' => $this->tabs(),
1022 1172 'channels' => $this->channels,
1023 1173 'watch' => $this->watch,
1024 1174 'client' => $this->client_path(),