Skip to content

Commit

Permalink
Fix UI bugs (#96)
Browse files Browse the repository at this point in the history
* Update default.qss to better support for Cleanlooks theme in PyQt6

- Emptied default.qss to allow system dark and light modes to apply properly.
- Ensures compatibility with Cleanlooks theme in PyQt6.

* Update .gitignore to exclude .idea directory

- Added .idea/ to .gitignore to prevent JetBrains IDE configuration files from being tracked.
- Helps keep the repository clean and focused on source code.

* Fix typos and update default window dimensions in cemu.ini

- Corrected typos in the configuration file.
- Updated default width and height values for better display of the window.

* Add exception handling for missing syscall files in cemu/arch/__init__.py

- Added exception handling to manage cases where syscall files do not exist for specific architectures (e.g., Generic).
- Display a PyQt6 QMessageBox with the exception message to inform the user.

* Add function to determine dark mode in cemu/ui/utils.py

- Added is_dark_mode function to check if the current palette is in dark mode.
- Uses QPalette to determine if the window color value is less than 128.

* Enhance RegisterWidget in cemu/ui/registers.py

- Changed column width from 60 to 80 in RegisterWidget class.
- Improves the display of register names and values in the table.

* Enhance MemoryMappingWidget and fix typos in cemu/ui/mapping.py

- Increased column widths to 120 for better display in MemoryMappingWidget.
- Set fixed width of MemoryMapTableWidget to 350.
- Corrected typo in permission checkbox label from 'eXecute' to 'Execute'.

* Add tooltips to CommandWidget buttons in cemu/ui/command.py

- Enhanced CommandWidget to show tooltips when hovering over QPushButtons.
- Tooltips provide additional context for each button's functionality.

* Fix crashes and add error handling in cemu/emulator.py

- Wrapped self.setup() method in try-except to handle exceptions gracefully.
- Added QMessageBox to display error messages when setup fails.
- Removed unnecessary reassignment of self.codelines in reset method to prevent crashes.
- Modified next_instruction method to return Optional, returning None when no instructions remain instead of raising an exception.

* Enhance AssemblyView and fix bugs in cemu/ui/codeeditor.py

- Increased fixed width of AssemblyView from 140 to 230 for better display.
- Removed background color stylesheet for better harmony in dark mode.
- Fixed bug in update_assembly_code_pane method to correctly handle line returns on Windows by using '\n' instead of os.linesep.

* Organize imports and remove unnecessary return statements in cemu/ui/highlighter.py

- Organized imports for better readability and maintenance.
- Removed unneeded return statements at the end of None type methods.

* Enhance dock widget positions, implement StartInFullScreen, and fix various bugs in cemu/ui/main.py

- Enhanced dock widget positions for better user experience in CEmuWindow class.
- Implemented StartInFullScreen with global settings stored in ini file.
- Changed default window width and height to 800x600 instead of 1600x800.
- Fixed bug in Architecture menu for proper switching between x86 AT&T and Intel syntax in different bits (16, 32, 64).
- Fixed bug in showShortcutPopup method with incorrect string format syntax.
- Fixed crash in EmulationRunner class method run when no instructions remain.

* Enhance code highlighting in dark mode

- Add various functions for ui dark mode in cemu/ui/utils.py
- Improve syntax highlighting for ui dark mode in cemu/ui/highlighter.py

* Update README.md

- Added new screenshots for GUI

* Add type hints and some minor changes to improve code readability
  • Loading branch information
LuLuKitty1998 authored Jul 30, 2024
1 parent d45b14b commit 4cad30a
Show file tree
Hide file tree
Showing 15 changed files with 190 additions and 117 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ __pycache__
.coverage*
build
dist
/.idea
/.venv
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ python -m cemu

This should produce a GUI similar to this:

![cemu-gui](https://i.imgur.com/iHtWvTL.png)
![cemu-gui](images/screenshots/cemu-windows-light.png)
![cemu-gui](images/screenshots/cemu-windows-dark.png)

### In the terminal

Expand Down
9 changes: 8 additions & 1 deletion cemu/arch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import unicorn

from cemu.const import SYSCALLS_PATH
from ..ui.utils import popup, PopupType

if TYPE_CHECKING:
import cemu.core
Expand Down Expand Up @@ -75,7 +76,13 @@ def syscalls(self):

if not self.__syscalls:
syscall_dir = SYSCALLS_PATH / str(self.__context.os)
fpath = syscall_dir / (self.syscall_filename + ".csv")

try:
fpath = syscall_dir / (self.syscall_filename + ".csv")
except ValueError as e:
popup(str(e), PopupType.Error, "No Syscall File Error")
return {}

self.__syscalls = {}
if fpath.exists():
with fpath.open("r") as fd:
Expand Down
20 changes: 10 additions & 10 deletions cemu/emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,22 @@
from typing import Any, Callable, Optional

import unicorn
from PyQt6.QtWidgets import QMessageBox

import cemu.const
import cemu.core
from cemu.exceptions import CemuEmulatorMissingRequiredSection
import cemu.os
from cemu.ui.utils import popup
import cemu.utils

from cemu.log import dbg, error, info, warn

from cemu.const import (
MEMORY_TEXT_SECTION_NAME,
MEMORY_DATA_SECTION_NAME,
MEMORY_STACK_SECTION_NAME,
)

from cemu.exceptions import CemuEmulatorMissingRequiredSection
from cemu.log import dbg, error, info, warn
from .arch import is_x86, is_x86_32, x86
from .memory import MemorySection
from .ui.utils import popup, PopupType


@unique
Expand Down Expand Up @@ -104,7 +102,6 @@ def __init__(self):
def reset(self):
self.vm = None
self.code = b""
self.codelines = ""
self.sections = MEMORY_MAP_DEFAULT_LAYOUT[:]
self.registers = EmulationRegisters(
{name: 0 for name in cemu.core.context.architecture.registers}
Expand Down Expand Up @@ -378,14 +375,14 @@ def __populate_text_section(self) -> bool:
self.vm.mem_write(text_section.address, self.code)
return True

def next_instruction(self, code: bytes, addr: int) -> cemu.utils.Instruction:
def next_instruction(self, code: bytes, addr: int) -> Optional[cemu.utils.Instruction]:
"""
Returns a string disassembly of the first instruction from `code`.
"""
for insn in cemu.utils.disassemble(code, 1, addr):
return insn

raise Exception("should never be here")
return None

def hook_code(
self, emu: unicorn.Uc, address: int, size: int, user_data: Any
Expand Down Expand Up @@ -514,7 +511,10 @@ def set(self, new_state: EmulatorState):
#
# Make sure there's always an emulation environment ready
#
self.setup()
try:
self.setup()
except Exception as e:
popup(str(e), PopupType.Error, "Emulator setup error")

#
# If we stopped from execution (i.e RUNNING -> [IDLE,FINISHED]), refresh registers
Expand Down
10 changes: 0 additions & 10 deletions cemu/styles/default.qss
Original file line number Diff line number Diff line change
@@ -1,10 +0,0 @@
QMainWindow,
QWidget {
background-color: darkgray;
}

QTextEdit,
QLineEdit,
QTableWidget {
background-color: white;
}
6 changes: 3 additions & 3 deletions cemu/templates/cemu.ini
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ DefaultSyntax = Intel
#
# Default window size for cemu
#
WindowWidth = 1600
WindowHeigth = 800
WindowWidth = 800
WindowHeight = 600


#
# Whether cemu should start in full screen
#
StartInFullScreen = false
StartInFullScreen = False


#
Expand Down
36 changes: 11 additions & 25 deletions cemu/ui/codeeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import typing
from typing import Optional

from PyQt6.QtCore import Qt, QVariant
from PyQt6.QtGui import QFont, QTextFormat
Expand All @@ -21,7 +22,7 @@
DEFAULT_ASSEMBLY_VIEW_FONT,
DEFAULT_ASSEMBLY_VIEW_FONT_SIZE,
DEFAULT_CODE_VIEW_FONT,
DEFAULT_CODE_VIEW_FONT_SIZE,
DEFAULT_CODE_VIEW_FONT_SIZE
)
from cemu.log import error

Expand All @@ -34,30 +35,22 @@


class CodeEdit(QTextEdit):
def __init__(self, parent):
def __init__(self, parent: Optional[QWidget] = None):
super(CodeEdit, self).__init__(parent)
self.cursorPositionChanged.connect(self.UpdateHighlightedLine)
self.setFont(
QFont(DEFAULT_CODE_VIEW_FONT, pointSize=DEFAULT_CODE_VIEW_FONT_SIZE)
)
self.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
return

def UpdateHighlightedLine(self):
selection = QTextEdit.ExtraSelection()
selection.format.setBackground(self.palette().alternateBase())
selection.format.setProperty(
QTextFormat.Property.FullWidthSelection, QVariant(True)
)
selection.format.setProperty(QTextFormat.Property.FullWidthSelection, QVariant(True))
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
self.setExtraSelections(
[
selection,
]
)
return
self.setExtraSelections([selection])


class AssemblyView(QTextEdit):
Expand All @@ -66,9 +59,9 @@ def __init__(self, parent, editor: CodeEdit):
self.rootWindow = parent.rootWindow
self.setReadOnly(True)
self.setFont(QFont(DEFAULT_ASSEMBLY_VIEW_FONT, DEFAULT_ASSEMBLY_VIEW_FONT_SIZE))
self.setFixedWidth(140)
self.setFixedWidth(230)
self.setFrameStyle(QFrame.Shape.Panel | QFrame.Shape.NoFrame)
self.setStyleSheet("background-color: rgb(211, 211, 211);")
# self.setStyleSheet("background-color: rgb(211, 211, 211);")
self.editor = editor
self.editor.textChanged.connect(self.update_assembly_code_pane)
self.__last_assembly_error_msg = ""
Expand All @@ -80,7 +73,7 @@ def update_assembly_code_pane(self):
#
text: str = self.editor.toPlainText()
cur: int = self.editor.textCursor().position()
if cur < 1 or text[cur - 1] != os.linesep:
if cur < 1 or text[cur - 1] != '\n':
return

#
Expand All @@ -89,8 +82,8 @@ def update_assembly_code_pane(self):
pane_width = self.width() // 10
lines: list[str] = text.splitlines()
bytecode_lines: list[str] = [
"",
] * pane_width
"",
] * pane_width
assembly_failed_lines: list[int] = []

for i in range(len(lines)):
Expand Down Expand Up @@ -121,7 +114,6 @@ def update_assembly_code_pane(self):
self.__last_assembly_error_msg = msg

self.setText(os.linesep.join(bytecode_lines))
return


class CodeWithAssemblyFrame(QFrame):
Expand All @@ -135,7 +127,6 @@ def __init__(self, parent: CodeEditorFrame):
layout.addWidget(self.__asm_widget)
layout.addWidget(self.__code_widget)
self.setLayout(layout)
return

@property
def code_editor(self):
Expand All @@ -152,7 +143,6 @@ def __init__(self, parent: CodeWidget):
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.addWidget(inner_frame)
return


class CodeWidget(QDockWidget):
Expand All @@ -172,20 +162,16 @@ def __init__(self, parent, *args, **kwargs):
widget = QWidget(self)
widget.setLayout(layout)
self.setWidget(widget)
return

def onUpdateText(self):
cemu.core.context.emulator.codelines = self.getCleanContent()
return

def onCursorPositionChanged(self):
self.UpdateTitleLabel()
return

def UpdateTitleLabel(self):
row_num, col_num = get_cursor_position(self.editor)
self.widget_title_label.setText(f"Code (Line:{row_num+1} Column:{col_num+1})")
return
self.widget_title_label.setText(f"Code (Line:{row_num + 1} Column:{col_num + 1})")

def getCleanContent(self) -> str:
"""
Expand Down
12 changes: 1 addition & 11 deletions cemu/ui/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs):
button = QPushButton(buttons[name].label)
button.clicked.connect(buttons[name].on_click)
button.setShortcut(sc.shortcut(buttons[name].shortcut))
button.setToolTip(name)
self.buttons[name] = button
layout.addWidget(self.buttons[name])

Expand All @@ -71,7 +72,6 @@ def __init__(self, parent: CEmuWindow, *args, **kwargs):
self.emulator.add_state_change_cb(
EmulatorState.FINISHED, self.onFinishedUpdateCommandButtons
)
return

def onClickRunAll(self) -> None:
"""
Expand All @@ -81,7 +81,6 @@ def onClickRunAll(self) -> None:
self.emulator.use_step_mode = False
# self.emulator.stop_now = False
self.emulator.set(EmulatorState.RUNNING)
return

def onClickStepNext(self) -> None:
"""
Expand All @@ -90,20 +89,17 @@ def onClickStepNext(self) -> None:
self.emulator.use_step_mode = True
# self.emulator.stop_now = False
self.emulator.set(EmulatorState.RUNNING)
return

def onClickStop(self):
"""
Callback function for "Stop execution"
"""
if not self.emulator.is_running:
popup("Emulator is not running...")
return

dbg("Stopping emulation...")
self.emulator.set(EmulatorState.FINISHED)
info("Emulation context has stopped")
return

def onClickReset(self):
"""
Expand All @@ -115,7 +111,6 @@ def onClickReset(self):
dbg("Tearing down VM on user demand...")
self.emulator.set(EmulatorState.TEARDOWN)
info("Emulation context reset")
return

def onClickCheckCode(self) -> None:
"""
Expand All @@ -136,36 +131,31 @@ def onClickCheckCode(self) -> None:
popup_style = PopupType.Error

popup(msg, popup_style, title=title)
return

def onRunningUpdateCommandButtons(self) -> None:
"""On `running` state, disable all buttons"""
self.buttons["run"].setDisabled(True)
self.buttons["step"].setDisabled(True)
self.buttons["stop"].setDisabled(True)
self.buttons["reset"].setDisabled(True)
return

def onNotRunningUpdateCommandButtons(self) -> None:
"""On `not running` state, we can do all but stop"""
self.buttons["run"].setDisabled(False)
self.buttons["step"].setDisabled(False)
self.buttons["stop"].setDisabled(True)
self.buttons["reset"].setDisabled(True)
return

def onIdleUpdateCommandButtons(self) -> None:
"""On `idle` state, we can either step more, run all or stop"""
self.buttons["stop"].setDisabled(False)
self.buttons["step"].setDisabled(False)
self.buttons["run"].setDisabled(False)
self.buttons["reset"].setDisabled(True)
return

def onFinishedUpdateCommandButtons(self):
"""In the finished state, we can only completely reset the emulation context"""
self.buttons["run"].setDisabled(True)
self.buttons["step"].setDisabled(True)
self.buttons["stop"].setDisabled(True)
self.buttons["reset"].setDisabled(False)
return
Loading

0 comments on commit 4cad30a

Please sign in to comment.