В прошлом выпуске был сделан локальный менеджер памяти для создания элементов интерфейса:
Само создание – именно на лету, а не заранее – нужно в принципе, но изначально потребовалось для реализации выпадающего списка (он же комбобокс). Выпадающий список в спокойном состоянии это поле с кнопкой:
Но стоит его потревожить, у него появляются новые элементы:
Это во-первых простыня со списком, и во-вторых, скроллбар (ползунок и две кнопки), если список слишком велик.
Теоретически можно было бы все эти элементы сразу включить в состав комбобокса, и просто делать их видимыми или невидимыми.
Но есть нюанс
В уже существующей реализации каждый элемент имеет свой ограничивающий прямоугольник, и прямоугольники детей находятся внутри прямоугольников родителей. Обработка событий иерархична. Чтобы попасть курсором мыши в потомка, нужно сначала попасть в его родителя.
Теперь посмотрим: прямоугольник комбобокса представляет собой узкую полоску, а прямоугольник простыни находится вообще за его пределами, то есть в мы в него просто не попадём при текущих правилах обработки.
При раскрытии комбобокса можно менять его прямоугольник так, чтобы он вмещал и простыню тоже, при закрытии – возвращать обратно.
Но есть нюанс
Предположим, после комбобокса в окно были добавлены ещё элементы:
В иерархии рисования они находятся поверх комбобокса и всех его детей, и при раскрытии комбобокса получим такую картину:
Хмм... Ну, тогда при раскрытии комбобокса можно перемещать его выше по иерархии... Наверное да, но это уже становится как-то сложно.
Радикальное решение
Простыня добавляется не как потомок комбобокса, а как вообще новый, отдельный корневой GUI-элемент. Таким образом гарантируется, что добавленная простыня будет всегда находиться поверх всех вообще элементов. Её нужно только спозиционировать так, чтобы она находилась под комбобоксом:
Но вот что получается, если перетащить окно:
Окно сдвинулось, а простыня осталась там, где и была, так как она полностью независима.
Обращу внимание, что если простыню добавить как потомка непосредственно окна, то она также нарисуется выше всех в этом окне, и более того, будет перемещаться вместе с окном.
Но посмотрите, как работают стандартные комбобоксы – они просто никогда никуда не перемещаются в открытом состоянии. Любое событие вне их приводит к их закрытию.
Поэтому насчёт перемещения можно не беспокоиться. В любом случае простыня должна исчезнуть сразу, как только мы обратимся к другому элементу.
Помещение простыни в список диспетчера в виде отдельного дерева со своим корневым UID кажется мне мне более простым вариантом как для добавления (не надо модифицировать дерево окна), так и для последующего удаления.
Подобным поведением обладает не только комбобокс. Есть, к примеру, выпадающие меню (по механике те же комбобоксы) и всплывающие подсказки, которые также появляются поверх всего и пропадают, как только фокус сместился на другой элемент. Возможно, у таких элементов есть специальное название, но я пойду своим путём и назову их волатильными.
Новая работа для диспетчера
Когда диспетчер обрабатывает поступившее событие, он должен отслеживать, был ли открыт какой-либо волатильный элемент, и в случае чего закрывать его. Сделать это довольно просто, так как такой элемент может быть только один, и расположен он будет всегда в конце списка, так что даже убирать за ним ничего не придётся.
Обработку события в диспетчере я в очередной раз структурно переиначил. Теперь я предварительно получаю комбинацию состояний, состоящую из условий:
0: Событие никуда не попало, и до этого ничего не было
1: Событие никуда не попало, но до этого что-то было
2: Событие куда-то попало, но до этого ничего не было
3: Событие куда-то попало, и до этого что-то было
Эти четыре состояния позволяют более упорядоченно пройтись по условиям и каждую ситуацию обработать более понятным образом, чем в предыдущем варианте.
Дополнительно я вычисляю состояние "тот же самый элемент". Это когда событие попало в тот же элемент, который был до этого. Если же это не так, то что-то поменялось и именно в этот момент можно проверить наличие волатильных элементов и удалить их при необходимости.
Для настройки волатильного элемента и типа события, которое должно его уничтожить, в диспетчере создам два дополнительных поля:
Поле volatile_index не обязательно должно соответствовать самому волатильному элементу. Это может быть любой элемент, потеря фокуса которого приводит к удалению волатильного элемента. То есть он работает как триггер. А сам волатильный элемент запоминать не надо, так как он просто последний в списке.
Дальнейшая схема такая: при клике на открывающей кнопке комбобокса создаётся элемент "простыня" GUI_ItemList, и добавляется в диспетчер как совершенно независимое дерево:
Но есть нюанс
Сразу забегу вперёд и скажу, что так оно нифига работать не будет, и я аж несколько дней раздумывал над проблемой. Она заключается вот в чём:
При нажатии на кнопку появляется простыня. При повторном нажатии на кнопку она должна исчезать, но... Стандартное нажатие на кнопку это когда нажали и отпустили. А уже при нажатии сработает удаление волатильного элемента и простыня уже исчезнет. Можно сказать – ну и что такого, она же всё равно должна была исчезнуть. Да, но... надо ведь ещё отпустить кнопку. И когда мы её отпустим, простыня снова появится, так как до этого она исчезла!
Не буду здесь вдаваться в долгие объяснения, просто поверьте – была очень острая и нерешаемая проблема синхронизации состояний комбобокса для разных сценариев, таких как открытие и закрытие комбобокса, открытие одного комбобокса, а затем открытие другого и т.п. Если работало что-то одно, тогда не работало другое, и наоборот. Это не решалось вообще никак, никакими дополнительными статусами, потому что в самой логике был изъян.
Группа сообщников
Пришлось сесть и расписать, как вообще происходит взаимодействие элементов. Например, сама простыня может содержать ползунок и кнопки, и кликая на них, мы точно так же уведём фокус с волатильного элемента и он исчезнет. В случае с потомками можно доходить до родителя и проверять его на волатильность и не давать ему исчезать. Но также можно кликать на кнопку в комбобоксе или сам комбобокс, которые не являются родителями простыни. Что делать, как это всё объединить?
В общем случае примем, что вообще любые элементы, независимо ни от какой иерархии и в любом количестве, могут быть связаны с волатильным элементом так, что при событиях на этих элементах волатильный не должен уничтожаться.
Я решил это следующим образом:
- Ввёл новый статус GUI_STATUS_VOLATILE.
- Когда создаётся волатильный элемент, то сущность, ответственная за его создание, проставляет статус GUI_STATUS_VOLATILE этому элементу и всем его потомкам, а также всем другим элементам, которым необходимо (в нашем случае комбобоксу и его потомкам).
- Когда волатильный элемент теряет фокус, мы смотрим, какой элемент его получил. Если у этого элемента есть статус GUI_STATUS_VOLATILE, то он "свой", и тогда волатильный элемент мы не удаляем.
Таким образом, все необходимые элементы организуются в группу сообщников.
Посмотрим, как это сделать. Для начала, функция, которая проставляет статус элементу и его потомкам:
Это легко, так как у каждого элемента указано количество всех вложенных потомков.
Далее, что проиcходит при нажатии на кнопку в комбобоксе? Она отрабатывает как стандартная кнопка, и посылает своему родителю-комбобоксу команду на открытие GUI_CMD_OPEN. Вот как обрабатывает её комбобокс:
Практически как раньше, только добавилось проставление статуса GUI_STATUS_VOLATILE самому комбобоксу с детьми и созданной простыне с её возможными детьми. Также комбобоксу проставляется статус GUI_STATUS_SELECTED, чтобы пометить его как открытый. Если же он уже находился в данном статусе, то повторное нажатие на кнопку приведёт к выполнению ветки с GUI_dispatcher_remove_volatile().
Вот как удаляется волатильный элемент:
Он удаляется простой модификацией индексов и освобождением занятой памяти (GUI_dispatcher_remove_tree()), так как в конце списка ничего передвигать не надо. Но перед этим вызывается специфическая функция завершения в зависимости от типа удаляемого элемента. На данный момент этот тип только один – GUI_ITEM_LIST – и вызывается функция GUI_itemlist_close(). Зачем?
Дело в том, что когда мы открыли простыню, а затем кликнули куда-то в другое место, то простыню просто так удалить нельзя, потому что комбобокс в данный момент имеет статус открытого. Чтобы в следующий раз всё сработало корректно, нужно вернуть его в статус закрытого. Поэтому и нужна финализирующая функция:
Она ничего не меняет напрямую в комбобоксе, потому что не должна знать, как он устроен. Вместо этого она формирует и посылает команду закрытия, чтобы комбобокс разобрался сам.
Ну он и разбирается сам:
Он очищает свой статус открытого и снимает с себя и потомков статусы волатильности.
Функция GUI_dispatcher_remove_volatile() также вызывается диспетчером тогда, когда у текущего целевого элемента события отсутствует статус волатильности.
Вот таким вот довольно затейливым способом удалось победить данную конструкцию.
Я не сильно доволен получившимся результатом, но надеюсь, больше таких затруднений не будет.
Код на гитхабе:
Видео с демонстрацией: