diff --git a/Assets/White-OpenNoteLogo.svg b/Assets/White-OpenNoteLogo.svg new file mode 100644 index 0000000..591dcf6 --- /dev/null +++ b/Assets/White-OpenNoteLogo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Assets/icons/svg_add_page.svg b/Assets/icons/svg_add_page.svg new file mode 100644 index 0000000..548f706 --- /dev/null +++ b/Assets/icons/svg_add_page.svg @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_align_center.svg b/Assets/icons/svg_align_center.svg new file mode 100644 index 0000000..92d15d7 --- /dev/null +++ b/Assets/icons/svg_align_center.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_align_left.svg b/Assets/icons/svg_align_left.svg new file mode 100644 index 0000000..a04bce0 --- /dev/null +++ b/Assets/icons/svg_align_left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_align_right.svg b/Assets/icons/svg_align_right.svg new file mode 100644 index 0000000..79600b9 --- /dev/null +++ b/Assets/icons/svg_align_right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_arrow.svg b/Assets/icons/svg_arrow.svg new file mode 100644 index 0000000..9d990ed --- /dev/null +++ b/Assets/icons/svg_arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_bulletUA.svg b/Assets/icons/svg_bulletUA.svg new file mode 100644 index 0000000..74a1694 --- /dev/null +++ b/Assets/icons/svg_bulletUA.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_bulletUR.svg b/Assets/icons/svg_bulletUR.svg new file mode 100644 index 0000000..7aee439 --- /dev/null +++ b/Assets/icons/svg_bulletUR.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_bullet_number.svg b/Assets/icons/svg_bullet_number.svg new file mode 100644 index 0000000..ab2ddca --- /dev/null +++ b/Assets/icons/svg_bullet_number.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_bullets.svg b/Assets/icons/svg_bullets.svg new file mode 100644 index 0000000..fb527c0 --- /dev/null +++ b/Assets/icons/svg_bullets.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_close.svg b/Assets/icons/svg_close.svg new file mode 100644 index 0000000..e25249d --- /dev/null +++ b/Assets/icons/svg_close.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_copy.svg b/Assets/icons/svg_copy.svg new file mode 100644 index 0000000..4bd7a9a --- /dev/null +++ b/Assets/icons/svg_copy.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Assets/icons/svg_crop.svg b/Assets/icons/svg_crop.svg new file mode 100644 index 0000000..5c0d05f --- /dev/null +++ b/Assets/icons/svg_crop.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_cut.svg b/Assets/icons/svg_cut.svg new file mode 100644 index 0000000..f915297 --- /dev/null +++ b/Assets/icons/svg_cut.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_date.svg b/Assets/icons/svg_date.svg new file mode 100644 index 0000000..ec709e1 --- /dev/null +++ b/Assets/icons/svg_date.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_dateTime.svg b/Assets/icons/svg_dateTime.svg new file mode 100644 index 0000000..8c47144 --- /dev/null +++ b/Assets/icons/svg_dateTime.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_delete.svg b/Assets/icons/svg_delete.svg new file mode 100644 index 0000000..dec5ed3 --- /dev/null +++ b/Assets/icons/svg_delete.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_expand.svg b/Assets/icons/svg_expand.svg new file mode 100644 index 0000000..7084297 --- /dev/null +++ b/Assets/icons/svg_expand.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_flip_horizontal.svg b/Assets/icons/svg_flip_horizontal.svg new file mode 100644 index 0000000..f58662e --- /dev/null +++ b/Assets/icons/svg_flip_horizontal.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_flip_vertical.svg b/Assets/icons/svg_flip_vertical.svg new file mode 100644 index 0000000..f43b4d0 --- /dev/null +++ b/Assets/icons/svg_flip_vertical.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_fullscreen.svg b/Assets/icons/svg_fullscreen.svg new file mode 100644 index 0000000..3a39a6c --- /dev/null +++ b/Assets/icons/svg_fullscreen.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_hyperlink.svg b/Assets/icons/svg_hyperlink.svg new file mode 100644 index 0000000..930588a --- /dev/null +++ b/Assets/icons/svg_hyperlink.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_insert_space.svg b/Assets/icons/svg_insert_space.svg new file mode 100644 index 0000000..455ddfe --- /dev/null +++ b/Assets/icons/svg_insert_space.svg @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_paper.svg b/Assets/icons/svg_paper.svg new file mode 100644 index 0000000..3a7d0d5 --- /dev/null +++ b/Assets/icons/svg_paper.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_paste.svg b/Assets/icons/svg_paste.svg new file mode 100644 index 0000000..9376c18 --- /dev/null +++ b/Assets/icons/svg_paste.svg @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_pictures.svg b/Assets/icons/svg_pictures.svg new file mode 100644 index 0000000..c51204f --- /dev/null +++ b/Assets/icons/svg_pictures.svg @@ -0,0 +1,6 @@ + + + +picture + + \ No newline at end of file diff --git a/Assets/icons/svg_question.svg b/Assets/icons/svg_question.svg new file mode 100644 index 0000000..f0e4806 --- /dev/null +++ b/Assets/icons/svg_question.svg @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_redo.svg b/Assets/icons/svg_redo.svg new file mode 100644 index 0000000..a96d7e5 --- /dev/null +++ b/Assets/icons/svg_redo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_rename.svg b/Assets/icons/svg_rename.svg new file mode 100644 index 0000000..f64df38 --- /dev/null +++ b/Assets/icons/svg_rename.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Assets/icons/svg_rotate_left.svg b/Assets/icons/svg_rotate_left.svg new file mode 100644 index 0000000..1999b02 --- /dev/null +++ b/Assets/icons/svg_rotate_left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_rotate_right.svg b/Assets/icons/svg_rotate_right.svg new file mode 100644 index 0000000..f87f18a --- /dev/null +++ b/Assets/icons/svg_rotate_right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_screensnip.svg b/Assets/icons/svg_screensnip.svg new file mode 100644 index 0000000..4584889 --- /dev/null +++ b/Assets/icons/svg_screensnip.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_search.svg b/Assets/icons/svg_search.svg new file mode 100644 index 0000000..a19fb2a --- /dev/null +++ b/Assets/icons/svg_search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Assets/icons/svg_shrink.svg b/Assets/icons/svg_shrink.svg new file mode 100644 index 0000000..ed75d69 --- /dev/null +++ b/Assets/icons/svg_shrink.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_strikethrough.svg b/Assets/icons/svg_strikethrough.svg new file mode 100644 index 0000000..7da4dad --- /dev/null +++ b/Assets/icons/svg_strikethrough.svg @@ -0,0 +1,6 @@ + + + strikethrough-line + + + \ No newline at end of file diff --git a/Assets/icons/svg_table.svg b/Assets/icons/svg_table.svg new file mode 100644 index 0000000..9c21ff1 --- /dev/null +++ b/Assets/icons/svg_table.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Assets/icons/svg_textHighlightColor.svg b/Assets/icons/svg_textHighlightColor.svg new file mode 100644 index 0000000..1cc087a --- /dev/null +++ b/Assets/icons/svg_textHighlightColor.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_time.svg b/Assets/icons/svg_time.svg new file mode 100644 index 0000000..bfd356a --- /dev/null +++ b/Assets/icons/svg_time.svg @@ -0,0 +1,15 @@ + + + + time / 18 - time, clock, date, time icon + + + + + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_tray.svg b/Assets/icons/svg_tray.svg new file mode 100644 index 0000000..d4b9c8f --- /dev/null +++ b/Assets/icons/svg_tray.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_undo.svg b/Assets/icons/svg_undo.svg new file mode 100644 index 0000000..d463200 --- /dev/null +++ b/Assets/icons/svg_undo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/icons/svg_windowed.svg b/Assets/icons/svg_windowed.svg new file mode 100644 index 0000000..6bde8f2 --- /dev/null +++ b/Assets/icons/svg_windowed.svg @@ -0,0 +1 @@ + diff --git a/Models/DraggableContainer.py b/Models/DraggableContainer.py index 57b248d..5a6202f 100644 --- a/Models/DraggableContainer.py +++ b/Models/DraggableContainer.py @@ -1,5 +1,6 @@ from PySide6.QtCore import * from PySide6.QtGui import * +from PySide6.QtGui import QKeyEvent from PySide6.QtWidgets import * from Modules.Enums import * from Modules.EditorSignals import editorSignalsInstance, ChangedWidgetAttribute @@ -24,9 +25,10 @@ class DraggableContainer(QWidget): menu = None mode = Mode.NONE position = None - outFocus = Signal(bool) - newGeometry = Signal(QRect) + outFocus = Signal(bool) # Signal emitted when the container loses focus + newGeometry = Signal(QRect) # Signal emitted when the container's geometry changes + # Initializes a DraggableContainer object def __init__(self, childWidget, editorFrame): super().__init__(parent=editorFrame) @@ -52,8 +54,11 @@ def __init__(self, childWidget, editorFrame): self.menu = self.buildDragContainerMenu() if hasattr(self.childWidget, "newGeometryEvent"): self.newGeometry.connect(childWidget.newGeometryEvent) - editorSignalsInstance.widgetAttributeChanged.connect(self.widgetAttributeChanged) + + editorSignalsInstance.widgetAttributeChanged.connect(self.deselect_and_delete) + + def setChildWidget(self, childWidget): if childWidget: self.childWidget = childWidget @@ -64,23 +69,24 @@ def setChildWidget(self, childWidget): self.vLayout.addWidget(childWidget) self.vLayout.setContentsMargins(0,0,0,0) - def eventFilter(self, obj, event): + def eventFilter(self, obj, e): - # If child widget resized itsself, resize this drag container, not ideal bc child resizes on hover - if isinstance(event, QResizeEvent): + # If child widget resized itself, resize this drag container, not ideal bc child resizes on hover + if isinstance(e, QResizeEvent): self.resize(self.childWidget.size()) return False + # Displays the container menu at a given position def popupShow(self, pt: QPoint): global_ = self.mapToGlobal(pt) self.m_showMenu = True self.menu.exec(global_) self.m_showMenu = False - + def mousePressEvent(self, e: QMouseEvent): self.position = QPoint(e.globalX() - self.geometry().x(), e.globalY() - self.geometry().y()) - print("DC MOUSE PRESS") + print("Draggable Container MOUSE PRESS") # Undo related # self.old_x = e.globalX() @@ -93,15 +99,36 @@ def mousePressEvent(self, e: QMouseEvent): print("NOT EDIT") return if not e.buttons() and Qt.LeftButton: - print("DC GOT MOUSE PRESS") + print("Draggable Container GOT MOUSE PRESS") self.setCursorShape(e.pos()) + + return True if e.button() == Qt.RightButton: self.popupShow(e.pos()) e.accept() + self.childWidget.setAttribute(Qt.WA_TransparentForMouseEvents, False) + self.childWidget.setFocus() + # checks if the child is a textbox + if (isinstance(self.childWidget, QTextEdit)): + # sets cursor position to where mouse is upon clicking + self.childWidget.setCursorPosition(e) + # check settings at cursor and match the visual of the toolbar toggle with the current cursor setting + + + #brings text cursor to cursor position but causes exception + '''if isinstance(self.childWidget, TextboxWidget): + self.childWidget.setCursorPosition(e) + else: + # Handle the case where self.childWidget is not a TextBox + pass''' + + + # On double click, focus on child and make mouse events pass through this container to child def mouseDoubleClickEvent(self, e: QMouseEvent): + print("MOUSEDOUBLECLICKEVENT FROM DRAGGABLE CONTAINER") self.childWidget.setAttribute(Qt.WA_TransparentForMouseEvents, False) self.childWidget.setFocus() return @@ -114,18 +141,21 @@ def mouseReleaseEvent(self, e: QMouseEvent): return True # Dont let the release go to the editor frame def leaveEvent(self, e: QMouseEvent): + #if(self.childWidget.hasFocus() == False): self.childWidget.setAttribute(Qt.WA_TransparentForMouseEvents, True) self.setStyleSheet("border: none;") + # If mouse leaves draggable container, set focus to the editor + #if self.childWidget.hasFocus(): + # self.setFocus()''' - # Delete this DC if childWidget says it's empty - if hasattr(self.childWidget, "checkEmpty"): - if self.childWidget.checkEmpty(): - editorSignalsInstance.widgetRemoved.emit(self) - - # ??? - if self.childWidget.hasFocus(): - self.setFocus() + # this is to lose the border of the draggable container if the container is no longer in focus + '''def focusOutEvent(self, event: QMouseEvent): + print("FOCUS OUT EVENT CALLED FROM DRAGGABLE CONTAINER") + self.childWidget.setAttribute(Qt.WA_TransparentForMouseEvents, True) + self.setStyleSheet("border: none;") + super().focusOutEvent(event)''' + # Builds and returns the context menu for the container def buildDragContainerMenu(self): # Get custom menu actions from child widget @@ -142,24 +172,113 @@ def buildDragContainerMenu(self): menu.addAction(item) # Add standard menu actions - delete = QAction("Delete", self) - delete.triggered.connect(lambda: editorSignalsInstance.widgetRemoved.emit(self)) - menu.addAction(delete) + cut = QAction("Cu&t", self) + cut.triggered.connect(lambda: editorSignalsInstance.widgetCut.emit(self)) - copy = QAction("Copy", self) + copy = QAction("&Copy", self) copy.triggered.connect(lambda: editorSignalsInstance.widgetCopied.emit(self)) - menu.addAction(copy) - - cut = QAction("Cut", self) - cut.triggered.connect(lambda: editorSignalsInstance.widgetCut.emit(self)) - menu.addAction(cut) + + paste = QAction("&Paste", self) + paste.triggered.connect(lambda: self.pasteWidget(self.pos())) + + delete = QAction("&Delete", self) + delete.triggered.connect(lambda: editorSignalsInstance.widgetRemoved.emit(self)) + + menu.addActions([cut, copy, paste, delete]) + + menu.addSeparator() + + link = QAction("&Link", self) + # link.triggered.connect(lambda: self.insertLink(event.pos())) + + menu.addAction(link) + + menu.addSeparator() + + orderMenu = QMenu("&Order", self) + + bringForwards = QAction("Bring &Forwards", self) + bringForwards.triggered.connect(self.childWidget.raise_()) + + orderMenu.addAction(bringForwards) + + # not ready for deployment + # menu.addMenu(orderMenu) + + # text boxes + if isinstance(self.childWidget, QTextEdit): + table = QAction("&Table", self) + # table.triggered.connect(lambda: self.newWidgetOnSection(TableWidget, event.pos())) + + menu.addAction(table) + + menu.addSeparator() + + # images + elif isinstance(self.childWidget, QLabel): + # Create submenus for rotate and resize + rotateMenu = QMenu("R&otate", self) + rotateMenu.setStyleSheet("font-size: 11pt;") + + resizeMenu = QMenu("&Resize", self) + resizeMenu.setStyleSheet("font-size: 11pt;") + + # Create actions for rotate submenu + rotateRightAction = QAction("Rotate &Right 90°", self) + rotateRightAction.triggered.connect(self.childWidget.rotate90Right) + rotateRightAction.setIcon(QIcon('./Assets/icons/svg_rotate_right')) + + rotateLeftAction = QAction("Rotate &Left 90°", self) + rotateLeftAction.triggered.connect(self.childWidget.rotate90Left) + rotateLeftAction.setIcon(QIcon('./Assets/icons/svg_rotate_left')) + + flipHorizontal = QAction("Flip &Horizontal", self) + flipHorizontal.triggered.connect(self.childWidget.flipHorizontal) + flipHorizontal.setIcon(QIcon('./Assets/icons/svg_flip_horizontal')) + + flipVertical = QAction("Flip &Vertical", self) + flipVertical.triggered.connect(self.childWidget.flipVertical) + flipVertical.setIcon(QIcon('./Assets/icons/svg_flip_vertical')) + + # Add actions to rotate submenu + rotateMenu.addActions([rotateLeftAction, rotateRightAction, flipHorizontal, flipVertical]) + + # Create actions for resize submenu + shrinkImageAction = QAction("&Shrink Image", self) + shrinkImageAction.triggered.connect(self.childWidget.shrinkImage) + shrinkImageAction.setIcon(QIcon('./Assets/icons/svg_shrink')) + + expandImageAction = QAction("&Expand Image", self) + expandImageAction.triggered.connect(self.childWidget.expandImage) + expandImageAction.setIcon(QIcon('./Assets/icons/svg_expand')) + + # Add actions to resize submenu + resizeMenu.addActions([shrinkImageAction, expandImageAction]) + + # Add submenus to the main menu + menu.addMenu(rotateMenu) + menu.addMenu(resizeMenu) + + # tables + elif isinstance(self.childWidget, QWidget): + tableMenu = QMenu("&Table", self) + tableMenu.setStyleSheet("font-size: 11pt;") + + addRow = QAction("Add Row", self) + addRow.triggered.connect(self.childWidget.addRow) + + addCol = QAction("Add Column", self) + addCol.triggered.connect(self.childWidget.addCol) + + tableMenu.addActions([addRow, addCol]) + menu.addMenu(tableMenu) # Add any non-widget type menu actions from child for item in customMenuItems: if type(item) != QWidgetAction: menu.addAction(item) - return menu + return menu # returns the QMenu: The constructed context menu # Determine which cursor to show and which mode to be in when user is interacting with the box def setCursorShape(self, e_pos: QPoint): @@ -217,7 +336,7 @@ def setCursorShape(self, e_pos: QPoint): self.setCursor(QCursor(Qt.SizeVerCursor)) self.mode = Mode.RESIZEB else: - self.setCursor(QCursor(Qt. ArrowCursor)) + self.setCursor(QCursor(Qt.ArrowCursor)) self.mode = Mode.MOVE # Determine how to handle the mouse being moved inside the box @@ -246,6 +365,7 @@ def mouseMoveEvent(self, e: QMouseEvent): # debt: To make images resize better, ImageWidget should probaly implement this and setCursorShape # So that it can make the cursor move with the corners of pixmap and not corners of this container if (self.mode != Mode.MOVE) and e.buttons() and Qt.LeftButton: + child_widget = self.childWidget if self.mode == Mode.RESIZETL: # Left - Top newwidth = e.globalX() - self.position.x() - self.geometry().x() newheight = e.globalY() - self.position.y() - self.geometry().y() @@ -257,6 +377,8 @@ def mouseMoveEvent(self, e: QMouseEvent): toMove = e.globalPos() - self.position self.resize(e.x(), self.geometry().height() - newheight) self.move(self.x(), toMove.y()) + '''if hasattr(self.childWidget, "newGeometryEvent"): + self.newGeometry.connect(self.childWidget.newGeometryEvent)''' elif self.mode== Mode.RESIZEBL: # Left - Bottom newwidth = e.globalX() - self.position.x() - self.geometry().x() toMove = e.globalPos() - self.position @@ -277,33 +399,59 @@ def mouseMoveEvent(self, e: QMouseEvent): elif self.mode == Mode.RESIZER: # Right self.resize(e.x(), self.height()) elif self.mode == Mode.RESIZEBR:# Right - Bottom - self.resize(e.x(), e.y()) + #if child is a image, resize differently + if isinstance(child_widget, QLabel): + # change this + self.resize(e.x(), e.y()) + else: + self.resize(e.x(), e.y()) self.parentWidget().repaint() self.newGeometry.emit(self.geometry()) + + # Used for deselecting text and removing container if empty + def deselect_and_delete(self, changedWidgetAttribute): + #print(f"changedWidgetAttribute is {changedWidgetAttribute} and value is {value}") + child_widget = self.childWidget + + # If clicking on no object, deselect text + if hasattr(child_widget, "deselectText") and (changedWidgetAttribute == ChangedWidgetAttribute.LoseFocus): + child_widget.deselectText() + # remove the widget if empty + if hasattr(self.childWidget, "checkEmpty") and isinstance(child_widget, QTextEdit): + if self.childWidget.checkEmpty(): + print("Removing empty container") + editorSignalsInstance.widgetRemoved.emit(self) + + def connectTableSignals(self, tableWidget): + tableWidget.rowAdded.connect(self.resizeTable) + def resizeTable(self): + self.resize(self.childWidget.size()) + self.newGeometry.emit(self.geometry()) + self.parentWidget().repaint() - # Pass the event to the child widget if this container is focuesd, and childwidget implements the method to receive it - def widgetAttributeChanged(self, changedWidgetAttribute, value): - - cw = self.childWidget - - if hasattr(cw, "changeFontSizeEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.FontSize): - cw.changeFontSizeEvent(value) - - if hasattr(cw, "changeFontBoldEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.FontBold): - cw.changeFontBoldEvent() - - if hasattr(cw, "changeFontItalicEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.FontItalic): - cw.changeFontItalicEvent() - - if hasattr(cw, "changeFontUnderlineEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.FontUnderline): - cw.changeFontUnderlineEvent() + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() == Qt.Key_Shift: + print("SHIFT PRESSED") + self.shift_pressed = True + else: + super().keyPressEvent(event) + def keyReleaseEvent(self, event: QKeyEvent) -> None: + if event.key() == Qt.Key_Shift: + print("SHIFT RELEASED") + self.shift_pressed = False + else: + super().keyReleaseEvent(event) - if hasattr(cw, "changeFontEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.Font): - cw.changeFontEvent(value) + # Pastes a widget at the specified position + def pasteWidget(self, clickPos): + widgetOnClipboard = self.clipboard.getWidgetToPaste() - if hasattr(cw, "changeFontColorEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.FontColor): - cw.changeFontColorEvent(value) + dc = DraggableContainer(widgetOnClipboard, self) # Create a new DraggableContainer with the widget to paste + self.undoHandler.pushCreate(dc) + editorSignalsInstance.widgetAdded.emit(dc) # Notify section that widget was added + editorSignalsInstance.changeMade.emit() - if hasattr(cw, "changeBackgroundColorEvent") and (changedWidgetAttribute == ChangedWidgetAttribute.BackgroundColor): - cw.changeBackgroundColorEvent(value) + # Move the container to the specified position and show it + dc.move(clickPos.x(), clickPos.y()) + dc.show() diff --git a/Models/Editor.py b/Models/Editor.py index 89101c8..721b7c9 100644 --- a/Models/Editor.py +++ b/Models/Editor.py @@ -13,16 +13,19 @@ from Views.EditorFrameView import EditorFrameView from Views.NotebookTitleView import NotebookTitleView from Views.SectionView import SectionView +from Modules.Titlebar import Build_titlebar class Editor(QMainWindow): def __init__(self): super().__init__() - # self.setWindowFlag(Qt.FramelessWindowHint) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + # self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.CustomizeWindowHint) # This changes the window back to a state that allows resizing self.notebook = NotebookModel('Untitled Notebook') # Current notebook object # self.selected = None # Selected object (for font attributes of TextBox) # View-Controllers that let the user interact with the underlying models + self.titlebar = Build_titlebar(self) self.notebookTitleView = NotebookTitleView(self.notebook.title) self.frameView = EditorFrameView(self) self.pageView = PageView(self.notebook.pages) @@ -30,8 +33,105 @@ def __init__(self): self.autosaver = Autosaver(self) # Waits for change signals and saves the notebook self.setFocus() + self.undoStack = [] + + self.settings = QSettings("UNT - Team Olive", "OpenNote") #pre-saved settings needed for window state restoration build_ui(self) + + action_names = self.save_toolbar_actions([self.fileToolbar, self.homeToolbar, self.insertToolbar, self.drawToolbar, self.pluginToolbar]) + self.titlebar.set_action_names(action_names) + + def closeEvent(self, event): + + print("Window closing event triggered") + + self.settings.setValue("geometry", self.saveGeometry()) + self.settings.setValue("windowState", self.saveState()) + super().closeEvent(event) + + # Handles the window showing event, it restores the window's geometry and state + def showEvent(self, event): + print("Window showing event triggered") + + self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) + self.restoreState(self.settings.value("windowState", self.saveState())) + super().showEvent(event) + # def focusInEvent(self, event): # self.repaint() + # Handles changes in window state, particularly the window state change event + def changeEvent(self, event): + if event.type() == QEvent.Type.WindowStateChange: + self.titlebar.window_state_changed(self.windowState()) + super().changeEvent(event) + event.accept() + + def triggerUndo(self): + print("Item added to Undo Stack") + focused_widget = self.focusWidget() + + + if focused_widget and isinstance(focused_widget, (QTextEdit, QLineEdit)): + cursor = focused_widget.textCursor() # Get the cursor of the widget + if cursor.position() > 0: # Check if there's a character to delete + cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor) # Select character to the left + char_to_delete = cursor.selectedText() # Get the selected text + self.undoStack.append(char_to_delete) # Append it to the stack + print(f"Stored '{char_to_delete}' in redo stack") + + + backspace_event = QKeyEvent(QEvent.KeyPress, Qt.Key_Backspace, Qt.NoModifier) + backspace_release_event = QKeyEvent(QEvent.KeyRelease, Qt.Key_Backspace, Qt.NoModifier) + + QApplication.postEvent(focused_widget, backspace_event) + QApplication.postEvent(focused_widget, backspace_release_event) + + def triggerRedo(self): + if not self.undoStack: + print("No characters to redo") + return + + char_to_redo = self.undoStack.pop() # Get the last character from the stack + focused_widget = self.focusWidget() + + if focused_widget and isinstance(focused_widget, (QTextEdit, QLineEdit)): + redo_event = QKeyEvent(QEvent.KeyPress, 0, Qt.NoModifier, text=char_to_redo) + QApplication.postEvent(focused_widget, redo_event) + print(f"Redo action: Typed '{char_to_redo}'") + print(f"Item Removed from stack") + else: + print("Focused widget is not a QTextEdit or QLineEdit") + + # mousePress, mouseMove, and mouseRelease handle mouse move events inside the window + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self.moveFlag = True + self.movePosition = event.globalPos() - self.pos() + event.accept() + + def mouseMoveEvent(self, event): + if Qt.LeftButton and self.moveFlag: + self.setWindowState(Qt.WindowNoState) + self.move(event.globalPos() - self.movePosition) + event.accept() + + def mouseReleaseEvent(self, QMouseEvent): + self.moveFlag = False + + # Collects action names from the toolbars + def save_toolbar_actions(self, toolbars): + action_names = [] + for toolbar in toolbars: #loops through all toolbars + for action in toolbar.actions(): + if action.objectName(): + action_names.append(action.objectName()) # Add the object name of the action to the list + + # Loop through child widgets of the toolbar (buttons, tool buttons, combo boxes) + for widget in toolbar.findChildren(QPushButton) + toolbar.findChildren(QToolButton) + toolbar.findChildren(QComboBox): + if widget.objectName() and widget.objectName() != "qt_toolbar_ext_button": + action_names.append(widget.objectName()) # Add the object name of the widget to the list + + return action_names # Return the list of action names collected from the toolbars diff --git a/Models/NotebookModel.py b/Models/NotebookModel.py index 6ecf21e..c92a71e 100644 --- a/Models/NotebookModel.py +++ b/Models/NotebookModel.py @@ -1,5 +1,8 @@ + +# Represents a notebook in the application. class NotebookModel: def __init__(self, title): - self.path = None - self.title = title - self.pages = [] # PageModel[] + self.path = None # The file path of the notebook if saved + self.title = title # The title of the notebook + + self.pages = [] # PageModel[] - List to store PageModel instances representing pages in the notebook diff --git a/Models/PageModel.py b/Models/PageModel.py index 2643010..77ca4e3 100644 --- a/Models/PageModel.py +++ b/Models/PageModel.py @@ -1,9 +1,11 @@ import uuid +# Represents a page within a notebook + class PageModel: def __init__(self, title: str, parentUuid: int = 0): self.title = title - self.sections = [] # SectionModel[] + self.sections = [] # SectionModel[] - List to store SectionModel instances representing sections in the page # These are used to represent the tree structure, the model is not actually concerned with parent and children # The tree structure lets us build a view that we can interact with as if there were really nested pages @@ -12,9 +14,9 @@ def __init__(self, title: str, parentUuid: int = 0): @staticmethod def newRootPage(): - rootPage = PageModel("Notebook Pages") + rootPage = PageModel("Notebook") rootPage.__uuid = 0 - return rootPage + return rootPage # A new root page def isRoot(self): return self.__uuid == 0 diff --git a/Models/SectionModel.py b/Models/SectionModel.py index 522d404..f9c9df9 100644 --- a/Models/SectionModel.py +++ b/Models/SectionModel.py @@ -6,7 +6,7 @@ class SectionModel: def __init__(self, title: str): self.title = title - self.widgets: List[DraggableContainer] = [] + self.widgets: List[DraggableContainer] = [] # List of draggable containers in the section, each container holds an instance of a widget (ex. TextboxWidget) # When saving, convert the list of DraggableContainers to a list of their child widget's models def __getstate__(self): diff --git a/Modules/BuildUI.py b/Modules/BuildUI.py index 6b105a4..11c4ff2 100644 --- a/Modules/BuildUI.py +++ b/Modules/BuildUI.py @@ -3,120 +3,445 @@ from PySide6.QtCore import * from PySide6.QtGui import * +from PySide6.QtSvg import * from PySide6.QtWidgets import * -from Modules.EditorSignals import editorSignalsInstance, ChangedWidgetAttribute +from Models.DraggableContainer import DraggableContainer +from Widgets.Textbox import * + +from Modules.EditorSignals import editorSignalsInstance, ChangedWidgetAttribute, CheckSignal +from Modules.Undo import UndoHandler +from Widgets.Table import * + +from Views.EditorFrameView import * + +import subprocess FONT_SIZES = [7, 8, 9, 10, 11, 12, 13, 14, 18, 24, 36, 48, 64, 72, 96, 144, 288] -#builds the application's UI def build_ui(editor): - print("Building UI...") + print("Building UI") + editor.setWindowTitle("OpenNote") + editor.setWindowIcon(QIcon('./Assets/OpenNoteLogo.png')) + editor.resize(800, 450) + editor.setAcceptDrops(True) - #editor.statusBar = editor.statusBar() - build_window(editor) - build_menubar(editor) - #build_toolbar(editor) - # Application's main layout (grid) - gridLayout = QGridLayout() - gridContainerWidget = QWidget() - editor.setCentralWidget(gridContainerWidget) - gridContainerWidget.setLayout(gridLayout) + # Checks wether mac is in dark mode, then sets proper window css + if check_appearance() == False: + with open('./Styles/styles.qss',"r") as fh: + editor.setStyleSheet(fh.read()) + else: + with open('./Styles/stylesDark.qss',"r") as fh: + editor.setStyleSheet(fh.read()) - gridLayout.setSpacing(3) - gridLayout.setContentsMargins(6, 6, 0, 0) + build_tabbar(editor) + # build_menubar(editor) + # build_toolbar(editor) - gridLayout.setColumnStretch(0, 1) # The left side (index 0) will take up 1/7? of the space of the right + # Main layout of the app + gridLayout = QGridLayout() + gridLayout.setSpacing(0) + gridLayout.setContentsMargins(0, 0, 0, 0) gridLayout.setColumnStretch(1, 7) - # Left side of the app's layout + centralWidget = QWidget() + centralWidget.setLayout(gridLayout) + + # Sets up layout of each bar + topSideLayout = QVBoxLayout() + topSideContainerWidget = QWidget() + topSideContainerWidget.setLayout(topSideLayout) + topSideLayout.setContentsMargins(0, 0, 0, 0) + topSideLayout.setSpacing(0) + + barsLayout = QVBoxLayout() + barsLayoutContainerWidget = QWidget() + barsLayoutContainerWidget.setLayout(barsLayout) + barsLayout.setContentsMargins(7, 0, 7, 0) + barsLayout.setSpacing(0) + + # Add the bars to the layout with individual margins + barsLayout.addWidget(editor.tabbar) + # barsLayout.addWidget(editor.menubar) + # barsLayout.addWidget(editor.homeToolbar) + # barsLayout.addWidget(editor.insertToolbar) + # barsLayout.addWidget(editor.drawToolbar) + + topSideLayout.addWidget(editor.titlebar, 0) + topSideLayout.addWidget(barsLayoutContainerWidget, 1) + # topSideLayout.addWidget(editor.homeToolbar, 2) + # topSideLayout.addWidget(editor.insertToolbar, 2) + # topSideLayout.addWidget(editor.drawToolbar, 2) + + # Sets up left side notebook view leftSideLayout = QVBoxLayout() leftSideContainerWidget = QWidget() leftSideContainerWidget.setLayout(leftSideLayout) leftSideLayout.setContentsMargins(0, 0, 0, 0) leftSideLayout.setSpacing(0) - # Right side of the app's layout + leftSideLayout.addWidget(editor.notebookTitleView, 0) + leftSideLayout.addWidget(editor.pageView, 1) + + # Sets up right side section view rightSideLayout = QVBoxLayout() rightSideContainerWidget = QWidget() rightSideContainerWidget.setLayout(rightSideLayout) rightSideLayout.setContentsMargins(0, 0, 0, 0) rightSideLayout.setSpacing(0) - rightSideLayout.setStretch(0, 0) - rightSideLayout.setStretch(1, 1) - # Add appropriate widgets (ideally just view controllers) to their layouts - leftSideLayout.addWidget(editor.notebookTitleView, 0) - leftSideLayout.addWidget(editor.pageView, 1) # Page view has max stretch factor rightSideLayout.addWidget(editor.sectionView, 0) - rightSideLayout.addWidget(editor.frameView, 1) # Frame view has max stretch factor + rightSideLayout.addWidget(editor.frameView, 1) - # Add L+R container's widgets to the main grid - gridLayout.addWidget(leftSideContainerWidget, 0, 0) - gridLayout.addWidget(rightSideContainerWidget, 0, 1) + gridLayout.addWidget(topSideContainerWidget, 0, 0, 1, 2, alignment = Qt.AlignmentFlag.AlignTop) + gridLayout.addWidget(leftSideContainerWidget, 1, 0, 1, 1) + gridLayout.addWidget(rightSideContainerWidget, 1, 1, 1, 2) + + editor.setCentralWidget(centralWidget) + + #Saves window size + #editor.restoreGeometry(editor.settings.value("geometry", editor.saveGeometry())) + #editor.restoreState(editor.settings.value("windowState", editor.saveState())) + +# Constructs the tab bar with different tabs like File, Home, Insert, Draw, and Plugins +# It adds toolbars to these tabs and connects them to their functions +def build_tabbar(editor): + editor.tabbar = QTabWidget() + editor.tabbar.setTabsClosable(False) + + editor.fileToolbar = QToolBar() + editor.homeToolbar = QToolBar() + editor.insertToolbar = QToolBar() + editor.drawToolbar = QToolBar() + editor.pluginToolbar = QToolBar() -def build_window(editor): - editor.setWindowTitle("OpenNote") - editor.setWindowIcon(QIcon('./Assets/OpenNoteLogo.png')) - editor.setAcceptDrops(True) - with open('./Styles/styles.qss',"r") as fh: - editor.setStyleSheet(fh.read()) -def build_menubar(editor): - file = editor.menuBar().addMenu('&File') - plugins = editor.menuBar().addMenu('&Plugins') + # Toolbars + # Constructs toolbars for different functionalities like home, insert, and draw + # It adds actions and buttons to these toolbars and connects them to their functions + # --------------------------------------------------------------------------------- - new_file = build_action(editor, 'assets/icons/svg_file_open', 'New Notebook...', 'New Notebook', False) + def handleCheck(action): + action.setChecked(True) + #editorSignalsInstance.checkMade.connect(editor.checkMade) + + # For adding space to the left the first button added to a toolbar + spacer1 = QWidget() + spacer1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + spacer1.setFixedWidth(3) + + spacer2 = QWidget() + spacer2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + spacer2.setFixedWidth(7) + + spacer3 = QWidget() + spacer3.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + spacer3.setFixedWidth(3) + + # --------------------------------------------------------------------------------- + # fileToolbar code + + editor.fileToolbar.setObjectName('fileToolbar') + editor.fileToolbar.setIconSize(QSize(18,18)) + editor.fileToolbar.setMovable(False) + editor.fileToolbar.setVisible(False) + editor.fileToolbar.setFixedHeight(40) + editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, editor.fileToolbar) + + new_file = build_button(editor.fileToolbar, './Assets/icons/svg_file_open', 'New Notebook', 'New Notebook', False) new_file.setShortcut(QKeySequence.StandardKey.New) - new_file.triggered.connect(lambda: new(editor)) + new_file.clicked.connect(lambda: new(editor)) - open_file = build_action(editor, 'assets/icons/svg_file_open', 'Open Notebook...', 'Open Notebook', False) + open_file = build_button(editor.fileToolbar, './Assets/icons/svg_file_open', 'Open Notebook', 'Open Notebook', False) open_file.setShortcut(QKeySequence.StandardKey.Open) - open_file.triggered.connect(lambda: load(editor)) + open_file.clicked.connect(lambda: load(editor)) - save_file = build_action(editor, 'assets/icons/svg_file_save', 'Save Notebook', 'Save Notebook', False) + save_file = build_button(editor.fileToolbar, './Assets/icons/svg_file_save', 'Save Notebook', 'Save Notebook', False) save_file.setShortcut(QKeySequence.StandardKey.Save) - save_file.triggered.connect(lambda: save(editor)) + save_file.clicked.connect(lambda: save(editor)) - save_fileAs = build_action(editor, 'assets/icons/svg_file_save', 'Save Notebook As...', 'Save Notebook As', False) + save_fileAs = build_button(editor.fileToolbar, './Assets/icons/svg_file_save', 'Save Notebook As...', 'Save Notebook As', False) save_fileAs.setShortcut(QKeySequence.fromString('Ctrl+Shift+S')) - save_fileAs.triggered.connect(lambda: saveAs(editor)) + save_fileAs.clicked.connect(lambda: saveAs(editor)) + + editor.fileToolbar.addWidget(new_file) + editor.fileToolbar.addWidget(open_file) + editor.fileToolbar.addWidget(save_file) + editor.fileToolbar.addWidget(save_fileAs) + + # --------------------------------------------------------------------------------- + # homeToolbar code + + editor.homeToolbar.setObjectName('homeToolbar') + editor.homeToolbar.setIconSize(QSize(18, 18)) + editor.homeToolbar.setMovable(False) + editor.homeToolbar.setFixedHeight(40) + editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, editor.homeToolbar) - file.addActions([new_file, open_file, save_file, save_fileAs]) + paste = build_action(editor.homeToolbar, './Assets/icons/svg_paste', "Paste", "Paste", False) + paste.triggered.connect(editor.frameView.toolbar_paste) -def build_toolbar(editor): - toolbar = QToolBar() - toolbar.setIconSize(QSize(15, 15)) - toolbar.setMovable(False) - editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar) + cut = build_action(editor.homeToolbar, './Assets/icons/svg_cut', "Cut", "Cut", False) + cut.triggered.connect(lambda: editorSignalsInstance.widgetCut.emit(DraggableContainer)) + + copy = build_action(editor.homeToolbar, './Assets/icons/svg_copy', "Copy", "Copy", False) + copy.triggered.connect(lambda: editorSignalsInstance.widgetCopied.emit()) - font = QFontComboBox() - font.currentFontChanged.connect(lambda x: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Font, font.currentFont())) + font_family = QFontComboBox() + font_family.setObjectName("Font") + font_family.setFixedWidth(150) + default_font = font_family.currentFont().family() + print(f"default font is {default_font}") + font_family.currentFontChanged.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Font, font_family.currentFont().family())) - size = QComboBox() - size.addItems([str(fs) for fs in FONT_SIZES]) - size.currentIndexChanged.connect(lambda x: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontSize, int(size.currentText()))) + font_size = QComboBox() + font_size.setObjectName("Font Size") + font_size.setFixedWidth(50) + font_size.addItems([str(fs) for fs in FONT_SIZES]) + # default text size is 11 + default_font_size_index = 4 + font_size.setCurrentIndex(default_font_size_index) + font_size.currentIndexChanged.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontSize, int(font_size.currentText()))) - fontColor = build_action(toolbar, 'assets/icons/svg_font_color', "Font Color", "Font Color", False) - fontColor.triggered.connect(lambda x: openGetColorDialog(purpose = "font")) + #current issues: + # - Alternates between working and not working + # - Textboxes do not remember settings like if font is toggled or current font size - bgColor = build_action(toolbar, 'assets/icons/svg_font_bucket', "Text Box Color", "Text Box Color", False) - bgColor.triggered.connect(lambda x: openGetColorDialog(purpose = "background")) + bgColor = build_action(editor.homeToolbar, './Assets/icons/svg_font_bucket', "Background Color", "Background Color", False) + #bgColor.triggered.connect(lambda: openGetColorDialog(purpose = "background")) + #current bug, alternates between activating and not working when using + bgColor.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.BackgroundColor, QColorDialog.getColor())) - bold = build_action(toolbar, 'assets/icons/bold', "Bold", "Bold", True) - bold.triggered.connect(lambda x: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontBold, None)) + textHighlightColor = build_action(editor.homeToolbar, './Assets/icons/svg_textHighlightColor', "Text Highlight Color", "Text Highlight Color", True) - italic = build_action(toolbar, 'assets/icons/italic.svg', "Italic", "Italic", True) - italic.triggered.connect(lambda x: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontItalic, None)) + textHighlightColor.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.TextHighlightColor, QColorDialog.getColor())) - underline = build_action(toolbar, 'assets/icons/underline.svg', "Underline", "Underline", True) - underline.triggered.connect(lambda x: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontUnderline, None)) + #defines font color icon appearance and settings + fontColor = build_action(editor.homeToolbar, './Assets/icons/svg_font_color', "Font Color", "Font Color", False) + fontColor.triggered.connect(lambda: openGetColorDialog(purpose = "font")) - toolbar.addWidget(font) - toolbar.addWidget(size) - toolbar.addActions([bgColor, fontColor, bold, italic, underline]) + bold = build_action(editor.homeToolbar, './Assets/icons/bold', "Bold", "Bold", True) + bold.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontBold, None)) + italic = build_action(editor.homeToolbar, './Assets/icons/italic.svg', "Italic", "Italic", True) + italic.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontItalic, None)) + + underline = build_action(editor.homeToolbar, './Assets/icons/underline.svg', "Underline", "Underline", True) + underline.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontUnderline, None)) + + strikethrough = build_action(editor.homeToolbar, './Assets/icons/svg_strikethrough.svg', "Strikethrough", "Strikethrough", True) + strikethrough.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Strikethrough, None)) + + refactor = build_action(editor.homeToolbar, './Assets/icons/bold', "Bold", "Bold", True) + refactor.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Refactor, None)) + + delete = build_action(editor.homeToolbar, './Assets/icons/svg_delete', "Delete", "Delete", False) + delete.triggered.connect(lambda: editorSignalsInstance.widgetRemoved.emit(DraggableContainer)) + + # Bullets with placeholder for more bullet options + bullets = build_action(editor.homeToolbar, './Assets/icons/svg_bullets', "Bullets", "Bullets", False) + bullets.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Bullet, None)) + + + # Adds the undo and redo buttons to the toolbar + undo = build_action(editor.homeToolbar, './Assets/icons/svg_undo', "Undo", "Undo", False) + redo = build_action(editor.homeToolbar, './Assets/icons/svg_redo', "Redo", "Redo", False) + + editor.homeToolbar.addWidget(spacer1) + editor.homeToolbar.addActions([paste, cut, copy]) + + editor.homeToolbar.addSeparator() + + editor.homeToolbar.addWidget(font_family) + editor.homeToolbar.addWidget(font_size) + + editor.homeToolbar.addSeparator() + + editor.homeToolbar.addActions([undo, redo, bold, italic, underline, strikethrough, fontColor, textHighlightColor, bgColor, delete, bullets]) + + # numbering menu start + numbering_menu = QMenu(editor) + + bullet_num = build_action(numbering_menu, './Assets/icons/svg_bullet_number', "", "", False) + bullet_num.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Bullet_Num, None)) + + bullet_num = build_action(numbering_menu, './Assets/icons/svg_bullet_number', "", "", False) + bullet_num.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Bullet_Num, None)) + bulletUpperA = build_action(numbering_menu, './Assets/icons/svg_bulletUA', "", "", False) + bulletUpperA.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.BulletUA, None)) + bulletUpperR = build_action(numbering_menu, './Assets/icons/svg_bulletUR', "", "", False) + bulletUpperR.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.BulletUR, None)) + + numbering_menu.addActions([bullet_num, bulletUpperA, bulletUpperR]) + + numbering = build_menubutton(editor, './Assets/icons/svg_bullet_number', "", "Numbering", "width:35px;", numbering_menu) + + editor.homeToolbar.addWidget(numbering) + + # QActionGroup used to display that only one can be toggled at a time + align_group = QActionGroup(editor.homeToolbar) + + align_left = build_action(align_group,"./Assets/icons/svg_align_left","Align Left","Align Left", True) + align_left.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignLeft, None)) + align_center = build_action(align_group,"./Assets/icons/svg_align_center","Align Center","Align Center", True) + align_center.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignCenter, None)) + align_right = build_action(align_group, "./Assets/icons/svg_align_right", "Align Right", "Align Right", True) + align_right.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignRight, None)) + + align_group.addAction(align_left) + align_group.addAction(align_center) + align_group.addAction(align_right) + editor.homeToolbar.addActions(align_group.actions()) + + editor.homeToolbar.addSeparator() + + + # --------------------------------------------------------------------------------- + # Classic Home Toolbar + + editor.classicToolbar = QToolBar() + editor.classicToolbar.setObjectName('classicHomeToolbar') + editor.classicToolbar.setIconSize(QSize(18,18)) + editor.classicToolbar.setFixedHeight(40) + editor.classicToolbar.setMovable(False) + editor.classicToolbar.setVisible(False) + editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, editor.classicToolbar) + + + + # --------------------------------------------------------------------------------- + # insertToolbar code + + editor.insertToolbar.setObjectName('insertToolbar') + editor.insertToolbar.setIconSize(QSize(18,18)) + editor.insertToolbar.setFixedHeight(40) + editor.insertToolbar.setMovable(False) + editor.insertToolbar.setVisible(False) + editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, editor.insertToolbar) + + table = build_button(editor.insertToolbar, './Assets/icons/svg_table', "Table", "Add a Table", False) + table.clicked.connect(editor.frameView.toolbar_table) + + insertSpace = build_button(editor.insertToolbar, './Assets/icons/svg_insert_space', "Insert Space", "Insert Space", False) + + screensnip = build_button(editor.insertToolbar, './Assets/icons/svg_screensnip', "Screensnip", "Screensnip", False) + screensnip.clicked.connect(editor.frameView.toolbar_snipScreen) + + pictures = build_button(editor.insertToolbar, './Assets/icons/svg_pictures', "Pictures" , "Pictures", False) + pictures.clicked.connect(editor.frameView.toolbar_pictures) + + hyperlink = build_button(editor.insertToolbar, './Assets/icons/svg_hyperlink', "Hyperlink", "Hyperlink", False) + hyperlink.clicked.connect(editor.frameView.toolbar_hyperlink) + + # date and time menu start + dateTime_menu = QMenu(editor) + + date = build_action(dateTime_menu, './Assets/icons/svg_date', "Date", "Date", False) + date.triggered.connect(editor.frameView.toolbar_date) + + time = build_action(dateTime_menu, './Assets/icons/svg_time', "Time", "Time", False) + time.triggered.connect(editor.frameView.toolbar_time) + + dateTime_menu.addActions([date, time]) + + dateTime = build_menubutton(editor, './Assets/icons/svg_dateTime', "Date && Time", "Date & Time", "width:120px;", dateTime_menu) + + editor.insertToolbar.addWidget(spacer2) + + editor.insertToolbar.addWidget(table) + + editor.insertToolbar.addSeparator() + + editor.insertToolbar.addWidget(insertSpace) + + editor.insertToolbar.addSeparator() + + editor.insertToolbar.addWidget(screensnip) + editor.insertToolbar.addWidget(pictures) + + editor.insertToolbar.addSeparator() + + editor.insertToolbar.addWidget(hyperlink) + + editor.insertToolbar.addSeparator() + + editor.insertToolbar.addWidget(dateTime) + + # --------------------------------------------------------------------------------- + # drawToolbar code + + editor.drawToolbar.setObjectName('drawToolbar') + editor.drawToolbar.setIconSize(QSize(18, 18)) + editor.drawToolbar.setFixedHeight(40) + editor.drawToolbar.setMovable(False) + editor.drawToolbar.setVisible(False) + editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, editor.drawToolbar) + + undo_action = QAction(QIcon('./Assets/icons/undo.png'), '&Undo', editor) + undo.triggered.connect(editor.triggerUndo) + + redo_action = QAction(QIcon('./Assets/icons/redo.png'), '&Redo', editor) + redo.triggered.connect(editor.triggerRedo) + # redo.triggered.connect(editor.frameView.triggerRedo) + + paperColor= build_action(editor.drawToolbar, './Assets/icons/svg_paper', "Paper Color", "Paper Color", False) + paperColor.triggered.connect(lambda: editor.frameView.pageColor(QColorDialog.getColor())) + + editor.drawToolbar.addWidget(spacer3) + editor.drawToolbar.addActions([undo, redo]) + + editor.drawToolbar.addSeparator() + + editor.drawToolbar.addAction(paperColor) + + # --------------------------------------------------------------------------------- + # pluginToolbar code + + editor.pluginToolbar.setObjectName('pluginToolbar') + editor.pluginToolbar.setIconSize(QSize(18,18)) + editor.pluginToolbar.setFixedHeight(40) + editor.pluginToolbar.setMovable(False) + editor.pluginToolbar.setVisible(False) + editor.addToolBar(Qt.ToolBarArea.TopToolBarArea, editor.pluginToolbar) + + add_widget = build_button(editor, './Assets/icons/svg_question', 'Add Custom Widget', 'Add Custom Widget', False) + + editor.pluginToolbar.addWidget(add_widget) + # --------------------------------------------------------------------------------- + + editor.tabbar.addTab(editor.fileToolbar, '&File') + editor.tabbar.addTab(editor.homeToolbar, '&Home') + editor.tabbar.addTab(editor.insertToolbar, '&Insert') + editor.tabbar.addTab(editor.drawToolbar, '&Draw') + editor.tabbar.addTab(editor.pluginToolbar, '&Plugins') + + # Sets the first shown tab as the home tab + editor.tabbar.setCurrentIndex(1) + +def check_appearance(): + """Checks DARK/LIGHT mode of macos.""" + cmd = 'defaults read -g AppleInterfaceStyle' + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=True) + return bool(p.communicate()[0]) + +# toggles the visibility of the specified toolbar based on user interaction +def set_toolbar_visibility(editor, triggered_toolbar): + # Find all toolbars in the editor + toolbars = editor.findChildren(QToolBar) + + # Iterate over each toolbar + for toolbar in toolbars: + if toolbar.objectName() == triggered_toolbar: + # Toggle the visibility of the triggered toolbar + print(toolbar.objectName(),"visibility change") + toolbar.setVisible(not toolbar.isVisible()) + else: + # Hide all other toolbars + toolbar.setVisible(False) + +# opens a color dialog for selecting font color or background color def openGetColorDialog(purpose): color = QColorDialog.getColor() if color.isValid(): @@ -125,7 +450,34 @@ def openGetColorDialog(purpose): elif purpose == "background": editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.BackgroundColor, color) -def build_action(parent, icon_path, action_name, set_status_tip, set_checkable): +# creates a QAction object with the specified parameters +def build_action(parent, icon_path, action_name, tooltip, checkable): action = QAction(QIcon(icon_path), action_name, parent) - action.setStatusTip(set_status_tip) + action.setObjectName(action_name) + action.setStatusTip(tooltip) + action.setCheckable(checkable) return action + +# creates a QPushButton widget with the specified parameters +def build_button(parent, icon_path, text, tooltip, checkable): + button = QPushButton(parent) + button.setIcon(QIcon(icon_path)) + button.setObjectName(text) + button.setText(text) + button.setToolTip(tooltip) + button.setCheckable(checkable) + return button + +# creates a QToolButton widget with an associated menu +def build_menubutton(parent, icon_path, text, tooltip, style, menu): + button = QToolButton(parent) + button.setIconSize(QSize(18,18)) + button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + button.setPopupMode(QToolButton.MenuButtonPopup) + button.setIcon(QIcon(icon_path)) + button.setText(text) + button.setToolTip(tooltip) + button.setObjectName(tooltip) + button.setStyleSheet(style) + button.setMenu(menu) + return button diff --git a/Modules/Clipboard.py b/Modules/Clipboard.py index e67c7d9..aad99b6 100644 --- a/Modules/Clipboard.py +++ b/Modules/Clipboard.py @@ -2,17 +2,21 @@ class Clipboard: def __init__(self): + + # Initialize variables to store copied widget state and class self.copiedWidgetState = None self.copiedWidgetClass = None editorSignalsInstance.widgetCopied.connect(self.copyWidgetEvent) + # triggered when a widget is copied def copyWidgetEvent(self, draggableContainer): - widget = draggableContainer.childWidget - + widget = draggableContainer.childWidget # Get the child widget from the draggable container + print("copy widget") self.copiedWidgetClass = type(widget) self.copiedWidgetState = widget.__getstate__() + # def getWidgetToPaste(self): widgetState = self.copiedWidgetState widgetClass = self.copiedWidgetClass @@ -20,4 +24,4 @@ def getWidgetToPaste(self): newWidget = widgetClass.__new__(widgetClass) # Get uninitialized instance of widget class newWidget.__setstate__(widgetState) # Initialize the widget instance with its setstate method - return newWidget + return newWidget # Return the initialized widget instance diff --git a/Modules/EditorSignals.py b/Modules/EditorSignals.py index 97d3873..65dd164 100644 --- a/Modules/EditorSignals.py +++ b/Modules/EditorSignals.py @@ -3,6 +3,8 @@ from enum import Enum class ChangedWidgetAttribute(Enum): + # Used for unique signals + # Add variable to create a unique signal BackgroundColor = 0 FontColor = 1 Font = 2 @@ -10,7 +12,23 @@ class ChangedWidgetAttribute(Enum): FontBold = 4 FontItalic = 5 FontUnderline = 6 - + TextHighlightColor = 7 + Bullet = 8 + Bullet_Num = 9 + LoseFocus = 10 + + BulletUR = 11 + BulletUA = 12 + AlignLeft = 13 + AlignCenter = 14 + AlignRight = 15 + PaperColor = 16 + + Strikethrough = 17 + Refactor = 18 + +class CheckSignal(Enum): + BoldCheck = 0 # Cant be statically typed because importing the classes causes circular imports class EditorSignals(QObject): @@ -23,7 +41,7 @@ class EditorSignals(QObject): widgetRemoved = Signal(object) widgetCopied = Signal(object) widgetCut = Signal(object) - + # Recieves any widget model, and the section model to add the instance of DraggableContainer to widgetShouldLoad = Signal(object, object) @@ -33,4 +51,10 @@ class EditorSignals(QObject): # Recieves nothing, used by autosaver changeMade = Signal() + # Clear Selection + loseFocus = Signal() + + # used for checking if a toggle should be toggled off + checkMade = Signal() + editorSignalsInstance = EditorSignals() diff --git a/Modules/Load.py b/Modules/Load.py index b618271..5add389 100644 --- a/Modules/Load.py +++ b/Modules/Load.py @@ -2,7 +2,7 @@ import os from Modules.Save import Autosaver - +import pyautogui from PySide6.QtWidgets import * from PySide6.QtCore import * from PySide6.QtGui import * @@ -13,8 +13,8 @@ def new(editor): print("RAN NEW") destroy(editor) - - editor.notebook = NotebookModel('Untitled') + p_name = pyautogui.prompt("Enter Notebook Name") + editor.notebook = NotebookModel(p_name) editor.notebookTitleView.setText(editor.notebook.title) editor.selected = None editor.autosaver = Autosaver(editor) @@ -23,6 +23,8 @@ def new(editor): # Loads models.notebook.Notebook class from file def load(editor): print("LOADING") + + # Open file dialog for selecting notebook file path, accept = QFileDialog.getOpenFileName( editor, 'Open Notebook', @@ -47,6 +49,7 @@ def load(editor): def load_most_recent_notebook(editor): print("LOAD RECENT RAN") + # Search for most recent notebook file files = [] saves_directory = os.path.join(os.getcwd(), 'Saves') for file in os.listdir(saves_directory): @@ -60,6 +63,7 @@ def load_most_recent_notebook(editor): if (f.endswith(".on") or f.endswith(".ontemp")): print("FOUND: " + str(f)) try: + # Open and load the notebook # prob need load from file function, dup functionality file = open(os.path.join(os.getcwd() + "\\Saves", f), 'rb') destroy(editor) diff --git a/Modules/Multiselect.py b/Modules/Multiselect.py index 3d656da..d07b7c5 100644 --- a/Modules/Multiselect.py +++ b/Modules/Multiselect.py @@ -5,6 +5,7 @@ from PySide6.QtGui import * from PySide6.QtWidgets import * from Models.DraggableContainer import DraggableContainer +from Modules.EditorSignals import editorSignalsInstance,ChangedWidgetAttribute class MultiselectMode(Enum): NONE = 0, @@ -26,6 +27,24 @@ def __init__(self, editorFrame): self.dragInitEventPos = None # Position of the event that started the dragging self.dragOffset = None # Offset of object that is used to drag the others from the first object in the editors list + + # for handling deselect + self.installEventFilter() + # Connect signal for widget removal + editorSignalsInstance.widgetRemoved.connect(self.removeWidgetFromList) + + editorSignalsInstance.widgetCut.connect(self.cutWidgetEvent) + + # Slot to remove widget from object list + @Slot(QWidget) + def removeWidgetFromList(self, widget): + if widget in self.selectedObjects: + self.selectedObjects.remove(widget) + + # install event filter to editorframe + def installEventFilter(self): + self.editorFrame.installEventFilter(self) + def eventFilter(self, obj, event): multiselector = self @@ -55,8 +74,31 @@ def eventFilter(self, obj, event): multiselector.focusObjectIfInMultiselect() return False + + # Handle click outside the selected objects to deselect + if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton: + if multiselector.mode == MultiselectMode.HAS_SELECTED_OBJECTS: + multiselector.deselectIfClickOutsideObjects(event) + print("DESELECT from multiselect") + return False + # check if clicked outside + def deselectIfClickOutsideObjects(self, event): + # Check if the click is outside any selected objects + clickPos = event.globalPos() + for o in self.selectedObjects: + o_tl_pos = o.mapToGlobal(QPoint(0, 0)) + o_br_pos = o_tl_pos + QPoint(o.width(), o.height()) + + if o_tl_pos.x() <= clickPos.x() <= o_br_pos.x() and o_tl_pos.y() <= clickPos.y() <= o_br_pos.y(): + # Click is inside a selected object, do not deselect' + print("Click is inside a selected object, do not deselect") + return + + # Click is outside all selected objects, deselect + self.finishDraggingObjects() + def beginDrawingArea(self, event): self.mode = MultiselectMode.IS_DRAWING_AREA self.drawingWidget.setStyleSheet(TextBoxStyles.INFOCUS.value) @@ -66,38 +108,43 @@ def beginDrawingArea(self, event): self.drawAreaStartGlobalPos = event.globalPos() def continueDrawingArea(self, event): - width = event.pos().x() - self.drawAreaStartLocalPos.x() - height = event.pos().y() - self.drawAreaStartLocalPos.y() + start_x = self.drawAreaStartLocalPos.x() + start_y = self.drawAreaStartLocalPos.y() - self.drawingWidget.resize(width, height) + end_x = event.pos().x() + end_y = event.pos().y() + width = abs(end_x - start_x) + height = abs(end_y - start_y) + + # Calculate the x and y for setting the geometry + x = min(start_x, end_x) + y = min(start_y, end_y) + + # Adjust the geometry of the drawingWidget + self.drawingWidget.setGeometry(x, y, width, height) def finishDrawingArea(self, event): editor = self.editorFrame.editor - # Throw away if the area of the selection is negative (user dragged from bottom right to top left) - if self.drawAreaStartGlobalPos.x() > event.globalPos().x() or self.drawAreaStartGlobalPos.y() > event.globalPos().y(): - self.drawingWidget.hide() - self.finishDraggingObjects() - # You could probably switch around the coordinates in here and modify drawing logic to support other drag directions - return + # Get the coordinates of the top-left corner of the selection area + start_x = min(self.drawAreaStartGlobalPos.x(), event.globalPos().x()) + start_y = min(self.drawAreaStartGlobalPos.y(), event.globalPos().y()) - # Get all DraggableContainers in the selection + # Get the coordinates of the bottom-right corner of the selection area + end_x = max(self.drawAreaStartGlobalPos.x(), event.globalPos().x()) + end_y = max(self.drawAreaStartGlobalPos.y(), event.globalPos().y()) + + # Iterate through objects and check if they are inside the selection area sectionView = self.editorFrame.editor.sectionView currentSectionIndex = sectionView.tabs.currentIndex() currentSectionModel = sectionView.sectionModels[currentSectionIndex] currentSectionModelWidgets = currentSectionModel.widgets - for o in currentSectionModelWidgets: - print(o) - # Map all positions to global for correct coord checks - ob_tl_pos = o.mapToGlobal(QPoint(0, 0)) # Object top left corner - start_pos = self.drawAreaStartGlobalPos - end_pos = event.globalPos() + for o in currentSectionModelWidgets: + ob_tl_pos = o.mapToGlobal(QPoint(0, 0)) - # If object x + width is between start and end x, and object y + height is between start and end y - if ob_tl_pos.x() > start_pos.x() and ob_tl_pos.x() + o.width() < end_pos.x(): - if ob_tl_pos.y() > start_pos.y() and ob_tl_pos.y() + o.height() < end_pos.y(): - self.selectedObjects.append(o) + if start_x <= ob_tl_pos.x() <= end_x and start_y <= ob_tl_pos.y() <= end_y: + self.selectedObjects.append(o) if len(self.selectedObjects) > 0: self.mode = MultiselectMode.HAS_SELECTED_OBJECTS @@ -106,6 +153,13 @@ def finishDrawingArea(self, event): print("SELECTION COUNT: ", len(self.selectedObjects)) + # highlight all text for textwidgets + for o in self.selectedObjects: + # Checks if child is textwidget + if (isinstance(o.childWidget, QTextEdit)): + print("TEXT WIDGET FOUND. HIGHLIGHTING") + # Call function in textwidget to highlight all text in their textbox + o.childWidget.selectAllText() # Hide selection area self.drawingWidget.hide() @@ -120,7 +174,7 @@ def dragObjects(self, event): # Get the position that each object would move to objectPositions = [] - for o in reversed(self.selectedObjects): # fuck it, reversed + for o in reversed(self.selectedObjects): # fuck it, reversed # You have to know this, focus # Position of the first (in the reversed array) object's top left corner + the offset of the click inside the selected object - the position of the object to move @@ -179,3 +233,15 @@ def focusObjectIfInMultiselect(self): o.setStyleSheet(TextBoxStyles.INFOCUS.value) except: self.finishDraggingObjects() + # Allow removing all selected objects + def removeWidgetEvent(self): + for obj in self.selectedObjects: + editorSignalsInstance.changeMade.emit(obj) + + # Allow cutting all selected objects + def cutWidgetEvent(self): + for obj in self.selectedObjects: + editorSignalsInstance.widgetCopied.emit(obj) + editorSignalsInstance.widgetRemoved.emit(obj) + + # Paste works from clipboard, meaning have to store all objects to clipboard \ No newline at end of file diff --git a/Modules/Screensnip.py b/Modules/Screensnip.py index 86950eb..9dd22f9 100644 --- a/Modules/Screensnip.py +++ b/Modules/Screensnip.py @@ -13,43 +13,58 @@ class SnippingWidget(QWidget): def __init__(self): super(SnippingWidget, self).__init__() - self.setWindowFlags(Qt.WindowStaysOnTopHint) + + # Set window attributes based on the platform + if platform == "linux": + self.setWindowFlags(Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground) + + self.setWindowFlags(Qt.WindowStaysOnTopHint) self.parent = None self.screen = QApplication.instance().primaryScreen() self.setGeometry(0, 0, self.screen.size().width(), self.screen.size().height()) - self.begin = QPoint() - self.end = QPoint() + self.begin = self.end = QPoint() self.onSnippingCompleted = None self.event_pos = None def start(self, event_pos): print("SnippingWidget.start") SnippingWidget.is_snipping = True - self.setWindowOpacity(0.3) + + # Set window opacity and cursor based on the platform + if platform != "linux": + self.setWindowOpacity(0.3) + QApplication.setOverrideCursor(QCursor(Qt.CrossCursor)) self.event_pos = event_pos self.show() print("SnippingWidget.start done") + # handle painting the snipping rectangle def paintEvent(self, event): if SnippingWidget.is_snipping: - brush_color = (128, 128, 255, 100) + #brush_color = (128, 128, 255, 100) + brush_color = (0, 0, 0, 0) lw = 3 opacity = 0.3 + + if platform != "linux": + self.setWindowOpacity(opacity) + + qp = QPainter(self) + # Set pen and brush for drawing the rectangle + qp.setPen(QPen(QColor('black'), lw)) + qp.setBrush(QColor(*brush_color)) + rect = QRectF(self.begin, self.end) + qp.drawRect(rect) else: + # Reset coordinates and brush color when snipping is not in progress self.begin = QPoint() self.end = QPoint() brush_color = (0, 0, 0, 0) lw = 0 opacity = 0 - self.setWindowOpacity(opacity) - qp = QPainter(self) - qp.setPen(QPen(QColor('black'), lw)) - qp.setBrush(QColor(*brush_color)) - rect = QRectF(self.begin, self.end) - qp.drawRect(rect) - def mousePressEvent(self, event): self.begin = event.pos() self.end = self.begin @@ -60,28 +75,42 @@ def mouseMoveEvent(self, event): self.update() def mouseReleaseEvent(self, event): + + # Set the flag to indicate that snipping is complete SnippingWidget.is_snipping = False QApplication.restoreOverrideCursor() - x1 = min(self.begin.x(), self.end.x()) - y1 = min(self.begin.y(), self.end.y()) - x2 = max(self.begin.x(), self.end.x()) - y2 = max(self.begin.y(), self.end.y()) + # Calculate the coordinates of the snipped area + rect = self.geometry() + x1 = min(self.begin.x(), self.end.x()) + rect.left() + y1 = min(self.begin.y(), self.end.y()) + rect.top() + x2 = max(self.begin.x(), self.end.x()) + rect.left() + y2 = max(self.begin.y(), self.end.y()) + rect.top() + + # Repaint the widget and process any pending events self.repaint() QApplication.processEvents() - if platform == "darwin": - img = ImageGrab.grab(bbox=( (x1 ) * 2, (y1 + 55 ) * 2, (x2 ) * 2, (y2 + 55) * 2)) - else: - img = ImageGrab.grab(bbox=(x1 + 10, y1 + 30, x2 + 10, y2 + 40)) - + try: + # Capture the screenshot of the snipped area + if platform == "darwin": + #img = ImageGrab.grab(bbox=( (x1 ) * 2, (y1 + 55 ) * 2, (x2 ) * 2, (y2 + 55) * 2)) [may be needed for different mac version - testing in progress] + img = ImageGrab.grab(bbox=(x1, y1 + 55, x2, y2 + 55)) # For mac version 14.1.2 + else: + img = ImageGrab.grab(bbox=(x1 + 2, y1 + 2, x2 - 1, y2 - 1)) + + except Exception as e: + print(f"Error grabbing screenshot: {e}") + img = None try: + # Convert the captured image to OpenCV format for further processing img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) except: img = None - + + # Trigger the snipping completed callback with the captured image if self.onSnippingCompleted is not None: self.onSnippingCompleted(img) diff --git a/Modules/Titlebar.py b/Modules/Titlebar.py new file mode 100644 index 0000000..dace464 --- /dev/null +++ b/Modules/Titlebar.py @@ -0,0 +1,148 @@ +from PySide6.QtCore import * +from PySide6.QtGui import * +from PySide6.QtSvg import * +from PySide6.QtWidgets import * + +from Models.DraggableContainer import DraggableContainer +from Widgets.Textbox import * + +from Modules.BuildUI import * +from Modules.EditorSignals import editorSignalsInstance, ChangedWidgetAttribute +from Modules.Undo import UndoHandler + +from Views.EditorFrameView import * + +class Build_titlebar(QWidget): + def __init__(self, parent): + super().__init__(parent) + print("Building Titlebar") + self.setAutoFillBackground(True) + self.setBackgroundRole(QPalette.ColorRole.Highlight) + self.setFixedHeight(49) + self.setObjectName("titlebar") + # self.setStyleSheet("background-color: rgb(119, 25, 170);") + self.initial_pos = None + + titlebarLayout = QHBoxLayout(self) + titlebarLayout.setContentsMargins(0, 0, 0, 0) + titlebarLayout.setSpacing(0) + + self.logo = build_titlebutton('./Assets/White-OpenNoteLogo', "logo", None) + self.logo.setFixedSize(QSize(49, 49)) + + self.title = QLabel("OpenNote", self) + self.title.setStyleSheet("color: white;") + self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.title.setMaximumWidth(parent.width() // 5.5) + if title := parent.windowTitle(): + self.title.setText(title) + + self.leftSpacer = QWidget(self) + self.leftSpacer.setMinimumWidth(parent.width() // 5.5) + + # Search bar + self.searchbarWidget = QWidget(self) + self.searchbarLayout = QVBoxLayout() + self.searchbarWidget.setLayout(self.searchbarLayout) + self.searchbarWidget.setObjectName("searchbarWidget") + self.searchbarLayout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.searchbar = QLineEdit(self) + self.searchbar.setClearButtonEnabled(True) + self.searchbar.addAction(QIcon("./Assets/Icons/svg_search"), QLineEdit.LeadingPosition) + self.searchbar.setMaximumWidth(parent.width() // 2) + self.searchbar.setPlaceholderText("Search") + self.searchbar.returnPressed.connect(self.perform_search) + self.searchbarLayout.addWidget(self.searchbar) + + self.rightSpacer = QWidget(self) + self.rightSpacer.setMinimumWidth(parent.width() // 6) + + # the names for windowed and fullscreen might be swapped, but it works how it should so 👍 + self.tray = build_titlebutton('./Assets/icons/svg_tray', "tray", self.window().showMinimized) + # self.windowed = build_titlebutton('./Assets/icons/svg_windowed', "windowed", self.windowed_window) + # self.fullscreen = build_titlebutton('./Assets/icons/svg_fullscreen', "fullscreen", self.fullscreen_window) + self.windowed = build_titlebutton('./Assets/icons/svg_windowed', "windowed", self.window().showNormal) + self.fullscreen = build_titlebutton('./Assets/icons/svg_fullscreen', "fullscreen", self.window().showMaximized) + + self.close = build_titlebutton('./Assets/icons/svg_close', "close", self.window().close) + + titlebarLayout.addWidget(self.logo) + + titlebarLayout.addWidget(self.title) + + titlebarLayout.addWidget(self.leftSpacer) + + titlebarLayout.addWidget(self.searchbarWidget) + + titlebarLayout.addWidget(self.rightSpacer) + + titlebarLayout.addWidget(self.tray) + titlebarLayout.addWidget(self.windowed) + titlebarLayout.addWidget(self.fullscreen) + titlebarLayout.addWidget(self.close) + + def window_state_changed(self, state): + self.windowed.setVisible(state != Qt.WindowState.WindowNoState) + self.fullscreen.setVisible(state == Qt.WindowState.WindowNoState) + + def set_action_names(self, action_names): + # Populate the completer with search suggestions + self.search_suggestions = action_names + completer = QCompleter(self.search_suggestions) + completer.setCaseSensitivity(Qt.CaseInsensitive) + self.searchbar.setCompleter(completer) + + def perform_search(self): + search_text = self.searchbar.text().lower() + + # Dictionary mapping search keywords to corresponding actions + actions = { + "paste": lambda: None, # editor.frameView.toolbar_paste + "cut": lambda: None, + "copy": lambda: None, + "paste": lambda: None, + "font": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Font, font_family.currentFont().family()), + "font size": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontSize, int(font_size.currentText())), + "bold": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontBold, None), + "italic": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontItalic, None), + "underline": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontUnderline, None), + "font color": lambda: openGetColorDialog(purpose = "font"), + "text highlight color": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.TextHighlightColor, QColorDialog.getColor()), + "strikethrough": lambda: lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Strikethrough, None), + "background color": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.BackgroundColor, QColorDialog.getColor()), + "delete": lambda: None, + "bullets": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Bullet, None), + "numbering": None, # QMenu lambda call??? + "align left": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignLeft, None), + "align center": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignCenter, None), + "align right": lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignRight, None), + "table": None, # editor.frameView.toolbar_table + "insert space": None, + "screensnip": None, # editor.frameView.toolbar_snipScreen + "pictures": None, # editor.frameView.toolbar_pictures + "hyperlink": None, # editor.frameView.toolbar_hyperlink + "date & time": None, # also qmenu + "undo": lambda: None, + "redo": lambda: None, + "paper color": lambda: None #editor.frameView.pageColor(QColorDialog.getColor()), + } + + # Check if the search_text corresponds to a known action, and execute it if found + action = actions.get(search_text) + if action: + action() + else: + # Handle unknown action + pass + +def build_titlebutton(icon_path, object_name, on_click): + button = QToolButton() + button.setIcon(QIcon(icon_path)) + button.setObjectName(object_name) + button.clicked.connect(on_click) + button.setFocusPolicy(Qt.FocusPolicy.NoFocus) + button.setFixedSize(QSize(49, 49)) + button.setStyleSheet("QToolButton { border: none; padding: 0px;}") + return button + \ No newline at end of file diff --git a/Modules/Undo.py b/Modules/Undo.py index d44eefc..51b7e55 100644 --- a/Modules/Undo.py +++ b/Modules/Undo.py @@ -4,6 +4,8 @@ from Modules.EditorSignals import editorSignalsInstance import os + +#current version of undo does not work. When using ctrl+z in textboxes, it uses the QTextEdit default settings for ctrl+z(undo) and ctrl+y(redo) to make it appear as if undo does work class UndoActionCreate: def __init__(self, draggableContainer): self.draggableContainer = draggableContainer diff --git a/Styles/styles.qss b/Styles/styles.qss index 247ef03..cc089fb 100644 --- a/Styles/styles.qss +++ b/Styles/styles.qss @@ -2,12 +2,50 @@ border: none; padding: 0; margin: 0; - font-family: "Segoe UI"; - font-size: 16pt; + font-family: "calibri", sans-serif; + font-size: 12pt; +} + +QMainWindow { + background-color: rgb(230, 229, 235); } QMenuBar { - background-color: rgb(169, 101, 255); + background-color: rgb(230, 229, 235); + padding: 5px 10px; + color: rgb(0, 0, 0); +} + +QWidget#titlebar { + background-color: rgb(119, 25, 170); +} + +QWidget#titlebar QToolButton, +QWidget#titlebar QWidget, +QWidget#searchbarWidget { + background-color: rgb(119, 25, 170); +} + +QWidget#searchbarWidget QLineEdit { + border-radius: 3px; + background-color: rgb(225,209,235); + color: rgb(119, 25, 170); + height: 60px; +} + +QWidget#searchbarWidget QLineEdit::hover, +QWidget#searchbarWidget QLineEdit::focus { + background-color: white; +} + +QToolButton#tray::hover, +QToolButton#windowed::hover, +QToolButton#fullscreen::hover { + background-color: rgb(108, 23, 154); +} + +QToolButton#close::hover { + background-color: rgb(232, 17, 35); } QMenuBar::item:selected { @@ -15,7 +53,9 @@ QMenuBar::item:selected { } QToolBar { - margin: 10px, 10px, 0, 0; + border-radius: 8px; + spacing: 10px; + background-color: white; } QComboBox { @@ -32,7 +72,8 @@ QPushButton#font_color { height: 20px; } -QToolButton::hover, QPushButton#font_color::hover { +QToolBar QToolButton::hover, +QToolBar QPushButton::hover { background-color: #cecece; } @@ -40,34 +81,52 @@ QToolButton::checked { background-color: #b1b1b1; } -QToolButton::checked::hover { +QToolBar QToolButton::checked::hover { background-color: #9b9b9b; } QTreeView { - background-color: #c2c2c2; + background-color: rgb(230, 229, 235); } QPushButton { - padding: 5px, 5px, 0, 0; + padding: 5px 0; } QLabel#notebook_title { - padding: 5px, 5px, 0, 0; + padding: 5px; text-align: center; background-color: #d9d9d9; } +QLabel#notebook_title::hover { + padding: 5px; + text-align: center; + border-radius: 3px; + background-color: white; +} + QLabel#pages_title { - padding: 5px, 5px, 5px, 0; + padding: 5px; background-color: #c2c2c2; } +QLabel#pages_title::hover { + padding: 5px; + border-radius: 3px; + background-color: white; +} + QPushButton#addPage { padding-bottom: 5px; background-color: #d9d9d9; } QPushButton#addPage::hover { - background-color: #cecece; + background-color: red; } + +QTextEdit { + selection-background-color: #F0F0F0; + selection-color: #000000 +} \ No newline at end of file diff --git a/Styles/stylesDark.qss b/Styles/stylesDark.qss new file mode 100644 index 0000000..07765b1 --- /dev/null +++ b/Styles/stylesDark.qss @@ -0,0 +1,73 @@ +* { + border: none; + padding: 0; + margin: 0; + font-family: "Segoe UI"; + font-size: 16pt; +} + +QMenuBar { + background-color: rgb(40, 20, 60); +} + +QMenuBar::item:selected { + background-color: rgb(60, 20, 60); +} + +QToolBar { + margin: 10px, 10px, 0, 0; +} + +QComboBox { + width: 50px; +} + +QToolButton { + width: 25px; + height: 25px; +} + +QPushButton#font_color { + width: 30px; + height: 20px; +} + +QToolButton::hover, QPushButton#font_color::hover { + background-color: #393939; +} + +QToolButton::checked { + background-color: #272727; +} + +QToolButton::checked::hover { + background-color: #1f1f1f; +} + +QTreeView { + background-color: #292929; +} + +QPushButton { + padding: 5px, 5px, 0, 0; +} + +QLabel#notebook_title { + padding: 5px, 5px, 0, 0; + text-align: center; + background-color: #333333; +} + +QLabel#pages_title { + padding: 5px, 5px, 5px, 0; + background-color: #292929; +} + +QPushButton#addPage { + padding-bottom: 5px; + background-color: #333333; +} + +QPushButton#addPage::hover { + background-color: #393939; +} diff --git a/Views/EditorFrameView.py b/Views/EditorFrameView.py index 5232ead..9e08527 100644 --- a/Views/EditorFrameView.py +++ b/Views/EditorFrameView.py @@ -7,23 +7,56 @@ import sys from Modules.Multiselect import Multiselector, MultiselectMode +from Modules.Clipboard import Clipboard +from Modules.EditorSignals import editorSignalsInstance, ChangedWidgetAttribute +from Modules.Undo import UndoHandler +from Modules.Screensnip import SnippingWidget from Models.DraggableContainer import DraggableContainer from Widgets.Textbox import TextboxWidget -from Modules.EditorSignals import editorSignalsInstance from Widgets.Image import ImageWidget -from Modules.Screensnip import SnippingWidget -from Widgets.Table import TableWidget -from Modules.Clipboard import Clipboard -from Modules.Undo import UndoHandler +from Widgets.Table import * +from Widgets.Link import LinkDialog + +import subprocess + + # Handles all widget display (could be called widget view, but so could draggablecontainer) class EditorFrameView(QWidget): + SETTINGS_KEY = "BackgroundColor" # Key for saving the background color setting + def __init__(self, editor): super(EditorFrameView, self).__init__() + + def check_appearance(): + """Checks DARK/LIGHT mode of macos.""" + cmd = 'defaults read -g AppleInterfaceStyle' + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=True) + return bool(p.communicate()[0]) + + # Reference to the main editor window self.editor = editor # Store reference to the editor (QMainWindow) self.editorFrame = QFrame(editor) - self.editorFrame.setStyleSheet("background-color: white;") + + #Default + self.currentBackgroundColor = self.loadBackgroundColor() or QColor(255, 255, 255) + + is_dark_mode = check_appearance() + white = QColor("white") + + #background page color + if is_dark_mode and (self.currentBackgroundColor == white or self.currentBackgroundColor == QColor(255, 255, 255)): + self.setStyleSheet(f"background-color: rgb(31, 31, 30);") + print("In dark mode, use dark mode color because the background is white or pciked white") + elif not is_dark_mode: + self.setStyleSheet(f"background-color: {self.currentBackgroundColor.name()};") + print("Set background color based on saved color in light mode") + else: + self.setStyleSheet(f"background-color: {self.currentBackgroundColor.name()};") + self.saveBackgroundColor() + print("Saving non-white color as the current background color") # Layout for the editor frame layout = QVBoxLayout(self) @@ -43,16 +76,21 @@ def __init__(self, editor): self.installEventFilter(self.multiselector) # Undo setup - self.shortcut = QShortcut(QKeySequence("Ctrl+Z"), self) - self.shortcut.setContext(Qt.ApplicationShortcut) - self.shortcut.activated.connect(self.undoHandler.undo) - self.undoHandler.undoWidgetDelete.connect(self.undoWidgetDeleteEvent) + #self.shortcut = QShortcut(QKeySequence("Ctrl+Z"), self) + #self.shortcut.setContext(Qt.ApplicationShortcut) + #self.shortcut.activated.connect(self.triggerUndo) + + print("BUILT FRAMEVIEW") - print("BUILT FRAMEVIEW") + def triggerUndo(self): + print("triggerUndo Called") + self.undoHandler.undo + self.undoHandler.undoWidgetDelete.connect(self.undoWidgetDeleteEvent) def pasteWidget(self, clickPos): widgetOnClipboard = self.clipboard.getWidgetToPaste() + # Create draggable container for pasted widget dc = DraggableContainer(widgetOnClipboard, self) self.undoHandler.pushCreate(dc) editorSignalsInstance.widgetAdded.emit(dc) # Notify section that widget was added @@ -63,10 +101,12 @@ def pasteWidget(self, clickPos): def snipScreen(self, clickPos): def onSnippingCompleted(imageMatrix): # Called after screensnipper gets image self.editor.setWindowState(Qt.WindowActive) + self.editor.setWindowFlags(Qt.WindowStaysOnTopHint) self.editor.showMaximized() if imageMatrix is None: return + #Create image widget from the captured image widgetModel = ImageWidget.newFromMatrix(clickPos, imageMatrix) dc = DraggableContainer(widgetModel, self) self.undoHandler.pushCreate(dc) @@ -118,6 +158,9 @@ def removeWidgetEvent(self, draggableContainer): def cutWidgetEvent(self, draggableContainer): editorSignalsInstance.widgetCopied.emit(draggableContainer) editorSignalsInstance.widgetRemoved.emit(draggableContainer) + + def copyWidgetEvent(self, draggableContainer): + editorSignalsInstance.widgetCopied.emit(draggableContainer) # Loading a preexisting (saved) widget into the frame inside a DraggableContainer # Then add that DC instance reference to the sectionModel's widgets[] for runtime @@ -149,39 +192,153 @@ def mouseReleaseEvent(self, event): # Releasing the mouse after clicking to add text else: + print("CREATE DRAGGABLE CONTAINER") self.newWidgetOnSection(TextboxWidget, event.pos()) def mousePressEvent(self, event): print("EDITORFRAME MOUSEPRESS") editor = self.editor + #calls textwidget's clearSelectionSignal + if event.button() == Qt.LeftButton: + if self.rect().contains(event.pos()): + editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.LoseFocus, None) + super().mousePressEvent(event) + # Open context menu on right click if event.buttons() == Qt.RightButton: frame_menu = QMenu(self) - - add_image = QAction("Add Image", self) - add_image.triggered.connect(lambda: self.newWidgetOnSection(ImageWidget, event.pos())) - frame_menu.addAction(add_image) - - add_table = QAction("Add Table", editor) - add_table.triggered.connect(lambda: self.newWidgetOnSection(TableWidget, event.pos())) - frame_menu.addAction(add_table) + # frame_menu.setStyleSheet("font-size: 11pt;") paste = QAction("Paste", editor) paste.triggered.connect(lambda: self.pasteWidget(event.pos())) - frame_menu.addAction(paste) + + insert_Link = QAction("Insert Link", editor) + insert_Link.triggered.connect(lambda: self.insertLink(event.pos())) + add_table = QAction("Add Table", editor) + add_table.triggered.connect(lambda: self.newWidgetOnSection(TableWidget, event.pos())) + #add_table.triggered.connect(self.show_table_popup) + + # not necessary + ''' + add_image = QAction("Add Image", self) + add_image.triggered.connect(lambda: self.newWidgetOnSection(ImageWidget, event.pos())) + take_screensnip = QAction("Snip Screen", editor) take_screensnip.triggered.connect(lambda: self.snipScreen(event.pos())) - frame_menu.addAction(take_screensnip) add_custom_widget = QAction("Add Custom Widget", editor) add_custom_widget.triggered.connect(lambda: self.addCustomWidget(event)) - frame_menu.addAction(add_custom_widget) - + ''' + + frame_menu.addAction(paste) + + frame_menu.addSeparator() + + frame_menu.addAction(insert_Link) + + frame_menu.addSeparator() + + frame_menu.addAction(add_table) + + ''' + frame_menu.addSeparator() + + frame_menu.addActions([add_image, take_screensnip, add_custom_widget]) + ''' + frame_menu.exec(event.globalPos()) - def addCustomWidget(self, event): + def insertLink(self, clickPos): + link_dialog = LinkDialog() + result = link_dialog.exec_() #Execute the dialog and wait for user input + if result == QDialog.Accepted: + + link_address, display_text = link_dialog.get_link_data() # Get the link address and display text from the dialog + textboxWidget = TextboxWidget.new(clickPos) # Create a new TextboxWidget at the specified position + textboxWidget.insertTextLink(link_address, display_text) # Insert the hyperlink into the TextboxWidget + + # Create a DraggableContainer for the TextboxWidget and show it + dc = DraggableContainer(textboxWidget, self) + dc.show() + + self.undoHandler.pushCreate(dc) + editorSignalsInstance.widgetAdded.emit(dc) + editorSignalsInstance.changeMade.emit() + + def insertDate(self, clickPos): + current_date = QDateTime.currentDateTime().toString("M/d/yyyy") + textboxWidget = TextboxWidget.new(clickPos) + textboxWidget.insertPlainText(current_date) + dc = DraggableContainer(textboxWidget, self) + dc.show() + self.undoHandler.pushCreate(dc) + editorSignalsInstance.widgetAdded.emit(dc) + editorSignalsInstance.changeMade.emit() + + def insertTime(self, clickPos): + current_time = QDateTime.currentDateTime().toString("h:mm AP") + textboxWidget = TextboxWidget.new(clickPos) + textboxWidget.insertPlainText(current_time) + dc = DraggableContainer(textboxWidget, self) + dc.show() + self.undoHandler.pushCreate(dc) + editorSignalsInstance.widgetAdded.emit(dc) + editorSignalsInstance.changeMade.emit() + + def center_of_screen(self): + editor_frame_geometry = self.editorFrame.geometry() + print(f"editor_frame_geometry.width() is {editor_frame_geometry.width()}") + print(f"editor_frame_geometry.height() is {editor_frame_geometry.height()}") + center_x = (editor_frame_geometry.width() - 200) // 2 + center_y = (editor_frame_geometry.height() - 200) // 2 + return center_x, center_y + + # Used for calling functions in toolbar + def toolbar_paste(self): + print("toolbar_paste pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.pasteWidget(clickPos) + + def toolbar_table(self): + print("toolbar_table pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.newWidgetOnSection(TableWidget, clickPos) + + def toolbar_snipScreen(self): + print("toolbar_snipScreen pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.snipScreen(clickPos) + + def toolbar_pictures(self): + print("toolbar_pictures pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.newWidgetOnSection(ImageWidget, clickPos) + + def toolbar_hyperlink(self): + print("toolbar_hyperlink pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.insertLink(clickPos) + + def toolbar_date(self): + print("toolbar_date pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.insertDate(clickPos) + + def toolbar_time(self): + print("toolbar_time pressed") + center_x, center_y = self.center_of_screen() + clickPos = QPoint(center_x, center_y) + self.insertTime(clickPos) + + def addCustomWidget(self, e): def getCustomWidgets(): customWidgets = {} # dict where entries are {name: class} @@ -206,17 +363,45 @@ def getCustomWidgets(): item_action = QAction(customWidget[0], self) def tmp(c, pos): return lambda: self.newWidgetOnSection(c, pos) - item_action.triggered.connect(tmp(customWidget[1], event.pos())) + item_action.triggered.connect(tmp(customWidget[1], e.pos())) pluginMenu.addAction(item_action) - pluginMenu.exec(event.globalPos()) - - def mouseMoveEvent(self, event): # This event is only called after clicking down on the frame and dragging + pluginMenu.exec(e.globalPos()) + def mouseMoveEvent(self, e): # This event is only called after clicking down on the frame and dragging # Set up multi-select on first move of mouse drag if self.multiselector.mode != MultiselectMode.IS_DRAWING_AREA: - self.multiselector.beginDrawingArea(event) + self.multiselector.beginDrawingArea(e) # Resize multi-select widget on mouse every proceeding mouse movement (dragging) else: - self.multiselector.continueDrawingArea(event) + self.multiselector.continueDrawingArea(e) + + def slot_action1(self, item): + print("Action 1 triggered") + + # Handles changes to the background color of the editor frame. + def pageColor(self, color: QColor): + print("CHANGE BACKGROUND COLOR EVENT") + if color.isValid(): + self.currentBackgroundColor = color + self.editorFrame.setStyleSheet(f"background-color: {color.name()};") + self.saveBackgroundColor() + + #Loads the previously saved background color from settings + def loadBackgroundColor(self): + settings = QSettings() + color = settings.value(self.SETTINGS_KEY, type=QColor) + return color + + # Saves the current background color to settings + def saveBackgroundColor(self): + settings = QSettings() + settings.setValue(self.SETTINGS_KEY, self.currentBackgroundColor) + + # Retrieves the current background color of the editor frame + def getCurrentBackgroundColor(self): + return self.currentBackgroundColor + + + diff --git a/Views/NotebookTitleView.py b/Views/NotebookTitleView.py index ca8dbb1..5faadfc 100644 --- a/Views/NotebookTitleView.py +++ b/Views/NotebookTitleView.py @@ -9,6 +9,7 @@ def __init__(self, notebookTitle: str): self.notebookTitle = notebookTitle # Reference to the title on the notebook model + # Creating the QTextEdit widget for displaying and editing the title self.titleWidget = QTextEdit() self.titleWidget.setText(self.notebookTitle) self.titleWidget.setFixedHeight(30) diff --git a/Views/PageView.py b/Views/PageView.py index 65922c6..427d3de 100644 --- a/Views/PageView.py +++ b/Views/PageView.py @@ -11,6 +11,9 @@ # Page view and controller class PageView(QWidget): + # Class variable to keep track of page count + pageCount = 1 + def __init__(self, pageModels: List[PageModel]): super(PageView, self).__init__() @@ -57,7 +60,7 @@ def loadPages(self, pageModels: List[PageModel]): root.appendRow([rootPage]) # Create a first page - newPageModel = PageModel('New Page', 0) + newPageModel = PageModel(f'New Page {self.pageCount}', 0) newPage = QStandardItem(newPageModel.title) newPage.setData(newPageModel) newPage.setEditable(False) @@ -123,15 +126,31 @@ def openMenu(self, position: QModelIndex): level = 0 menu = QMenu() - addChildAction = menu.addAction(self.tr("Add Page")) - addChildAction.triggered.connect(partial(self.addPage, level, clickedIndex)) - if not page.data().isRoot(): # Dont delete the root page + renamePageAction = menu.addAction(self.tr("Rename Page")) + renamePageAction.setIcon(QIcon('./Assets/icons/svg_rename')) + renamePageAction.triggered.connect(partial(self.renamePage, page)) + deletePageAction = menu.addAction(self.tr("Delete Page")) + deletePageAction.setIcon(QIcon('./Assets/icons/svg_delete')) deletePageAction.triggered.connect(partial(self.deletePage, page)) - renamePageAction = menu.addAction(self.tr("Rename Page")) - renamePageAction.triggered.connect(partial(self.renamePage, page)) + addChildAction = menu.addAction(self.tr("New Page")) + addChildAction.setIcon(QIcon('./Assets/icons/svg_add_page')) + addChildAction.triggered.connect(partial(self.addPage, level, clickedIndex)) + + + #Current Issue: settings do not save because autosave only saves editor state + ''' + mergePageAction = menu.addAction(self.tr("Merge Pages")) + mergePageAction.triggered.connect(partial(self.mergePages, page)) + ''' + # Why even have this??? + # changeTextColorAction = menu.addAction(self.tr("Change Text Color")) + # changeTextColorAction.triggered.connect(partial(self.changeTextColor, page)) + + PageColorAction = menu.addAction(self.tr("Page Color")) + PageColorAction.triggered.connect(partial(self.PageColor, page)) menu.exec_(self.sender().viewport().mapToGlobal(position)) # Dont let the right click reach the viewport, context menu will still open but this will stop the page from being selected @@ -140,22 +159,28 @@ def eventFilter(self, source, event): if(event.button() == Qt.RightButton): return True return False - + def addPage(self, level: int, clickedIndex: QModelIndex): - # New page added under parent (what the user right clicked on) + + # New page added to root + # will add functionallity for page groups which can be nested + parentPage = self.model.itemFromIndex(clickedIndex) + while not parentPage.data().isRoot(): + parentPage = parentPage.parent() parentPageUUID = parentPage.data().getUUID() # Create a new page model, set that as the data for the new page - newPageModel = PageModel('New Page', parentPageUUID) + self.pageCount += 1 + newPageModel = PageModel(f'New Page {self.pageCount}', parentPageUUID) newPage = QStandardItem(newPageModel.title) newPage.setData(newPageModel) newPage.setEditable(False) parentPage.appendRow([newPage]) # Add to UI self.pageModels.append(newPageModel) # Add to array of PageModel - self.tree.expand(clickedIndex) + self.tree.expand(clickedIndex) def deletePage(self, page: QStandardItem): deletePages = [page] @@ -209,3 +234,69 @@ def changePage(self, current: QModelIndex, previous: QModelIndex): print("CHANGED PAGE TO: " + newPage.data().title) editorSignalsInstance.pageChanged.emit(newPageModel) # Tell the sectionView that the page has changed + + + # Not sure if working as intended + ''' + def mergePages(self, page: QStandardItem): + selectedIndexes = self.tree.selectedIndexes() + + if len(selectedIndexes) == 2: + page1Item = self.model.itemFromIndex(selectedIndexes[0]) + page2Item = self.model.itemFromIndex(selectedIndexes[1]) + + # Extract the PageModel objects from the selected QStandardItem objects + page1Model = page1Item.data() + page2Model = page2Item.data() + + # Prompt the user for the name of the page to merge into the second page + name, ok = QInputDialog.getText(self, 'Merge Pages', f'Enter the name of the page to merge into "{page2Model.title}":') + if not ok: + return + + if name != page1Model.title: + QMessageBox.warning(self, 'Invalid Page Name', 'The entered page name does not match the selected page.', QMessageBox.Ok) + return + + # Confirm the merge operation + reply = QMessageBox.question(self, 'Confirm Merge', 'This action cannot be undone. Are you sure you want to merge the pages?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.No: + return + + # Merge the sections of the two pages into one page + mergedSections = page1Model.sections + page2Model.sections + newPageModel = PageModel(page2Model.title, page2Model.getParentUUID(), mergedSections) + + # Create a new QStandardItem for the merged page + newPageItem = QStandardItem(newPageModel.title) + newPageItem.setData(newPageModel) + newPageItem.setEditable(False) + + # Insert the new merged page at the bottom of the children of the root + root = self.model.invisibleRootItem() + index = root.rowCount() # Get the last index + root.insertRow(index, [newPageItem]) + + # Remove the original two pages from the model + root.removeRow(page1Item.row()) + root.removeRow(page2Item.row()) + + # Update the pageModels list + self.pageModels.remove(page1Model) + self.pageModels.remove(page2Model) + self.pageModels.append(newPageModel) + + # Expand the tree to show the changes + self.tree.expandAll() + else: + QMessageBox.warning(self, 'Invalid Selection', 'Please select exactly two pages to merge.', QMessageBox.Ok) + ''' + def changeTextColor(self, page: QStandardItem): + color = QColorDialog.getColor() + if color.isValid(): + page.setForeground(color) + + def PageColor(self, page: QStandardItem): + color = QColorDialog.getColor() + if color.isValid(): + page.setBackground(color) \ No newline at end of file diff --git a/Views/SectionView.py b/Views/SectionView.py index b26d198..eb6eb24 100644 --- a/Views/SectionView.py +++ b/Views/SectionView.py @@ -98,14 +98,17 @@ def openMenu(self, position: QPoint): sectionModel = self.tabs.tabData(clickedSectionIndex) menu = QMenu() - addSectionAction = menu.addAction(self.tr("Add Section")) - addSectionAction.triggered.connect(partial(self.addSection, sectionModel, clickedSectionIndex)) + renameSectionAction = menu.addAction(self.tr("Rename Section")) + renameSectionAction.setIcon(QIcon('./Assets/icons/svg_rename')) + renameSectionAction.triggered.connect(partial(self.renameSection, sectionModel, clickedSectionIndex)) deleteSectionAction = menu.addAction(self.tr("Delete Section")) + deleteSectionAction.setIcon(QIcon('./Assets/icons/svg_delete')) deleteSectionAction.triggered.connect(partial(self.deleteSection, sectionModel, clickedSectionIndex)) - renameSectionAction = menu.addAction(self.tr("Rename Section")) - renameSectionAction.triggered.connect(partial(self.renameSection, sectionModel, clickedSectionIndex)) + addSectionAction = menu.addAction(self.tr("Add Section")) + addSectionAction.setIcon(QIcon('./Assets/icons/svg_add_page')) + addSectionAction.triggered.connect(partial(self.addSection, sectionModel, clickedSectionIndex)) menu.exec(self.tabs.mapToGlobal(position)) diff --git a/Widgets/Image.py b/Widgets/Image.py index 64971e1..04b159d 100644 --- a/Widgets/Image.py +++ b/Widgets/Image.py @@ -1,6 +1,8 @@ from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * +from PySide6.QtWidgets import QFileDialog + from Modules.Enums import WidgetType @@ -19,17 +21,17 @@ def __init__(self, x, y, w, h, image_matrix): bytes_per_line = 3 * matrix_width q_image = QImage(image_matrix.data, matrix_width, matrix_height, bytes_per_line, QImage.Format_BGR888) self.q_pixmap = QPixmap(q_image) - self.setPixmap(self.q_pixmap.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) # Scale to widget geometry + self.setPixmap(self.q_pixmap.scaled(w, h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) # Scale to widget geometry self.setGeometry(x, y, w, h) # this should get fixed - self.persistantGeometry = self.geometry() + self.persistantGeometry = self.geometry() # Handle resize def newGeometryEvent(self, newGeometry): new_w = newGeometry.width() new_h = newGeometry.height() if (self.w != new_w) or (self.h != new_h): # Not exactly sure how object's width and height attribute gets updated but this works - self.setPixmap(self.q_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + self.setPixmap(self.q_pixmap.scaled(new_w, new_h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) pixmap_rect = self.pixmap().rect() w = pixmap_rect.width() @@ -38,7 +40,7 @@ def newGeometryEvent(self, newGeometry): self.persistantGeometry = newGeometry - @staticmethod + '''@staticmethod def new(clickPos): # Get path from user @@ -54,6 +56,33 @@ def new(clickPos): image = ImageWidget(clickPos.x(), clickPos.y(), w, h, image_matrix) # Note: the editorframe will apply pos based on event return image + ''' + # uses inbuilt qt file dialog + @staticmethod + def new(clickPos): + + + # Create a dummy parent widget for the file dialog + dummy_parent = QWidget() + + # Get path from user + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog # Use Qt's built-in dialog instead of the native platform dialog + path, _ = QFileDialog.getOpenFileName(dummy_parent, 'Add Image', '', 'Images (*.png *.xpm *.jpg *.bmp *.jpeg);;All Files (*)', options=options) + + # Check if the user selected a file + if path: + # Get image size + image_matrix = cv2.imread(path) + h, w, _ = image_matrix.shape + + # Create image and add to notebook + image = ImageWidget(clickPos.x(), clickPos.y(), w, h, image_matrix) + return image + + # Return None or handle the case where the user cancels the dialog + return None + @staticmethod # Special staticmethod that screensnip uses def newFromMatrix(clickPos, imageMatrix): @@ -70,3 +99,130 @@ def __getstate__(self): def __setstate__(self, state): self.__init__(state['geometry'].x(), state['geometry'].y(), state['geometry'].width(), state['geometry'].height(), state['image_matrix']) + + def customMenuItems(self): + def build_action(parent, icon_path, action_name, set_status_tip, set_checkable): + action = QAction(QIcon(icon_path), action_name, parent) + action.setStatusTip(set_status_tip) + action.setCheckable(set_checkable) + return action + + + toolbarBottom = QToolBar() + toolbarBottom.setIconSize(QSize(16, 16)) + toolbarBottom.setMovable(False) + + #crop = build_action(toolbarTop, './Assets/icons/svg_crop', "Crop", "Crop", False) + + flipHorizontal = build_action(toolbarBottom, './Assets/icons/svg_flip_horizontal', "Horizontal Flip", "Horizontal Flip", False) + flipHorizontal.triggered.connect(self.flipHorizontal) + flipVertical = build_action(toolbarBottom, './Assets/icons/svg_flip_vertical', "Vertical Flip", "Vertical Flip", False) + flipVertical.triggered.connect(self.flipVertical) + + rotateLeftAction = build_action(toolbarBottom, './Assets/icons/svg_rotate_left', "Rotate 90 degrees Left", "Rotate 90 degrees Left", False) + rotateLeftAction.triggered.connect(self.rotate90Left) + rotateRightAction = build_action(toolbarBottom, './Assets/icons/svg_rotate_right', "Rotate 90 degrees Right", "Rotate 90 degrees Right", False) + + rotateRightAction.triggered.connect(self.rotate90Right) + + shrinkImageAction = build_action(toolbarBottom, 'Assets/icons/svg_shrink', "Shrink", "Shrink", False) + shrinkImageAction.triggered.connect(self.shrinkImage) + expandImageAction = build_action(toolbarBottom, 'Assets/icons/svg_expand', "Expand", "Expand", False) + expandImageAction.triggered.connect(self.expandImage) + + + toolbarBottom.addActions([rotateLeftAction, rotateRightAction, flipHorizontal, flipVertical, shrinkImageAction, expandImageAction]) + + qwaBottom = QWidgetAction(self) + qwaBottom.setDefaultWidget(toolbarBottom) + + return [qwaBottom] + + def flipVertical(self): + # Flip the image matrix vertically using OpenCV + parent_widget = self.parentWidget() + if parent_widget: + newX, newY = parent_widget.x(), parent_widget.y() + new_width, new_height = parent_widget.height(), parent_widget.width() + parent_widget.setGeometry(newX, newY, new_width, new_height) + + self.image_matrix = cv2.flip(self.image_matrix, 0) + self.updatePixmap() + + + def flipHorizontal(self): + # Flip the image matrix horizontally using OpenCV + + parent_widget = self.parentWidget() + if parent_widget: + newX, newY = parent_widget.x(), parent_widget.y() + new_width, new_height = parent_widget.height(), parent_widget.width() + parent_widget.setGeometry(newX, newY, new_width, new_height) + + self.image_matrix = cv2.flip(self.image_matrix, 1) + self.updatePixmap() + + + def rotate90Left(self): + # Rotate the image matrix 90 degrees to the left using OpenCV + self.w, self.h = self.h, self.w + + parent_widget = self.parentWidget() + if parent_widget: + newX, newY = parent_widget.x(), parent_widget.y() + new_width, new_height = parent_widget.height(), parent_widget.width() + parent_widget.setGeometry(newX, newY, new_width, new_height) + self.image_matrix = cv2.rotate(self.image_matrix, cv2.ROTATE_90_COUNTERCLOCKWISE) + + self.updatePixmap() + def rotate90Right(self): + # Rotate the image matrix 90 degrees to the right using OpenCV + self.w, self.h = self.h, self.w + + # Access parent and update geometry + parent_widget = self.parentWidget() + if parent_widget: + newX, newY = parent_widget.x(), parent_widget.y() + new_width, new_height = parent_widget.height(), parent_widget.width() + parent_widget.setGeometry(newX, newY, new_width, new_height) + + self.image_matrix = cv2.rotate(self.image_matrix, cv2.ROTATE_90_CLOCKWISE) + self.updatePixmap() + + def shrinkImage(self): + # Decrease image size by 10% + self.w = int(self.w * 0.9) + self.h = int(self.h * 0.9) + parent_widget = self.parentWidget() + if parent_widget: + newX, newY = parent_widget.x(), parent_widget.y() + new_width, new_height = self.w, self.h + parent_widget.setGeometry(newX, newY, new_width, new_height) + self.setPixmap(self.q_pixmap.scaled(self.w, self.h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) + + def expandImage(self): + # Increase image size by 10% + self.w = int(self.w * 1.1) + self.h = int(self.h * 1.1) + parent_widget = self.parentWidget() + if parent_widget: + newX, newY = parent_widget.x(), parent_widget.y() + new_width, new_height = self.w, self.h + parent_widget.setGeometry(newX, newY, new_width, new_height) + self.setPixmap(self.q_pixmap.scaled(self.w, self.h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) + + # Updates display. Note: Keeps Aspect Ratio + def updateImageSize(self): + # Update the displayed pixmap with the new size + self.setPixmap(self.q_pixmap.scaled(self.w, self.h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + + + def updatePixmap(self): + # Update the QImage and QPixmap + matrix_height, matrix_width, _ = self.image_matrix.shape + bytes_per_line = 3 * matrix_width + q_image = QImage(self.image_matrix.data, matrix_width, matrix_height, bytes_per_line, QImage.Format_BGR888) + self.q_pixmap = QPixmap(q_image) + + # Update the displayed pixmap + self.setPixmap(self.q_pixmap.scaled(self.w, self.h, Qt.KeepAspectRatio, Qt.SmoothTransformation)) diff --git a/Widgets/Link.py b/Widgets/Link.py new file mode 100644 index 0000000..7258efe --- /dev/null +++ b/Widgets/Link.py @@ -0,0 +1,47 @@ +from PySide6.QtCore import * +from PySide6.QtGui import * +from PySide6.QtWidgets import * + + +FONT_SIZES = [7, 8, 9, 10, 11, 12, 13, 14, 18, 24, 36, 48, 64, 72, 96, 144, 288] +class LinkDialog(QDialog): + def __init__(self): + super().__init__() + self.setWindowTitle("Insert Link") + layout = QVBoxLayout() + + # to stay on top of application + self.setWindowFlag(Qt.WindowStaysOnTopHint) + + self.link_label = QLabel("Link Address:") + self.link_textbox = QLineEdit() + self.display_label = QLabel("Display Text:") + self.display_textbox = QLineEdit() + + layout.addWidget(self.link_label) + layout.addWidget(self.link_textbox) + layout.addWidget(self.display_label) + layout.addWidget(self.display_textbox) + + ok_button = QPushButton("OK") + ok_button.clicked.connect(self.accept) + cancel_button = QPushButton("Cancel") + cancel_button.clicked.connect(self.reject) + + layout.addWidget(ok_button) + layout.addWidget(cancel_button) + + self.setLayout(layout) + + self.setModal(False) + + def get_link_data(self): + link_address = self.link_textbox.text() + display_text = self.display_textbox.text() + return link_address, display_text + def mousePressEvent(self, event): + # Check if the mouse click is outside the dialog + if event.button() == Qt.LeftButton and not self.rect().contains(event.globalPos()): + self.close() + + diff --git a/Widgets/Table.py b/Widgets/Table.py index 7f615ff..d3ad592 100644 --- a/Widgets/Table.py +++ b/Widgets/Table.py @@ -8,7 +8,8 @@ def __init__(self, x, y, w, h, rows, cols): # The actual table widget self.table = QTableWidget(rows, cols, self) - # Hide the horizontal and vertical headers + # Hides the horizontal and vertical headers + # These actually look really cool tho self.table.horizontalHeader().setVisible(False) self.table.verticalHeader().setVisible(False) @@ -42,17 +43,14 @@ def addCol(self): @staticmethod def new(clickPos: QPoint): - return TableWidget(clickPos.x(), clickPos.y(), 200, 200, 2, 2) - - def customMenuItems(self): - addRow = QAction("Add Row", self) - addRow.triggered.connect(self.addRow) - - addCol = QAction("Add Column", self) - addCol.triggered.connect(self.addCol) - - return [addRow, addCol] - + dialog = TablePopupWindow() + if dialog.exec_() == QDialog.Accepted: + rows_input, cols_input = dialog.get_table_data() + print(f"rows input is {rows_input} cols_input is {cols_input}") + table_widget = TableWidget(clickPos.x(), clickPos.y(), 200, 200, int(rows_input), int(cols_input)) + + return table_widget + def __getstate__(self): state = {} @@ -79,3 +77,55 @@ def __setstate__(self, state): for i in range(colCnt): for j in range(rowCnt): self.table.setItem(j, i, QTableWidgetItem(state['tableData'][i][j])) + +def show_table_popup(self): + popup = TablePopupWindow() + popup.exec_() + #def undo_triggered(self): + # Call the EditorFrameView's triggerUndo method + #self.EditorFrameView.triggerUndo() + +class TablePopupWindow(QDialog): + def __init__(self): + super().__init__() + + # to stay on top of application + self.setWindowFlag(Qt.WindowStaysOnTopHint) + + self.setWindowTitle("Table Configuration") + self.layout = QVBoxLayout() + + self.rows_input = QLineEdit(self) + self.rows_input.setPlaceholderText("Enter number of rows:") + self.layout.addWidget(self.rows_input) + + self.cols_input = QLineEdit(self) + colNum = self.cols_input.setPlaceholderText("Enter number of columns:") + self.layout.addWidget(self.cols_input) + + create_table_button = QPushButton("Create Table") + self.layout.addWidget(create_table_button) + create_table_button.clicked.connect(self.accept) + #create error message if no data is entered or if number of rows or columns are < 1 + + cancel_button = QPushButton("Cancel") + self.layout.addWidget(cancel_button) + cancel_button.clicked.connect(self.reject) + + + self.setLayout(self.layout) + + self.setModal(False) + + def get_table_data(self): + rows_input = self.rows_input.text() + cols_input = self.cols_input.text() + return rows_input, cols_input + + def create_table(self): + print("table") + + def mousePressEvent(self, event): + # Check if the mouse click is outside the dialog + if event.button() == Qt.LeftButton and not self.rect().contains(event.globalPos()): + self.close() diff --git a/Widgets/Textbox.py b/Widgets/Textbox.py index 998d2fe..4312a7a 100644 --- a/Widgets/Textbox.py +++ b/Widgets/Textbox.py @@ -1,24 +1,90 @@ from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * +from Modules.EditorSignals import editorSignalsInstance, ChangedWidgetAttribute, CheckSignal + +import subprocess FONT_SIZES = [7, 8, 9, 10, 11, 12, 13, 14, 18, 24, 36, 48, 64, 72, 96, 144, 288] + class TextboxWidget(QTextEdit): - def __init__(self, x, y, w = 15, h = 30, t = ''): + def __init__(self, x, y, w=15, h=30, t=""): super().__init__() - self.setGeometry(x, y, w, h) # This sets geometry of DraggableObject + def check_appearance(): + """Checks DARK/LIGHT mode of macos.""" + cmd = 'defaults read -g AppleInterfaceStyle' + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=True) + return bool(p.communicate()[0]) + + self.setGeometry(x, y, w, h) # This sets geometry of DraggableObject self.setText(t) + # self.setStyleSheet("background-color: rgba(0, 0, 0, 0); selection-background-color: #FFFFFF;") + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.textChanged.connect(self.textChangedEvent) - self.setStyleSheet('background-color: rgba(0, 0, 0, 0);') + editorSignalsInstance.widgetAttributeChanged.connect(self.widgetAttributeChanged) + + + + if check_appearance() == True: + self.setStyleSheet("background-color: rgba(31,31,30,255);") + #self.changeBackgroundColorEvent(31, 31, 30) + self.setTextColor("white") + self.changeAllTextColors("white") + else: + self.setStyleSheet("background-color: rgba(0, 0, 0, 0);") + #self.changeBackgroundColorEvent(0,0,0) + self.setTextColor("black") + self.changeAllTextColors("black") + + self.setTextInteractionFlags(Qt.TextEditorInteraction | Qt.TextBrowserInteraction) + self.installEventFilter(self) + + def eventFilter(self, obj, event): + if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab: + self.handleTabKey() + return True # To prevent the default Tab key behavior + '''if event.type() == QEvent.FocusOut: + if self.checkEmpty(): + parent = self.parent() + if parent is not None: + parent.deleteLater()''' + + return super(TextboxWidget, self).eventFilter(obj, event) + + # upon clicking somewhere else, remove selection of highlighted text + def setCursorPosition(self, event): + print("SET TEXT CURSOR POSITION TO MOUSE POSITION") + cursor = self.cursorForPosition(event.pos()) + self.setTextCursor(cursor) + + # Initial size is w=15, h=30. once changes to textbox has been detected, change the size def textChangedEvent(self): + width = 150 + height = 50 if len(self.toPlainText()) < 2: - self.resize(100, 100) + self.resize(width, height) + else: + # handle cases where text reaches past the textbox + document = self.document() + documentSize = document.size().toSize() + documentHeight = documentSize.height() + # the document height is the text height. This is to expand the widget size to match document height + if(height < documentHeight): + height = documentHeight + self.resize(width, height) + def focusOutEvent(self, event): + super().focusOutEvent(event) + + + + @staticmethod def new(clickPos: QPoint): @@ -27,69 +93,200 @@ def new(clickPos: QPoint): def __getstate__(self): data = {} - data['geometry'] = self.parentWidget().geometry() - data['content'] = self.toHtml() - data['stylesheet'] = self.styleSheet() + data["geometry"] = self.parentWidget().geometry() + data["content"] = self.toHtml() + data["stylesheet"] = self.styleSheet() return data def __setstate__(self, data): - self.__init__(data['geometry'].x(), data['geometry'].y(), data['geometry'].width(), data['geometry'].height(), data['content']) - self.setStyleSheet(data['stylesheet']) + self.__init__( + data["geometry"].x(), + data["geometry"].y(), + data["geometry"].width(), + data["geometry"].height(), + data["content"], + ) + self.setStyleSheet(data["stylesheet"]) def checkEmpty(self): if len(self.toPlainText()) < 1: return True return False + + # Handles events from any toolbar button + def widgetAttributeChanged(self, changedWidgetAttribute, value): + # dictionary of toolbar functions + attribute_functions = { + # note: for functions with no value passed, lambda _ will allow it to pass with no value + + # font functions + ChangedWidgetAttribute.FontSize: lambda val: self.changeFontSizeEvent(val), + ChangedWidgetAttribute.FontBold: lambda _: self.changeFontBoldEvent(), + ChangedWidgetAttribute.FontItalic: lambda _: self.changeFontItalicEvent(), + ChangedWidgetAttribute.FontUnderline: lambda _: self.changeFontUnderlineEvent(), + ChangedWidgetAttribute.Strikethrough: lambda _: self.setStrikeOut(), + ChangedWidgetAttribute.Font: lambda val: self.changeFontEvent(val), + ChangedWidgetAttribute.FontColor: lambda val: self.changeFontColorEvent(val), + ChangedWidgetAttribute.TextHighlightColor: lambda val: self.changeTextHighlightColorEvent(val), + + # background color functions + ChangedWidgetAttribute.BackgroundColor: lambda val: self.changeBackgroundColorEvent(val), + ChangedWidgetAttribute.PaperColor: lambda val: self.paperColor(val), # not implemented yet + + # Bullet list functions + ChangedWidgetAttribute.Bullet: lambda _: self.bullet_list("bulletReg"), + ChangedWidgetAttribute.Bullet_Num: lambda _: self.bullet_list("bulletNum"), + ChangedWidgetAttribute.BulletUA: lambda _: self.bullet_list("bulletUpperA"), + ChangedWidgetAttribute.BulletUR: lambda _: self.bullet_list("bulletUpperR"), + + # Alignment functions + ChangedWidgetAttribute.AlignLeft: lambda _: self.changeAlignmentEvent("alignLeft"), + ChangedWidgetAttribute.AlignCenter: lambda _: self.changeAlignmentEvent("alignCenter"), + ChangedWidgetAttribute.AlignRight: lambda _: self.changeAlignmentEvent("alignRight") + } + # if current widget is in focus + if (self.hasFocus or self.parentWidget().hasFocus) and changedWidgetAttribute in attribute_functions: + #if self.hasFocus() and changedWidgetAttribute in attribute_functions: + print(f"{changedWidgetAttribute} {value}") + # Calls the function in the dictionary + attribute_functions[changedWidgetAttribute](value) + else: + # Handle invalid attribute or other cases + pass def customMenuItems(self): - def build_action(parent, icon_path, action_name, set_status_tip, set_checkable): - action = QAction(QIcon(icon_path), action_name, parent) - action.setStatusTip(set_status_tip) - action.setCheckable(set_checkable) - return action - - toolbarTop = QToolBar() - toolbarTop.setIconSize(QSize(25, 25)) - toolbarTop.setMovable(False) - - toolbarBottom = QToolBar() - toolbarBottom.setIconSize(QSize(25, 25)) - toolbarBottom.setMovable(False) - - font = QFontComboBox() - font.currentFontChanged.connect(lambda x: self.setCurrentFontCustom(font.currentFont() if x else self.currentFont())) - - size = QComboBox() - size.addItems([str(fs) for fs in FONT_SIZES]) - size.currentIndexChanged.connect(lambda x: self.setFontPointSizeCustom(FONT_SIZES[x] if x else self.fontPointSize())) - - bold = build_action(toolbarBottom, 'assets/icons/svg_font_bold', "Bold", "Bold", True) - bold.toggled.connect(lambda x: self.setFontWeightCustom(700 if x else 500)) - - italic = build_action(toolbarBottom, 'assets/icons/svg_font_italic', "Italic", "Italic", True) - italic.toggled.connect(lambda x: self.setFontItalicCustom(True if x else False)) - - underline = build_action(toolbarBottom, 'assets/icons/svg_font_underline', "Underline", "Underline", True) - underline.toggled.connect(lambda x: self.setFontUnderlineCustom(True if x else False)) - - fontColor = build_action(toolbarBottom, 'assets/icons/svg_font_color', "Font Color", "Font Color", False) - fontColor.triggered.connect(lambda x: self.setTextColorCustom(QColorDialog.getColor())) - - bgColor = build_action(toolbarBottom, 'assets/icons/svg_font_bucket', "Text Box Color", "Text Box Color", False) - bgColor.triggered.connect(lambda x: self.setBackgroundColor(QColorDialog.getColor())) - - toolbarTop.addWidget(font) - toolbarTop.addWidget(size) - toolbarBottom.addActions([bold, italic, underline, fontColor, bgColor]) - qwaTop = QWidgetAction(self) - qwaTop.setDefaultWidget(toolbarTop) - qwaBottom = QWidgetAction(self) - qwaBottom.setDefaultWidget(toolbarBottom) - - return [qwaTop, qwaBottom] + def build_action(parent, icon_path, action_name, set_status_tip, set_checkable): + action = QAction(QIcon(icon_path), action_name, parent) + action.setStatusTip(set_status_tip) + action.setCheckable(set_checkable) + return action + + toolbarTop = QToolBar() + toolbarTop.setIconSize(QSize(16, 16)) + toolbarTop.setMovable(False) + + toolbarBottom = QToolBar() + toolbarBottom.setIconSize(QSize(16, 16)) + toolbarBottom.setMovable(False) + + font = QFontComboBox() + font.setFixedWidth(150) + font.currentFontChanged.connect( + lambda x: self.setCurrentFontCustom( + font.currentFont() if x else self.currentFont() + ) + ) + + size = QComboBox() + size.setFixedWidth(50) + size.addItems([str(fs) for fs in FONT_SIZES]) + size.currentIndexChanged.connect( + lambda x: self.setFontPointSizeCustom( + FONT_SIZES[x] if x else self.fontPointSize() + ) + ) + + align_left = build_action(toolbarBottom, "./Assets/icons/svg_align_left", "Align Left", "Align Left", False) + # align_left.triggered.connect(lambda x: self.setAlignment(Qt.AlignLeft)) + align_left.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignLeft, None)) + + + align_center = build_action(toolbarBottom, "./Assets/icons/svg_align_center", "Align Center", "Align Center", False) + # align_center.triggered.connect(lambda x: self.setAlignment(Qt.AlignCenter)) + align_center.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignCenter, None)) + + + align_right = build_action(toolbarBottom, "./Assets/icons/svg_align_right", "Align Right", "Align Right", False) + align_right.triggered.connect(lambda x: self.setAlignment(Qt.AlignRight)) + align_right.triggered.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.AlignRight, None)) + + + bold = build_action( + toolbarBottom, "./Assets/icons/svg_font_bold", "Bold", "Bold", True + ) + bold.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontBold, None)) + + italic = build_action(toolbarBottom, "./Assets/icons/svg_font_italic", "Italic", "Italic", True) + italic.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontItalic, None)) + + underline = build_action(toolbarBottom,"./Assets/icons/svg_font_underline","Underline","Underline",True,) + underline.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontUnderline, None)) + + strikethrough = build_action(toolbarBottom,"./Assets/icons/svg_strikethrough", "Strikethrough", "Strikethrough", True) + strikethrough.toggled.connect(lambda: editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.Strikethrough, None)) + + + fontColor = build_action(toolbarBottom,"./Assets/icons/svg_font_color","Font Color","Font Color",False) + fontColor.triggered.connect( + lambda: self.setTextColorCustom(QColorDialog.getColor()) + ) + + bgColor = build_action(toolbarBottom,"./Assets/icons/svg_font_bucket","Background Color","Background Color",False) + # bgColor.triggered.connect(lambda: self.setBackgroundColor(QColorDialog.getColor())) + bgColor.triggered.connect(lambda: self.changeBackgroundColorEvent(QColorDialog.getColor())) + textHighlightColor = build_action(toolbarBottom,"./Assets/icons/svg_textHighlightColor","Text Highlight Color","Text Highlight Color",False,) + textHighlightColor.triggered.connect(lambda: self.changeTextHighlightColorEvent(QColorDialog.getColor())) + + bullets = build_action(toolbarBottom, "./Assets/icons/svg_bullets", "Bullets", "Bullets", True) + bullets.toggled.connect(lambda: self.bullet_list("bulletReg")) + + toolbarTop.addWidget(font) + toolbarTop.addWidget(size) + + toolbarBottom.addActions( + [ + bold, + italic, + underline, + # strikethrough, + textHighlightColor, + fontColor, + bullets + ] + ) + + # numbering menu has to be added inbetween + numbering_menu = QMenu(self) + bullets_num = numbering_menu.addAction(QIcon("./Assets/icons/svg_bullet_number"), "") + bulletUpperA = numbering_menu.addAction(QIcon("./Assets/icons/svg_bulletUA"), "") + bulletUpperR = numbering_menu.addAction(QIcon("./Assets/icons/svg_bulletUR"), "") + + bullets_num.triggered.connect(lambda: self.bullet_list("bulletNum")) + bulletUpperA.triggered.connect(lambda: self.bullet_list("bulletUpperA")) + bulletUpperR.triggered.connect(lambda: self.bullet_list("bulletUpperR")) + + + numbering = QToolButton(self) + numbering.setIcon(QIcon("./Assets/icons/svg_bullet_number")) + numbering.setPopupMode(QToolButton.MenuButtonPopup) + numbering.setMenu(numbering_menu) + + # This code would fix an error on the command line but it also makes it not look good soooo + numbering.setParent(numbering_menu) + + toolbarBottom.addWidget(numbering) + + # not required for right-click menu as they arent originally present in OneNote + ''' + toolbarBottom.addActions( + [ + bgColor, + align_left, + align_center, + align_right + ] + ) + ''' + qwaTop = QWidgetAction(self) + qwaTop.setDefaultWidget(toolbarTop) + qwaBottom = QWidgetAction(self) + qwaBottom.setDefaultWidget(toolbarBottom) + + return [qwaTop, qwaBottom] def setFontItalicCustom(self, italic: bool): if not self.applyToAllIfNoSelection(lambda: self.setFontItalic(italic)): + print("setFontItalicCustom Called") self.setFontItalic(italic) def setFontWeightCustom(self, weight: int): @@ -115,8 +312,7 @@ def setTextColorCustom(self, color): def setBackgroundColor(self, color: QColor): rgb = color.getRgb() - self.setStyleSheet(f'background-color: rgb({rgb[0]}, {rgb[1]}, {rgb[2]});') - + self.setStyleSheet(f"background-color: rgb({rgb[0]}, {rgb[1]}, {rgb[2]});") # If no text is selected, apply to all, else apply to selection def applyToAllIfNoSelection(self, func): @@ -137,3 +333,347 @@ def applyToAllIfNoSelection(self, func): cursor.clearSelection() self.setTextCursor(cursor) return True + + def changeFontSizeEvent(self, weight): + print("changeFontSizeEvent Called") + self.setFontWeightCustom(weight) + + # for communicating the signal editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontItalic, None) + # current issue for Event Functions: Only affects highlighted + + def changeFontItalicEvent(self): + cursor = self.textCursor() + current_format = cursor.charFormat() + + # Checks if currently selected text is italics + is_italic = current_format.fontItalic() + + # toggles the italics + current_format.setFontItalic(not is_italic) + + # Apply modified format to selected text + cursor.setCharFormat(current_format) + + # Update text cursor with modified format + self.setTextCursor(cursor) + + def changeFontBoldEvent(self): + cursor = self.textCursor() + current_format = cursor.charFormat() + + # Checks if currently selected text is bold + is_bold = current_format.fontWeight() == 700 + + # toggles the italics + if is_bold: + current_format.setFontWeight(500) + else: + current_format.setFontWeight(700) + # Apply modified format to selected text + cursor.setCharFormat(current_format) + + # Update text cursor with modified format + self.setTextCursor(cursor) + + def changeFontUnderlineEvent(self): + cursor = self.textCursor() + current_format = cursor.charFormat() + + # Checks if currently selected text is bold + is_underlined = current_format.fontUnderline() + + # toggles the underline + current_format.setFontUnderline(not is_underlined) + + # Apply modified format to selected text + cursor.setCharFormat(current_format) + + # Update text cursor with modified format + self.setTextCursor(cursor) + + def bullet_list(self, bulletType): + cursor = self.textCursor() + textList = cursor.currentList() + + if textList: + start = cursor.selectionStart() + end = cursor.selectionEnd() + removed = 0 + for i in range(textList.count()): + item = textList.item(i - removed) + if item.position() <= end and item.position() + item.length() > start: + textList.remove(item) + blockCursor = QTextCursor(item) + blockFormat = blockCursor.blockFormat() + blockFormat.setIndent(0) + blockCursor.mergeBlockFormat(blockFormat) + removed += 1 + + cursor = self.textCursor() + cursor.setBlockFormat(QTextBlockFormat()) # Clear any previous block format + + self.setTextCursor(cursor) + self.setFocus() + + else: + listFormat = QTextListFormat() + + if bulletType == "bulletNum": + style = QTextListFormat.ListDecimal + if bulletType == "bulletReg": + style = QTextListFormat.ListDisc + if bulletType == "bulletLowerA": + style = QTextListFormat.ListLowerAlpha + if bulletType == "bulletLowerR": + style = QTextListFormat.ListLowerRoman + if bulletType == "bulletUpperA": + style = QTextListFormat.ListUpperAlpha + if bulletType == "bulletUpperR": + style = QTextListFormat.ListUpperRoman + + listFormat.setStyle(style) + cursor.createList(listFormat) + + self.setTextCursor(cursor) + self.setFocus() + def changeAlignmentEvent(self, alignmentType): + print("Alignment Event Called") + cursor = self.textCursor() + blockFormat = cursor.blockFormat() + + if alignmentType == "alignLeft": + blockFormat.setAlignment(Qt.AlignLeft) + elif alignmentType == "alignCenter": + blockFormat.setAlignment(Qt.AlignCenter) + elif alignmentType == "alignRight": + blockFormat.setAlignment(Qt.AlignRight) + + cursor.setBlockFormat(blockFormat) + self.setTextCursor(cursor) + self.setFocus() + + def handleTabKey(self): + cursor = self.textCursor() + block = cursor.block() + block_format = block.blockFormat() + textList = cursor.currentList() + + if textList: + if cursor.atBlockStart() and block.text().strip() == "": + current_indent = block_format.indent() + + if current_indent < 11: + + if current_indent % 3 == 0: + block_format.setIndent(current_indent + 1) + cursor.setBlockFormat(block_format) + cursor.beginEditBlock() + list_format = QTextListFormat() + currentStyle = textList.format().style() + + if currentStyle == QTextListFormat.ListDisc: + list_format.setStyle(QTextListFormat.ListCircle) + if currentStyle == QTextListFormat.ListDecimal: + list_format.setStyle(QTextListFormat.ListLowerAlpha) + if currentStyle == QTextListFormat.ListLowerAlpha: + list_format.setStyle(QTextListFormat.ListLowerRoman) + if currentStyle == QTextListFormat.ListLowerRoman: + list_format.setStyle(QTextListFormat.ListDecimal) + if currentStyle == QTextListFormat.ListUpperAlpha: + list_format.setStyle(QTextListFormat.ListLowerAlpha) + if currentStyle == QTextListFormat.ListUpperRoman: + list_format.setStyle(QTextListFormat.ListLowerAlpha) + + cursor.createList(list_format) + cursor.endEditBlock() + + if current_indent % 3 == 1: + block_format.setIndent(current_indent + 1) + cursor.setBlockFormat(block_format) + cursor.beginEditBlock() + list_format = QTextListFormat() + currentStyle = textList.format().style() + + if currentStyle == QTextListFormat.ListCircle: + list_format.setStyle(QTextListFormat.ListSquare) + if currentStyle == QTextListFormat.ListLowerAlpha: + list_format.setStyle(QTextListFormat.ListLowerRoman) + if currentStyle == QTextListFormat.ListLowerRoman: + list_format.setStyle(QTextListFormat.ListDecimal) + if currentStyle == QTextListFormat.ListDecimal: + list_format.setStyle(QTextListFormat.ListLowerAlpha) + + cursor.createList(list_format) + cursor.endEditBlock() + + if current_indent % 3 == 2: + block_format.setIndent(current_indent + 1) + cursor.setBlockFormat(block_format) + cursor.beginEditBlock() + list_format = QTextListFormat() + currentStyle = textList.format().style() + + if currentStyle == QTextListFormat.ListSquare: + list_format.setStyle(QTextListFormat.ListDisc) + if currentStyle == QTextListFormat.ListLowerRoman: + list_format.setStyle(QTextListFormat.ListDecimal) + if currentStyle == QTextListFormat.ListDecimal: + list_format.setStyle(QTextListFormat.ListLowerAlpha) + if currentStyle == QTextListFormat.ListLowerAlpha: + list_format.setStyle(QTextListFormat.ListLowerRoman) + + cursor.createList(list_format) + cursor.endEditBlock() + + cursor.insertText("") + cursor.movePosition(QTextCursor.StartOfBlock) + self.setTextCursor(cursor) + + else: + cursor.insertText(" ") + self.setTextCursor(cursor) + pass + + def insertTextLink(self, link_address, display_text): + self.setOpenExternalLinks(True) + link_html = f'{display_text}' + cursor = self.textCursor() + cursor.insertHtml(link_html) + QDesktopServices.openUrl(link_html) + + def changeFontSizeEvent(self, value): + # todo: when textbox is in focus, font size on toolbar should match the font size of the text + + cursor = self.textCursor() + current_format = cursor.charFormat() + + current_format.setFontPointSize(value) + cursor.setCharFormat(current_format) + + self.setTextCursor(cursor) + + def changeFontEvent(self, font_style): + cursor = self.textCursor() + current_format = cursor.charFormat() + current_format.setFont(font_style) + + cursor.setCharFormat(current_format) + + self.setTextCursor(cursor) + + # Changes font text color + def changeFontColorEvent(self, new_font_color): + cursor = self.textCursor() + current_format = cursor.charFormat() + + color = QColor(new_font_color) + if color.isValid(): + current_format.setForeground(color) + + cursor.setCharFormat(current_format) + + # to not get stuck on highlighted text + # self.deselectText() + # self.setTextCursor(cursor) + + # Changes color of whole background + + def changeBackgroundColorEvent(self, color: QColor): + print("CHANGE BACKGROUND COLOR EVENT") + + if color.isValid(): + rgb = color.getRgb() + self.setStyleSheet(f"QTextEdit {{background-color: rgb({rgb[0]}, {rgb[1]}, {rgb[2]}); }}") + + self.deselectText() + + # Changes textbox background color + def changeTextHighlightColorEvent(self, new_highlight_color): + cursor = self.textCursor() + current_format = cursor.charFormat() + + color = QColor(new_highlight_color) + if color.isValid(): + current_format.setBackground(color) + + cursor.setCharFormat(current_format) + # self.deselectText() + + # self.setTextCursor(cursor) + + # Used to remove text highlighting + def deselectText(self): + cursor = self.textCursor() + cursor.clearSelection() + self.setTextCursor(cursor) + + # Adds bullet list to text + def changeBulletEvent(self): + # put bullet function here + print("bullet press") + + def changeAllTextColors(self, new_color): + cursor = self.textCursor() + cursor.movePosition(QTextCursor.Start) + + while not cursor.atEnd(): + cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor) + char_format = cursor.charFormat() + + if char_format.foreground().color() == Qt.black or Qt.white: + char_format.setForeground(QColor(new_color)) + cursor.setCharFormat(char_format) + + cursor.movePosition(QTextCursor.Start) + self.setTextCursor(cursor) + + def setStrikeOut(self): + print("strikeout event") + cursor = self.textCursor() + format = cursor.charFormat() + + # Toggle strikethrough + format.setFontStrikeOut(not format.fontStrikeOut()) + + # Apply the new format to the selected text + #cursor.mergeCharFormat(format) + cursor.setCharFormat(format) + + #cursor.movePosition(QTextCursor.End) + self.setTextCursor(cursor) + #self.setFocus() + + + + def refactorTest(self): + cursor = self.textCursor() + current_format = cursor.charFormat() + + # Checks if currently selected text is bold + is_bold = current_format.fontWeight() == 700 + + # toggles the italics + if is_bold: + current_format.setFontWeight(500) + else: + current_format.setFontWeight(700) + # Apply modified format to selected text + cursor.setCharFormat(current_format) + cursor.mergeCharFormat(format) + # Emit signal if no text is bold to toggle bold off + if not is_bold: + editorSignalsInstance.widgetAttributeChanged.emit(ChangedWidgetAttribute.FontBold, None) + # Update text cursor with modified format + self.setTextCursor(cursor) + + def selectAllText(self): + print("Select All Text function from textwidget called") + cursor = self.textCursor() + cursor.setPosition(0) + cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor) + self.setTextCursor(cursor) + + def removeWidget(self): + if(self.checkEmpty()): + print("removing widget") + editorSignalsInstance.widgetRemoved.emit(self) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f573bd8..8c8e968 100644 Binary files a/requirements.txt and b/requirements.txt differ