Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2b06775c5 |
No files matched your search
@@ -1,117 +0,0 @@
|
|||||||
name: Build and Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main", "dev" ]
|
|
||||||
tags: [ "v*" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main", "dev" ]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-linux:
|
|
||||||
name: Build Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install System Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libxkbcommon-x11-0 \
|
|
||||||
libxcb-icccm4 \
|
|
||||||
libxcb-image0 \
|
|
||||||
libxcb-keysyms1 \
|
|
||||||
libxcb-randr0 \
|
|
||||||
libxcb-render-util0 \
|
|
||||||
libxcb-xinerama0 \
|
|
||||||
libxcb-xinput0 \
|
|
||||||
libxcb-xfixes0 \
|
|
||||||
libxcb-shape0 \
|
|
||||||
libgl1 \
|
|
||||||
libegl1 \
|
|
||||||
libdbus-1-3 \
|
|
||||||
libx11-xcb1
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.10'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
- name: Build Executable
|
|
||||||
run: |
|
|
||||||
python build_app.py
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: TeletextEditor-Linux
|
|
||||||
path: dist/TeletextEditor_Linux
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
name: Build Windows
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: tobix/pywine:3.10
|
|
||||||
steps:
|
|
||||||
- name: Install Node.js
|
|
||||||
run: |
|
|
||||||
apt-get clean
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --fix-missing nodejs npm
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
wine python -m pip install --upgrade pip
|
|
||||||
wine pip install -r requirements.txt
|
|
||||||
|
|
||||||
- name: Build Executable
|
|
||||||
run: |
|
|
||||||
wine python build_app.py
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: TeletextEditor-Windows
|
|
||||||
path: dist/TeletextEditor_Windows.exe
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
needs: [build-linux, build-windows]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
steps:
|
|
||||||
- name: Download Linux Artifact
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: TeletextEditor-Linux
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
- name: Download Windows Artifact
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: TeletextEditor-Windows
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
dist/TeletextEditor_Linux
|
|
||||||
dist/TeletextEditor_Windows.exe
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Build Linux
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Linux
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install System Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libxkbcommon-x11-0 \
|
||||||
|
libxcb-icccm4 \
|
||||||
|
libxcb-image0 \
|
||||||
|
libxcb-keysyms1 \
|
||||||
|
libxcb-randr0 \
|
||||||
|
libxcb-render-util0 \
|
||||||
|
libxcb-xinerama0 \
|
||||||
|
libxcb-xinput0 \
|
||||||
|
libxcb-xfixes0 \
|
||||||
|
libxcb-shape0 \
|
||||||
|
libgl1 \
|
||||||
|
libegl1 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libx11-xcb1
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Build Executable
|
||||||
|
run: |
|
||||||
|
pyinstaller --onefile --windowed --name TeletextEditor_Linux --paths src --add-data "app_icon.png:." src/main.py
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: TeletextEditor-Linux
|
||||||
|
path: dist/TeletextEditor_Linux
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: Build Windows
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Windows
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: tobix/pywine:3.10
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: |
|
||||||
|
apt-get clean
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --fix-missing nodejs npm
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
wine python -m pip install --upgrade pip
|
||||||
|
wine pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Build Executable
|
||||||
|
run: |
|
||||||
|
wine pyinstaller --onefile --windowed --hidden-import=pkgutil --hidden-import=PyQt6.sip --collect-all PyQt6 --name TeletextEditor_Windows.exe --paths src --add-data "app_icon.png;." --icon=app_icon.ico src/main.py
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: TeletextEditor-Windows
|
||||||
|
path: dist/TeletextEditor_Windows.exe
|
||||||
+4
-6
@@ -11,11 +11,9 @@ def clean_build_dirs():
|
|||||||
print(f"Cleaning {d}...")
|
print(f"Cleaning {d}...")
|
||||||
shutil.rmtree(d)
|
shutil.rmtree(d)
|
||||||
|
|
||||||
for f in os.listdir("."):
|
spec_file = "TeletextEditor_Linux.spec" if platform.system() == "Linux" else "TeletextEditor_Windows.spec"
|
||||||
if f.endswith(".spec"):
|
if os.path.exists(spec_file):
|
||||||
print(f"Removing {f}...")
|
os.remove(spec_file)
|
||||||
os.remove(f)
|
|
||||||
|
|
||||||
|
|
||||||
def build():
|
def build():
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
@@ -24,7 +22,7 @@ def build():
|
|||||||
# Determine separator for --add-data
|
# Determine separator for --add-data
|
||||||
sep = ";" if system == "Windows" else ":"
|
sep = ";" if system == "Windows" else ":"
|
||||||
|
|
||||||
# Base PyInstaller command configuration for Linux and Windows builds
|
# Base command
|
||||||
base_cmd = [
|
base_cmd = [
|
||||||
sys.executable, "-m", "PyInstaller",
|
sys.executable, "-m", "PyInstaller",
|
||||||
"--onefile",
|
"--onefile",
|
||||||
|
|||||||
@@ -86,6 +86,25 @@ class TeletextCanvas(QWidget):
|
|||||||
self.cursor_is_graphics = False # Tracked during draw
|
self.cursor_is_graphics = False # Tracked during draw
|
||||||
# Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere
|
# Blinking cursor timer could be added, for now static inverted is fine or toggle on timer elsewhere
|
||||||
|
|
||||||
|
# Highlight state (for search results)
|
||||||
|
self.highlight_row = -1
|
||||||
|
self.highlight_start_x = -1
|
||||||
|
self.highlight_end_x = -1
|
||||||
|
|
||||||
|
def set_highlight(self, row, start_x, end_x):
|
||||||
|
self.highlight_row = row
|
||||||
|
self.highlight_start_x = start_x
|
||||||
|
self.highlight_end_x = end_x
|
||||||
|
self.redraw()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def clear_highlight(self):
|
||||||
|
self.highlight_row = -1
|
||||||
|
self.highlight_start_x = -1
|
||||||
|
self.highlight_end_x = -1
|
||||||
|
self.redraw()
|
||||||
|
self.update()
|
||||||
|
|
||||||
def get_byte_at(self, x, y):
|
def get_byte_at(self, x, y):
|
||||||
if not self.page: return 0
|
if not self.page: return 0
|
||||||
|
|
||||||
@@ -129,15 +148,14 @@ class TeletextCanvas(QWidget):
|
|||||||
packet = p
|
packet = p
|
||||||
break
|
break
|
||||||
|
|
||||||
if not packet:
|
if packet:
|
||||||
|
if 0 <= self.cursor_x < 40:
|
||||||
|
packet.data[self.cursor_x] = byte_val
|
||||||
|
self.redraw()
|
||||||
|
self.emit_cursor_change()
|
||||||
|
else:
|
||||||
# Create packet if missing
|
# Create packet if missing
|
||||||
packet = create_blank_packet(self.page.magazine, self.cursor_y)
|
self.handle_input(chr(byte_val)) # Lazy reuse or duplicate create logic
|
||||||
self.page.packets.append(packet)
|
|
||||||
|
|
||||||
if 0 <= self.cursor_x < 40:
|
|
||||||
packet.data[self.cursor_x] = byte_val
|
|
||||||
self.redraw()
|
|
||||||
self.emit_cursor_change()
|
|
||||||
|
|
||||||
def set_page(self, page: Page):
|
def set_page(self, page: Page):
|
||||||
self.page = page
|
self.page = page
|
||||||
@@ -385,7 +403,16 @@ class TeletextCanvas(QWidget):
|
|||||||
if draw_bg:
|
if draw_bg:
|
||||||
# If double height, draw taller background
|
# If double height, draw taller background
|
||||||
h_bg = self.cell_h * 2 if double_height else self.cell_h
|
h_bg = self.cell_h * 2 if double_height else self.cell_h
|
||||||
painter.fillRect(x, y, self.cell_w, h_bg, bg)
|
|
||||||
|
# Check for highlight
|
||||||
|
is_highlighted = (row == self.highlight_row and self.highlight_start_x <= c <= self.highlight_end_x)
|
||||||
|
|
||||||
|
if is_highlighted:
|
||||||
|
# Draw highlight color (e.g. Dark Yellow / Orange to contrast white text)
|
||||||
|
highlight_color = QColor(180, 150, 0)
|
||||||
|
painter.fillRect(x, y, self.cell_w, h_bg, highlight_color)
|
||||||
|
else:
|
||||||
|
painter.fillRect(x, y, self.cell_w, h_bg, bg)
|
||||||
|
|
||||||
# Draw Foreground
|
# Draw Foreground
|
||||||
if draw_fg:
|
if draw_fg:
|
||||||
|
|||||||
+186
-29
@@ -3,13 +3,14 @@ from PyQt6.QtWidgets import (
|
|||||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton,
|
QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton,
|
||||||
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication,
|
QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication,
|
||||||
QCheckBox, QDialog, QGridLayout
|
QCheckBox, QDialog, QGridLayout, QRadioButton, QFrame
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
|
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
|
||||||
from PyQt6.QtCore import Qt, QRect, QTimer
|
from PyQt6.QtCore import Qt, QRect, QTimer, pyqtSignal
|
||||||
|
|
||||||
from .io import load_t42, save_t42, save_tti, decode_hamming_8_4, encode_hamming_8_4
|
from .io import load_t42, save_t42, save_tti, decode_hamming_8_4, encode_hamming_8_4
|
||||||
from .renderer import TeletextCanvas, create_blank_packet
|
from .renderer import TeletextCanvas, create_blank_packet
|
||||||
|
from .charsets import get_char
|
||||||
import copy
|
import copy
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -113,23 +114,60 @@ class MosaicDialog(QDialog):
|
|||||||
close_btn.clicked.connect(self.accept)
|
close_btn.clicked.connect(self.accept)
|
||||||
self.layout().addWidget(close_btn)
|
self.layout().addWidget(close_btn)
|
||||||
|
|
||||||
class HexLineEdit(QLineEdit):
|
class FindDialog(QDialog):
|
||||||
def keyPressEvent(self, event: QKeyEvent):
|
findNext = pyqtSignal(str, bool) # query, is_hex
|
||||||
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
findPrev = pyqtSignal(str, bool) # query, is_hex
|
||||||
event.accept()
|
|
||||||
self.returnPressed.emit()
|
|
||||||
return
|
|
||||||
elif event.key() == Qt.Key.Key_Escape:
|
|
||||||
event.accept()
|
|
||||||
main_win = self.window()
|
|
||||||
if hasattr(main_win, 'canvas'):
|
|
||||||
main_win.canvas.setFocus()
|
|
||||||
return
|
|
||||||
super().keyPressEvent(event)
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def __init__(self, parent=None):
|
||||||
super().mousePressEvent(event)
|
super().__init__(parent)
|
||||||
self.selectAll()
|
self.setWindowTitle("Find")
|
||||||
|
self.setFixedWidth(350)
|
||||||
|
self.setLayout(QVBoxLayout())
|
||||||
|
|
||||||
|
# Query Input
|
||||||
|
self.query_input = QLineEdit()
|
||||||
|
self.query_input.setPlaceholderText("Search text or hex (e.g. 1C 02)...")
|
||||||
|
self.layout().addWidget(QLabel("Find:"))
|
||||||
|
self.layout().addWidget(self.query_input)
|
||||||
|
|
||||||
|
# Mode Selection
|
||||||
|
mode_layout = QHBoxLayout()
|
||||||
|
self.radio_text = QRadioButton("Text")
|
||||||
|
self.radio_text.setChecked(True)
|
||||||
|
self.radio_hex = QRadioButton("Hex")
|
||||||
|
mode_layout.addWidget(self.radio_text)
|
||||||
|
mode_layout.addWidget(self.radio_hex)
|
||||||
|
mode_layout.addStretch()
|
||||||
|
self.layout().addLayout(mode_layout)
|
||||||
|
|
||||||
|
# Navigation Buttons
|
||||||
|
nav_layout = QHBoxLayout()
|
||||||
|
self.btn_prev = QPushButton("Previous")
|
||||||
|
self.btn_prev.clicked.connect(self.on_find_prev)
|
||||||
|
self.btn_next = QPushButton("Next")
|
||||||
|
self.btn_next.clicked.connect(self.on_find_next)
|
||||||
|
self.btn_next.setDefault(True)
|
||||||
|
nav_layout.addWidget(self.btn_prev)
|
||||||
|
nav_layout.addWidget(self.btn_next)
|
||||||
|
self.layout().addLayout(nav_layout)
|
||||||
|
|
||||||
|
# Status Label
|
||||||
|
self.status_label = QLabel("")
|
||||||
|
self.status_label.setStyleSheet("color: gray;")
|
||||||
|
self.layout().addWidget(self.status_label)
|
||||||
|
|
||||||
|
def on_find_next(self):
|
||||||
|
query = self.query_input.text()
|
||||||
|
if query:
|
||||||
|
self.findNext.emit(query, self.radio_hex.isChecked())
|
||||||
|
|
||||||
|
def on_find_prev(self):
|
||||||
|
query = self.query_input.text()
|
||||||
|
if query:
|
||||||
|
self.findPrev.emit(query, self.radio_hex.isChecked())
|
||||||
|
|
||||||
|
def set_status(self, text):
|
||||||
|
self.status_label.setText(text)
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -148,6 +186,11 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
|
self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]
|
||||||
|
|
||||||
|
# Search State
|
||||||
|
self.find_dialog = None
|
||||||
|
self.search_results = [] # List of (Page, row, col, length)
|
||||||
|
self.current_search_index = -1
|
||||||
|
|
||||||
# UI Components
|
# UI Components
|
||||||
self.central_widget = QWidget()
|
self.central_widget = QWidget()
|
||||||
self.setCentralWidget(self.central_widget)
|
self.setCentralWidget(self.central_widget)
|
||||||
@@ -167,7 +210,7 @@ class MainWindow(QMainWindow):
|
|||||||
# Hex Inspector
|
# Hex Inspector
|
||||||
hex_layout = QVBoxLayout()
|
hex_layout = QVBoxLayout()
|
||||||
hex_label = QLabel("Hex Value:")
|
hex_label = QLabel("Hex Value:")
|
||||||
self.hex_input = HexLineEdit()
|
self.hex_input = QLineEdit()
|
||||||
self.hex_input.setMaxLength(2)
|
self.hex_input.setMaxLength(2)
|
||||||
self.hex_input.setPlaceholderText("00")
|
self.hex_input.setPlaceholderText("00")
|
||||||
self.hex_input.returnPressed.connect(self.on_hex_entered)
|
self.hex_input.returnPressed.connect(self.on_hex_entered)
|
||||||
@@ -489,6 +532,13 @@ class MainWindow(QMainWindow):
|
|||||||
paste_action.triggered.connect(self.paste_page_content)
|
paste_action.triggered.connect(self.paste_page_content)
|
||||||
edit_menu.addAction(paste_action)
|
edit_menu.addAction(paste_action)
|
||||||
|
|
||||||
|
edit_menu.addSeparator()
|
||||||
|
|
||||||
|
find_action = QAction("Find...", self)
|
||||||
|
find_action.setShortcut("Ctrl+F")
|
||||||
|
find_action.triggered.connect(self.open_find_dialog)
|
||||||
|
edit_menu.addAction(find_action)
|
||||||
|
|
||||||
delete_page_action = QAction("Delete Page", self)
|
delete_page_action = QAction("Delete Page", self)
|
||||||
delete_page_action.triggered.connect(self.delete_page)
|
delete_page_action.triggered.connect(self.delete_page)
|
||||||
edit_menu.addAction(delete_page_action)
|
edit_menu.addAction(delete_page_action)
|
||||||
@@ -702,6 +752,118 @@ class MainWindow(QMainWindow):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Export Failed", f"Failed to export TTI: {e}")
|
QMessageBox.critical(self, "Export Failed", f"Failed to export TTI: {e}")
|
||||||
|
|
||||||
|
def open_find_dialog(self):
|
||||||
|
if not self.find_dialog:
|
||||||
|
self.find_dialog = FindDialog(self)
|
||||||
|
self.find_dialog.findNext.connect(lambda q, h: self.execute_search(q, h, forward=True))
|
||||||
|
self.find_dialog.findPrev.connect(lambda q, h: self.execute_search(q, h, forward=False))
|
||||||
|
self.find_dialog.finished.connect(self.canvas.clear_highlight)
|
||||||
|
|
||||||
|
self.find_dialog.show()
|
||||||
|
self.find_dialog.raise_()
|
||||||
|
self.find_dialog.activateWindow()
|
||||||
|
self.find_dialog.query_input.setFocus()
|
||||||
|
self.find_dialog.query_input.selectAll()
|
||||||
|
|
||||||
|
def execute_search(self, query, is_hex, forward=True):
|
||||||
|
# If query or mode changed, restart search
|
||||||
|
is_new_search = False
|
||||||
|
if not hasattr(self, '_last_query') or self._last_query != query or self._last_is_hex != is_hex:
|
||||||
|
is_new_search = True
|
||||||
|
self._last_query = query
|
||||||
|
self._last_is_hex = is_hex
|
||||||
|
self.search_results = []
|
||||||
|
|
||||||
|
if is_hex:
|
||||||
|
# Parse hex query: "1C 02" -> b'\x1c\x02'
|
||||||
|
try:
|
||||||
|
clean_hex = query.replace(" ", "").replace("0x", "")
|
||||||
|
search_bytes = bytes.fromhex(clean_hex)
|
||||||
|
except ValueError:
|
||||||
|
self.find_dialog.set_status("Invalid hex string")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
search_text = query.lower()
|
||||||
|
|
||||||
|
# Iterate all pages
|
||||||
|
for page in self.service.pages:
|
||||||
|
# Determine language for this page
|
||||||
|
lang = page.language
|
||||||
|
key = (page.magazine, page.page_number)
|
||||||
|
if key in self.language_overrides:
|
||||||
|
lang = self.language_overrides[key]
|
||||||
|
|
||||||
|
# Check each packet
|
||||||
|
for packet in page.packets:
|
||||||
|
if packet.row > 25: continue
|
||||||
|
|
||||||
|
if is_hex:
|
||||||
|
# Hex search in raw data
|
||||||
|
# Strip parity for comparison? Teletext usually uses 7-bit + parity.
|
||||||
|
# The search should probably be parity-agnostic.
|
||||||
|
raw_data = bytes([b & 0x7F for b in packet.data])
|
||||||
|
idx = raw_data.find(search_bytes)
|
||||||
|
while idx != -1:
|
||||||
|
self.search_results.append((page, packet.row, idx, len(search_bytes)))
|
||||||
|
idx = raw_data.find(search_bytes, idx + 1)
|
||||||
|
else:
|
||||||
|
# Text search in decoded string
|
||||||
|
row_str = ""
|
||||||
|
for b in packet.data:
|
||||||
|
# Map byte to char, but treat control codes as spaces for search
|
||||||
|
bv = b & 0x7F
|
||||||
|
if bv < 0x20:
|
||||||
|
row_str += " "
|
||||||
|
else:
|
||||||
|
row_str += get_char(bv, lang)
|
||||||
|
|
||||||
|
idx = row_str.lower().find(search_text)
|
||||||
|
while idx != -1:
|
||||||
|
self.search_results.append((page, packet.row, idx, len(search_text)))
|
||||||
|
idx = row_str.lower().find(search_text, idx + 1)
|
||||||
|
|
||||||
|
if not self.search_results:
|
||||||
|
self.find_dialog.set_status("No matches found")
|
||||||
|
self.current_search_index = -1
|
||||||
|
self.canvas.clear_highlight()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_search_index = 0
|
||||||
|
else:
|
||||||
|
# Continue search
|
||||||
|
if forward:
|
||||||
|
self.current_search_index = (self.current_search_index + 1) % len(self.search_results)
|
||||||
|
else:
|
||||||
|
self.current_search_index = (self.current_search_index - 1) % len(self.search_results)
|
||||||
|
|
||||||
|
self.find_dialog.set_status(f"Match {self.current_search_index + 1} of {len(self.search_results)}")
|
||||||
|
self.show_search_result(self.search_results[self.current_search_index])
|
||||||
|
|
||||||
|
def show_search_result(self, result):
|
||||||
|
page, row, col, length = result
|
||||||
|
|
||||||
|
# Switch to page
|
||||||
|
mag_pnum_key = (page.magazine, page.page_number)
|
||||||
|
|
||||||
|
# Find and select in list
|
||||||
|
for i in range(self.page_list.count()):
|
||||||
|
item = self.page_list.item(i)
|
||||||
|
if item.data(Qt.ItemDataRole.UserRole) == mag_pnum_key:
|
||||||
|
self.page_list.setCurrentItem(item)
|
||||||
|
# This triggers on_page_selected which populates subpages
|
||||||
|
break
|
||||||
|
|
||||||
|
# Select correct subpage
|
||||||
|
for i in range(self.subpage_combo.count()):
|
||||||
|
if self.subpage_combo.itemData(i) == page:
|
||||||
|
self.subpage_combo.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Highlight on canvas
|
||||||
|
self.canvas.set_highlight(row, col, col + length - 1)
|
||||||
|
# Also move cursor to start of match
|
||||||
|
self.canvas.set_cursor(col, row)
|
||||||
|
|
||||||
def copy_page_content(self):
|
def copy_page_content(self):
|
||||||
if not self.current_page:
|
if not self.current_page:
|
||||||
return
|
return
|
||||||
@@ -796,6 +958,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def push_undo_state(self):
|
def push_undo_state(self):
|
||||||
if not self.current_page: return
|
if not self.current_page: return
|
||||||
|
self.canvas.clear_highlight()
|
||||||
# Push deep copy of current page
|
# Push deep copy of current page
|
||||||
snapshot = copy.deepcopy(self.current_page)
|
snapshot = copy.deepcopy(self.current_page)
|
||||||
self.undo_stack.append(snapshot)
|
self.undo_stack.append(snapshot)
|
||||||
@@ -960,28 +1123,22 @@ class MainWindow(QMainWindow):
|
|||||||
self.update_crc_display()
|
self.update_crc_display()
|
||||||
|
|
||||||
def on_hex_entered(self):
|
def on_hex_entered(self):
|
||||||
text = self.hex_input.text().strip()
|
text = self.hex_input.text()
|
||||||
try:
|
try:
|
||||||
val = int(text, 16)
|
val = int(text, 16)
|
||||||
if 0 <= val <= 255:
|
if 0 <= val <= 255:
|
||||||
# Update canvas input
|
# Update canvas input
|
||||||
|
# We can call handle_input with char, OR set byte directly.
|
||||||
# Direct byte set is safer for non-printable.
|
# Direct byte set is safer for non-printable.
|
||||||
self.push_undo_state()
|
self.push_undo_state()
|
||||||
self.canvas.set_byte_at_cursor(val)
|
self.canvas.set_byte_at_cursor(val)
|
||||||
# Advance cursor: move one index to right, or wrap to next row and move all the way left
|
self.canvas.setFocus() # Return focus to canvas
|
||||||
if self.canvas.cursor_x < self.canvas.cols - 1:
|
|
||||||
self.canvas.move_cursor(1, 0)
|
|
||||||
else:
|
|
||||||
new_y = min(self.canvas.rows - 1, self.canvas.cursor_y + 1)
|
|
||||||
self.canvas.set_cursor(0, new_y)
|
|
||||||
self.hex_input.setFocus()
|
|
||||||
self.hex_input.selectAll()
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass # Ignore invalid hex
|
pass # Ignore invalid hex
|
||||||
|
|
||||||
# Input Handling (Editor Logic)
|
# Input Handling (Editor Logic)
|
||||||
def keyPressEvent(self, event: QKeyEvent):
|
def keyPressEvent(self, event: QKeyEvent):
|
||||||
if not self.current_page or self.hex_input.hasFocus():
|
if not self.current_page:
|
||||||
return
|
return
|
||||||
|
|
||||||
key = event.key()
|
key = event.key()
|
||||||
|
|||||||
Reference in new issue
Block a user