diff --git a/main.py b/main.py index bea5f61..02f9eb4 100644 --- a/main.py +++ b/main.py @@ -17,7 +17,7 @@ from AppKit import ( NSUserInterfaceLayoutOrientationVertical, NSLayoutAttributeCenterX, NSLayoutAttributeCenterY, NSLayoutAttributeWidth, NSLayoutAttributeHeight, NSLayoutAttributeTop, NSLayoutAttributeBottom, NSLayoutAttributeLeading, - NSLayoutAttributeTrailing + NSLayoutAttributeTrailing, NSScrollView, NSTextView, NB ) from Foundation import NSObject, NSTimer, NSDate @@ -26,7 +26,7 @@ load_dotenv() class ItemSenseApp(NSObject): def applicationDidFinishLaunching_(self, notification): self.window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( - NSMakeRect(0, 0, 800, 600), + NSMakeRect(0, 0, 800, 700), # Increased height for text view NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable, NSBackingStoreBuffered, False @@ -44,8 +44,42 @@ class ItemSenseApp(NSObject): # Image View for Camera Feed self.image_view = NSImageView.alloc().init() self.image_view.setImageScaling_(0) # NSImageScaleProportionallyDown + # Add constraint for height logic later if needed, but stackview handles it well enough for basic self.stack_view.addView_inGravity_(self.image_view, 1) # Top gravity + # Result View (Scrollable Text) + self.scroll_view = NSScrollView.alloc().init() + self.scroll_view.setHasVerticalScroller_(True) + self.scroll_view.setBorderType_(2) # NSBezelBorder + + # Determine size (approx) - needed for the content view mostly + # But StackView will resize it. Let's give it an intrinsic size or min size constraint if possible. + # For simplicity in PyObjC without autolayout constraints helpers: + self.scroll_view.setHeightAdjustLimit_(1.0) # ??? No, simple init is usually fine in stackview + + # Text View + content_size = self.scroll_view.contentSize() + self.text_view = NSTextView.alloc().initWithFrame_(NSMakeRect(0, 0, content_size.width, content_size.height)) + self.text_view.setMinSize_(NSMakeSize(0.0, content_size.height)) + self.text_view.setMaxSize_(NSMakeSize(float("inf"), float("inf"))) + self.text_view.setVerticallyResizable_(True) + self.text_view.setHorizontallyResizable_(False) + self.text_view.setAutoresizingMask_(18) # NSViewWidthSizable | NSViewHeightSizable + self.text_view.textContainer().setContainerSize_(NSMakeSize(content_size.width, float("inf"))) + self.text_view.textContainer().setWidthTracksTextView_(True) + self.text_view.setEditable_(False) + self.text_view.setRichText_(False) + + self.scroll_view.setDocumentView_(self.text_view) + + # We need to ensure the scroll view has some height. StackView might crush it if not careful. + # We can set a frame size, or add a constraint. + # Let's simple try adding it to stack view. + self.stack_view.addView_inGravity_(self.scroll_view, 2) + + # Hide initially or just empty? Let's keep it visible so layout stabilizes. + self.text_view.setString_("") + # Capture Button self.capture_button = NSButton.buttonWithTitle_target_action_("Capture", self, "captureClicked:") self.stack_view.addView_inGravity_(self.capture_button, 3) # Bottom gravity @@ -56,6 +90,7 @@ class ItemSenseApp(NSObject): self.cap = cv2.VideoCapture(0) if not self.cap.isOpened(): NSLog("Error: Could not open camera") + self.text_view.setString_("Error: Could not open camera.") # State self.is_capturing = True @@ -81,12 +116,9 @@ class ItemSenseApp(NSObject): ret, frame = self.cap.read() if ret: self.current_frame = frame # Store BGR frame - - # Convert BGR to RGB rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) height, width, channels = rgb_frame.shape - # Using PPM format is fast (uncompressed) header = f"P6 {width} {height} 255 ".encode() data = header + rgb_frame.tobytes() ns_data = objc.lookUpClass("NSData").dataWithBytes_length_(data, len(data)) @@ -100,10 +132,21 @@ class ItemSenseApp(NSObject): self.is_capturing = False self.capture_button.setTitle_("Processing...") self.capture_button.setEnabled_(False) + self.text_view.setString_("Analyzing image...") # Start background processing threading.Thread(target=self.processImage).start() + def resetScan_(self, sender): + print("Resetting...") + self.text_view.setString_("") + self.capture_button.setTitle_("Capture") + self.capture_button.setAction_("captureClicked:") + self.is_capturing = True + + # Restart timer if it was invalidated? No, we didn't invalidate it, just guarded with is_capturing. + # So it should resume immediately. + def processImage(self): try: if self.current_frame is None: @@ -117,7 +160,7 @@ class ItemSenseApp(NSObject): client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) response = client.chat.completions.create( - model="gpt-4o-mini", # Fallback to 4o-mini as 5-mini is hypothetical/beta + model="gpt-4o-mini", messages=[ { "role": "user", @@ -142,16 +185,18 @@ class ItemSenseApp(NSObject): self.performSelectorOnMainThread_withObject_waitUntilDone_("handleError:", str(e), False) def handleResponse_(self, result): - print(f"OpenAI Response: {result}") - # For now, just reset state (waiting for Spec 003 for UI display) - self.capture_button.setTitle_("Captured (Check Console)") - # In reality, we'd wait for user to reset, but let's re-enable reset flow in Spec 003. - # For this spec, just showing we got the response is enough. + print(f"OpenAI Response received") + self.text_view.setString_(result) + self.capture_button.setTitle_("Scan Another") + self.capture_button.setEnabled_(True) + self.capture_button.setAction_("resetScan:") def handleError_(self, error_msg): print(f"Error: {error_msg}") + self.text_view.setString_(f"Error: {error_msg}") self.capture_button.setTitle_("Error - Try Again") self.capture_button.setEnabled_(True) + self.capture_button.setAction_("captureClicked:") # Ensure it resets to capture logic self.is_capturing = True if __name__ == "__main__": diff --git a/specs/003-result-display.md b/specs/003-result-display.md index b9752da..0b00f1a 100644 --- a/specs/003-result-display.md +++ b/specs/003-result-display.md @@ -1,5 +1,7 @@ # Feature: Result Display (PyObjC) +## Status: COMPLETE + ## Description Display the analysis results natively in the AppKit UI.