Интерфейс данного приложения построен путем реверс-инжиниринга оригинального эмулятора БЭВМ
Весь интерфейс общего интерфейса строится на элементах View.
Вот пример из оригинальной программы:
public class BasicView extends BCompPanel {
private final CPU cpu;
private final RunningCycleView cycleview;
public BasicView(GUI gui) {
super( ... );
this.cpu = gui.getCPU();
this.setSignalListeners(new SignalListener[0]);
this.add(new ALUView(DisplayStyles.REG_C_X_BV, 245, 181, 90));
this.cycleview = new RunningCycleView(this.cpu, DisplayStyles.REG_INSTR_X_BV, 245);
this.add(this.cycleview);
}
public void panelActivate() {
this.cycleview.update();
super.panelActivate();
}
public String getPanelName() {
return "Базовая ЭВМ";
}
...
}
Следует отметить это:
public abstract class BCompPanel extends ActivateblePanel
Можно либо наследоваться от BcompPanel или же сразу от ActivateblePanel, так как все элементы добавляются в общий массив ActivateblePanel[].
Из оригинальной БЭВМ от BcompPanel наследуются: BasicView(первое окно), IOView(окно ввода\вывода), MPView(МПУ)
А от ActivateblePanel напрямую: AssemblerView
Для добавления кастомных окон используется ActivateblePanel.
Если коротко, то элементы добавляются в методе GUI.init():
public void init() {
this.cmanager = new ComponentManager(GUI.this);
this.bcomp.startTimer();
basicView = new BasicView(this);
ioView = new IOView(this, this.pairgui);
panes = new ActivateblePanel[]{
basicView, // кастомизированные view
ioView,
new MPView(this),
new AssemblerView(this),
new CheatSheetView(this),
new MemoryView(this),
new ConsoleView(this),
new SettingsView(this)
};
...
Как можно отсюда заметить в каждый view - элемент передается экземпляр класс GUI. Это очень важно так как с помощью него мы сможем получить доступ к внутренним объектам эмулятора таким как память или регистры.
Как говорилось ранее, новый view должен наследоваться от ActivateblePanel и желательно иметь конструктор, принимающий в качестве аргумента элемент GUI, чтобы получить или редактировать внутренние данные приложения.
Пример View-элемента:
public class SettingsView extends ActivateblePanel {
private final GUI gui;
Image img ;
public SettingsView(GUI aGui){
this.gui = aGui;
setSleepTimeSettings(0, 0);
JButton activeBusColorChooserBtn = new JButton("Выбрать цвет для активной стрелки");
JButton busColorChooserBtn = new JButton("Выбрать цвет стрелок");
busColorChooserBtn.addActionListener(a-> createColorChooseWindow(0));
activeBusColorChooserBtn.addActionListener(a-> createColorChooseWindow(1));
JButton backgroundSelectBtn = new JButton("Выбрать фоновое изображение");
backgroundSelectBtn.addActionListener( a -> createFileChooseWindow());
JButton setDefaultBtn = new JButton("Установить настройки по умолчанию");
setDefaultBtn.addActionListener(a -> Settings.setDefault());
activeBusColorChooserBtn.setBounds(20, 30, 250, 30);
busColorChooserBtn.setBounds(20 + 250, 30, 250, 30);
backgroundSelectBtn.setBounds(20, 60, 500, 30);
setDefaultBtn.setBounds(20, 100, 500 , 30);
this.add(activeBusColorChooserBtn);
this.add(busColorChooserBtn);
this.add(backgroundSelectBtn);
this.add(setDefaultBtn);
}
private void setSleepTimeSettings(int x, int y){
JLabel label = new JLabel("Время в мс между тактами(по умолчанию ~6 мс): ");
JTextField value = new JTextField();
JButton submit = new JButton("Применить");
submit.addActionListener( (a) ->{
// ...
this.gui.getCPU().setTickFinishListener(() -> {
this.gui.stepFinishViewElements(); // строго до вызова метода sleep
try {
Thread.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
// some code ...
Здесь представлена часть элемента SettingsView, который позволяет изменять настройки.
В конструкторе мы создаем и отрисовываем нужные нам элементы (кнопки, поля ввода и пр).
Затем на элементы кнопок типа "submit" мы добавляем ActionListener, чтобы обрабатывать нажатие кнопки.
Здесь можно увидеть бизнес-логику кнопки для установки тактовой частоты. Делается это через поле gui, определенное как объект, переданный через конструктор. С помощью него можно получить CPU и установить Runnable объект-функцию для операции после завершения такта.
Два метода наследованных от ActivateblePanel - это методы вызываемые при отркытии панели и его закрытии.
В оригинальной БЭВМ элементы создается в конструкторе, а не при открытии панели, что оправданно, так как элементы - не сложные и не нагружают приложение даже будучи отрисованными.
Поэтому во всех View - элементы создаются в его конструкторе. В оригинальном приложении они также используются для передачи фокуса какому-нибудь управляющему элементу, например, клавишный регистр.
Листинг метода из AssemblerView:
public void panelActivate() {
this.text.requestFocus();
}
Но иногда приходилось вставлять туда, относительно, более сложные куски кода.
Наример, при создании ConsoleView (панель консоли) нужно было всегда проверять наличие ввода на стороне CLI-класса. Отсюда могла возникнуть излишняя нагрузка системы. Поэтому я прибегнул, как мне кажется, не совсем хорошей идее:
- Создается поле типа Thread, который циклически опрашивает наличие на новый ввод (создается в конструкторе)
- Когда панель активируется этот поток запускается
- При закртытии панели - прерывается
- Когда пользователь опять заходит в этот панель все начинается со второго пункта
@Override
public void panelActivate() {
// может возникнуть java.lang.IllegalThreadStateException
this.consoleInputField.requestFocus();
if (!cliThreadIsActive) {
newCLISession();
cliThread.start(); // запуск потока!
cliThreadIsActive = true;
this.gui.getCPU().setTickFinishListener(() -> {
if (cli.getSleep() > 0) {
try {
Thread.sleep((long) cli.getSleep());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
while (!outputStream.available()) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!initializedOnce) {
//newLine(outputStream.readString());
initializedOnce = true;
} else outputStream.readString();
}
}
@Override
public void panelDeactivate() {
cliThread.interrupt(); // прерывание
cliThreadIsActive = false;
this.gui.getCPU().setTickFinishListener(() -> {
try {
Thread.sleep(Settings.getTickFinishSleepTime());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
Трудности, которые встречались при разработке extended-версии эмулятора
Первая проблема с который я столкнулся - это было понятие механизма работы чужого не документированного кода. Приходилось иногда узнавать как что-то работает "методом тыка". Ранее я уже сталкивался с ней, когда разрабатывал эмулятор БЭВМ для бота, поэтому что-то базовое было.
Благодаря тому, что были хорошо названы поля и методы (классы) - я кое-как разобрался с логикой работы внешнего интерфейса эмулятора и поверхностно внутреннего устройства.
Вторая проблема была более непонятной, так как найти метод который устанавливает тактовую частоту было делом не из легких. Но в конце концов я нашел метод который вызывался при завершении одного такта.
Суть проблемы в том, что у консольной версии и у графической разные тактовые частоты. Для графического режима было важно наблюдать за изменениями элементов CPU. А в консольной версии необходимо было получить быструю трассировку данных.
На листинге выше вы можете наблюдать изменение тактовой частоты при активации и деактивации панели консоли.
Забавной, но критичной проблемой была отрисовка стрелок. Было два вида:
- Когда в первый раз написал свой метод вызываемый при окончании такта - стрелки перестали отображатся.
- При изменении настроек цвета, их цвет менялся только при активации
Первая проблема решилась добавлением в метод окончания такта функцию отрисовки стрелок (пришлось копаться глубоко внутри исходников)
Вторая проблема решилась также повторной отрисовкой стрелок при изменении настроек.