├── .github └── ISSUE_TEMPLATE │ └── feature_request.md ├── .gitignore ├── BUILD.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── Themes ├── dark-blue.json ├── dark.json ├── light-fire.json ├── light.json ├── monokai.json ├── neutral-green.json ├── ocean.json ├── vulcano.json └── winter.json ├── app_data_ ├── data.json └── version.json ├── build_info └── build.md ├── config.json ├── images ├── bell-default.png ├── bell-update.png ├── close.png ├── logo.ico ├── play.png ├── png-logo.png ├── run.png ├── ss.png ├── ss2.png └── ss3.png ├── requirements.txt └── src ├── Core ├── edit_manager.py ├── file_manager.py ├── misc_manager.py ├── run.py ├── session.py ├── templates.py ├── theme_manager.py ├── view_manager.py └── web.py ├── GUI ├── diff.py ├── gui.py ├── info_win.py ├── kilo_tools.py ├── minimap.py ├── paint.py ├── profile.py ├── right_panel.py ├── status_bar.py ├── tab_bar.py ├── text_editor.py └── treeview.py ├── Server ├── client.py ├── competitive_companion.py └── server.py ├── Tools ├── kilonova.py ├── pbinfo.py └── scrap.py ├── Update └── internal.py └── main.py /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/ 2 | *.cfg 3 | *.spec 4 | build/ 5 | dist 6 | build_info/self_build.md 7 | .vscode 8 | template.cpp 9 | *.log 10 | *.exe 11 | template.in 12 | template.out 13 | Templates 14 | profile.txt 15 | session_file.txt 16 | recent_dir.txt 17 | Snippets 18 | src/**/__pycache__/ 19 | app_data_/secret.key -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Build Code Nimble 2 | 3 | To build from source you need to clone this repo: 4 | `git clone https://github.com/HojdaAdelin/CodeNimble.git` 5 | 6 | 1. You need to download these libraries: 7 | - PySide6 8 | ```sh 9 | pip install PySide6 10 | ``` 11 | 12 | 2. Open a CMD in the Code Nimble source folder and run the code from [build-file](build_info/build.md) 13 | 14 | 3. Open the dist folder created in the source and run CodeNimble.exe from the CodeNimble folder -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 2.6.0 2 | 3 | ### Added: 4 | 5 | 1. Encryption for username & password in app_data_ 6 | 2. New style for status bar 7 | 3. Major highlighter update: 8 | - preprocessor mapping 9 | - functions 10 | - user data type 11 | - numeric sufix 12 | - new colors 13 | 4. New autocompleter style 14 | 5. Multi-cursor editing 15 | 6. Minimap 16 | 7. Navigation to the definition 17 | 18 | ### Fixed/Changes: 19 | 20 | 1. Improved github repo structure 21 | 2. Fixed autocompleter insertion 22 | 23 | # Version 2.5.0 24 | 25 | ### Major updates 26 | 27 | 1. Kilonova.ro submit function -> submit code directly from this application 28 | 29 | ### Added: 30 | 31 | 1. New theme light-fire 32 | 2. Added a new menu in right panel "Submit code" that will host the submit function on platforms 33 | 3. Extended the functionality of the Testing tab by adding up to 5 test sets 34 | 4. Settings tab in right panel 35 | 5. Pre template on startup 36 | 6. Competitive companion support 37 | 7. New README 38 | 39 | ### Fixed/Changes: 40 | 41 | 1. New highlighter color for: light, dark, ocean, dark-blue themes 42 | 2. Fixed autocompletion templates in editor 43 | 3. Fixed documentation bug 44 | 4. Github API connection errors 45 | 5. Pre-input run button in testing tab instead of in-app fetch 46 | 47 | # Version 2.2 48 | 49 | ### Added: 50 | 51 | 1. Monokai & winter themes 52 | 2. New inbox messages 53 | 3. Update system(BETA) 54 | 4. Time in inbox 55 | 5. Documentation in right panel 56 | 6. New folder for app data -> app_data_ 57 | 58 | ### Fixed/Changes: 59 | 60 | 1. Fixed application size -> problem viewer can't be used for now 61 | 2. Changed inbox style 62 | 3. Changed profile & new file dialog interfaces 63 | 4. Go to line, find & replace redesign 64 | 5. Fixed some interfaces exit when close the main instance 65 | 6. Changed autocompletion keywords & highlighter 66 | 7. Redesign for template & theme interfaces 67 | 68 | # Version 2.1 69 | 70 | ### Major improvements: 71 | 72 | 1. Problem viewer -> mini google inside the app 73 | 74 | ### Added: 75 | 76 | 1. Inbox 77 | 2. FOR-{} completions e.g FOR-J, FOR-X -> int j, int x -> for() 78 | 3. Author Details -> Templates 79 | 4. New tab switch mode in right frame 80 | 5. "neutral-green.json" theme 81 | 6. "vulcano.json" theme 82 | 7. Credits in config.json 83 | 8. Tool bar in left frame 84 | 85 | ### Fixed/Changes: 86 | 87 | 1. Highlighter improvement 88 | 2. Changed the pbinfo interface 89 | 3. Fixed resized window bug 90 | 91 | # Version 2.0 92 | 93 | ### Added: 94 | 95 | 1. Server status in status bar 96 | 2. Save session 97 | 3. Dark-blue theme 98 | 4. LAN password 99 | 5. Theme changer 100 | 6. Kilonova.ro tools 101 | 7. Python support 102 | 8. Scrollbar in suggestions 103 | 9. Right panel 104 | 10. New input & output system 105 | 11. Expected output and check test casses 106 | 12. Submit code to pbinfo.ro directly from the app 107 | 13. Open site function in Home 108 | 14. New code highlight system 109 | 15. New submit code button 110 | 16. Fetch test cases: pbinfo.ro support 111 | 17. Fetch test cases: kilonova.ro support 112 | 18. Fetch test cases: codeforces.com support 113 | 19. Fetch test cases: atcoder.jp support 114 | 20. New change log in app 115 | 21. Run python files 116 | 22. New local server interface 117 | 23. Added buttons for theme system 118 | 24. Clear terminal function 119 | 25. Code formatting 120 | 26. Output comparator 121 | 27. Added highlight color palette to Themes json 122 | 28. PAINT: When clicking on a color it will change into pencil 123 | 29. Go to line feature 124 | 125 | ### Fixed/Changes: 126 | 127 | 1. Fixed delete file function 128 | 2. Changed template & theme interface 129 | 3. Fixed suggestion box 130 | 4. Changed open recent list 131 | 5. Fixed exit 132 | 6. Changed treeview indicator 133 | 7. Removed Guide from Home 134 | 8. Fixed run 135 | 9. Fixed tab function for suggestions 136 | 10. Fixed templates 137 | 11. Fixed double paste issue 138 | 12. UTF-8 support 139 | 140 | # Version: 1.5 141 | 142 | ### Added: 143 | 144 | - Paint Mode 145 | - Integrate paint window in app 146 | - Change colors in paint mode 147 | - Local Server 148 | - Profile 149 | - Changed treeview arrow 150 | - Server Panel 151 | - Connected users list in panel 152 | - Autocomplete for CPP when ENTER 153 | - Button 2 bind in file tab 154 | - Tabs in paint mode 155 | - Text highlighted for python 156 | - Autocomplete \", \', \* 157 | - Settings 158 | - Settings for status bar 159 | - Added CHANGELOG.md 160 | - Timer in status bar 161 | - Added "*" in file tab when the file is modified 162 | - Now you can run the textbox content(you don't need to save the file everytime) 163 | - File tab pack forget when there aren't any tabs 164 | - New theme system with json files 165 | - When suggestion is mapped and hit ENTER will autocomplete code 166 | - Ocean theme 167 | - Update text highlighted 168 | - BUILD.md 169 | - CONTRIBUTION.md 170 | - New cursor position when use ENTER between {} 171 | 172 | ### Fixed: 173 | 174 | - Fixed autocompletion when list isn't mapped 175 | - Fixed Ctrl+Backspace 176 | - Fixded save 177 | - Input & Output won't be displayed when populate treeview 178 | - Redraw textbox after find & replace 179 | - Fix run 180 | 181 | # Version: 1.4 182 | 183 | ### Added: 184 | 185 | - Tab bar 186 | - Open files from treeview in tab bar 187 | - Close tab function 188 | - Quick input & output 189 | - Open file in input & output 190 | - Save input 191 | - Open input & output from treeview 192 | - Code completion for C++ 193 | - Ctrl+Tab & Ctrl+Shift+Tab 194 | - Move tabs 195 | - Count lines & words 196 | - Hide & unhide status bar 197 | - Added status bar to config 198 | - Added default_file to config 199 | - Save & remove default folder 200 | - Autocomplete for INT, VOID, LONG 201 | - Run icon in status bar 202 | - Removed 'new' 203 | - Added status bar notifications to config 204 | - Custom templates & use custom templates 205 | 206 | ### Fixed: 207 | 208 | - Update close folder 209 | 210 | 211 | # Version: 1.3 212 | 213 | ### Added: 214 | 215 | - Utility menu 216 | - Run code 217 | - Bind for Ctrl+Backspace 218 | - New version available notify in status bar 219 | - Binds for replace & find 220 | - Treeview 221 | - Open folder 222 | - Close folder 223 | - Open files from treeview 224 | - Delete files from Treeview 225 | - Move files & folders in treeview 226 | - Toggle treeview 227 | - Bind for Run 228 | - Add file in treeview folders 229 | - Change all windows title bars 230 | - Delete folder in treeview 231 | - Rename files in treeview 232 | - Unsaved file management in treeview 233 | - Rename folder in treeview 234 | - Open in Explorer function 235 | - Add folder in treeview 236 | - Pack to grid 237 | 238 | ### Fixed: 239 | 240 | - Fix zoom 241 | - Fix exit function 242 | 243 | 244 | # Version: 1.2 245 | 246 | ### Added: 247 | 248 | - New templates: C, Java, Html, C++ Competitive 249 | - Save default file location 250 | - Remove default file 251 | - Replace all 252 | - Binds 253 | - Report bugs 254 | - Fullscreen 255 | - Remake text highlighted 256 | - Autocomplete parenthesis 257 | - Shortcuts for: IF, DO, WHILE, FOR 258 | - Get latest version 259 | 260 | ### Fixed: 261 | 262 | - Fix save as file 263 | - Fix text highlighted lag 264 | - Fix coloring multiple char after (, [, etc 265 | - Fix select all when using .cpp 266 | - Fix undo problem in textbox 267 | 268 | 269 | # Version: 1.1 270 | 271 | ### Added: 272 | 273 | - New version window height 274 | - New menu "Textures" 275 | - New main menu hover color 276 | - Light theme 277 | - Config for themes 278 | - New README 279 | - Changed file type in open dialog 280 | - New status bar text 281 | - Reset zoom 282 | 283 | ### Fixed: 284 | 285 | - Fix tab space size 286 | - Fix themes 287 | 288 | 289 | # Version: 1.0 290 | 291 | ### Added: 292 | 293 | - Text highlighted for C++ 294 | - Resize version window 295 | - New menu 'Templates' 296 | - C++ template 297 | - Clear text 298 | - Installer 299 | 300 | ### Fixed: 301 | 302 | - Exclude text highlighted from any type files except .cpp 303 | - Fix select all -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Steps: 4 | 5 | 1. Open an issue even if you want to add something new because Code Nimble is a competitibe programming based editor so any new feature is not suitable for this. We need relevant features so the first think is to open an issue :) 6 | 7 | 2. If one of the core member approve your request we will start implementing the feature and work with you if you want. 8 | 9 | 3. If you already make the changes you can open a PR after the approving 10 | 11 | ## Notes: 12 | 13 | 1. This is a light code editor so we need light features 14 | 2. Test your code before making a PR 15 | 3. Document the new code you created so we can understand in less time what you wanted to implement -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeNimble 2 | 3 | **CodeNimble** is a lightweight code editor designed specifically for competitive programming. With its sleek design and optimized features, it helps you write and test your code efficiently. 4 | 5 | --- 6 | 7 | ## Table of Contents 8 | - [Features](#features) 9 | - [Gallery](#gallery) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Change Log](#change-log) 13 | - [Contributing](#contributing) 14 | - [License](#license) 15 | 16 | --- 17 | 18 | ## Features 19 | - **Lightweight**: Minimalist design with competitive programming in mind. 20 | - **Fast**: Optimized for speed and performance. 21 | - **Customizable**: Easy to tweak and extend functionality. 22 | - **Fetch test cases**: Fetch test cases with competitive companion or with offline tools 23 | - **Submit code**: Submit code directly on kilonova and pbinfi platforms 24 | - **Templates**: Create a custom file with a custom code 25 | - **Paint window**: You can do your sketches and notes with this tool 26 | - **LAN server**: Cooperate with your friend on the same problem 27 | - **Input, Output & Expected Output**: Manage input, output and expected output easily while testing your code efficiently 28 | - **Run & Support**: C++ & python support. 29 | 30 | --- 31 | 32 | ## Gallery 33 | Below are screenshots showcasing the app's interface and features: 34 | 35 |
36 | Screenshot 1 37 | Screenshot 2 38 | Screenshot 3 39 |
40 | 41 | --- 42 | 43 | ## Installation 44 | To install CodeNimble, follow these steps: 45 | 1. Clone the repository: 46 | ```bash 47 | git clone https://github.com/HojdaAdelin/CodeNimble.git 48 | ``` 49 | 2. Navigate to the project directory: 50 | ```bash 51 | cd codenimble 52 | ``` 53 | 3. Install dependencies: 54 | ```bash 55 | pip install -r requirements.txt 56 | ``` 57 | 4. Build: 58 | Run the code from [build-file](build_info/build.md) in the CMD. 59 | 60 | 5. Run: 61 | Open the dist folder created in the source and run CodeNimble.exe from the CodeNimble folder 62 | 63 | --- 64 | 65 | ## Usage 66 | - Open the app and open your work folder or any folder. 67 | - Create a C++, Python file or use a template from the menu. 68 | - Code using the editor. 69 | - Fetch test cases of the problem you want to solve. 70 | - Run your code with pre-input. 71 | - Check expected output result. 72 | - Submit your code (if you use pbinfo or kilonova platforms). 73 | 74 | --- 75 | 76 | ## Change Log 77 | To see the full list of changes, check the [CHANGELOG](CHANGELOG.md). 78 | 79 | --- 80 | 81 | ## Contributing 82 | We welcome contributions! Please read our [CONTRIBUTING](CONTRIBUTING.md) guide for more details on how to contribute to this project. 83 | 84 | --- 85 | 86 | ## License 87 | This project is licensed under the [AGPL License](LICENSE). See the LICENSE file for details. 88 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | ### Since november 2024 the newest version of this software will be FULLY supported until a new update will come. 6 | 7 | | Version | Supported | Support time | 8 | | ------- | ------------------ | ------------ | 9 | | Latest version | :white_check_mark: | Until new update | 10 | | Older versions | ❌ | | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you find a vulnerability please open an issue and we will fix it! 15 | -------------------------------------------------------------------------------- /Themes/dark-blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#1A1F29", 3 | "treeview_background": "#1A1F29", 4 | "text_color": "#B0BEC5", 5 | "button_color": "#2E3440", 6 | "button_hover_color": "#3B4252", 7 | "border_color": "#4C566A", 8 | "item_hover_background_color": "#3B4252", 9 | "item_hover_text_color": "#B0BEC5", 10 | "separator_color": "#4C566A", 11 | "highlight_color": "#2E3440", 12 | "line_number_background": "#2C313C", 13 | "line_number_text_color": "#81A1C1", 14 | "editor_background": "#2C313C", 15 | "editor_foreground": "#B0BEC5", 16 | "selection_background_color": "#434C5E", 17 | "status_bar_background": "#2E3440", 18 | "ctn_words": "#4C566A", 19 | "minimap": { 20 | "background": "#2C313C", 21 | "text": "#ffffff", 22 | "highlight": "#2E3440" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#474aa1", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#81A1C1", 29 | "string_color": "#A3BE8C", 30 | "comment_color": "#5C6370", 31 | "include_color": "#5C6370", 32 | "parenthesis_color": "#BF616A", 33 | "number_color": "#D08770", 34 | "symbol_color": "#88C0D0" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#333333", 3 | "treeview_background": "#333333", 4 | "text_color": "#ffffff", 5 | "button_color": "#555555", 6 | "button_hover_color": "#777777", 7 | "border_color": "#444444", 8 | "item_hover_background_color": "#555555", 9 | "item_hover_text_color": "#ffffff", 10 | "separator_color": "#444444", 11 | "highlight_color": "#555555", 12 | "line_number_background": "#454545", 13 | "line_number_text_color": "#bfbfbf", 14 | "editor_background": "#454545", 15 | "editor_foreground": "#ffffff", 16 | "selection_background_color": "#333333", 17 | "status_bar_background": "#333333", 18 | "ctn_words": "#444444", 19 | "minimap": { 20 | "background": "#454545", 21 | "text": "#ffffff", 22 | "highlight": "#555555" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#ff9900", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#ff6b6b", 29 | "string_color": "#ffb86c", 30 | "comment_color": "#7a7a7a", 31 | "include_color": "#7a7a7a", 32 | "parenthesis_color": "#ff4d4d", 33 | "number_color": "#f1fa8c", 34 | "symbol_color": "#8be9fd" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/light-fire.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#f7f2f2", 3 | "treeview_background": "#f7f2f2", 4 | "text_color": "#4d0d0d", 5 | "button_color": "#d98a8a", 6 | "button_hover_color": "#bf6060", 7 | "border_color": "#a84545", 8 | "item_hover_background_color": "#f2dcdc", 9 | "item_hover_text_color": "#4d0d0d", 10 | "separator_color": "#a84545", 11 | "highlight_color": "#e6b3b3", 12 | "line_number_background": "#f2e6e6", 13 | "line_number_text_color": "#8c3b3b", 14 | "editor_background": "#f9f6f6", 15 | "editor_foreground": "#4d0d0d", 16 | "selection_background_color": "#e6cfcf", 17 | "status_bar_background": "#f7f2f2", 18 | "ctn_words": "#a84545", 19 | "minimap": { 20 | "background": "#f9f6f6", 21 | "text": "#000000", 22 | "highlight": "#e6b3b3" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#ff0077", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#e63939", 29 | "string_color": "#a83232", 30 | "comment_color": "#bf6060", 31 | "include_color": "#bf6060", 32 | "parenthesis_color": "#cc3a3a", 33 | "number_color": "#a84545", 34 | "symbol_color": "#8f5a5a" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/light.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#FFFFFF", 3 | "treeview_background": "#EEEEEE", 4 | "text_color": "#333333", 5 | "button_color": "#DDDDDD", 6 | "button_hover_color": "#CCCCCC", 7 | "border_color": "#CCCCCC", 8 | "item_hover_background_color": "#EEEEEE", 9 | "item_hover_text_color": "#333333", 10 | "separator_color": "#DDDDDD", 11 | "highlight_color": "#F0F0F0", 12 | "line_number_background": "#F7F7F7", 13 | "line_number_text_color": "#888888", 14 | "editor_background": "#F7F7F7", 15 | "editor_foreground": "#333333", 16 | "selection_background_color": "#E0E0E0", 17 | "status_bar_background": "#EEEEEE", 18 | "ctn_words": "#CCCCCC", 19 | "minimap": { 20 | "background": "#F7F7F7", 21 | "text": "#000000", 22 | "highlight": "#F0F0F0" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#ff6f00", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#d75f5f", 29 | "string_color": "#c18457", 30 | "comment_color": "#a0a0a0", 31 | "include_color": "#a0a0a0", 32 | "parenthesis_color": "#bf4040", 33 | "number_color": "#b6933d", 34 | "symbol_color": "#5694b5" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/monokai.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#272822", 3 | "treeview_background": "#272822", 4 | "text_color": "#F8F8F2", 5 | "button_color": "#3E3D32", 6 | "button_hover_color": "#75715E", 7 | "border_color": "#3E3D32", 8 | "item_hover_background_color": "#49483E", 9 | "item_hover_text_color": "#F8F8F2", 10 | "separator_color": "#3E3D32", 11 | "highlight_color": "#49483E", 12 | "line_number_background": "#2D2E27", 13 | "line_number_text_color": "#75715E", 14 | "editor_background": "#272822", 15 | "editor_foreground": "#F8F8F2", 16 | "selection_background_color": "#49483E", 17 | "status_bar_background": "#272822", 18 | "ctn_words": "#3E3D32", 19 | "minimap": { 20 | "background": "#272822", 21 | "text": "#ffffff", 22 | "highlight": "#ffffff" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#145299", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#F92672", 29 | "string_color": "#A6E22E", 30 | "comment_color": "#75715E", 31 | "include_color": "#75715E", 32 | "parenthesis_color": "#F92672", 33 | "number_color": "#AE81FF", 34 | "symbol_color": "#66D9EF" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/neutral-green.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#1e1e1e", 3 | "treeview_background": "#1e1e1e", 4 | "text_color": "#e0e0e0", 5 | "button_color": "#3a3a3a", 6 | "button_hover_color": "#3a3d33", 7 | "border_color": "#3a3d33", 8 | "item_hover_background_color": "#3a3d33", 9 | "item_hover_text_color": "#e0e0e0", 10 | "separator_color": "#2a2a2a", 11 | "highlight_color": "#3a3d33", 12 | "line_number_background": "#2c2c2c", 13 | "line_number_text_color": "#b0b0b0", 14 | "editor_background": "#2c2c2c", 15 | "editor_foreground": "#e0e0e0", 16 | "selection_background_color": "#3a3a3a", 17 | "status_bar_background": "#1e1e1e", 18 | "ctn_words": "#2a2a2a", 19 | "minimap": { 20 | "background": "#2c2c2c", 21 | "text": "#ffffff", 22 | "highlight": "#3a3d33" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#15b345", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#8bc34a", 29 | "string_color": "#b2ff59", 30 | "comment_color": "#7f7f7f", 31 | "include_color": "#7f7f7f", 32 | "parenthesis_color": "#e06c75", 33 | "number_color": "#d19a66", 34 | "symbol_color": "#56b6c2" 35 | } -------------------------------------------------------------------------------- /Themes/ocean.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#1B3B4F", 3 | "treeview_background": "#1B3B4F", 4 | "text_color": "#A8C6DF", 5 | "button_color": "#2A5674", 6 | "button_hover_color": "#347A9D", 7 | "border_color": "#3D6A82", 8 | "item_hover_background_color": "#2E4D64", 9 | "item_hover_text_color": "#A8C6DF", 10 | "separator_color": "#3D6A82", 11 | "highlight_color": "#2A5674", 12 | "line_number_background": "#1F4A60", 13 | "line_number_text_color": "#7DA3BA", 14 | "editor_background": "#1F4A60", 15 | "editor_foreground": "#A8C6DF", 16 | "selection_background_color": "#315C76", 17 | "status_bar_background": "#2A5674", 18 | "ctn_words": "#3D6A82", 19 | "minimap": { 20 | "background": "#1F4A60", 21 | "text": "#ffffff", 22 | "highlight": "#2A5674" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#1576b3", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#87CEEB", 29 | "string_color": "#5EC17F", 30 | "comment_color": "#67839A", 31 | "include_color": "#67839A", 32 | "parenthesis_color": "#FF7F7F", 33 | "number_color": "#D3A06A", 34 | "symbol_color": "#5BA5CF" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/vulcano.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#2d0707", 3 | "treeview_background": "#3b0b0b", 4 | "text_color": "#ffffff", 5 | "button_color": "#a31919", 6 | "button_hover_color": "#c12323", 7 | "border_color": "#a31919", 8 | "item_hover_background_color": "#c12323", 9 | "item_hover_text_color": "#ffffff", 10 | "separator_color": "#a31919", 11 | "highlight_color": "#7f0f0f", 12 | "line_number_background": "#3b0b0b", 13 | "line_number_text_color": "#ffffff", 14 | "editor_background": "#3b0b0b", 15 | "editor_foreground": "#ffffff", 16 | "selection_background_color": "#a31919", 17 | "status_bar_background": "#2d0707", 18 | "ctn_words": "#3b0b0b", 19 | "minimap": { 20 | "background": "#3b0b0b", 21 | "text": "#ffffff", 22 | "highlight": "#7f0f0f" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#ff4da3", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#ff5757", 29 | "string_color": "#ff9999", 30 | "comment_color": "#b3b3b3", 31 | "include_color": "#b3b3b3", 32 | "parenthesis_color": "#ff7575", 33 | "number_color": "#ffcc80", 34 | "symbol_color": "#ff4081" 35 | } 36 | -------------------------------------------------------------------------------- /Themes/winter.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#1C3D5A", 3 | "treeview_background": "#24476B", 4 | "text_color": "#E0F7FA", 5 | "button_color": "#3B6478", 6 | "button_hover_color": "#5089A0", 7 | "border_color": "#5BA4B4", 8 | "item_hover_background_color": "#5089A0", 9 | "item_hover_text_color": "#E0F7FA", 10 | "separator_color": "#4C6B84", 11 | "highlight_color": "#5089A0", 12 | "line_number_background": "#315770", 13 | "line_number_text_color": "#A7D8E8", 14 | "editor_background": "#1C3D5A", 15 | "editor_foreground": "#E0F7FA", 16 | "selection_background_color": "#3B6478", 17 | "status_bar_background": "#1C3D5A", 18 | "ctn_words": "#3B6478", 19 | "minimap": { 20 | "background": "#1C3D5A", 21 | "text": "#ffffff", 22 | "highlight": "#5089A0" 23 | }, 24 | 25 | "user_type_color": "#ff9900", 26 | "function_color": "#52e6fa", 27 | "preprocessor_color": "##61AFEF", 28 | "keyword_color": "#FF4D4D", 29 | "string_color": "#A1EFD3", 30 | "comment_color": "#D2F0F4", 31 | "include_color": "#D2F0F4", 32 | "parenthesis_color": "#FF4D4D", 33 | "number_color": "#FFE066", 34 | "symbol_color": "#80CBC4" 35 | } 36 | -------------------------------------------------------------------------------- /app_data_/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "pbinfo": { 3 | "username": "", 4 | "password": "" 5 | }, 6 | "kilonova": { 7 | "username": "", 8 | "password": "" 9 | } 10 | } -------------------------------------------------------------------------------- /app_data_/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.6.0" 3 | } -------------------------------------------------------------------------------- /build_info/build.md: -------------------------------------------------------------------------------- 1 | pyinstaller --clean --noconfirm -w --onedir --windowed -i "images/logo.ico" --add-data "images/logo.ico;images/" --add-data "images/png-logo.png;images/" --add-data "images/run.png;images/" --add-data "images/close.png;images/" --add-data "images/bell-default.png;images/" --add-data "images/bell-update.png;images/" --add-data "Themes/dark.json;Themes/" --add-data "Themes/light.json;Themes/" --add-data "Themes/ocean.json;Themes/" --add-data "Themes/dark-blue.json;Themes/" --add-data "Themes/neutral-green.json;Themes/" --add-data "Themes/vulcano.json;Themes/" --add-data "Themes/winter.json;Themes/" --add-data "Themes/monokai.json;Themes/" --add-data "app_data_/version.json;app_data_/" --add-data "Themes/light-fire.json;Themes/" --add-data "app_data_/data.json;app_data_/" --name "CodeNimble" src/main.py -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "font_family": "Arial", 3 | "font_size": "20px", 4 | "editor_font_size": 20, 5 | "profile_name": "user", 6 | "theme": "dark", 7 | "session": { 8 | "opened_folder": "", 9 | "opened_file": "" 10 | }, 11 | "startup": { 12 | "pre_template": "1", 13 | "template": "" 14 | } 15 | } -------------------------------------------------------------------------------- /images/bell-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/bell-default.png -------------------------------------------------------------------------------- /images/bell-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/bell-update.png -------------------------------------------------------------------------------- /images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/close.png -------------------------------------------------------------------------------- /images/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/logo.ico -------------------------------------------------------------------------------- /images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/play.png -------------------------------------------------------------------------------- /images/png-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/png-logo.png -------------------------------------------------------------------------------- /images/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/run.png -------------------------------------------------------------------------------- /images/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/ss.png -------------------------------------------------------------------------------- /images/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/ss2.png -------------------------------------------------------------------------------- /images/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HojdaAdelin/CodeNimble/36fb2c261f4dbd7e5773d2196c4eb086ba6c2e5f/images/ss3.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | PySide6>=6.7.2 3 | flask>=3.0.2 4 | beautifulsoup4>=4.12.3 5 | aiortc>=1.9.0 6 | cryptography>=42.0.4 -------------------------------------------------------------------------------- /src/Core/edit_manager.py: -------------------------------------------------------------------------------- 1 | class EditManager: 2 | def __init__(self, text_widget): 3 | self.text_widget = text_widget 4 | 5 | def undo(self): 6 | self.text_widget.undo() 7 | 8 | def redo(self): 9 | self.text_widget.redo() 10 | 11 | def copy(self): 12 | if self.text_widget.textCursor().hasSelection(): 13 | self.text_widget.copy() 14 | 15 | def cut(self): 16 | if self.text_widget.textCursor().hasSelection(): 17 | self.text_widget.cut() 18 | 19 | def paste(self): 20 | self.text_widget.paste() 21 | 22 | def delete(self): 23 | 24 | cursor = self.text_widget.textCursor() 25 | if cursor.hasSelection(): 26 | cursor.removeSelectedText() 27 | 28 | def clear(self): 29 | 30 | self.text_widget.clear() 31 | 32 | def select_all(self): 33 | self.text_widget.selectAll() 34 | -------------------------------------------------------------------------------- /src/Core/file_manager.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QFileDialog, QTextEdit, QPlainTextEdit, QDialog, QPushButton, QVBoxLayout, QLineEdit 2 | from PySide6.QtCore import QDir 3 | from PySide6.QtGui import QIcon 4 | import os 5 | 6 | class FileManager: 7 | def __init__(self): 8 | self.opened_filename = None 9 | self.opened_foldername = None 10 | 11 | def open_file(self, text_widget: QTextEdit,tab_bar,path=None, file_dialog_title="Open File"): 12 | if path: 13 | tab_bar.add_tab(path) 14 | self.opened_filename = path 15 | 16 | with open(path, "r", encoding="utf-8") as file: 17 | file_content = file.read() 18 | 19 | text_widget.clear() 20 | text_widget.setPlainText(file_content) 21 | return 22 | filename, _ = QFileDialog.getOpenFileName(None, file_dialog_title, "", "All files (*.*)") 23 | 24 | if filename: 25 | tab_bar.add_tab(filename) 26 | self.opened_filename = filename 27 | 28 | with open(filename, "r", encoding="utf-8") as file: 29 | file_content = file.read() 30 | 31 | text_widget.clear() 32 | text_widget.setPlainText(file_content) 33 | 34 | def save_file(self, text_widget: QTextEdit): 35 | 36 | if self.opened_filename: 37 | content = text_widget.toPlainText() 38 | 39 | with open(self.opened_filename, "w", encoding="utf-8") as file: 40 | file.write(content) 41 | print(f"Saved: {self.opened_filename}") 42 | else: 43 | self.save_as_file(text_widget) 44 | 45 | def save_as_file(self, text_widget: QTextEdit): 46 | 47 | content = text_widget.toPlainText() 48 | filename, _ = QFileDialog.getSaveFileName(None, "Save As", os.getcwd(), "Text files (*.txt);;All files (*.*)") 49 | 50 | if filename: 51 | self.opened_filename = filename 52 | 53 | with open(filename, "w", encoding="utf-8") as file: 54 | file.write(content) 55 | print(f"Saved As: {filename}") 56 | 57 | def get_file_content(self, path=None): 58 | if path: 59 | with open(path, "r", encoding="utf-8") as file: 60 | file_content = file.read() 61 | else: 62 | with open(self.opened_filename, "r", encoding="utf-8") as file: 63 | file_content = file.read() 64 | 65 | return file_content 66 | 67 | def change_opened_filename(self, filename): 68 | self.opened_filename = filename 69 | 70 | def get_opened_filename(self): 71 | return self.opened_filename 72 | 73 | def get_opened_foldername(self): 74 | return self.opened_foldername 75 | 76 | def open_folder(self, treeview,win,path=None, folder_dialog_title="Open Folder"): 77 | if path: 78 | self.opened_foldername = path 79 | treeview.model.setRootPath(path) 80 | treeview.tree.setRootIndex(treeview.model.index(path)) 81 | win.splitter.setSizes([250] + win.splitter.sizes()[1:]) 82 | return 83 | foldername = QFileDialog.getExistingDirectory(None, folder_dialog_title, "", QFileDialog.ShowDirsOnly) 84 | 85 | if foldername: 86 | self.opened_foldername = foldername 87 | treeview.model.setRootPath(foldername) 88 | treeview.tree.setRootIndex(treeview.model.index(foldername)) 89 | win.splitter.setSizes([250] + win.splitter.sizes()[1:]) 90 | win.status_bar.toggle_inbox_icon(f"Opened: {foldername}") 91 | 92 | def close_folder(self, treeview, win): 93 | if self.opened_foldername: 94 | win.status_bar.toggle_inbox_icon(f"Closed: {self.opened_foldername}", "orange") 95 | self.opened_foldername = None 96 | treeview.model.setRootPath(QDir.rootPath()) 97 | treeview.tree.setRootIndex(treeview.model.index(QDir.rootPath())) 98 | treeview.tree.setRootIndex(treeview.model.index("")) 99 | win.splitter.setSizes([0] + win.splitter.sizes()[1:]) 100 | 101 | def new_file(self, tab_bar, theme): 102 | self.dialog = QDialog() 103 | self.dialog.setWindowTitle("New File") 104 | self.dialog.setWindowIcon(QIcon("images/png-logo.png")) 105 | 106 | layout = QVBoxLayout() 107 | 108 | # Entry field for file name 109 | self.text_box = QLineEdit(self.dialog) 110 | self.text_box.setPlaceholderText("Enter file name") 111 | layout.addWidget(self.text_box) 112 | 113 | # Button to create the file 114 | self.create_button = QPushButton("Create", self.dialog) 115 | layout.addWidget(self.create_button) 116 | 117 | # Apply the theme when the dialog is created 118 | self.apply_theme(theme) 119 | 120 | def create_file(): 121 | filename = self.text_box.text().strip() 122 | if filename: 123 | if not "." in filename: 124 | filename += ".txt" 125 | 126 | # Determine the file path based on opened_foldername or default path 127 | if self.opened_foldername: 128 | filepath = os.path.join(self.opened_foldername, filename) 129 | else: 130 | filepath = os.path.join(os.getcwd(), filename) 131 | 132 | try: 133 | with open(filepath, "x") as file: 134 | self.opened_filename = filepath 135 | tab_bar.add_tab(filepath) 136 | except FileExistsError: 137 | pass 138 | 139 | self.dialog.close() 140 | 141 | self.create_button.clicked.connect(create_file) 142 | 143 | self.dialog.setLayout(layout) 144 | self.dialog.exec() 145 | 146 | def apply_theme(self, theme): 147 | if self.dialog and self.text_box and self.create_button: 148 | # Apply styles to the dialog 149 | self.dialog.setStyleSheet(f""" 150 | QDialog {{ 151 | background-color: {theme['background_color']}; 152 | }} 153 | """) 154 | 155 | # Apply styles to the text box 156 | self.text_box.setStyleSheet(f""" 157 | QLineEdit {{ 158 | background-color: {theme.get("editor_background")}; 159 | color: {theme.get("editor_foreground")}; 160 | border: 1px solid {theme.get("border_color")}; 161 | padding: 5px; 162 | }} 163 | """) 164 | 165 | # Apply styles to the create button 166 | self.create_button.setStyleSheet(f""" 167 | QPushButton {{ 168 | background-color: {theme.get("button_color")}; 169 | color: {theme.get("text_color")}; 170 | padding: 5px; 171 | border: 1px solid {theme.get('border_color')}; 172 | }} 173 | QPushButton:hover {{ 174 | background-color: {theme.get("button_hover_color")}; 175 | }} 176 | """) 177 | 178 | def create_file(self,text, ext, tab_bar): 179 | if self.opened_foldername: 180 | filename = self.opened_foldername +"/template" + ext 181 | else: 182 | filename = "template" + ext 183 | 184 | count = 1 185 | while os.path.exists(filename): 186 | if self.opened_foldername: 187 | filename = self.opened_foldername + f"/template{count}" + ext 188 | else: 189 | filename = f"template{count}" + ext 190 | count += 1 191 | 192 | with open(filename, "w", encoding="utf-8") as file: 193 | file.write(text) 194 | self.opened_filename = filename 195 | tab_bar.add_tab(filename) 196 | 197 | def return_extension(self, path=None): 198 | if path: 199 | filename = path 200 | elif self.opened_filename: 201 | filename = self.opened_filename 202 | else: 203 | return "" 204 | 205 | _, ext = os.path.splitext(filename) 206 | return ext if ext else "" -------------------------------------------------------------------------------- /src/Core/misc_manager.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QTextEdit 2 | from PySide6.QtGui import QTextCursor, QIcon 3 | from PySide6.QtCore import Qt 4 | 5 | class MiscManager: 6 | def __init__(self, text_widget, theme): 7 | self.text_widget = text_widget 8 | self.theme = theme 9 | 10 | def find_text(self): 11 | dialog = QDialog() 12 | dialog.setWindowTitle("Find Text") 13 | dialog.setWindowIcon(QIcon("images/png-logo.png")) 14 | dialog.setFixedSize(300, 100) # Dezactivează redimensionarea 15 | 16 | layout = QVBoxLayout() 17 | 18 | entry = QLineEdit(dialog) 19 | entry.setPlaceholderText("Enter text to find") 20 | layout.addWidget(entry) 21 | 22 | find_button = QPushButton("Find", dialog) 23 | layout.addWidget(find_button) 24 | 25 | dialog.setLayout(layout) 26 | 27 | # Apply theme 28 | dialog.setStyleSheet(f""" 29 | QDialog {{ 30 | background-color: {self.theme['background_color']}; 31 | color: {self.theme['text_color']}; 32 | }} 33 | QLineEdit {{ 34 | background-color: {self.theme.get("editor_background")}; 35 | color: {self.theme.get("editor_foreground")}; 36 | border: 1px solid {self.theme.get("border_color")}; 37 | padding: 5px; 38 | }} 39 | QPushButton {{ 40 | background-color: {self.theme.get("button_color")}; 41 | color: {self.theme.get("text_color")}; 42 | padding: 5px; 43 | border: 1px solid {self.theme.get('border_color')}; 44 | }} 45 | QPushButton:hover {{ 46 | background-color: {self.theme.get("button_hover_color")}; 47 | }} 48 | """) 49 | 50 | def find_action(): 51 | search_text = entry.text().strip() 52 | if not search_text: 53 | QMessageBox.warning(dialog, "No Text Entered", "Please enter text to find.") 54 | return 55 | 56 | cursor = self.text_widget.textCursor() 57 | if cursor.hasSelection(): 58 | cursor.setPosition(cursor.selectionEnd(), QTextCursor.MoveAnchor) 59 | 60 | found = self.text_widget.find(search_text) # Fără QTextDocument.FindWholeWords 61 | 62 | if not found: 63 | cursor.movePosition(QTextCursor.Start) 64 | self.text_widget.setTextCursor(cursor) 65 | found = self.text_widget.find(search_text) 66 | 67 | if found: 68 | cursor = self.text_widget.textCursor() 69 | cursor.setPosition(cursor.selectionStart(), QTextCursor.MoveAnchor) 70 | cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(search_text)) 71 | self.text_widget.setTextCursor(cursor) 72 | 73 | find_button.clicked.connect(find_action) 74 | dialog.exec() 75 | 76 | def replace_text(self): 77 | dialog = QDialog() 78 | dialog.setWindowTitle("Replace Text") 79 | dialog.setWindowIcon(QIcon("images/png-logo.png")) 80 | dialog.setFixedSize(400, 150) # Dezactivează redimensionarea 81 | 82 | layout = QVBoxLayout() 83 | 84 | find_entry = QLineEdit(dialog) 85 | find_entry.setPlaceholderText("Enter text to find") 86 | layout.addWidget(find_entry) 87 | 88 | replace_entry = QLineEdit(dialog) 89 | replace_entry.setPlaceholderText("Enter replacement text") 90 | layout.addWidget(replace_entry) 91 | 92 | button_layout = QHBoxLayout() 93 | 94 | find_button = QPushButton("Find", dialog) 95 | button_layout.addWidget(find_button) 96 | 97 | replace_button = QPushButton("Replace", dialog) 98 | button_layout.addWidget(replace_button) 99 | 100 | replace_all_button = QPushButton("Replace All", dialog) 101 | button_layout.addWidget(replace_all_button) 102 | 103 | layout.addLayout(button_layout) 104 | dialog.setLayout(layout) 105 | 106 | # Apply theme 107 | dialog.setStyleSheet(f""" 108 | QDialog {{ 109 | background-color: {self.theme['background_color']}; 110 | color: {self.theme['text_color']}; 111 | }} 112 | QLineEdit {{ 113 | background-color: {self.theme.get("editor_background")}; 114 | color: {self.theme.get("editor_foreground")}; 115 | border: 1px solid {self.theme.get("border_color")}; 116 | padding: 5px; 117 | }} 118 | QPushButton {{ 119 | background-color: {self.theme.get("button_color")}; 120 | color: {self.theme.get("text_color")}; 121 | padding: 5px; 122 | border: 1px solid {self.theme.get('border_color')}; 123 | }} 124 | QPushButton:hover {{ 125 | background-color: {self.theme.get("button_hover_color")}; 126 | }} 127 | """) 128 | 129 | def find_action(): 130 | search_text = find_entry.text().strip() 131 | if not search_text: 132 | QMessageBox.warning(dialog, "No Text Entered", "Please enter text to find.") 133 | return 134 | 135 | cursor = self.text_widget.textCursor() 136 | if cursor.hasSelection(): 137 | cursor.setPosition(cursor.selectionEnd(), QTextCursor.MoveAnchor) 138 | 139 | found = self.text_widget.find(search_text) # Fără QTextDocument.FindWholeWords 140 | 141 | if not found: 142 | cursor.movePosition(QTextCursor.Start) 143 | self.text_widget.setTextCursor(cursor) 144 | found = self.text_widget.find(search_text) 145 | 146 | if found: 147 | cursor = self.text_widget.textCursor() 148 | cursor.setPosition(cursor.selectionStart(), QTextCursor.MoveAnchor) 149 | cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(search_text)) 150 | self.text_widget.setTextCursor(cursor) 151 | 152 | def replace_action(): 153 | search_text = find_entry.text().strip() 154 | replace_text = replace_entry.text().strip() 155 | 156 | if not search_text: 157 | QMessageBox.warning(dialog, "No Text Entered", "Please enter text to find.") 158 | return 159 | 160 | cursor = self.text_widget.textCursor() 161 | 162 | # Dacă textul selectat este cel care trebuie înlocuit, se face înlocuirea 163 | if cursor.hasSelection() and cursor.selectedText() == search_text: 164 | cursor.insertText(replace_text) 165 | 166 | # Mută cursorul la următoarea apariție 167 | found = self.text_widget.find(search_text) 168 | 169 | if not found: 170 | # Dacă textul nu este găsit, începem de la începutul documentului 171 | cursor.movePosition(QTextCursor.Start) 172 | self.text_widget.setTextCursor(cursor) 173 | found = self.text_widget.find(search_text) 174 | 175 | if found: 176 | cursor = self.text_widget.textCursor() 177 | cursor.setPosition(cursor.selectionStart(), QTextCursor.MoveAnchor) 178 | cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(search_text)) 179 | self.text_widget.setTextCursor(cursor) 180 | 181 | def replace_all_action(): 182 | search_text = find_entry.text().strip() 183 | replace_text = replace_entry.text().strip() 184 | 185 | if not search_text: 186 | QMessageBox.warning(dialog, "No Text Entered", "Please enter text to find.") 187 | return 188 | 189 | cursor = self.text_widget.textCursor() 190 | cursor.beginEditBlock() 191 | 192 | self.text_widget.moveCursor(QTextCursor.Start) 193 | while self.text_widget.find(search_text): 194 | cursor = self.text_widget.textCursor() 195 | if cursor.selectedText() == search_text: 196 | cursor.insertText(replace_text) 197 | 198 | cursor.endEditBlock() 199 | 200 | find_button.clicked.connect(find_action) 201 | replace_button.clicked.connect(replace_action) 202 | replace_all_button.clicked.connect(replace_all_action) 203 | dialog.exec() 204 | 205 | def go_to_line(self): 206 | dialog = QDialog() 207 | dialog.setWindowTitle("Go To Line") 208 | dialog.setWindowIcon(QIcon("images/png-logo.png")) 209 | dialog.setFixedSize(300, 100) # Dezactivează redimensionarea 210 | 211 | layout = QVBoxLayout() 212 | 213 | entry = QLineEdit(dialog) 214 | entry.setPlaceholderText("Enter line number") 215 | layout.addWidget(entry) 216 | 217 | go_button = QPushButton("Go", dialog) 218 | layout.addWidget(go_button) 219 | 220 | dialog.setLayout(layout) 221 | 222 | # Apply theme 223 | dialog.setStyleSheet(f""" 224 | QDialog {{ 225 | background-color: {self.theme['background_color']}; 226 | color: {self.theme['text_color']}; 227 | }} 228 | QLineEdit {{ 229 | background-color: {self.theme.get("editor_background")}; 230 | color: {self.theme.get("editor_foreground")}; 231 | border: 1px solid {self.theme.get("border_color")}; 232 | padding: 5px; 233 | }} 234 | QPushButton {{ 235 | background-color: {self.theme.get("button_color")}; 236 | color: {self.theme.get("text_color")}; 237 | padding: 5px; 238 | border: 1px solid {self.theme.get('border_color')}; 239 | }} 240 | QPushButton:hover {{ 241 | background-color: {self.theme.get("button_hover_color")}; 242 | }} 243 | """) 244 | 245 | def go_action(): 246 | line_number_str = entry.text().strip() 247 | if not line_number_str.isdigit(): 248 | QMessageBox.warning(dialog, "Invalid Input", "Please enter a valid line number.") 249 | return 250 | 251 | line_number = int(line_number_str) 252 | if line_number <= 0: 253 | QMessageBox.warning(dialog, "Invalid Line Number", "Line number must be greater than 0.") 254 | return 255 | 256 | cursor = self.text_widget.textCursor() 257 | block_number = line_number - 1 258 | 259 | cursor.movePosition(QTextCursor.Start) 260 | cursor.movePosition(QTextCursor.Down, QTextCursor.MoveAnchor, block_number) 261 | self.text_widget.setTextCursor(cursor) 262 | self.text_widget.ensureCursorVisible() 263 | 264 | go_button.clicked.connect(go_action) 265 | dialog.exec() 266 | -------------------------------------------------------------------------------- /src/Core/run.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import sys 4 | import tempfile 5 | from PySide6.QtWidgets import QApplication, QMessageBox, QTextEdit, QWidget, QVBoxLayout, QPushButton 6 | from PySide6.QtCore import Qt 7 | import threading 8 | import time 9 | 10 | 11 | def run(text_widget, file_manager,win): 12 | file_path = file_manager.get_opened_filename() 13 | if file_path is None: 14 | win.status_bar.toggle_inbox_icon(f"No files are open.", "red") 15 | return 16 | if file_path.endswith(".cpp"): 17 | run_cpp_file(text_widget, file_manager) 18 | elif file_path.endswith(".py"): 19 | run_python_file(text_widget, file_manager) 20 | else: 21 | win.status_bar.toggle_inbox_icon(f"Only .cpp & .py files can be run.", "red") 22 | return 23 | 24 | def run_cpp_file(text_widget, file_manager): 25 | file_path = file_manager.get_opened_filename() 26 | print(file_path) # Verificăm calea fișierului 27 | code_content = text_widget.toPlainText() 28 | 29 | current_file_dir = os.path.dirname(file_path) 30 | 31 | with tempfile.NamedTemporaryFile(delete=False, suffix=".cpp", mode='w', encoding='utf-8', dir=current_file_dir) as temp_file: 32 | temp_file.write(code_content) 33 | temp_file_path = temp_file.name 34 | 35 | base_name = os.path.splitext(os.path.basename(file_path))[0] 36 | executable_name = os.path.join(current_file_dir, f"{base_name}.exe") 37 | error_log = os.path.join(current_file_dir, f"{base_name}_error.log") 38 | 39 | # Folosim ghilimele pentru a gestiona calea corect 40 | compile_command = f'g++ "{temp_file_path}" -o "{executable_name}" 2> "{error_log}"' 41 | print(compile_command) # Afișăm comanda pentru debugging 42 | 43 | compile_process = subprocess.run(compile_command, shell=True, cwd=current_file_dir) 44 | 45 | if compile_process.returncode != 0: 46 | run_command = f'start cmd /k type "{error_log}"' 47 | subprocess.run(run_command, shell=True, cwd=current_file_dir) 48 | time.sleep(1) 49 | else: 50 | run_command = f'start cmd /k "{os.path.basename(executable_name)}"' 51 | subprocess.run(run_command, shell=True, cwd=current_file_dir) 52 | time.sleep(1) 53 | 54 | # Curățăm fișierele temporare 55 | os.remove(temp_file_path) 56 | if os.path.exists(executable_name): 57 | os.remove(executable_name) 58 | if os.path.exists(error_log): 59 | os.remove(error_log) 60 | 61 | def run_python_file(text_widget, file_manager): 62 | file_path = file_manager.get_opened_filename() 63 | code_content = text_widget.toPlainText() 64 | 65 | current_file_dir = os.path.dirname(file_path) 66 | 67 | with tempfile.NamedTemporaryFile(delete=False, suffix=".py", mode='w', encoding='utf-8', dir=current_file_dir) as temp_file: 68 | temp_file.write(code_content) 69 | temp_file_path = temp_file.name 70 | 71 | base_name = os.path.splitext(os.path.basename(file_path))[0] 72 | error_log = os.path.join(current_file_dir, f"{base_name}_error.log") 73 | 74 | # Folosim ghilimele pentru a gestiona corect căile de fișiere 75 | run_command = f'python "{temp_file_path}" 2> "{error_log}"' 76 | run_process = subprocess.run(run_command, shell=True, cwd=current_file_dir) 77 | 78 | if run_process.returncode != 0: 79 | show_errors_command = f'start cmd /k type "{error_log}"' 80 | subprocess.run(show_errors_command, shell=True, cwd=current_file_dir) 81 | time.sleep(1) 82 | else: 83 | result_command = f'start cmd /k python "{os.path.basename(temp_file_path)}"' 84 | subprocess.run(result_command, shell=True, cwd=current_file_dir) 85 | time.sleep(1) 86 | 87 | # Curățăm fișierele temporare 88 | os.remove(temp_file_path) 89 | if os.path.exists(error_log): 90 | os.remove(error_log) 91 | 92 | def pre_input_run(text_widget, right_panel, file_manager, win): 93 | file_path = file_manager.get_opened_filename() 94 | if file_path is None: 95 | win.status_bar.toggle_inbox_icon(f"No files are open.", "red") 96 | return 97 | 98 | if not file_path.endswith(".cpp"): 99 | win.status_bar.toggle_inbox_icon(f"Only .cpp files can be run.", "red") 100 | return 101 | 102 | code_content = text_widget.toPlainText() 103 | pre_input = right_panel.input_box.toPlainText().strip() 104 | if len(pre_input) == 0: 105 | win.status_bar.toggle_inbox_icon(f"You need to set the pre-input from right panel!", "orange") 106 | return 107 | 108 | current_file_dir = os.path.dirname(file_path) 109 | 110 | try: 111 | cpp_file_path = os.path.join(current_file_dir, "temp_code.cpp") 112 | with open(cpp_file_path, 'w', encoding='utf-8') as cpp_file: 113 | cpp_file.write(code_content) 114 | 115 | executable_path = os.path.join(current_file_dir, "temp_code") 116 | 117 | compile_process = subprocess.run(["g++", cpp_file_path, "-o", executable_path], capture_output=True, text=True) 118 | 119 | if compile_process.returncode != 0: 120 | compile_errors = compile_process.stderr 121 | right_panel.output_box.setPlainText(f"Compilation failed:\n{compile_errors}") 122 | return 123 | 124 | run_process = subprocess.Popen(executable_path, 125 | stdin=subprocess.PIPE, 126 | stdout=subprocess.PIPE, 127 | stderr=subprocess.PIPE, 128 | text=True) 129 | output, error = run_process.communicate(input=pre_input) 130 | 131 | if run_process.returncode != 0: 132 | right_panel.output_box.setPlainText(f"Runtime error:\n{error}") 133 | return 134 | 135 | right_panel.output_box.setPlainText(output) 136 | 137 | expected_output = right_panel.expected_box.toPlainText().strip() 138 | passing = True 139 | if len(expected_output) != 0: 140 | new_output = right_panel.output_box.toPlainText().strip() 141 | 142 | expected_words = expected_output.split() 143 | new_words = new_output.split() 144 | 145 | if len(expected_words) != len(new_words): 146 | passing = False 147 | right_panel.output_label.setText("Different number of words") 148 | right_panel.output_label.setStyleSheet("color: red") 149 | else: 150 | for i in range(len(new_words)): 151 | if expected_words[i] != new_words[i]: 152 | passing = False 153 | right_panel.output_label.setText(f"Wrong answer on test case {i + 1}") 154 | right_panel.output_label.setStyleSheet("color: red") 155 | break 156 | 157 | if passing: 158 | right_panel.output_label.setText("All test cases passed") 159 | right_panel.output_label.setStyleSheet("color: green") 160 | 161 | except Exception as e: 162 | right_panel.output_box.setPlainText(f"Exception occurred:\n{str(e)}") 163 | 164 | finally: 165 | if os.path.exists(cpp_file_path): 166 | os.remove(cpp_file_path) 167 | if os.path.exists(executable_path): 168 | os.remove(executable_path) 169 | -------------------------------------------------------------------------------- /src/Core/session.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def save_session(file_manager): 4 | folder_name = file_manager.get_opened_foldername() 5 | file_name = file_manager.get_opened_filename() 6 | with open('config.json', 'r', encoding="utf-8") as f: 7 | data = json.load(f) 8 | data['session']['opened_folder'] = folder_name 9 | data['session']['opened_file'] = file_name 10 | 11 | with open('config.json', 'w', encoding="utf-8") as f: 12 | json.dump(data, f, indent=4) 13 | 14 | def reset_session(): 15 | with open('config.json', 'r', encoding="utf-8") as f: 16 | data = json.load(f) 17 | data['session']['opened_folder'] = "None" 18 | data['session']['opened_file'] = "None" 19 | 20 | with open('config.json', 'w', encoding="utf-8") as f: 21 | json.dump(data, f, indent=4) 22 | 23 | def session_engine(win): 24 | with open('config.json', 'r', encoding="utf-8") as f: 25 | data = json.load(f) 26 | if data['session']['opened_folder'] != "None" and data['session']['opened_folder']: 27 | win.file_manager.open_folder(win.tree_view, win, data['session']['opened_folder']) 28 | if data['session']['opened_file'] != "None" and data['session']['opened_file']: 29 | win.file_manager.open_file(win.editor, win.tab_bar, data['session']['opened_file']) 30 | -------------------------------------------------------------------------------- /src/Core/templates.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtWidgets, QtCore, QtGui 2 | import os 3 | 4 | # Coduri template 5 | cpp_text = """#include 6 | 7 | int main() 8 | { 9 | std::cout << "Hello World"; 10 | return 0; 11 | }""" 12 | 13 | c_text = """#include 14 | 15 | int main() 16 | { 17 | printf("Hello World"); 18 | 19 | return 0; 20 | }""" 21 | 22 | java_text = """public class Main 23 | { 24 | public static void main(String[] args) { 25 | System.out.println("Hello World"); 26 | } 27 | }""" 28 | 29 | html_text = """ 30 | 31 | 32 | 33 | 34 | Document 35 | 36 | 37 | 38 | 39 | """ 40 | 41 | competitive_text = """#include 42 | 43 | using namespace std; 44 | 45 | #define ll long long 46 | 47 | void solution() { 48 | 49 | } 50 | 51 | int main() 52 | { 53 | solution(); 54 | 55 | return 0; 56 | }""" 57 | 58 | def return_content(type): 59 | if type == "cpp": 60 | return cpp_text 61 | elif type == "c": 62 | return c_text 63 | elif type == "java": 64 | return java_text 65 | elif type == "html": 66 | return html_text 67 | elif type == "competitive": 68 | return competitive_text 69 | 70 | 71 | 72 | def apply_theme(widget, theme): 73 | # Apply the theme to the widget 74 | palette = widget.palette() 75 | palette.setColor(QtGui.QPalette.Window, QtGui.QColor(theme['background_color'])) 76 | palette.setColor(QtGui.QPalette.WindowText, QtGui.QColor(theme['text_color'])) 77 | palette.setColor(QtGui.QPalette.Button, QtGui.QColor(theme['button_color'])) 78 | palette.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(theme['text_color'])) 79 | palette.setColor(QtGui.QPalette.Base, QtGui.QColor(theme['editor_background'])) 80 | palette.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(theme['line_number_background'])) 81 | palette.setColor(QtGui.QPalette.Text, QtGui.QColor(theme['editor_foreground'])) 82 | palette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(theme['highlight_color'])) 83 | palette.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(theme['text_color'])) 84 | 85 | widget.setPalette(palette) 86 | 87 | widget.setStyleSheet(f""" 88 | QPushButton {{ 89 | background-color: {theme.get("button_color")}; 90 | color: {theme.get("text_color")}; 91 | padding: 5px; 92 | border: 1px solid {theme.get('border_color')}; 93 | }} 94 | QPushButton:hover {{ 95 | background-color: {theme.get("button_hover_color")}; 96 | }} 97 | QLineEdit {{ 98 | background-color: {theme.get("editor_background")}; 99 | color: {theme.get("editor_foreground")}; 100 | border: 1px solid {theme.get("border_color")}; 101 | padding: 5px; 102 | }} 103 | QPlainTextEdit {{ 104 | background-color: {theme.get("editor_background")}; 105 | color: {theme.get("editor_foreground")}; 106 | border: 1px solid {theme.get("border_color")}; 107 | padding: 5px; 108 | }} 109 | QListWidget {{ 110 | background-color: {theme['editor_background']}; 111 | color: {theme['editor_foreground']}; 112 | border: 1px solid {theme['border_color']}; 113 | }} 114 | QListWidget::item:selected {{ 115 | background-color: {theme['item_hover_background_color']}; 116 | color: {theme['item_hover_text_color']}; 117 | }} 118 | QLabel {{ 119 | color: {theme['text_color']}; 120 | }} 121 | """) 122 | 123 | def create_template(text_widget, file_manager, theme): 124 | def open_template_window(): 125 | template_window = QtWidgets.QWidget() 126 | template_window.setWindowTitle("CodeNimble - Create Templates") 127 | template_window.setWindowIcon(QtGui.QIcon("images/logo.ico")) 128 | 129 | layout = QtWidgets.QVBoxLayout(template_window) 130 | 131 | name_label = QtWidgets.QLabel("Name:") 132 | name_label.setFont(QtGui.QFont("Arial", 20)) 133 | layout.addWidget(name_label) 134 | 135 | name_box = QtWidgets.QLineEdit() 136 | name_box.setFont(QtGui.QFont("Arial", 30)) 137 | layout.addWidget(name_box) 138 | 139 | content_label = QtWidgets.QLabel("Text:") 140 | content_label.setFont(QtGui.QFont("Arial", 20)) 141 | layout.addWidget(content_label) 142 | 143 | text_box = QtWidgets.QPlainTextEdit() 144 | text_box.setFont(QtGui.QFont("Arial", 16)) 145 | layout.addWidget(text_box) 146 | 147 | apply_theme(template_window, theme) 148 | 149 | def create_template_file(): 150 | template_name_full = name_box.text().strip() 151 | if '.' in template_name_full: 152 | template_name, extension = template_name_full.rsplit('.', 1) 153 | else: 154 | template_name = template_name_full 155 | extension = 'txt' # Default extension if none provided 156 | 157 | template_content = text_box.toPlainText().strip() 158 | 159 | if not template_name: 160 | QtWidgets.QMessageBox.critical(template_window, "Error", "Template name cannot be empty!") 161 | return 162 | 163 | curr_dir = os.getcwd() 164 | tmp_folder = os.path.join(curr_dir, 'Templates') 165 | if not os.path.isdir(tmp_folder): 166 | os.makedirs(tmp_folder) 167 | 168 | template_path = os.path.join(tmp_folder, f"{template_name}.{extension}") 169 | 170 | if os.path.exists(template_path): 171 | QtWidgets.QMessageBox.critical(template_window, "Error", f"Template '{template_name}' already exists!") 172 | return 173 | 174 | if not template_content: 175 | QtWidgets.QMessageBox.critical(template_window, "Error", "Template content cannot be empty!") 176 | return 177 | 178 | with open(template_path, 'w') as template_file: 179 | template_file.write(template_content) 180 | 181 | QtWidgets.QMessageBox.information(template_window, "Success", f"Template '{template_name}.{extension}' created successfully!") 182 | 183 | create_button = QtWidgets.QPushButton("Create") 184 | create_button.setFont(QtGui.QFont("Arial", 16)) 185 | create_button.clicked.connect(create_template_file) 186 | layout.addWidget(create_button) 187 | 188 | template_window.setLayout(layout) 189 | template_window.resize(460, 560) 190 | template_window.show() 191 | 192 | open_template_window() 193 | 194 | def use_template(text_widget, file_manager, theme, tab_bar): 195 | def open_use_template_window(): 196 | template_window = QtWidgets.QWidget() 197 | template_window.setWindowTitle("CodeNimble - Use Templates") 198 | template_window.setWindowIcon(QtGui.QIcon("images/logo.ico")) 199 | 200 | layout = QtWidgets.QVBoxLayout(template_window) 201 | 202 | search_label = QtWidgets.QLabel("Search:") 203 | search_label.setFont(QtGui.QFont("Arial", 20)) 204 | layout.addWidget(search_label) 205 | 206 | search_box = QtWidgets.QLineEdit() 207 | search_box.setFont(QtGui.QFont("Arial", 30)) 208 | layout.addWidget(search_box) 209 | 210 | listbox = QtWidgets.QListWidget() 211 | listbox.setFont(QtGui.QFont("Arial", 16)) 212 | layout.addWidget(listbox) 213 | 214 | apply_theme(template_window, theme) 215 | 216 | curr_dir = os.getcwd() 217 | tmp_folder = os.path.join(curr_dir, 'Templates') 218 | if not os.path.isdir(tmp_folder): 219 | os.makedirs(tmp_folder) 220 | 221 | def update_listbox(): 222 | search_term = search_box.text().strip().lower() 223 | listbox.clear() 224 | for file in os.listdir(tmp_folder): 225 | if search_term in file.lower(): 226 | listbox.addItem(file) 227 | 228 | search_box.textChanged.connect(update_listbox) 229 | update_listbox() 230 | 231 | def use_selected_template(): 232 | selected = listbox.currentItem() 233 | if not selected: 234 | QtWidgets.QMessageBox.critical(template_window, "Error", "No template selected!") 235 | return 236 | 237 | template_file = selected.text() 238 | template_path = os.path.join(tmp_folder, template_file) 239 | 240 | with open(template_path, 'r') as file: 241 | template_content = file.read() 242 | 243 | _, extension = os.path.splitext(template_file) 244 | 245 | text_widget.clear() 246 | text_widget.insertPlainText(template_content) 247 | file_manager.create_file(template_content, extension, tab_bar) 248 | 249 | QtWidgets.QMessageBox.information(template_window, "Success", f"Used template {template_file}") 250 | 251 | use_button = QtWidgets.QPushButton("Use") 252 | use_button.setFont(QtGui.QFont("Arial", 16)) 253 | use_button.clicked.connect(use_selected_template) 254 | layout.addWidget(use_button) 255 | 256 | template_window.setLayout(layout) 257 | template_window.resize(400, 500) 258 | template_window.show() 259 | 260 | open_use_template_window() 261 | -------------------------------------------------------------------------------- /src/Core/theme_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide6 import QtWidgets, QtGui 3 | 4 | class ThemeManager: 5 | def __init__(self, win): 6 | self.win = win 7 | 8 | def use_theme(self, theme): 9 | self.win.change_theme(theme) 10 | 11 | def manager_view(self): 12 | def open_manager_view_window(): 13 | theme_window = QtWidgets.QWidget() 14 | theme_window.setWindowTitle("Theme Manager") 15 | theme_window.setWindowIcon(QtGui.QIcon("images/logo.ico")) 16 | 17 | layout = QtWidgets.QVBoxLayout(theme_window) 18 | 19 | search_label = QtWidgets.QLabel("Search:") 20 | search_label.setFont(QtGui.QFont("Arial", 20)) 21 | layout.addWidget(search_label) 22 | 23 | search_box = QtWidgets.QLineEdit() 24 | search_box.setFont(QtGui.QFont("Arial", 30)) 25 | layout.addWidget(search_box) 26 | 27 | listbox = QtWidgets.QListWidget() 28 | listbox.setFont(QtGui.QFont("Arial", 16)) 29 | layout.addWidget(listbox) 30 | tm = self.win.get_theme() 31 | theme_window.setStyleSheet(f""" 32 | QWidget {{ 33 | background-color: {tm['background_color']}; 34 | }} 35 | QPushButton {{ 36 | background-color: {tm.get("button_color")}; 37 | color: {tm.get("text_color")}; 38 | padding: 5px; 39 | border: 1px solid {tm.get('border_color')}; 40 | }} 41 | QPushButton:hover {{ 42 | background-color: {tm.get("button_hover_color")}; 43 | }} 44 | QLineEdit {{ 45 | background-color: {tm.get("editor_background")}; 46 | color: {tm.get("editor_foreground")}; 47 | border: 1px solid {tm.get("border_color")}; 48 | padding: 5px; 49 | }} 50 | QPlainTextEdit {{ 51 | background-color: {tm.get("editor_background")}; 52 | color: {tm.get("editor_foreground")}; 53 | border: 1px solid {tm.get("border_color")}; 54 | padding: 5px; 55 | }} 56 | QListWidget {{ 57 | background-color: {tm['editor_background']}; 58 | color: {tm['editor_foreground']}; 59 | border: 1px solid {tm['border_color']}; 60 | }} 61 | QListWidget::item:selected {{ 62 | background-color: {tm['item_hover_background_color']}; 63 | color: {tm['item_hover_text_color']}; 64 | }} 65 | QLabel {{ 66 | color: {tm['text_color']}; 67 | }} 68 | """) 69 | 70 | tmp_folder = "Themes" 71 | if not os.path.isdir(tmp_folder): 72 | os.makedirs(tmp_folder) 73 | 74 | def update_listbox(): 75 | search_term = search_box.text().strip().lower() 76 | listbox.clear() 77 | for file in os.listdir(tmp_folder): 78 | if search_term in file.lower() and file.endswith('.json'): 79 | listbox.addItem(file) 80 | 81 | search_box.textChanged.connect(update_listbox) 82 | update_listbox() 83 | 84 | def use_selected_theme(): 85 | selected = listbox.currentItem() 86 | if not selected: 87 | QtWidgets.QMessageBox.critical(theme_window, "Error", "No theme selected!") 88 | return 89 | 90 | theme_file = selected.text() 91 | theme_name, _ = os.path.splitext(theme_file) # Remove the .json extension 92 | self.use_theme(theme_name) 93 | 94 | use_button = QtWidgets.QPushButton("Use") 95 | use_button.setFont(QtGui.QFont("Arial", 16)) 96 | use_button.clicked.connect(use_selected_theme) 97 | layout.addWidget(use_button) 98 | 99 | theme_window.setLayout(layout) 100 | theme_window.resize(400, 500) 101 | theme_window.show() 102 | 103 | open_manager_view_window() 104 | -------------------------------------------------------------------------------- /src/Core/view_manager.py: -------------------------------------------------------------------------------- 1 | class ViewManager: 2 | def __init__(self, config, win): 3 | self.config = config 4 | self.win = win 5 | def zoom_in(self): 6 | size = int(self.config.get("editor_font_size")) 7 | if size < 50: 8 | self.config['editor_font_size'] = size+5 9 | self.win.editor.apply_settings() 10 | def zoom_out(self): 11 | size = int(self.config.get("editor_font_size")) 12 | if size > 15: 13 | self.config['editor_font_size'] = size-5 14 | self.win.editor.apply_settings() 15 | def reset_zoom(self): 16 | self.config['editor_font_size'] = 20 17 | self.win.editor.apply_settings() 18 | def fullscreen(self): 19 | if self.win.isFullScreen(): 20 | self.win.showNormal() 21 | else: 22 | self.win.showFullScreen() 23 | def status_bar(self): 24 | if self.win.status_bar.height() == 30: 25 | self.win.status_bar.setFixedHeight(0) 26 | else: 27 | self.win.status_bar.setFixedHeight(30) 28 | def left_panel(self): 29 | sz = self.win.splitter.sizes() 30 | if sz[0] == 0: 31 | self.win.splitter.setSizes([250] + self.win.splitter.sizes()[1:]) 32 | else: 33 | self.win.splitter.setSizes([0] + self.win.splitter.sizes()[1:]) 34 | 35 | def right_panel(self): 36 | sz = self.win.splitter.sizes() 37 | if sz[2] == 0: 38 | self.win.splitter.setSizes(self.win.splitter.sizes()[:-1] + [250]) 39 | else: 40 | self.win.splitter.setSizes(self.win.splitter.sizes()[:-1] + [0]) 41 | 42 | -------------------------------------------------------------------------------- /src/Core/web.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | def open_links(url): 3 | webbrowser.open(url) -------------------------------------------------------------------------------- /src/GUI/diff.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QMainWindow, QTextEdit, QFrame, QVBoxLayout, QScrollBar 2 | from PySide6.QtGui import QTextCursor, QColor, QFont, QIcon 3 | from PySide6.QtCore import Qt 4 | 5 | 6 | class OutputComparator(QMainWindow): 7 | def __init__(self, right_panel, theme, *args, **kwargs): 8 | super().__init__(*args, **kwargs) 9 | 10 | # Setează dimensiunea ferestrei 11 | self.setGeometry(100, 100, 450, 300) 12 | self.setWindowTitle("Output Comparator") 13 | self.setWindowIcon(QIcon("images/logo.ico")) 14 | 15 | # Creează un frame pentru a conține TextBox-ul și scrollbars 16 | frame = QFrame(self) 17 | layout = QVBoxLayout(frame) 18 | layout.setContentsMargins(0, 0, 0, 0) 19 | layout.setSpacing(0) 20 | # Creează TextBox-ul folosind QTextEdit 21 | self.textbox = QTextEdit(self) 22 | self.textbox.setReadOnly(True) 23 | self.textbox.setFont(QFont("Consolas", 16)) # Font size is set to 10 since 30 might be too large 24 | layout.addWidget(self.textbox) 25 | 26 | # Creează scrollbars și le asociază cu TextBox-ul 27 | self.scrollbar_y = QScrollBar(Qt.Vertical) 28 | self.textbox.setVerticalScrollBar(self.scrollbar_y) 29 | self.scrollbar_x = QScrollBar(Qt.Horizontal) 30 | self.textbox.setHorizontalScrollBar(self.scrollbar_x) 31 | 32 | self.setCentralWidget(frame) 33 | 34 | # Aplică tema 35 | self.apply_theme(theme) 36 | 37 | # Compară și afișează rezultatele 38 | self.compare_and_display(right_panel) 39 | 40 | def apply_theme(self, theme): 41 | # Setează culorile în funcție de tema primită 42 | self.textbox.setStyleSheet(f""" 43 | background-color: {theme.get('background_color', '#333333')}; 44 | color: {theme.get('text_color', '#ffffff')}; 45 | selection-background-color: {theme.get('selection_background_color', '#333333')}; 46 | """) 47 | 48 | self.scrollbar_y.setStyleSheet(f""" 49 | QScrollBar:vertical {{ 50 | background-color: {theme.get('background_color', '#333333')}; 51 | }} 52 | QScrollBar::handle:vertical {{ 53 | background-color: {theme.get('button_color', '#555555')}; 54 | }} 55 | QScrollBar::handle:vertical:hover {{ 56 | background-color: {theme.get('button_hover_color', '#777777')}; 57 | }} 58 | """) 59 | 60 | self.scrollbar_x.setStyleSheet(f""" 61 | QScrollBar:horizontal {{ 62 | background-color: {theme.get('background_color', '#333333')}; 63 | }} 64 | QScrollBar::handle:horizontal {{ 65 | background-color: {theme.get('button_color', '#555555')}; 66 | }} 67 | QScrollBar::handle:horizontal:hover {{ 68 | background-color: {theme.get('button_hover_color', '#777777')}; 69 | }} 70 | """) 71 | 72 | def compare_and_display(self, right_panel): 73 | # Obține textul din fiecare TextBox și îl transformă în liste 74 | output_list = right_panel.output_box.toPlainText().split() 75 | expected_list = right_panel.expected_box.toPlainText().split() 76 | 77 | # Curăță TextBox-ul 78 | self.textbox.clear() 79 | 80 | # Compară elementele din cele două liste și formatează textul 81 | for i, (output, expected) in enumerate(zip(output_list, expected_list), start=1): 82 | result = f"{i}. {expected} (" 83 | if output == expected: 84 | result += "correct)" 85 | self.textbox.setTextColor(QColor("green")) 86 | else: 87 | result += "incorrect)" 88 | self.textbox.setTextColor(QColor("#ff6363")) 89 | self.textbox.append(result) 90 | 91 | # Dacă există elemente în `expected_list` care nu au pereche în `output_list` 92 | if len(expected_list) > len(output_list): 93 | for i, expected in enumerate(expected_list[len(output_list):], start=len(output_list) + 1): 94 | result = f"{i}. {expected} (incorrect)" 95 | self.textbox.setTextColor(QColor("#ff6363")) 96 | self.textbox.append(result) 97 | 98 | # Mută cursorul la începutul TextBox-ului 99 | self.textbox.moveCursor(QTextCursor.Start) -------------------------------------------------------------------------------- /src/GUI/info_win.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget 2 | from PySide6.QtGui import QIcon, QFont, QColor 3 | from PySide6.QtCore import Qt 4 | import sys 5 | from Tools import scrap 6 | import json 7 | 8 | def get_current_version(): 9 | with open("app_data_/version.json", "r") as f: 10 | config_data = json.load(f) 11 | return config_data.get("version") 12 | 13 | class VersionWindow(QWidget): 14 | def __init__(self, theme=None): 15 | super().__init__() 16 | 17 | # Setează tema 18 | self.theme = theme 19 | 20 | self.setWindowTitle("CodeNimble - Version") 21 | self.setWindowIcon(QIcon("images/logo.ico")) 22 | 23 | # Dimensiunile ferestrei 24 | self.setFixedSize(350, 100) 25 | 26 | # Setare layout 27 | layout = QVBoxLayout() 28 | 29 | # Obține versiunea curentă și ultima versiune 30 | current_version = get_current_version() 31 | latest_version = scrap.get_latest_version_from_github("HojdaAdelin", "CodeNimble") 32 | 33 | # Creează etichetele 34 | self.current_version_label = QLabel(f"Current version: {current_version}") 35 | self.latest_version_label = QLabel(f"Latest version: {latest_version}") 36 | 37 | # Setează fontul pentru etichete 38 | font = QFont("Consolas", 18) 39 | self.current_version_label.setFont(font) 40 | self.latest_version_label.setFont(font) 41 | 42 | # Alinierea textului 43 | self.current_version_label.setAlignment(Qt.AlignCenter) 44 | self.latest_version_label.setAlignment(Qt.AlignCenter) 45 | 46 | # Adaugă etichetele la layout 47 | layout.addWidget(self.current_version_label) 48 | layout.addWidget(self.latest_version_label) 49 | 50 | # Setează layout-ul pentru fereastră 51 | self.apply_theme() 52 | self.setLayout(layout) 53 | 54 | def apply_theme(self): 55 | # Aplică culorile de fundal și text conform temei 56 | bg_color = QColor(self.theme.get("background_color", "#ffffff")) 57 | text_color = QColor(self.theme.get("text_color", "#000000")) 58 | 59 | # Setează culoarea de fundal pentru fereastră 60 | self.setStyleSheet(f"background-color: {bg_color.name()};") 61 | 62 | # Setează culoarea textului pentru etichete 63 | self.current_version_label.setStyleSheet(f"color: {text_color.name()};") 64 | self.latest_version_label.setStyleSheet(f"color: {text_color.name()};") 65 | 66 | def show_window(self): 67 | self.show() -------------------------------------------------------------------------------- /src/GUI/kilo_tools.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QMainWindow 2 | from PySide6.QtCore import Qt 3 | from PySide6.QtGui import QIcon, QFont 4 | 5 | from Tools import kilonova 6 | 7 | class Kilotools(QMainWindow): 8 | def __init__(self,theme,parent=None): 9 | super().__init__(parent) 10 | self.theme = theme 11 | self.setWindowTitle("Code Nimble - Kilonova tools") 12 | self.setGeometry(100, 100, 600, 200) 13 | self.setWindowIcon(QIcon("images/logo.ico")) 14 | self.setFixedSize(600, 200) 15 | 16 | self.apply_theme(self.theme) 17 | self.contest_name, self.contest_info = kilonova.contest_info() 18 | 19 | self.gui() 20 | 21 | def apply_theme(self, theme): 22 | self.setStyleSheet(f""" 23 | background-color: {theme['background_color']}; 24 | color: {theme['text_color']}; 25 | """) 26 | 27 | def gui(self): 28 | 29 | self.contest_label = QLabel(f"Latest contest: {self.contest_name}", self) 30 | self.contest_label.setFont(QFont("Arial", 12)) 31 | self.contest_label.setAlignment(Qt.AlignCenter) 32 | self.contest_label.setGeometry(50, 50, 500, 50) 33 | 34 | self.contest_status_label = QLabel(f"Status: {self.contest_info}", self) 35 | self.contest_status_label.setFont(QFont("Arial", 12)) 36 | self.contest_status_label.setAlignment(Qt.AlignCenter) 37 | self.contest_status_label.setGeometry(50, 120, 500, 50) -------------------------------------------------------------------------------- /src/GUI/minimap.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget 2 | from PySide6.QtGui import QColor, QPainter, QTextCursor, QFont 3 | from PySide6.QtCore import Qt 4 | 5 | class MiniMap(QWidget): 6 | def __init__(self, editor,theme, parent=None): 7 | super().__init__(parent) 8 | self.editor = editor 9 | self.setFixedWidth(100) 10 | 11 | self.font = QFont("Consolas", 3) 12 | self.font.setPointSize(3) 13 | self.line_spacing = 4 14 | self.theme = theme['minimap'] 15 | 16 | def setTheme(self, theme): 17 | for key in ["background", "text", "highlight"]: 18 | if key in theme: 19 | self.theme[key] = theme[key] 20 | self.update() 21 | 22 | def paintEvent(self, event): 23 | painter = QPainter(self) 24 | painter.setOpacity(0.8) 25 | painter.fillRect(self.rect(), QColor(self.theme["background"])) 26 | 27 | painter.setFont(self.font) 28 | 29 | font_metrics = painter.fontMetrics() 30 | char_width = font_metrics.horizontalAdvance("a") 31 | num_chars_fit = self.width() // char_width 32 | 33 | block = self.editor.document().begin() 34 | y_offset = 0 35 | while block.isValid(): 36 | text = block.text() 37 | truncated_text = text[:num_chars_fit] 38 | painter.setOpacity(0.8) 39 | painter.setPen(QColor(self.theme["text"])) 40 | painter.drawText(2, y_offset + self.line_spacing, truncated_text) 41 | y_offset += self.line_spacing 42 | block = block.next() 43 | 44 | visible_rect = self.editor.viewport().rect() 45 | cursor = self.editor.cursorForPosition(visible_rect.topLeft()) 46 | start_block = cursor.block().blockNumber() 47 | 48 | visible_lines = visible_rect.height() // self.editor.fontMetrics().height() 49 | 50 | highlighter_color = QColor(self.theme["highlight"]) 51 | highlighter_color.setAlpha(80) 52 | painter.setBrush(highlighter_color) 53 | painter.setPen(Qt.NoPen) 54 | painter.drawRect(0, start_block * self.line_spacing, self.width(), 55 | visible_lines * self.line_spacing) 56 | 57 | def mousePressEvent(self, event): 58 | y = event.pos().y() 59 | block_index = y // self.line_spacing 60 | document = self.editor.document() 61 | block = document.findBlockByNumber(block_index) 62 | 63 | if block.isValid(): 64 | cursor = QTextCursor(block) 65 | self.editor.setTextCursor(cursor) 66 | self.editor.centerCursor() 67 | -------------------------------------------------------------------------------- /src/GUI/paint.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QPushButton,QFrame, QStackedWidget 2 | from PySide6.QtGui import QIcon, QColor, QPainter, QPen, QPixmap 3 | from PySide6.QtCore import Qt 4 | import sys 5 | 6 | class CanvasWidget(QWidget): 7 | def __init__(self): 8 | super().__init__() 9 | self.image = QPixmap(1000, 500) 10 | self.image.fill(Qt.white) 11 | self.last_pos = None 12 | self.current_tool = "pencil" 13 | self.pencil_color = QColor("black") 14 | self.pen_width = 3 15 | self.eraser_width = 20 16 | self.setMouseTracking(True) 17 | 18 | def paintEvent(self, event): 19 | painter = QPainter(self) 20 | painter.drawPixmap(self.rect(), self.image) 21 | 22 | def mousePressEvent(self, event): 23 | if event.button() == Qt.LeftButton: 24 | self.last_pos = event.pos() 25 | 26 | def mouseMoveEvent(self, event): 27 | if event.buttons() & Qt.LeftButton and self.last_pos: 28 | painter = QPainter(self.image) 29 | if self.current_tool == "pencil": 30 | painter.setPen(QPen(self.pencil_color, self.pen_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) 31 | elif self.current_tool == "eraser": 32 | painter.setCompositionMode(QPainter.CompositionMode_Source) 33 | painter.setPen(QPen(Qt.white, self.eraser_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) 34 | painter.drawLine(self.last_pos, event.pos()) 35 | painter.end() 36 | self.update() 37 | self.last_pos = event.pos() 38 | 39 | def mouseReleaseEvent(self, event): 40 | self.last_pos = None 41 | 42 | def set_tool(self, tool): 43 | self.current_tool = tool 44 | 45 | def set_pen_color(self, color): 46 | self.pencil_color = color 47 | 48 | def clear_canvas(self): 49 | self.image.fill(Qt.white) 50 | self.update() 51 | 52 | class PaintApp(QMainWindow): 53 | def __init__(self, theme,parent=None): 54 | super().__init__(parent) 55 | 56 | self.setWindowTitle("Paint Mode") 57 | self.setGeometry(100, 100, 1000, 600) 58 | self.setFixedSize(1000, 600) 59 | self.setWindowIcon(QIcon("images/logo.ico")) 60 | 61 | self.theme = theme 62 | self.pencil_color = QColor("black") 63 | self.current_tool = "pencil" 64 | self.init_ui() 65 | self.apply_theme() 66 | 67 | def init_ui(self): 68 | # Tool bar 69 | self.tool_bar = QFrame(self) 70 | self.tool_bar.setFixedHeight(50) 71 | self.setCentralWidget(self.tool_bar) 72 | 73 | tool_bar_layout = QHBoxLayout(self.tool_bar) 74 | 75 | # Buttons 76 | self.btn_pencil = QPushButton("Pencil", self.tool_bar) 77 | self.btn_pencil.clicked.connect(self.use_pencil) 78 | tool_bar_layout.addWidget(self.btn_pencil) 79 | 80 | self.btn_eraser = QPushButton("Eraser", self.tool_bar) 81 | self.btn_eraser.clicked.connect(self.use_eraser) 82 | tool_bar_layout.addWidget(self.btn_eraser) 83 | 84 | self.btn_clear = QPushButton("Clear", self.tool_bar) 85 | self.btn_clear.clicked.connect(self.clear_canvas) 86 | tool_bar_layout.addWidget(self.btn_clear) 87 | 88 | # Color buttons 89 | colors = ["black", "red", "yellow", "green", "blue"] 90 | for color in colors: 91 | color_btn = QPushButton("", self.tool_bar) 92 | color_btn.setStyleSheet(f"background-color: {color}; width: 20px; height: 20px;") 93 | color_btn.clicked.connect(lambda checked, c=color: self.change_pencil_color(c)) 94 | tool_bar_layout.addWidget(color_btn) 95 | 96 | # Tab buttons 97 | self.tab_buttons = [] 98 | for i in range(5): 99 | tab_button = QPushButton(f"#{i+1}", self.tool_bar) 100 | tab_button.setFixedSize(65, 25) 101 | tab_button.clicked.connect(lambda checked, idx=i: self.switch_tab(idx)) 102 | tool_bar_layout.addWidget(tab_button) 103 | self.tab_buttons.append(tab_button) 104 | 105 | # Canvas stack (to switch between tabs) 106 | self.canvas_stack = QStackedWidget(self) 107 | self.canvas_stack.setGeometry(0, 50, 1000, 550) 108 | 109 | # Create canvases for each tab 110 | self.canvases = [] 111 | for i in range(5): 112 | canvas_widget = CanvasWidget() 113 | self.canvas_stack.addWidget(canvas_widget) 114 | self.canvases.append(canvas_widget) 115 | 116 | self.switch_tab(0) # Show the first tab by default 117 | 118 | def apply_theme(self): 119 | # Apply theme colors to UI elements 120 | self.setStyleSheet(f"background-color: {self.theme['background_color']}; color: {self.theme['text_color']};") 121 | self.tool_bar.setStyleSheet(f"background-color: {self.theme['background_color']};") 122 | for button in [self.btn_pencil, self.btn_eraser, self.btn_clear]: 123 | button.setStyleSheet(f""" 124 | background-color: {self.theme['button_color']}; 125 | color: {self.theme['text_color']}; 126 | border: 1px solid {self.theme['border_color']}; 127 | """) 128 | for button in self.tab_buttons: 129 | button.setStyleSheet(f""" 130 | background-color: {self.theme['button_color']}; 131 | color: {self.theme['text_color']}; 132 | border: 1px solid {self.theme['border_color']}; 133 | """) 134 | 135 | def use_pencil(self): 136 | self.current_tool = "pencil" 137 | for canvas in self.canvases: 138 | canvas.set_tool(self.current_tool) 139 | self.canvases[self.canvas_stack.currentIndex()].setCursor(Qt.ArrowCursor) 140 | 141 | def use_eraser(self): 142 | self.current_tool = "eraser" 143 | for canvas in self.canvases: 144 | canvas.set_tool(self.current_tool) 145 | self.canvases[self.canvas_stack.currentIndex()].setCursor(Qt.CrossCursor) 146 | 147 | def clear_canvas(self): 148 | self.canvases[self.canvas_stack.currentIndex()].clear_canvas() 149 | 150 | def change_pencil_color(self, color): 151 | self.pencil_color = QColor(color) 152 | for canvas in self.canvases: 153 | canvas.set_pen_color(self.pencil_color) 154 | self.use_pencil() 155 | 156 | def switch_tab(self, index): 157 | self.canvas_stack.setCurrentIndex(index) 158 | self.update_tab_buttons() 159 | 160 | def update_tab_buttons(self): 161 | for i, button in enumerate(self.tab_buttons): 162 | if i == self.canvas_stack.currentIndex(): 163 | button.setStyleSheet(f"background-color: {self.theme['button_hover_color']}; color: {self.theme['text_color']};") 164 | else: 165 | button.setStyleSheet(f"background-color: {self.theme['button_color']}; color: {self.theme['text_color']};") 166 | -------------------------------------------------------------------------------- /src/GUI/profile.py: -------------------------------------------------------------------------------- 1 | import json 2 | from PySide6.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget, QMessageBox 3 | from PySide6.QtGui import QIcon, QFont, QColor 4 | from PySide6.QtCore import Qt 5 | 6 | class ProfileWindow(QWidget): 7 | def __init__(self, theme=None,parent=None): 8 | super().__init__() 9 | 10 | # Setează tema 11 | self.theme = theme 12 | 13 | self.setWindowTitle("CodeNimble - Profile") 14 | self.setWindowIcon(QIcon("images/logo.ico")) 15 | 16 | # Dimensiunile ferestrei 17 | self.setFixedSize(300, 200) 18 | 19 | # Setare layout 20 | layout = QVBoxLayout() 21 | 22 | # Creează eticheta și câmpul de text 23 | self.profile_name_label = QLabel("Profile name") 24 | self.profile_name_entry = QLineEdit() 25 | self.save_button = QPushButton("Save") 26 | 27 | # Setează fontul pentru widget-uri 28 | font = QFont("Consolas", 16) 29 | self.profile_name_label.setFont(font) 30 | self.profile_name_entry.setFont(font) 31 | self.save_button.setFont(font) 32 | 33 | # Conectează butonul la funcția de salvare 34 | self.save_button.clicked.connect(self.save_profile_name) 35 | layout.addStretch() 36 | # Adaugă widget-urile la layout 37 | layout.addWidget(self.profile_name_label,alignment=Qt.AlignCenter) 38 | layout.addWidget(self.profile_name_entry) 39 | layout.addWidget(self.save_button) 40 | layout.addStretch() 41 | # Setează layout-ul pentru fereastră 42 | self.load_config() 43 | self.apply_theme() 44 | self.setLayout(layout) 45 | 46 | def apply_theme(self): 47 | # Aplică culorile de fundal și text conform temei 48 | bg_color = QColor(self.theme.get("background_color", "#ffffff")) 49 | text_color = QColor(self.theme.get("text_color", "#000000")) 50 | button_color = QColor(self.theme.get("button_color", "#555555")) 51 | button_hover_color = QColor(self.theme.get("button_hover_color", "#777777")) 52 | entry_bg = QColor(self.theme.get("editor_background", "#454545")) 53 | entry_fg = QColor(self.theme.get("editor_foreground", "#ffffff")) 54 | 55 | # Setează culoarea de fundal pentru fereastră 56 | self.setStyleSheet(f"background-color: {bg_color.name()};") 57 | 58 | # Setează culoarea textului pentru etichete 59 | self.profile_name_label.setStyleSheet(f"color: {text_color.name()};") 60 | 61 | # Setează stilul pentru câmpul de text 62 | self.profile_name_entry.setStyleSheet(f""" 63 | background-color: {self.theme.get("editor_background")}; 64 | color: {self.theme.get("editor_foreground")}; 65 | border: 1px solid {self.theme.get("border_color")}; 66 | padding: 5px; 67 | """) 68 | 69 | # Setează stilul pentru hover (necesită un stil CSS pentru hover) 70 | self.save_button.setStyleSheet(f""" 71 | QPushButton {{ 72 | background-color: {self.theme.get("button_color")}; 73 | color: {self.theme.get("text_color")}; 74 | padding: 5px; 75 | border: 1px solid {self.theme.get('border_color')}; 76 | }} 77 | QPushButton:hover {{ 78 | background-color: {self.theme.get("button_hover_color")}; 79 | }} 80 | """) 81 | 82 | def load_config(self): 83 | # Încarcă configurația din config.json 84 | try: 85 | with open('config.json', 'r') as f: 86 | config = json.load(f) 87 | self.profile_name_entry.setText(config.get("profile_name", "")) 88 | except FileNotFoundError: 89 | pass 90 | 91 | def save_profile_name(self): 92 | profile_name = self.profile_name_entry.text() 93 | if not profile_name.strip(): 94 | # Afișează un messagebox dacă câmpul este gol 95 | QMessageBox.warning(self, "Input Error", "Please enter a profile name.") 96 | return 97 | 98 | # Salvează configurația în config.json 99 | config = {"profile_name": profile_name} 100 | try: 101 | with open('config.json', 'r+') as f: 102 | existing_config = json.load(f) 103 | existing_config.update(config) 104 | f.seek(0) 105 | f.write(json.dumps(existing_config, indent=4)) 106 | f.truncate() 107 | except FileNotFoundError: 108 | with open('config.json', 'w') as f: 109 | json.dump(config, f, indent=4) 110 | 111 | # Confirmare salvare 112 | QMessageBox.information(self, "Success", "Profile name saved successfully.") -------------------------------------------------------------------------------- /src/GUI/right_panel.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import ( 2 | QWidget, QLabel, QTextEdit, QPushButton, QGridLayout, QSizePolicy, QStackedWidget, QPlainTextEdit, QVBoxLayout, QLineEdit, QFrame, QHBoxLayout, QMessageBox, QComboBox, QSpacerItem, QCheckBox 3 | ) 4 | from cryptography.fernet import Fernet 5 | import base64 6 | from PySide6.QtCore import Qt 7 | from PySide6.QtGui import QFont 8 | import threading 9 | import json 10 | from GUI import diff 11 | from Server import server 12 | from Server import client 13 | from Tools import pbinfo 14 | from Tools import kilonova 15 | 16 | class RightPanel(QWidget): 17 | def __init__(self, theme,win, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | self.win = win 20 | self.theme = theme 21 | self.server = None 22 | self.client = None 23 | # Crearea layout-ului principal 24 | self.main_layout = QVBoxLayout(self) 25 | self.setLayout(self.main_layout) 26 | 27 | self.functions = QComboBox(self) 28 | self.functions.addItems(["Testing","Submit code", "Documentation", "Server", "Settings"]) 29 | self.functions.setItemText 30 | self.functions.currentIndexChanged.connect(self.toggle_tabs) 31 | self.main_layout.addWidget(self.functions) 32 | 33 | # Crearea QStackedWidget pentru a comuta între tab-uri 34 | self.stacked_widget = QStackedWidget() 35 | self.main_layout.addWidget(self.stacked_widget) 36 | 37 | 38 | 39 | 40 | # Testing tab 41 | self.testing_widget = QWidget() 42 | self.testing_layout = QGridLayout(self.testing_widget) 43 | self.testing_widget.setLayout(self.testing_layout) 44 | 45 | self.layout = self.testing_layout 46 | self.layout.setContentsMargins(10, 10, 10, 10) 47 | self.layout.setSpacing(10) 48 | 49 | self.layout.setColumnStretch(0, 1) 50 | self.layout.setRowStretch(0, 0) # Pentru combo box 51 | self.layout.setRowStretch(1, 0) # Pentru label input 52 | self.layout.setRowStretch(2, 1) # Pentru input box 53 | self.layout.setRowStretch(3, 0) # Pentru label output 54 | self.layout.setRowStretch(4, 1) # Pentru output box 55 | self.layout.setRowStretch(5, 0) # Pentru label expected 56 | self.layout.setRowStretch(6, 1) # Pentru expected box 57 | self.layout.setRowStretch(7, 0) # Pentru butonul diff 58 | self.layout.setRowStretch(8, 0) # Pentru butonul fetch 59 | 60 | # Dicționar pentru stocarea test case-urilor 61 | self.test_cases = { 62 | "Test Case 1": {"input": "", "output": "", "expected": ""}, 63 | "Test Case 2": {"input": "", "output": "", "expected": ""}, 64 | "Test Case 3": {"input": "", "output": "", "expected": ""}, 65 | "Test Case 4": {"input": "", "output": "", "expected": ""}, 66 | "Test Case 5": {"input": "", "output": "", "expected": ""} 67 | } 68 | 69 | # Combo box pentru selectarea test case-ului 70 | self.test_selector = QComboBox(self) 71 | self.test_selector.addItems(list(self.test_cases.keys())) 72 | self.test_selector.currentTextChanged.connect(self.change_test_case) 73 | self.layout.addWidget(self.test_selector, 0, 0) 74 | 75 | # Label-ul pentru input 76 | self.input_label = QLabel("Input", self) 77 | self.layout.addWidget(self.input_label, 1, 0, Qt.AlignHCenter) 78 | 79 | # Textbox pentru input 80 | self.input_box = QTextEdit(self) 81 | self.input_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 82 | self.input_box.textChanged.connect(lambda: self.save_current_state("input")) 83 | self.layout.addWidget(self.input_box, 2, 0) 84 | 85 | # Label-ul pentru output 86 | self.output_label = QLabel("Output", self) 87 | self.layout.addWidget(self.output_label, 3, 0, Qt.AlignHCenter) 88 | 89 | # Textbox pentru output 90 | self.output_box = QTextEdit(self) 91 | self.output_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 92 | self.output_box.textChanged.connect(lambda: self.save_current_state("output")) 93 | self.layout.addWidget(self.output_box, 4, 0) 94 | 95 | # Label-ul pentru expected output 96 | self.expected_label = QLabel("Expected Output", self) 97 | self.layout.addWidget(self.expected_label, 5, 0, Qt.AlignHCenter) 98 | 99 | # Textbox pentru expected output 100 | self.expected_box = QTextEdit(self) 101 | self.expected_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 102 | self.expected_box.textChanged.connect(lambda: self.save_current_state("expected")) 103 | self.layout.addWidget(self.expected_box, 6, 0) 104 | 105 | # Butonul pentru comparare 106 | self.diff = QPushButton("Output comparator", self, clicked=self.diff_core) 107 | self.diff.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 108 | self.layout.addWidget(self.diff, 7, 0) 109 | 110 | # pre_input_button 111 | self.pre_input_button = QPushButton("Run with pre-input", self, clicked=self.win.pre_input_run_core) 112 | self.pre_input_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 113 | self.layout.addWidget(self.pre_input_button, 8, 0) 114 | 115 | # Adăugăm widget-ul „Testing" în stacked_widget 116 | self.stacked_widget.addWidget(self.testing_widget) 117 | 118 | 119 | 120 | 121 | # Crearea tab-ului „Server” 122 | self.server_widget = QWidget() 123 | self.server_layout = QVBoxLayout(self.server_widget) 124 | self.server_widget.setLayout(self.server_layout) 125 | # Password 126 | self.password_entry = QLineEdit(self) 127 | self.password_entry.setPlaceholderText("Password") 128 | self.server_layout.addWidget(self.password_entry) 129 | # Start server 130 | self.start_server = QPushButton("Start server", self) 131 | self.server_layout.addWidget(self.start_server) 132 | self.start_server.clicked.connect(self.start_server_option) 133 | # Crearea unui layout pentru butoanele „Connect” și „Disconnect” 134 | self.connect_buttons = QWidget() 135 | self.connect_buttons_layout = QHBoxLayout(self.connect_buttons) 136 | self.connect_button = QPushButton("Connect", self) 137 | self.connect_button.clicked.connect(self.connect_to_server) 138 | self.disconnect_button = QPushButton("Disconnect", self) 139 | self.disconnect_button.clicked.connect(self.disconnect_from_server) 140 | 141 | self.connect_buttons_layout.addWidget(self.connect_button) 142 | self.connect_buttons_layout.addWidget(self.disconnect_button) 143 | 144 | self.server_layout.addWidget(self.connect_buttons) 145 | 146 | # QPlainTextEdit pentru Server 147 | self.server_textbox = QPlainTextEdit(self) 148 | self.server_textbox.setReadOnly(True) 149 | self.server_textbox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 150 | self.server_layout.addWidget(self.server_textbox) 151 | 152 | # Entry între butonul „Send” și „QPlainTextEdit” 153 | self.entry = QLineEdit(self) 154 | self.server_layout.addWidget(self.entry) 155 | 156 | # Butonul „Send” 157 | self.send_button = QPushButton("Send", self) 158 | self.send_button.clicked.connect(self.send_message) 159 | self.send_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 160 | self.server_layout.addWidget(self.send_button) 161 | 162 | # Adăugăm widget-ul „Server” în stacked_widget 163 | self.stacked_widget.addWidget(self.server_widget) 164 | 165 | # Documentation 166 | self.documentation_widget = QWidget() 167 | self.documenation_layout = QVBoxLayout(self.documentation_widget) 168 | self.documentation_widget.setLayout(self.documenation_layout) 169 | 170 | # Text box pentru documentatie 171 | self.documentation_textbox = QTextEdit(self) 172 | self.documentation_textbox.setReadOnly(True) # Setăm să fie doar pentru citire 173 | self.documentation_textbox.setFont(QFont("Consolas", 12)) 174 | self.documentation_textbox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 175 | self.documentation_textbox.setMinimumHeight(200) # Poți ajusta valoarea dacă este necesar 176 | 177 | # Setăm textul complet al documentației 178 | self.documentation_textbox.setHtml(""" 179 |

Documentation

180 |

Autocomplete keywords:

181 |

Type one of the following keywords with CAPS then hit ENTER: CPP, FOR, INT, IF, WHILE.

182 |

Tip: You can change the default counter of the FOR loop using FOR-your new counter.

183 | """) 184 | 185 | self.documenation_layout.addWidget(self.documentation_textbox) 186 | 187 | self.stacked_widget.addWidget(self.documentation_widget) 188 | 189 | # Submit code 190 | 191 | self.submit_code = QWidget() 192 | self.submit_code_layout = QVBoxLayout(self.submit_code) 193 | self.submit_code.setLayout(self.submit_code_layout) 194 | 195 | self.submit_platform = QComboBox(self) 196 | self.submit_platform.addItems(["Kilonova", "Pbinfo"]) 197 | self.submit_platform.setCurrentText("Kilonova") 198 | self.submit_code_layout.addWidget(self.submit_platform, alignment=Qt.AlignTop) 199 | self.username = QLineEdit(self) 200 | self.username.setPlaceholderText("Username") 201 | self.submit_code_layout.addWidget(self.username, alignment=Qt.AlignTop) 202 | self.password = QLineEdit(self) 203 | self.password.setPlaceholderText("Password") 204 | self.password.setEchoMode(QLineEdit.Password) 205 | self.submit_code_layout.addWidget(self.password, alignment=Qt.AlignTop) 206 | self.problem_id = QLineEdit(self) 207 | self.problem_id.setPlaceholderText("Problem ID") 208 | self.submit_code_layout.addWidget(self.problem_id, alignment=Qt.AlignTop) 209 | self.submit_button = QPushButton("Submit", self) 210 | self.submit_button.clicked.connect(self.submit_core) 211 | self.submit_code_layout.addWidget(self.submit_button, alignment=Qt.AlignTop) 212 | 213 | # From config 214 | with open('app_data_/data.json', 'r') as file: 215 | self.credits = json.load(file) 216 | 217 | self.toggle_platforms() 218 | self.submit_platform.currentIndexChanged.connect(self.toggle_platforms) 219 | 220 | spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 221 | self.submit_code_layout.addItem(spacer) 222 | 223 | self.result_label = QLabel("Score: ", self) 224 | self.result_label.setFont(QFont("Consolas", 14, QFont.Bold)) 225 | self.submit_code_layout.addWidget(self.result_label, alignment=Qt.AlignBottom | Qt.AlignLeft) 226 | self.source_id_label = QLabel("Solution ID: ", self) 227 | self.source_id_label.setFont(QFont("Consolas", 14, QFont.Bold)) 228 | self.submit_code_layout.addWidget(self.source_id_label, alignment=Qt.AlignBottom | Qt.AlignLeft) 229 | 230 | self.stacked_widget.addWidget(self.submit_code) 231 | 232 | # Settings tab 233 | with open('config.json', 'r') as file: 234 | self.config = json.load(file) 235 | 236 | 237 | self.settings = QWidget() 238 | self.settings_layout = QVBoxLayout(self.settings) 239 | self.settings.setLayout(self.settings_layout) 240 | val = self.config['startup']['pre_template'] 241 | self.pre_template = QCheckBox("Startup template", self) 242 | if val == "0": 243 | self.pre_template.setChecked(True) 244 | self.settings_layout.addWidget(self.pre_template, alignment=Qt.AlignTop) 245 | self.pre_template_items = QComboBox(self) 246 | self.pre_template_items.addItems(["C++", "C++ Competitive", "C", "Java", "Html"]) 247 | self.settings_layout.addWidget(self.pre_template_items, alignment=Qt.AlignTop) 248 | 249 | spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 250 | self.settings_layout.addItem(spacer) 251 | 252 | self.save_button = QPushButton("Save", self) 253 | self.save_button.clicked.connect(self.save_settings) 254 | self.settings_layout.addWidget(self.save_button, alignment=Qt.AlignBottom) 255 | 256 | self.stacked_widget.addWidget(self.settings) 257 | 258 | # Aplicarea temei 259 | self.apply_theme(self.theme) 260 | self.user_name = self.config.get('profile_name') 261 | 262 | def safe_key(self, key: bytes, filename: str) -> bytes: 263 | with open(filename, 'wb') as file: 264 | file.write(key) 265 | 266 | def load_key(self, filename: str) -> bytes: 267 | with open(filename, 'rb') as file: 268 | return file.read() 269 | 270 | def toggle_platforms(self): 271 | try: 272 | key = self.load_key("app_data_/secret.key") 273 | except FileNotFoundError: 274 | key = Fernet.generate_key() 275 | self.safe_key(key, "app_data_/secret.key") 276 | 277 | fernet = Fernet(key) 278 | if self.submit_platform.currentIndex() == 1: 279 | if self.credits["pbinfo"].get("username") and self.credits["pbinfo"].get("password"): 280 | self.username.setText( 281 | fernet.decrypt(base64.urlsafe_b64decode(self.credits["pbinfo"]["username"])).decode('utf-8') 282 | ) 283 | self.password.setText( 284 | fernet.decrypt(base64.urlsafe_b64decode(self.credits["pbinfo"]["password"])).decode('utf-8') 285 | ) 286 | else: 287 | self.username.setText("") 288 | self.password.setText("") 289 | elif self.submit_platform.currentIndex() == 0: 290 | if self.credits["kilonova"].get("username") and self.credits["kilonova"].get("password"): 291 | self.username.setText( 292 | fernet.decrypt(base64.urlsafe_b64decode(self.credits["kilonova"]["username"])).decode('utf-8') 293 | ) 294 | self.password.setText( 295 | fernet.decrypt(base64.urlsafe_b64decode(self.credits["kilonova"]["password"])).decode('utf-8') 296 | ) 297 | else: 298 | self.username.setText("") 299 | self.password.setText("") 300 | 301 | 302 | def save_current_state(self, field): 303 | current_test = self.test_selector.currentText() 304 | if field == "input": 305 | self.test_cases[current_test]["input"] = self.input_box.toPlainText() 306 | elif field == "output": 307 | self.test_cases[current_test]["output"] = self.output_box.toPlainText() 308 | elif field == "expected": 309 | self.test_cases[current_test]["expected"] = self.expected_box.toPlainText() 310 | 311 | def change_test_case(self, test_case): 312 | # Încărcăm datele salvate pentru test case-ul selectat 313 | self.input_box.setText(self.test_cases[test_case]["input"]) 314 | self.output_box.setText(self.test_cases[test_case]["output"]) 315 | self.expected_box.setText(self.test_cases[test_case]["expected"]) 316 | 317 | def toggle_tabs(self): 318 | 319 | curr_funct = self.functions.currentIndex() 320 | if curr_funct == 0: 321 | self.show_testing_tab() 322 | elif curr_funct == 1: 323 | self.show_submit_pbinfo_tab() 324 | elif curr_funct == 2: 325 | self.show_documentation_tab() 326 | elif curr_funct == 3: 327 | self.show_server_tab() 328 | elif curr_funct == 4: 329 | self.show_settings_tab() 330 | 331 | def start_server_option(self): 332 | if self.password_entry.text() == "": 333 | QMessageBox.information(self, "Info", "You need to enter the password!") 334 | return 335 | self.server = server.ServerManager(password=self.password_entry.text()) 336 | 337 | def start_server_thread(): 338 | try: 339 | self.server.start_server() 340 | except Exception as e: 341 | print(f"Eroare la pornirea serverului: {e}") 342 | return 343 | 344 | # Pornim serverul pe un thread separat 345 | server_thread = threading.Thread(target=start_server_thread) 346 | server_thread.daemon = True # Asigură-te că thread-ul se închide odată cu aplicația 347 | server_thread.start() 348 | 349 | if not self.client: 350 | self.client = client.ClientManager(name=self.user_name, password=self.password_entry.text(), gui=self) 351 | self.client.connect_to_server() 352 | self.win.status_bar.update_server("connected") 353 | 354 | 355 | def connect_to_server(self): 356 | if self.password_entry.text() == "": 357 | QMessageBox.information(self, "Info", "You need to enter the password!") 358 | return 359 | if self.client: 360 | QMessageBox.information(self,"Info", "Already connected to the server.") 361 | else: 362 | self.client = client.ClientManager(name=self.user_name, password=self.password_entry.text(), gui=self) 363 | self.win.status_bar.update_server("connected") 364 | if not self.client.connect_to_server(): 365 | QMessageBox.critical(self, "Error", "Server error or wrong password!") 366 | self.client = None 367 | self.win.status_bar.update_server("none") 368 | 369 | 370 | def disconnect_from_server(self): 371 | if self.client: 372 | self.client.disconnect() 373 | self.client = None 374 | self.win.status_bar.update_server("none") 375 | 376 | def send_message(self): 377 | message = self.entry.text() 378 | if message and self.client: 379 | self.client.send_message(message) 380 | self.entry.clear() 381 | self.append_to_textbox(f"You: {message}") 382 | 383 | def submit_core(self): 384 | if self.username.text().strip() == "": 385 | self.win.status_bar.toggle_inbox_icon("Submit tools - Username is empty!", "orange") 386 | return 387 | if self.password.text().strip() == "": 388 | self.win.status_bar.toggle_inbox_icon("Submit tools - Password is empty!", "orange") 389 | return 390 | if self.problem_id.text().strip() == "": 391 | self.win.status_bar.toggle_inbox_icon("Submit tools - ID is empty!", "orange") 392 | return 393 | if self.win.editor.toPlainText().strip() == "": 394 | self.win.status_bar.toggle_inbox_icon("Submit tools - Source is empty!", "orange") 395 | return 396 | 397 | if self.submit_platform.currentIndex() == 0: 398 | kilonova.login_and_submit(self, self.username.text().strip(), self.password.text().strip(), self.win.file_manager.get_opened_filename(),self.problem_id.text().strip()) 399 | elif self.submit_platform.currentIndex() == 1: 400 | self.win.status_bar.toggle_inbox_icon("Pbinfo tools - Indisponible at the moment!", "red") 401 | #self.submit_interface = pbinfo.PbinfoInterface(self.source_id_label, self.result_label) 402 | #self.submit_interface.unit(self.username.text().strip(), self.password.text().strip(), self.problem_id.text().strip(), self.win.editor.toPlainText().strip()) 403 | 404 | def save_settings(self): 405 | self.config['editor_font_size'] = self.editor_font.text().strip() 406 | if self.pre_template.isChecked(): 407 | self.config['startup']['pre_template'] = "0" 408 | self.config['startup']['template'] = self.pre_template_items.currentText().strip() 409 | else: 410 | self.config['startup']['pre_template'] = "1" 411 | self.config['startup']['template'] = "" 412 | with open('config.json', 'w') as file: 413 | json.dump(self.config, file,indent=4) 414 | self.win.re_zoom(self.editor_font.text().strip()) 415 | self.win.status_bar.toggle_inbox_icon("Settings saved!") 416 | 417 | def append_to_textbox(self, message): 418 | self.server_textbox.appendPlainText(message) 419 | 420 | def show_settings_tab(self): 421 | self.stacked_widget.setCurrentWidget(self.settings) 422 | 423 | def show_submit_pbinfo_tab(self): 424 | self.stacked_widget.setCurrentWidget(self.submit_code) 425 | 426 | def show_testing_tab(self): 427 | self.stacked_widget.setCurrentWidget(self.testing_widget) 428 | 429 | def show_server_tab(self): 430 | self.stacked_widget.setCurrentWidget(self.server_widget) 431 | 432 | def show_documentation_tab(self): 433 | self.stacked_widget.setCurrentWidget(self.documentation_widget) 434 | 435 | def diff_core(self): 436 | self.diff_win = diff.OutputComparator(self, self.theme) 437 | self.diff_win.show() 438 | 439 | def apply_theme(self, theme): 440 | self.theme = theme 441 | self.setStyleSheet(f""" 442 | background-color: {theme.get("background_color")}; 443 | color: {theme.get("text_color")}; 444 | """) 445 | self.functions.setStyleSheet(f""" 446 | color: {theme.get('text_color')}; 447 | background-color: {theme.get('editor_background')}; 448 | border: 1px solid {theme.get('border_color')}; 449 | font-size: 14px; 450 | padding: 4px; 451 | """) 452 | self.submit_platform.setStyleSheet(f""" 453 | color: {theme.get('text_color')}; 454 | background-color: {theme.get('editor_background')}; 455 | border: 1px solid {theme.get('border_color')}; 456 | font-size: 14px; 457 | padding: 4px; 458 | """) 459 | self.input_label.setStyleSheet(f"color: {theme.get('text_color')};") 460 | self.output_label.setStyleSheet(f"color: {theme.get('text_color')};") 461 | self.expected_label.setStyleSheet(f"color: {theme.get('text_color')};") 462 | self.input_box.setStyleSheet(f""" 463 | background-color: {theme.get("editor_background")}; 464 | color: {theme.get("editor_foreground")}; 465 | border: 1px solid {theme.get("border_color")}; 466 | padding: 5px; 467 | """) 468 | self.output_box.setStyleSheet(f""" 469 | background-color: {theme.get("editor_background")}; 470 | color: {theme.get("editor_foreground")}; 471 | border: 1px solid {theme.get("border_color")}; 472 | padding: 5px; 473 | """) 474 | self.server_textbox.setStyleSheet(f""" 475 | background-color: {theme.get("editor_background")}; 476 | """) 477 | self.expected_box.setStyleSheet(f""" 478 | background-color: {theme.get("editor_background")}; 479 | color: {theme.get("editor_foreground")}; 480 | border: 1px solid {theme.get("border_color")}; 481 | padding: 5px; 482 | """) 483 | self.entry.setStyleSheet(f""" 484 | background-color: {theme.get("editor_background")}; 485 | color: {theme.get("editor_foreground")}; 486 | border: 1px solid {theme.get("border_color")}; 487 | padding: 5px; 488 | """) 489 | self.pre_template_items.setStyleSheet(f""" 490 | background-color: {theme.get("editor_background")}; 491 | color: {theme.get("editor_foreground")}; 492 | border: 1px solid {theme.get("border_color")}; 493 | padding: 5px; 494 | """) 495 | self.pre_template.setStyleSheet(f""" 496 | QCheckBox {{ 497 | color: {theme.get("text_color")}; /* Culoarea textului */ 498 | font-size: 16px; /* Dimensiunea fontului */ 499 | }} 500 | QCheckBox::indicator {{ 501 | border: 2px solid {theme.get("border_color")}; /* Bordura casetei de bifare */ 502 | width: 16px; /* Lățimea casetei */ 503 | height: 16px; /* Înălțimea casetei */ 504 | 505 | }} 506 | QCheckBox::indicator:checked {{ 507 | background-color: {theme.get("button_hover_color")}; /* Culoarea de fundal când este bifată */ 508 | border: 2px solid {theme.get("border_color")}; /* Bordura când este bifată */ 509 | }} 510 | 511 | """) 512 | 513 | self.password_entry.setStyleSheet(f""" 514 | background-color: {theme.get("editor_background")}; 515 | color: {theme.get("editor_foreground")}; 516 | border: 1px solid {theme.get("border_color")}; 517 | padding: 5px; 518 | """) 519 | self.test_selector.setStyleSheet(f""" 520 | background-color: {theme.get("editor_background")}; 521 | color: {theme.get("editor_foreground")}; 522 | border: 1px solid {theme.get("border_color")}; 523 | padding: 5px; 524 | """) 525 | 526 | self.username.setStyleSheet(f""" 527 | background-color: {theme.get("editor_background")}; 528 | color: {theme.get("editor_foreground")}; 529 | border: 1px solid {theme.get("border_color")}; 530 | padding: 5px; 531 | """) 532 | 533 | self.password.setStyleSheet(f""" 534 | background-color: {theme.get("editor_background")}; 535 | color: {theme.get("editor_foreground")}; 536 | border: 1px solid {theme.get("border_color")}; 537 | padding: 5px; 538 | """) 539 | 540 | self.problem_id.setStyleSheet(f""" 541 | background-color: {theme.get("editor_background")}; 542 | color: {theme.get("editor_foreground")}; 543 | border: 1px solid {theme.get("border_color")}; 544 | padding: 5px; 545 | """) 546 | 547 | button_style = f""" 548 | QPushButton {{ 549 | background-color: {theme.get("button_color")}; 550 | color: {theme.get("text_color")}; 551 | padding: 5px; 552 | border: 1px solid {theme.get('border_color')}; 553 | }} 554 | QPushButton:hover {{ 555 | background-color: {theme.get("button_hover_color")}; 556 | }} 557 | """ 558 | self.diff.setStyleSheet(button_style) 559 | self.pre_input_button.setStyleSheet(button_style) 560 | self.send_button.setStyleSheet(button_style) 561 | self.connect_button.setStyleSheet(button_style) 562 | self.disconnect_button.setStyleSheet(button_style) 563 | self.start_server.setStyleSheet(button_style) 564 | self.submit_button.setStyleSheet(button_style) 565 | self.save_button.setStyleSheet(button_style) 566 | -------------------------------------------------------------------------------- /src/GUI/status_bar.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QFrame, QLabel, QHBoxLayout, QWidget, QVBoxLayout 2 | from PySide6.QtGui import QPixmap, QMouseEvent, QFont, QEnterEvent 3 | from PySide6.QtCore import Qt, QTimer 4 | from Tools import scrap 5 | from datetime import datetime, timedelta 6 | from datetime import datetime 7 | 8 | class StatusBar(QFrame): 9 | def __init__(self, text_widget, theme, main): 10 | super().__init__() 11 | self.theme = theme 12 | self.main = main 13 | self.text_widget = text_widget 14 | self.current_version = "2.0" 15 | self.latest_version = scrap.get_latest_version_from_github("HojdaAdelin", "CodeNimble") 16 | self.text = "" 17 | self.hv_color = theme.get("status_bar_hover") 18 | self.based_color = theme.get("status_bar_background") 19 | self.ctn_words_color = theme.get("ctn_words") 20 | self.num_lines = 0 21 | self.num_words = 0 22 | self.start_time = None 23 | self.running = False 24 | self.timer_paused = False 25 | self.elapsed_time = timedelta(0) 26 | self.msg_log = [] 27 | self.msg_colors = [] 28 | self.setStyleSheet(f"background-color: {self.based_color};") 29 | self.setup_ui() 30 | self.apply_theme(theme) 31 | self.server_icon.mousePressEvent = self.on_inbox_icon_click 32 | 33 | def setup_ui(self): 34 | font_size = 12 35 | self.font = QFont("Arial", font_size) 36 | 37 | self.separator1 = QFrame(self) 38 | self.separator1.setFrameShape(QFrame.VLine) 39 | self.separator2 = QFrame(self) 40 | self.separator2.setFrameShape(QFrame.VLine) 41 | self.separator3 = QFrame(self) 42 | self.separator3.setFrameShape(QFrame.VLine) 43 | 44 | self.new_version_label = QLabel("New version available", self) 45 | self.new_version_label.setFont(self.font) 46 | self.new_version_label.setStyleSheet("background-color: green; color: black;") 47 | 48 | self.num_stats_label = QLabel("Lines: 0, Words: 0 ", self) 49 | self.num_stats_label.setFont(self.font) 50 | self.num_stats_label.setAlignment(Qt.AlignVCenter | Qt.AlignRight) 51 | self.num_stats_label.setStyleSheet(f"color: {self.theme['text_color']};") 52 | 53 | self.status_label = QLabel(self.text, self) 54 | self.status_label.setFont(self.font) 55 | self.status_label.setAlignment(Qt.AlignVCenter | Qt.AlignRight) 56 | self.status_label.setStyleSheet(f"color: {self.theme['text_color']};") 57 | 58 | # Setup server status label 59 | self.server_status = QLabel("Server: none", self) 60 | self.server_status.setFont(self.font) 61 | self.server_status.setAlignment(Qt.AlignVCenter | Qt.AlignRight) 62 | self.server_status.setStyleSheet(f"color: {self.theme['text_color']};") 63 | 64 | # Create server icon label and set the default icon (bell-default.png) 65 | self.server_icon = QLabel(self) 66 | default_icon_path = "images/bell-default.png" # Path to bell-default image 67 | pixmap = QPixmap(default_icon_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation) 68 | self.server_icon.setPixmap(pixmap) 69 | self.server_icon.setStyleSheet(f"background-color: {self.based_color};") 70 | 71 | image_path = "images/run.png" # Actualizează calea dacă este necesar 72 | pixmap = QPixmap(image_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation) 73 | self.run_img = QLabel(self) 74 | self.run_img.setPixmap(pixmap) 75 | self.run_img.setStyleSheet(f"background-color: {self.based_color};") 76 | self.run_img.setCursor(Qt.PointingHandCursor) 77 | 78 | # Timer setup (move to the left) 79 | self.timer = QLabel("00:00:00", self) 80 | self.timer.setFont(self.font) 81 | self.timer.setStyleSheet(f"color: {self.theme['text_color']};") 82 | self.timer.setCursor(Qt.PointingHandCursor) 83 | 84 | # Layout setup 85 | layout = QHBoxLayout(self) 86 | layout.addWidget(self.new_version_label) 87 | layout.addWidget(self.timer) 88 | layout.addStretch() 89 | layout.addWidget(self.server_icon) 90 | layout.addWidget(self.separator3) 91 | layout.addWidget(self.server_status) 92 | layout.addWidget(self.separator2) 93 | layout.addWidget(self.run_img) 94 | layout.addWidget(self.separator1) 95 | layout.addWidget(self.num_stats_label) 96 | layout.addWidget(self.status_label) 97 | layout.setContentsMargins(5, 2, 5, 2) # Adaugă margini pentru a evita lipirea elementelor de marginea barei 98 | self.setLayout(layout) 99 | 100 | # Connections 101 | self.run_img.mousePressEvent = self.on_run_click # Handle click event for run image 102 | self.run_img.enterEvent = self.on_run_hover_enter # Handle hover enter for run image 103 | self.run_img.leaveEvent = self.on_run_hover_leave # Handle hover leave for run image 104 | 105 | self.timer.mousePressEvent = self.on_timer_click # Handle click event for timer 106 | self.timer.enterEvent = self.on_timer_hover_enter # Handle hover enter for timer 107 | self.timer.leaveEvent = self.on_timer_hover_leave # Handle hover leave for timer 108 | 109 | # Version check 110 | if self.latest_version > self.current_version and isinstance(self.latest_version, int): 111 | self.new_version_label.setVisible(True) 112 | else: 113 | self.new_version_label.setVisible(False) 114 | 115 | def apply_theme(self, theme): 116 | self.setStyleSheet(f"background-color: {theme['status_bar_background']};") 117 | self.separator1.setStyleSheet(f"color: {theme['text_color']}; width: 2px;") 118 | self.separator2.setStyleSheet(f"color: {theme['text_color']}; width: 2px;") 119 | self.separator3.setStyleSheet(f"color: {theme['text_color']}; width: 2px;") 120 | self.status_label.setStyleSheet(f"color: {theme['text_color']};") 121 | self.num_stats_label.setStyleSheet(f"color: {theme['text_color']};") 122 | self.server_status.setStyleSheet(f"color: {theme['text_color']};") 123 | self.hv_color = theme.get("button_hover_color", "#4d4d4d") 124 | self.based_color = theme["status_bar_background"] 125 | self.run_img.setStyleSheet(f"background-color: {self.based_color};") 126 | self.timer.setStyleSheet(f"background-color: {self.based_color};") 127 | self.server_icon.setStyleSheet(f"background-color: {self.based_color};") 128 | 129 | def on_inbox_icon_click(self, event: QMouseEvent): 130 | if event.button() == Qt.LeftButton: # Verifică dacă s-a dat click stânga 131 | default_icon_path = "images/bell-default.png" 132 | new_pixmap = QPixmap(default_icon_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation) 133 | self.server_icon.setPixmap(new_pixmap) 134 | self.show_inbox_popup() 135 | 136 | def time_sec_h(self): 137 | acum = datetime.now() 138 | ora = acum.hour 139 | minut = acum.minute 140 | secunda = acum.second 141 | return ora, minut, secunda 142 | 143 | def show_inbox_popup(self): 144 | # Creează un QWidget care va afișa lista de mesaje 145 | if hasattr(self, 'inbox_popup') and self.inbox_popup.isVisible(): 146 | return 147 | self.inbox_popup = QWidget(self.main) 148 | self.inbox_popup.setStyleSheet( 149 | "background-color: rgba(0, 0, 0, 80); color: white; border-radius: 5px; padding: 5px;" 150 | ) 151 | 152 | # Creează un layout vertical pentru a afișa toate mesajele 153 | layout = QVBoxLayout(self.inbox_popup) 154 | 155 | # Adaugă fiecare mesaj din msg_log într-un QLabel 156 | for i, message in enumerate(reversed(self.msg_log)): # Afișează mesajele în ordinea inversă 157 | msg_label = QLabel(message) 158 | msg_label.setFont(self.font) 159 | msg_label.setStyleSheet(f"color: {self.msg_colors[-(i+1)]};") # Aplica culoarea corespunzătoare 160 | layout.addWidget(msg_label) 161 | 162 | # Ajustează dimensiunea popup-ului în funcție de conținut 163 | self.inbox_popup.adjustSize() 164 | 165 | # Calculează coordonatele pentru a-l poziționa deasupra iconiței 166 | global_pos = self.server_icon.mapToGlobal(self.server_icon.rect().center()) 167 | main_pos = self.main.mapFromGlobal(global_pos) 168 | popup_x = main_pos.x() - self.inbox_popup.width() // 2 169 | popup_y = main_pos.y() - self.inbox_popup.height() - 10 170 | 171 | self.inbox_popup.move(popup_x, popup_y) 172 | 173 | # Afișează și ridică popup-ul deasupra altor elemente 174 | self.inbox_popup.raise_() 175 | self.inbox_popup.show() 176 | 177 | # Focus pe popup, îl ascunde când utilizatorul dă click pe altceva 178 | self.inbox_popup.setFocus() 179 | self.inbox_popup.focusOutEvent = self.hide_inbox_popup 180 | 181 | def hide_inbox_popup(self, event): 182 | # Ascunde popup-ul când acesta pierde focusul 183 | self.inbox_popup.hide() 184 | 185 | def toggle_inbox_icon(self, text, color="#FFFFFF"): 186 | update_icon_path = "images/bell-update.png" 187 | new_pixmap = QPixmap(update_icon_path).scaled(20, 20, Qt.KeepAspectRatio, Qt.SmoothTransformation) 188 | self.server_icon.setPixmap(new_pixmap) 189 | self.popup_inbox(text, color) 190 | 191 | def popup_inbox(self, text, color): 192 | hour,minute, sec = self.time_sec_h() 193 | msg = f"[{hour}:{minute:02}:{sec:02}] {text}" 194 | # Creează un QLabel care va funcționa ca popup pe fereastra principală (self.main) 195 | self.popup_label = QLabel(msg, self.main) 196 | self.popup_label.setFont(self.font) 197 | self.popup_label.setStyleSheet( 198 | f"background-color: rgba(0, 0, 0, 80); color: {color}; border-radius: 5px; padding: 5px;" 199 | ) 200 | 201 | # Ajustează dimensiunea în funcție de text 202 | self.popup_label.adjustSize() 203 | 204 | # Calculează coordonatele pentru a-l poziționa deasupra iconiței, raportat la fereastra principală 205 | global_pos = self.server_icon.mapToGlobal(self.server_icon.rect().center()) 206 | main_pos = self.main.mapFromGlobal(global_pos) 207 | popup_x = main_pos.x() - self.popup_label.width() // 2 208 | popup_y = main_pos.y() - self.popup_label.height() - 10 # 5 pixeli deasupra iconiței 209 | 210 | self.popup_label.move(popup_x, popup_y) 211 | 212 | # Afișează și ridică popup-ul deasupra altor elemente 213 | self.popup_label.raise_() 214 | self.popup_label.show() 215 | self.msg_log.append(msg) 216 | self.msg_colors.append(color) 217 | 218 | # Folosește un timer pentru a ascunde popup-ul după 3 secunde 219 | QTimer.singleShot(2000, self.popup_label.hide) 220 | 221 | def start_timer(self, event): 222 | if not self.running: 223 | self.start_time = datetime.now() - self.elapsed_time 224 | self.running = True 225 | self.update_timer() 226 | else: 227 | self.timer_paused = not self.timer_paused 228 | if self.timer_paused: 229 | self.elapsed_time = datetime.now() - self.start_time 230 | else: 231 | self.start_time = datetime.now() - self.elapsed_time 232 | self.update_timer() 233 | 234 | def update_timer(self): 235 | if self.running and not self.timer_paused: 236 | elapsed_time = datetime.now() - self.start_time 237 | hours, remainder = divmod(elapsed_time.seconds, 3600) 238 | minutes, seconds = divmod(remainder, 60) 239 | time_str = f"{hours:02}:{minutes:02}:{seconds:02}" 240 | self.timer.setText(time_str) 241 | QTimer.singleShot(1000, self.update_timer) 242 | 243 | def on_run_hover_enter(self, event: QEnterEvent): 244 | self.run_img.setStyleSheet(f"background-color: {self.hv_color};") 245 | 246 | def on_run_hover_leave(self, event: QMouseEvent): 247 | self.run_img.setStyleSheet(f"background-color: {self.based_color};") 248 | 249 | def on_timer_hover_enter(self, event: QEnterEvent): 250 | self.timer.setStyleSheet(f"background-color: {self.hv_color};") 251 | 252 | def on_timer_hover_leave(self, event: QMouseEvent): 253 | self.timer.setStyleSheet(f"background-color: {self.based_color};") 254 | 255 | def on_run_click(self, event: QMouseEvent): 256 | if event.button() == Qt.LeftButton: 257 | self.main.run_core() 258 | 259 | def on_timer_click(self, event: QMouseEvent): 260 | if event.button() == Qt.LeftButton: 261 | self.start_timer(event) 262 | elif event.button() == Qt.MiddleButton: 263 | self.reset_timer() 264 | 265 | def reset_timer(self): 266 | self.elapsed_time = timedelta(0) 267 | self.running = False 268 | self.timer_paused = False 269 | self.timer.setText("00:00:00") 270 | 271 | def update_text(self, new_text): 272 | self.status_label.setText(new_text) 273 | 274 | def update_stats(self): 275 | content = self.text_widget.toPlainText() 276 | lines = content.count('\n') 277 | words = len(content.split()) 278 | self.num_lines = lines 279 | self.num_words = words 280 | stats_text = f"Lines: {lines+1}, Words: {words}" 281 | self.num_stats_label.setText(stats_text) 282 | 283 | def update_server(self, status): 284 | status_text = "Server: " + status 285 | self.server_status.setText(status_text) 286 | -------------------------------------------------------------------------------- /src/GUI/tab_bar.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import ( 2 | QWidget, QHBoxLayout, QTabWidget, QMessageBox 3 | ) 4 | from PySide6.QtCore import Qt 5 | from PySide6.QtGui import QMouseEvent, QShortcut, QKeySequence 6 | import os 7 | 8 | class TabBar(QWidget): 9 | def __init__(self, theme, text_widget, file_manager): 10 | super().__init__() 11 | 12 | self.theme = theme 13 | self.text_widget = text_widget 14 | self.file_manager = file_manager 15 | 16 | self.tabs = {} 17 | self.contents = {} 18 | self.modified_tabs = set() 19 | 20 | self.current_file_path = None 21 | 22 | self.init_ui() 23 | self.setup_shortcuts() 24 | self.connect_text_widget_signals() 25 | 26 | def init_ui(self): 27 | self.layout = QHBoxLayout() 28 | self.layout.setContentsMargins(0, 0, 0, 0) 29 | self.setLayout(self.layout) 30 | 31 | self.tab_widget = QTabWidget() 32 | self.tab_widget.setTabsClosable(True) 33 | self.tab_widget.setMovable(False) # Dezactivăm mutarea taburilor 34 | self.tab_widget.setElideMode(Qt.ElideNone) 35 | self.tab_widget.tabBar().setExpanding(False) 36 | self.tab_widget.tabBar().setDocumentMode(True) 37 | 38 | self.layout.addWidget(self.tab_widget) 39 | 40 | self.apply_stylesheet(self.theme) 41 | 42 | # Conectarea semnalelor 43 | self.tab_widget.tabCloseRequested.connect(self.close_tab) 44 | self.tab_widget.currentChanged.connect(self.switch_tab) 45 | self.tab_widget.tabBar().tabBarDoubleClicked.connect(self.on_tab_double_click) 46 | self.tab_widget.tabBar().installEventFilter(self) 47 | 48 | def apply_stylesheet(self, theme): 49 | # Stilizare personalizată pentru tab-uri 50 | self.tab_widget.setStyleSheet(f""" 51 | QTabWidget::pane {{ 52 | border: 0; 53 | }} 54 | QTabBar::tab {{ 55 | background: {theme['background_color']}; 56 | color: {theme['text_color']}; 57 | padding: 5px 10px; 58 | margin: 2px; 59 | border-radius: 4px; 60 | min-width: 100px; 61 | max-width: 200px; 62 | }} 63 | QTabBar::tab:selected {{ 64 | background: {theme['button_color']}; 65 | color: {theme['text_color']}; 66 | }} 67 | QTabBar::tab:!selected {{ 68 | background: {theme['background_color']}; 69 | color: {theme['text_color']}; 70 | }} 71 | QTabBar::close-button {{ 72 | image: url('images/close.png'); 73 | }} 74 | QTabBar::close-button:hover {{ 75 | image: url('images/close.png'); 76 | }} 77 | """) 78 | 79 | def setup_shortcuts(self): 80 | # Scurtături pentru navigarea între tab-uri 81 | next_tab_shortcut = QShortcut(QKeySequence("Ctrl+Tab"), self) 82 | next_tab_shortcut.activated.connect(self.next_tab) 83 | 84 | previous_tab_shortcut = QShortcut(QKeySequence("Ctrl+Shift+Tab"), self) 85 | previous_tab_shortcut.activated.connect(self.previous_tab) 86 | 87 | def connect_text_widget_signals(self): 88 | # Detectarea modificărilor în text_widget 89 | self.text_widget.textChanged.connect(self.on_text_changed) 90 | 91 | def add_tab(self, file_path=None): 92 | if file_path and file_path in self.tabs.values(): 93 | index = list(self.tabs.keys())[list(self.tabs.values()).index(file_path)] 94 | self.tab_widget.setCurrentIndex(index) 95 | return 96 | 97 | if file_path: 98 | file_name = os.path.basename(file_path) 99 | content = self.file_manager.get_file_content(file_path) 100 | else: 101 | file_name = "Untitled" 102 | content = "" 103 | 104 | new_tab = QWidget() 105 | index = self.tab_widget.addTab(new_tab, file_name) 106 | self.tabs[index] = file_path 107 | self.contents[file_path] = content 108 | self.tab_widget.setCurrentIndex(index) 109 | self.update_tab_title(index) 110 | self.text_widget.setPlainText(content) 111 | self.current_file_path = file_path 112 | self.file_manager.change_opened_filename(file_path) 113 | self.text_widget.document().setModified(False) 114 | self.setFixedHeight(30) 115 | 116 | def close_tab(self, index): 117 | if index < 0 or index >= self.tab_widget.count(): 118 | return 119 | 120 | # Obține calea fișierului pentru tab-ul care se închide 121 | file_path = self.tabs.get(index) 122 | if file_path is None: 123 | return 124 | 125 | # Obține conținutul original și cel asociat tab-ului care se închide 126 | original_content = self.file_manager.get_file_content(file_path) if file_path else "" 127 | if index == self.tab_widget.currentIndex(): 128 | tab_content = self.text_widget.toPlainText() 129 | else: 130 | tab_content = self.contents.get(file_path, "") 131 | # Verifică dacă există modificări nesalvate 132 | is_modified = original_content != tab_content 133 | 134 | if is_modified: 135 | # Asigură-te că tab-ul pe care vrei să-l închizi este activ 136 | current_index = self.tab_widget.currentIndex() 137 | self.tab_widget.setCurrentIndex(index) 138 | 139 | reply = QMessageBox.question( 140 | self, "Unsaved Changes", 141 | f"The file '{os.path.basename(file_path) if file_path else 'Untitled'}' has unsaved changes. Do you want to save them?", 142 | QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, 143 | QMessageBox.Yes 144 | ) 145 | 146 | if reply == QMessageBox.Yes: 147 | if file_path: 148 | self.file_manager.save_file(self.text_widget) 149 | else: 150 | saved_path = self.file_manager.save_file_as(self.text_widget) 151 | if saved_path: 152 | self.tabs[index] = saved_path 153 | self.contents[saved_path] = tab_content 154 | del self.contents[file_path] 155 | file_path = saved_path 156 | else: 157 | # Dacă utilizatorul a anulat salvarea, revenim la tab-ul original 158 | self.tab_widget.setCurrentIndex(current_index) 159 | return 160 | elif reply == QMessageBox.Cancel: 161 | # Dacă utilizatorul a anulat, revenim la tab-ul original 162 | self.tab_widget.setCurrentIndex(current_index) 163 | return 164 | 165 | # Eliminăm tab-ul 166 | self.tab_widget.removeTab(index) 167 | del self.tabs[index] 168 | if file_path in self.contents: 169 | del self.contents[file_path] 170 | 171 | # Actualizăm indexurile pentru tab-urile rămase 172 | self.update_tab_indices() 173 | 174 | # Verificăm dacă mai sunt tab-uri deschise 175 | if self.tab_widget.count() > 0: 176 | new_index = min(index, self.tab_widget.count() - 1) 177 | self.tab_widget.setCurrentIndex(new_index) 178 | new_file_path = self.tabs.get(new_index) 179 | if new_file_path and new_file_path in self.contents: 180 | self.current_file_path = new_file_path 181 | self.text_widget.setPlainText(self.contents[new_file_path]) 182 | self.file_manager.change_opened_filename(new_file_path) 183 | else: 184 | self.text_widget.clear() 185 | self.current_file_path = None 186 | self.file_manager.change_opened_filename(None) 187 | else: 188 | self.text_widget.clear() 189 | self.current_file_path = None 190 | self.file_manager.change_opened_filename(None) 191 | self.setFixedHeight(0) 192 | 193 | 194 | def switch_tab(self, index): 195 | if index == -1: 196 | return 197 | 198 | # Salvează conținutul tab-ului curent 199 | if self.current_file_path is not None: 200 | current_content = self.text_widget.toPlainText() 201 | self.contents[self.current_file_path] = current_content 202 | 203 | # Comută la noul tab 204 | new_file_path = self.tabs.get(index) 205 | 206 | if new_file_path is None or new_file_path not in self.contents: 207 | return # Iese din funcție dacă nu găsește un fișier valid 208 | 209 | self.current_file_path = new_file_path 210 | self.text_widget.setPlainText(self.contents[new_file_path]) 211 | self.file_manager.change_opened_filename(new_file_path) 212 | 213 | 214 | def on_text_changed(self): 215 | if self.current_file_path is None: 216 | return 217 | 218 | # Marchează tab-ul ca modificat 219 | current_index = self.tab_widget.currentIndex() 220 | self.update_tab_title(current_index, modified=True) 221 | 222 | def update_tab_title(self, index, title=None, modified=False): 223 | if title is None: 224 | file_path = self.tabs.get(index) 225 | title = os.path.basename(file_path) if file_path else "Untitled" 226 | 227 | # Eliminăm asteriscul complet 228 | self.tab_widget.setTabText(index, title) 229 | 230 | def next_tab(self): 231 | current_index = self.tab_widget.currentIndex() 232 | total_tabs = self.tab_widget.count() 233 | next_index = (current_index + 1) % total_tabs 234 | self.tab_widget.setCurrentIndex(next_index) 235 | 236 | def previous_tab(self): 237 | current_index = self.tab_widget.currentIndex() 238 | total_tabs = self.tab_widget.count() 239 | previous_index = (current_index - 1) % total_tabs 240 | self.tab_widget.setCurrentIndex(previous_index) 241 | 242 | def on_tab_double_click(self, index): 243 | # Poți implementa funcționalitatea dorită la dublu click (de exemplu, redenumire) 244 | pass 245 | 246 | def eventFilter(self, obj, event): 247 | if obj == self.tab_widget.tabBar(): 248 | if isinstance(event, QMouseEvent): 249 | if event.button() == Qt.MiddleButton: 250 | # Închide tab-ul la click pe butonul din mijloc 251 | tab_index = obj.tabAt(event.pos()) 252 | if tab_index != -1: 253 | self.close_tab(tab_index) 254 | return True 255 | return super().eventFilter(obj, event) 256 | 257 | def check_tab(self, file_path): 258 | return file_path in self.tabs.values() 259 | 260 | def save_current_tab(self): 261 | if self.current_file_path: 262 | content = self.text_widget.toPlainText() 263 | self.file_manager.save_file(self.text_widget) 264 | self.modified_tabs.discard(self.current_file_path) 265 | self.update_tab_title(self.tab_widget.currentIndex(), modified=False) 266 | 267 | def save_current_tab_as(self): 268 | if self.current_file_path: 269 | content = self.text_widget.toPlainText() 270 | saved_path = self.file_manager.save_file_as(self.text_widget) 271 | if saved_path: 272 | current_index = self.tab_widget.currentIndex() 273 | self.tabs[current_index] = saved_path 274 | self.contents[saved_path] = content 275 | del self.contents[self.current_file_path] 276 | self.current_file_path = saved_path 277 | self.file_manager.change_opened_filename(saved_path) 278 | self.modified_tabs.discard(saved_path) 279 | self.update_tab_title(current_index, os.path.basename(saved_path), modified=False) 280 | 281 | def get_open_files(self): 282 | return list(self.tabs.values()) 283 | 284 | def update_tab_indices(self): 285 | new_tabs = {} 286 | for i in range(self.tab_widget.count()): 287 | old_index = list(self.tabs.keys())[i] 288 | new_tabs[i] = self.tabs[old_index] 289 | self.tabs = new_tabs -------------------------------------------------------------------------------- /src/GUI/text_editor.py: -------------------------------------------------------------------------------- 1 | import re 2 | from PySide6.QtCore import Slot, Qt, QRect, QSize, QEvent, QRegularExpression 3 | from PySide6.QtGui import QColor, QPainter, QTextFormat, QFont, QTextCharFormat, QSyntaxHighlighter, QKeyEvent, QTextCursor 4 | from PySide6.QtWidgets import QPlainTextEdit, QWidget, QTextEdit, QListView, QCompleter 5 | 6 | class SuggestionManager: 7 | def __init__(self, theme): 8 | self.theme = theme 9 | self.keywords = [ 10 | "auto", "break", "case", "char", "const", "continue", "default", "do", 11 | "double", "else", "enum", "extern", "float", "goto", "using", 12 | "long", "register", "return", "short", "signed", "sizeof", "static", 13 | "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", 14 | "class", "namespace", "try", "catch", "throw", "public", "private", "protected", 15 | "virtual", "friend", "operator", "template", "this", "new", "delete","vector", 16 | "queue", "map", "unordered_map", "pair" 17 | ] 18 | self.functions = [ 19 | "cout", "cin", "endl", "printf", "scanf", "malloc", "free", "memcpy", "strlen", "strchr", "strcmp" 20 | ] 21 | self.completer = None 22 | 23 | def get_all_suggestions(self): 24 | return self.keywords + self.functions 25 | 26 | def create_completer(self, widget): 27 | words = self.get_all_suggestions() 28 | self.completer = QCompleter(words) 29 | self.completer.setWidget(widget) 30 | self.completer.setCompletionMode(QCompleter.PopupCompletion) 31 | self.completer.setCaseSensitivity(Qt.CaseInsensitive) 32 | self.completer.popup().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 33 | 34 | # Setarea unui QListView personalizat ca popup 35 | list_view = QListView() 36 | self.completer.setPopup(list_view) 37 | 38 | self.apply_theme(self.theme) 39 | 40 | return self.completer 41 | 42 | def set_completer_font_size(self, size): 43 | popup = self.completer.popup() 44 | font = popup.font() 45 | font.setPointSize(size) 46 | popup.setFont(font) 47 | 48 | def apply_theme(self, theme): 49 | 50 | # Aplicare stil CSS 51 | self.completer.popup().setStyleSheet(f""" 52 | QListView {{ 53 | background-color: {theme['treeview_background']}; 54 | color: {theme['text_color']}; 55 | border-radius: 8px; 56 | border: 1px solid {theme['border_color']}; 57 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); 58 | padding: 4px; 59 | }} 60 | QListView::item {{ 61 | padding: 6px 10px; 62 | }} 63 | QListView::item:selected {{ 64 | background-color: {theme['item_hover_background_color']}; 65 | color: {theme['item_hover_text_color']}; 66 | border-radius: 6px; 67 | transition: all 0.2s ease-in-out; 68 | }} 69 | QListView::item:hover {{ 70 | background-color: {theme['item_hover_background_color']}; 71 | }} 72 | QListView::separator {{ 73 | background-color: {theme['separator_color']}; 74 | height: 1px; 75 | margin: 4px 0; 76 | }} 77 | """) 78 | 79 | 80 | def insert_completion(self, completion, text_cursor): 81 | extra = len(completion) - len(self.completer.completionPrefix()) 82 | text_cursor.movePosition(QTextCursor.Left) 83 | text_cursor.movePosition(QTextCursor.EndOfWord) 84 | text_cursor.insertText(completion[-extra:]) 85 | return text_cursor 86 | 87 | def should_show_popup(self, completion_prefix): 88 | return len(completion_prefix) >= 1 89 | 90 | class LineNumberArea(QWidget): 91 | def __init__(self, editor): 92 | super().__init__(editor) 93 | self._code_editor = editor 94 | def sizeHint(self): 95 | return QSize(self._code_editor.line_number_area_width(), 0) 96 | 97 | def paintEvent(self, event): 98 | self._code_editor.lineNumberAreaPaintEvent(event) 99 | 100 | 101 | class CodeEditor(QPlainTextEdit): 102 | def __init__(self, config, theme): 103 | super().__init__() 104 | self.theme = theme 105 | self.highlighter = CPPHighlighter(self.document(), self.theme) 106 | self.line_number_area = LineNumberArea(self) 107 | self.setFrameStyle(QPlainTextEdit.NoFrame) 108 | self.setLineWrapMode(QPlainTextEdit.NoWrap) 109 | 110 | self.config = config 111 | 112 | self.suggestion_manager = SuggestionManager(self.theme) 113 | self.completer = self.suggestion_manager.create_completer(self) 114 | self.completer.activated.connect(self.insert_completion) 115 | self.apply_settings() 116 | self.apply_theme(self.theme) 117 | # Conectează semnalele și sloturile 118 | self.blockCountChanged[int].connect(self.update_line_number_area_width) 119 | self.updateRequest[QRect, int].connect(self.update_line_number_area) 120 | self.cursorPositionChanged.connect(self.highlight_current_line) 121 | 122 | self.update_line_number_area_width(0) 123 | self.highlight_current_line() 124 | self.setTabStopDistance(self.fontMetrics().horizontalAdvance(' ') * 4) 125 | 126 | self.multi_cursors = [] # Lista pentru cursori suplimentari 127 | self.setAttribute(Qt.WA_InputMethodEnabled, True) 128 | 129 | self.function_definitions = {} # Dictionar pentru a mapa numele funcțiilor la locațiile lor 130 | 131 | def apply_settings(self): 132 | font_size_str = self.config.get("editor_font_size", "10px") 133 | font_size = int(font_size_str) 134 | self.font = QFont("Courier", font_size) 135 | self.setFont(self.font) 136 | self.suggestion_manager.set_completer_font_size(font_size-5) 137 | 138 | def apply_theme(self, theme): 139 | highlight_color = theme.get("highlight_color", "#ffff99") 140 | self.highlight_color = QColor(highlight_color) 141 | self.highlight_current_line() 142 | self.background_color = QColor(theme["line_number_background"]) 143 | self.foreground_color = QColor(theme["line_number_text_color"]) 144 | self.highlighter.setTheme(theme) 145 | self.suggestion_manager.apply_theme(theme) 146 | 147 | def line_number_area_width(self): 148 | digits = 1 149 | max_num = max(1, self.blockCount()) 150 | while max_num >= 10: 151 | max_num *= 0.1 152 | digits += 1 153 | 154 | space = 3 + self.fontMetrics().horizontalAdvance('9') * digits 155 | return space 156 | 157 | # Multi line cursor 158 | def mousePressEvent(self, event): 159 | if event.modifiers() == Qt.ControlModifier: 160 | cursor = self.cursorForPosition(event.pos()) 161 | cursor.select(QTextCursor.WordUnderCursor) 162 | word = cursor.selectedText() 163 | 164 | if word in self.function_definitions: 165 | definition_cursor = QTextCursor(self.document()) 166 | definition_cursor.setPosition(self.function_definitions[word]) 167 | self.setTextCursor(definition_cursor) 168 | self.centerCursor() 169 | return 170 | 171 | self.multi_cursors.append(cursor) 172 | self.viewport().update() 173 | else: 174 | self.multi_cursors = [] 175 | super().mousePressEvent(event) 176 | 177 | def parseFunctions(self): 178 | self.function_definitions.clear() 179 | regex = QRegularExpression(r"^\s*(?:void|int|float|double|char|bool)\s+(\w+)\s*\(.*\)\s*\{") 180 | block = self.document().begin() 181 | while block.isValid(): 182 | match = regex.match(block.text()) 183 | if match.hasMatch(): 184 | function_name = match.captured(1) 185 | self.function_definitions[function_name] = block.position() 186 | block = block.next() 187 | 188 | # Code suggestions 189 | 190 | def insert_completion(self, completion): 191 | tc = self.textCursor() 192 | tc = self.suggestion_manager.insert_completion(completion, tc) 193 | self.setTextCursor(tc) 194 | 195 | def text_under_cursor(self): 196 | tc = self.textCursor() 197 | tc.select(QTextCursor.WordUnderCursor) 198 | return tc.selectedText() 199 | 200 | def update_completion(self): 201 | completion_prefix = self.text_under_cursor() 202 | 203 | if self.suggestion_manager.should_show_popup(completion_prefix): 204 | if completion_prefix != self.completer.completionPrefix(): 205 | self.completer.setCompletionPrefix(completion_prefix) 206 | self.completer.popup().setCurrentIndex( 207 | self.completer.completionModel().index(0, 0)) 208 | 209 | cr = self.cursorRect() 210 | cr.setWidth(self.completer.popup().sizeHintForColumn(0) + 211 | self.completer.popup().verticalScrollBar().sizeHint().width() + 30) 212 | self.completer.complete(cr) 213 | else: 214 | self.completer.popup().hide() 215 | 216 | def resizeEvent(self, e): 217 | super().resizeEvent(e) 218 | cr = self.contentsRect() 219 | width = self.line_number_area_width() 220 | rect = QRect(cr.left(), cr.top(), width, cr.height()) 221 | self.line_number_area.setGeometry(rect) 222 | 223 | def lineNumberAreaPaintEvent(self, event): 224 | painter = QPainter(self.line_number_area) 225 | painter.fillRect(event.rect(), self.background_color) 226 | 227 | block = self.firstVisibleBlock() 228 | block_number = block.blockNumber() 229 | offset = self.contentOffset() 230 | top = self.blockBoundingGeometry(block).translated(offset).top() 231 | bottom = top + self.blockBoundingRect(block).height() 232 | 233 | while block.isValid() and top <= event.rect().bottom(): 234 | if block.isVisible() and bottom >= event.rect().top(): 235 | painter.setFont(self.font) 236 | number = str(block_number + 1) 237 | painter.setPen(self.foreground_color) 238 | width = self.line_number_area.width() 239 | height = self.fontMetrics().height() 240 | painter.drawText(0, top, width, height, Qt.AlignRight, number) 241 | 242 | block = block.next() 243 | top = bottom 244 | bottom = top + self.blockBoundingRect(block).height() 245 | block_number += 1 246 | 247 | painter.end() 248 | 249 | @Slot() 250 | def update_line_number_area_width(self, newBlockCount): 251 | self.setViewportMargins(self.line_number_area_width(), 0, 0, 0) 252 | 253 | @Slot() 254 | def update_line_number_area(self, rect, dy): 255 | if dy: 256 | self.line_number_area.scroll(0, dy) 257 | else: 258 | width = self.line_number_area.width() 259 | self.line_number_area.update(0, rect.y(), width, rect.height()) 260 | 261 | if rect.contains(self.viewport().rect()): 262 | self.update_line_number_area_width(0) 263 | 264 | @Slot() 265 | def highlight_current_line(self): 266 | extra_selections = [] 267 | 268 | if not self.isReadOnly(): 269 | selection = QTextEdit.ExtraSelection() 270 | 271 | # Folosește culoarea de highlight din themes.json 272 | selection.format.setBackground(self.highlight_color) 273 | selection.format.setProperty(QTextFormat.FullWidthSelection, True) 274 | 275 | selection.cursor = self.textCursor() 276 | selection.cursor.clearSelection() 277 | 278 | extra_selections.append(selection) 279 | 280 | self.setExtraSelections(extra_selections) 281 | 282 | 283 | def insertCompletion(self, key): 284 | cursor = self.textCursor() 285 | 286 | # Define the pairs of characters and their type 287 | pairs = { 288 | Qt.Key_ParenLeft: (')', '('), 289 | Qt.Key_BracketLeft: (']', '['), 290 | Qt.Key_BraceLeft: ('}', '{'), 291 | 34: ('"', '"'), # 34 is the code for double quotes 292 | 39: ("'", "'") # 39 is the code for single quote 293 | } 294 | 295 | if key in pairs: 296 | closing, opening = pairs[key] 297 | 298 | # Insert the pair of characters 299 | cursor.insertText(opening + closing) 300 | 301 | # Position the cursor between the pair of characters 302 | cursor.movePosition(QTextCursor.PreviousCharacter) 303 | self.setTextCursor(cursor) 304 | 305 | 306 | def keyPressEvent(self, event): 307 | if self.multi_cursors: 308 | if event.key() in ( 309 | Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown, 310 | Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab, Qt.Key_Backspace, 311 | Qt.Key_Z 312 | ) or (event.modifiers() == Qt.ControlModifier and event.key() in (Qt.Key_C, Qt.Key_V, Qt.Key_A, Qt.Key_Z)): 313 | super().keyPressEvent(event) 314 | return 315 | new_cursors = [] 316 | for cursor in self.multi_cursors: 317 | cursor.beginEditBlock() 318 | cursor.insertText(event.text()) 319 | cursor.endEditBlock() 320 | new_cursors.append(cursor) 321 | self.multi_cursors = new_cursors 322 | self.viewport().update() 323 | self.parseFunctions() 324 | return 325 | if self.completer and self.completer.popup().isVisible(): 326 | if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab): 327 | current_item = self.completer.popup().currentIndex().data() 328 | if current_item: 329 | cursor = self.textCursor() 330 | cursor.select(QTextCursor.WordUnderCursor) 331 | current_word = cursor.selectedText() 332 | if current_item == current_word: 333 | self.completer.popup().hide() 334 | event.accept() 335 | return 336 | self.insert_completion(current_item) 337 | self.completer.popup().hide() 338 | event.accept() 339 | return 340 | elif event.key() == Qt.Key_Escape: 341 | self.completer.popup().hide() 342 | event.accept() 343 | return 344 | elif event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): 345 | self.completer.popup().keyPressEvent(event) 346 | return 347 | 348 | if event.key() == Qt.Key_Backspace and event.modifiers() == Qt.ControlModifier: 349 | super().keyPressEvent(event) 350 | return 351 | 352 | if event.key() in {Qt.Key_Return, Qt.Key_Enter}: 353 | self.handleNewLine() 354 | self.parseFunctions() 355 | return 356 | 357 | if event.key() in {Qt.Key_ParenLeft, Qt.Key_BracketLeft, Qt.Key_BraceLeft, 34, 39}: 358 | self.insertCompletion(event.key()) 359 | self.parseFunctions() 360 | return 361 | 362 | if event.key() == Qt.Key_Backspace and not event.modifiers(): 363 | self.handleBackspace() 364 | self.update_completion() 365 | self.parseFunctions() 366 | return 367 | 368 | super().keyPressEvent(event) 369 | self.update_completion() 370 | self.parseFunctions() 371 | 372 | def handleNewLine(self): 373 | cursor = self.textCursor() 374 | current_line = cursor.block().text() 375 | indent = self.getIndentation(current_line) 376 | 377 | completions = { 378 | "IF": "if() {\n\n}", 379 | "FOR": "for(int i = 1; i <= n; i++) {\n\n}", 380 | "WHILE": "while() {\n\n}", 381 | "INT": "int () {\n\n}", 382 | "CPP": "#include \n\nusing namespace std;\n\nint main() {\n\n return 0;\n}" 383 | } 384 | 385 | if current_line.strip().startswith("FOR-"): 386 | variable = current_line.split('-')[1] if len(current_line.split('-')) > 1 else 'i' 387 | code = re.sub(r'\bi\b', variable.lower(), completions["FOR"]) 388 | 389 | cursor.select(QTextCursor.BlockUnderCursor) 390 | cursor.removeSelectedText() 391 | cursor.insertText(code) 392 | self.setTextCursor(cursor) 393 | return 394 | 395 | for keyword, code_template in completions.items(): 396 | keyword_position = current_line.rfind(keyword) 397 | if keyword_position != -1 and cursor.positionInBlock() == keyword_position + len(keyword): 398 | code = code_template 399 | 400 | # Ștergem doar keyword-ul și inserăm codul de completare 401 | cursor.setPosition(cursor.block().position() + keyword_position, QTextCursor.KeepAnchor) 402 | cursor.removeSelectedText() 403 | cursor.insertText(code) 404 | 405 | # Setăm cursorul pentru utilizator 406 | cursor.movePosition(QTextCursor.StartOfBlock) 407 | cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) 408 | self.setTextCursor(cursor) 409 | return 410 | 411 | # Dacă cursorul este între paranteze 412 | if self.isCursorBetweenBrackets(cursor): 413 | super().keyPressEvent(QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier)) 414 | self.insertPlainText(indent + " ") 415 | self.insertPlainText("\n" + indent) 416 | cursor = self.textCursor() 417 | cursor.movePosition(QTextCursor.Up) 418 | cursor.movePosition(QTextCursor.EndOfLine) 419 | self.setTextCursor(cursor) 420 | else: 421 | super().keyPressEvent(QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier)) 422 | self.insertPlainText(indent) 423 | 424 | def getIndentation(self, line): 425 | return line[:len(line) - len(line.lstrip())] 426 | 427 | def isCursorBetweenBrackets(self, cursor): 428 | doc = self.document() 429 | pos = cursor.position() 430 | 431 | if pos > 0 and pos < doc.characterCount() - 1: 432 | prev_char = doc.characterAt(pos - 1) 433 | next_char = doc.characterAt(pos) 434 | return ((prev_char == '(' and next_char == ')') or 435 | (prev_char == '[' and next_char == ']') or 436 | (prev_char == '{' and next_char == '}')) 437 | return False 438 | 439 | def handleOpeningBracket(self, bracket): 440 | cursor = self.textCursor() 441 | super().keyPressEvent(QKeyEvent(QEvent.KeyPress, ord(bracket), Qt.NoModifier)) 442 | 443 | closing_bracket = {'{': '}', '[': ']', '(': ')'}[bracket] 444 | self.insertPlainText(closing_bracket) 445 | cursor.movePosition(QTextCursor.Left) 446 | self.setTextCursor(cursor) 447 | 448 | def handleClosingBracket(self, bracket): 449 | cursor = self.textCursor() 450 | next_char = self.document().characterAt(cursor.position()) 451 | 452 | if next_char == bracket: 453 | cursor.movePosition(QTextCursor.Right) 454 | self.setTextCursor(cursor) 455 | else: 456 | super().keyPressEvent(QKeyEvent(QEvent.KeyPress, ord(bracket), Qt.NoModifier)) 457 | 458 | def handleBackspace(self): 459 | cursor = self.textCursor() 460 | 461 | # Dacă există text selectat, îl ștergem și ieșim din funcție 462 | if cursor.hasSelection(): 463 | cursor.removeSelectedText() 464 | return 465 | 466 | # Verificăm dacă nu suntem la începutul documentului 467 | if cursor.atStart(): 468 | return 469 | 470 | # Salvăm poziția curentă 471 | position = cursor.position() 472 | 473 | # Obținem caracterul de dinainte de cursor 474 | cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor) 475 | char_before = cursor.selectedText() 476 | 477 | # Definim perechile de caractere 478 | pairs = {"(": ")", "[": "]", "{": "}", "\"": "\"", "'": "'"} 479 | 480 | # Verificăm dacă caracterul de dinainte este o deschidere de paranteză 481 | if char_before in pairs: 482 | # Salvăm selecția curentă 483 | cursor.setPosition(position) 484 | # Selectăm caracterul următor 485 | cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor) 486 | char_after = cursor.selectedText() 487 | 488 | # Dacă caracterul următor este perechea potrivită, ștergem ambele caractere 489 | if char_after == pairs[char_before]: 490 | cursor.removeSelectedText() 491 | cursor.deletePreviousChar() 492 | return 493 | 494 | # Dacă nu am șters o pereche, ștergem doar caracterul anterior 495 | cursor.setPosition(position) 496 | cursor.deletePreviousChar() 497 | 498 | def insertSuggestion(self, text): 499 | cursor = self.textCursor() 500 | 501 | # Selectează cuvântul sub cursor 502 | cursor.select(QTextCursor.WordUnderCursor) 503 | cursor.removeSelectedText() 504 | 505 | # Inserează textul completării 506 | cursor.insertText(text) 507 | self.setTextCursor(cursor) 508 | 509 | class CPPHighlighter(QSyntaxHighlighter): 510 | def __init__(self, parent=None, theme=None): 511 | super().__init__(parent) 512 | self.theme = theme or {} 513 | self._mappings = {} 514 | self.setup_highlighting() 515 | 516 | def setTheme(self, theme): 517 | self.theme = theme 518 | self._mappings.clear() 519 | self.setup_highlighting() 520 | self.rehighlight() 521 | 522 | def setup_highlighting(self): 523 | # Format pentru directive de preprocesor 524 | preprocessor_format = QTextCharFormat() 525 | preprocessor_format.setForeground(QColor(self.theme.get("preprocessor_color", "#61AFEF"))) 526 | self.add_mapping(r'#\b(?:define|ifdef|ifndef|endif|undef|if|elif|else|pragma|include)\b.*', preprocessor_format) 527 | 528 | # Format pentru #include cu <...> sau "..." 529 | include_format = QTextCharFormat() 530 | include_format.setForeground(QColor(self.theme.get("include_color", "#5C6370"))) 531 | self.add_mapping(r'#include\s*[<"].*?[>"]', include_format) 532 | 533 | # Format pentru keyword-uri 534 | keyword_format = QTextCharFormat() 535 | keyword_format.setForeground(QColor(self.theme.get("keyword_color", "#C678DD"))) 536 | keywords = r'\b(?:class|return|if|else|for|while|switch|case|break|continue|namespace|public|private|protected|void|int|float|double|char|bool|const|static|virtual|override|explicit|vector|cout|cin|short|long|signed|unsigned|struct|sizeof|typedef|using|throw|try|catch|default|goto)\b' 537 | self.add_mapping(keywords, keyword_format) 538 | 539 | # Format pentru simboluri și operatori 540 | symbol_format = QTextCharFormat() 541 | symbol_format.setForeground(QColor(self.theme.get("symbol_color", "#56b6c2"))) 542 | self.add_mapping(r'[()\[\]{}]|[\-*%&|^!~<>=?:;,+]', symbol_format) 543 | 544 | # Format pentru numere 545 | number_format = QTextCharFormat() 546 | number_format.setForeground(QColor(self.theme.get("number_color", "#d19a66"))) 547 | self.add_mapping(r'\b\d+(\.\d+)?(f|F|L|ULL|ll|u|U|l)?\b', number_format) 548 | 549 | # Format pentru stringuri între ghilimele 550 | string_format = QTextCharFormat() 551 | string_format.setForeground(QColor(self.theme.get("string_color", "#98C379"))) 552 | self.add_mapping(r'".*?"', string_format) 553 | 554 | # Format pentru stringuri între apostroafe 555 | self.add_mapping(r"""'[^'\n]*'""", string_format) 556 | 557 | # Format pentru comentarii single-line 558 | comment_format = QTextCharFormat() 559 | comment_format.setForeground(QColor(self.theme.get("comment_color", "#5C6370"))) 560 | self.add_mapping(r'\/\/.*', comment_format) 561 | 562 | # Format pentru comentarii bloc 563 | self.comment_block_format = comment_format 564 | 565 | # Format pentru identificatori de funcții 566 | function_format = QTextCharFormat() 567 | function_format.setForeground(QColor(self.theme.get("function_color", "#E5C07B"))) 568 | self.add_mapping(r'\b[A-Za-z_]\w*(?=\s*\()', function_format) 569 | 570 | # Format pentru tipuri de date definite de utilizator 571 | user_type_format = QTextCharFormat() 572 | user_type_format.setForeground(QColor(self.theme.get("user_type_color", "#D19A66"))) 573 | self.add_mapping(r'\b(?:class|struct)\s+([A-Za-z_]\w*)', user_type_format) 574 | 575 | def add_mapping(self, pattern, format): 576 | self._mappings[pattern] = format 577 | 578 | def highlightBlock(self, text): 579 | # Evidențierea bazată pe regex 580 | for pattern, format in self._mappings.items(): 581 | for match in re.finditer(pattern, text): 582 | start, end = match.span() 583 | self.setFormat(start, end - start, format) 584 | 585 | # Evidențierea comentariilor bloc multi-linie 586 | self.setCurrentBlockState(0) 587 | start_index = 0 if self.previousBlockState() != 1 else 0 588 | 589 | while start_index >= 0: 590 | start_index = text.find('/*', start_index) 591 | if start_index == -1: 592 | break 593 | 594 | end_index = text.find('*/', start_index + 2) 595 | if end_index == -1: 596 | self.setFormat(start_index, len(text) - start_index, self.comment_block_format) 597 | self.setCurrentBlockState(1) 598 | break 599 | else: 600 | self.setFormat(start_index, end_index - start_index + 2, self.comment_block_format) 601 | start_index = end_index + 2 602 | -------------------------------------------------------------------------------- /src/GUI/treeview.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QTreeView, QFileSystemModel, QVBoxLayout, QWidget, QLineEdit, QMenu, QMessageBox, QFileDialog 2 | from PySide6.QtCore import Qt, QDir, QModelIndex 3 | from PySide6.QtGui import QKeyEvent, QAction 4 | import os 5 | import shutil 6 | import subprocess 7 | 8 | class TreeView(QWidget): 9 | def __init__(self, theme, win): 10 | super().__init__() 11 | self.win = win 12 | self.layout = QVBoxLayout(self) 13 | self.layout.setContentsMargins(0, 0, 0, 0) 14 | 15 | # Crearea modelului pentru sistemul de fișiere 16 | self.model = QFileSystemModel() 17 | self.model.setRootPath(QDir.rootPath()) 18 | 19 | # Crearea QTreeView și configurarea acestuia 20 | self.tree = QTreeView() 21 | self.tree.setModel(self.model) 22 | 23 | # Ascunderea coloanelor suplimentare și a header-ului 24 | self.tree.setHeaderHidden(True) # Ascunde header-ul coloanelor 25 | self.tree.setColumnHidden(1, True) # Ascunde coloana Size 26 | self.tree.setColumnHidden(2, True) # Ascunde coloana Type 27 | self.tree.setColumnHidden(3, True) # Ascunde coloana Date Modified 28 | 29 | # Redimensionarea coloanei pentru numele fișierelor/folderelor 30 | self.tree.setColumnWidth(0, 250) # Ajustează lățimea coloanei pentru nume 31 | 32 | # Adăugarea TreeView la layout 33 | self.layout.addWidget(self.tree) 34 | 35 | # Crearea și configurarea QLineEdit pentru editare 36 | self.edit_line = QLineEdit(self) 37 | self.edit_line.setVisible(False) 38 | self.layout.addWidget(self.edit_line) 39 | 40 | # Conectarea semnalelor 41 | self.tree.setContextMenuPolicy(Qt.CustomContextMenu) 42 | self.tree.customContextMenuRequested.connect(self.open_context_menu) 43 | self.edit_line.returnPressed.connect(self.commit_edit) 44 | self.edit_line.editingFinished.connect(self.cancel_edit) 45 | self.edit_line.installEventFilter(self) 46 | self.tree.doubleClicked.connect(self.open_file_in_treeview) 47 | 48 | self.current_index = None 49 | self.current_action = None 50 | self.apply_theme(theme) 51 | 52 | def apply_theme(self, theme): 53 | self.tree.setStyleSheet(f""" 54 | QTreeView {{ 55 | background-color: {theme['treeview_background']}; 56 | color: {theme['text_color']}; 57 | selection-background-color: {theme['selection_background_color']}; 58 | }} 59 | QTreeView::item {{ 60 | background-color: {theme['treeview_background']}; 61 | color: {theme['text_color']}; 62 | }} 63 | QTreeView::item:selected {{ 64 | background-color: {theme['highlight_color']}; 65 | color: {theme['item_hover_text_color']}; 66 | }} 67 | """) 68 | 69 | def open_context_menu(self, position): 70 | index = self.tree.indexAt(position) 71 | if not index.isValid(): 72 | return 73 | 74 | # Creăm meniul contextual 75 | menu = QMenu(self) 76 | 77 | # Adăugăm opțiunea de "Add File" și "Add Folder" 78 | if self.model.isDir(index): 79 | add_file_action = QAction("Add File", self) 80 | add_file_action.triggered.connect(lambda: self.start_edit(index, "add_file")) 81 | menu.addAction(add_file_action) 82 | 83 | add_folder_action = QAction("Add Folder", self) 84 | add_folder_action.triggered.connect(lambda: self.start_edit(index, "add_folder")) 85 | menu.addAction(add_folder_action) 86 | 87 | # Adăugăm opțiunea de "Rename" 88 | rename_action = QAction("Rename", self) 89 | rename_action.triggered.connect(lambda: self.start_edit(index, "rename")) 90 | menu.addAction(rename_action) 91 | 92 | # Adăugăm opțiunea de "Delete" 93 | delete_action = QAction("Delete", self) 94 | delete_action.triggered.connect(lambda: self.delete_item(index)) 95 | menu.addAction(delete_action) 96 | 97 | # Adăugăm opțiunea de "Reveal in Explorer" 98 | reveal_action = QAction("Reveal in Explorer", self) 99 | reveal_action.triggered.connect(lambda: self.reveal_in_explorer(index)) 100 | menu.addAction(reveal_action) 101 | 102 | menu.exec(self.tree.viewport().mapToGlobal(position)) 103 | 104 | def start_edit(self, index: QModelIndex, action_type: str): 105 | if not index.isValid(): 106 | return 107 | 108 | # Setăm indexul curent și tipul de acțiune 109 | self.current_index = index 110 | self.current_action = action_type 111 | placeholder_text = { 112 | "rename": self.model.fileName(index), 113 | "add_file": "New File.txt", 114 | "add_folder": "New Folder" 115 | }.get(action_type, "") 116 | 117 | self.edit_line.setText(placeholder_text) 118 | self.edit_line.setVisible(True) 119 | self.edit_line.setGeometry(self.tree.visualRect(index)) 120 | self.edit_line.setFocus() 121 | self.edit_line.selectAll() 122 | 123 | def commit_edit(self): 124 | if not self.current_index or not self.current_action: 125 | return 126 | 127 | new_name = self.edit_line.text().strip() 128 | parent_path = self.model.filePath(self.current_index) 129 | if self.current_action == "rename": 130 | old_name = parent_path 131 | new_path = os.path.join(os.path.dirname(old_name), new_name) 132 | try: 133 | os.rename(old_name, new_path) 134 | self.model.setRootPath(QDir.rootPath()) # Reîncarcă modelul pentru a reflecta modificările 135 | self.current_index = None 136 | self.current_action = None 137 | self.edit_line.setVisible(False) 138 | except Exception as e: 139 | QMessageBox.warning(self, "Rename Error", f"Could not rename {old_name}: {e}") 140 | self.edit_line.setText(os.path.basename(old_name)) 141 | 142 | elif self.current_action == "add_file": 143 | file_path = os.path.join(parent_path, new_name) 144 | try: 145 | with open(file_path, 'w') as f: 146 | pass # Crează fișierul gol 147 | self.model.setRootPath(QDir.rootPath()) # Reîncarcă modelul pentru a reflecta modificările 148 | self.current_index = None 149 | self.current_action = None 150 | self.edit_line.setVisible(False) 151 | except Exception as e: 152 | QMessageBox.warning(self, "Add File Error", f"Could not create file {file_path}: {e}") 153 | 154 | elif self.current_action == "add_folder": 155 | folder_path = os.path.join(parent_path, new_name) 156 | try: 157 | os.makedirs(folder_path) 158 | self.model.setRootPath(QDir.rootPath()) # Reîncarcă modelul pentru a reflecta modificările 159 | self.current_index = None 160 | self.current_action = None 161 | self.edit_line.setVisible(False) 162 | except Exception as e: 163 | QMessageBox.warning(self, "Add Folder Error", f"Could not create folder {folder_path}: {e}") 164 | 165 | def cancel_edit(self): 166 | if self.current_index: 167 | self.edit_line.setVisible(False) 168 | self.current_index = None 169 | self.current_action = None 170 | 171 | def delete_item(self, index: QModelIndex): 172 | file_path = self.model.filePath(index) 173 | if QMessageBox.question(self, "Confirm Deletion", f"Are you sure you want to delete {file_path}?", QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: 174 | try: 175 | if self.model.isDir(index): 176 | shutil.rmtree(file_path) # Șterge folderul 177 | else: 178 | os.remove(file_path) # Șterge fișierul 179 | self.model.setRootPath(QDir.rootPath()) # Reîncarcă modelul pentru a reflecta modificările 180 | except Exception as e: 181 | QMessageBox.warning(self, "Delete Error", f"Could not delete {file_path}: {e}") 182 | 183 | def reveal_in_explorer(self, index: QModelIndex): 184 | file_path = self.model.filePath(index) 185 | try: 186 | if os.name == 'nt': # Windows 187 | os.startfile(file_path) 188 | elif os.name == 'posix': # macOS/Linux 189 | subprocess.run(['xdg-open', file_path], check=True) 190 | except Exception as e: 191 | QMessageBox.warning(self, "Reveal Error", f"Could not reveal {file_path} in explorer: {e}") 192 | 193 | def eventFilter(self, obj, event): 194 | if obj == self.edit_line and event.type() == QKeyEvent.KeyPress: 195 | if event.key() == Qt.Key_Escape: 196 | self.cancel_edit() 197 | return True 198 | return super().eventFilter(obj, event) 199 | 200 | def open_file_in_treeview(self, index: QModelIndex): 201 | if index.isValid(): 202 | if self.model.isDir(index): 203 | pass 204 | else: 205 | file_path = self.model.filePath(index) 206 | self.win.tab_bar.add_tab(file_path) 207 | 208 | -------------------------------------------------------------------------------- /src/Server/client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | 4 | class ClientManager: 5 | def __init__(self, name, password,gui, host='localhost', port=8080): 6 | self.name = name 7 | self.password = password 8 | self.host = host 9 | self.port = port 10 | self.client_socket = None 11 | self.gui = gui # Referință către interfața grafică pentru actualizarea QPlainTextEdit 12 | 13 | def connect_to_server(self): 14 | """Conectarea la server.""" 15 | self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 16 | 17 | try: 18 | # Încearcă să te conectezi la server 19 | self.client_socket.connect((self.host, self.port)) 20 | except ConnectionRefusedError: 21 | print("Conexiune esuata: Serverul nu este pornit sau nu este disponibil.") 22 | self.client_socket.close() 23 | return False 24 | except socket.error as e: 25 | print(f"Eroare la conectare: {e}") 26 | self.client_socket.close() 27 | return False 28 | 29 | # Trimiterea parolei 30 | print(f"Trimit parola: '{self.password}'") 31 | self.client_socket.send(self.password.encode('utf-8')) 32 | 33 | # Așteptarea confirmării de la server pentru parolă 34 | response = self.client_socket.recv(1024).decode('utf-8').strip() 35 | if response != "OK": 36 | print(f"Conectare esuată: {response}") 37 | self.client_socket.close() 38 | return False 39 | 40 | # Trimiterea numelui clientului după confirmarea parolei 41 | print(f"Trimit numele: '{self.name}'") 42 | self.client_socket.send(self.name.encode('utf-8')) 43 | 44 | print(f"{self.name} s-a conectat la server.") 45 | 46 | # Pornește un thread pentru a asculta mesajele de la server 47 | threading.Thread(target=self.receive_messages, daemon=True).start() 48 | return True 49 | 50 | def send_message(self, message): 51 | """Trimiterea unui mesaj către server.""" 52 | if self.client_socket: 53 | self.client_socket.send(message.encode('utf-8')) 54 | 55 | def receive_messages(self): 56 | """Ascultă și afișează mesajele de la server.""" 57 | while True: 58 | try: 59 | message = self.client_socket.recv(1024).decode('utf-8') 60 | if message and self.gui: 61 | self.gui.append_to_textbox(message) # Actualizează QPlainTextEdit 62 | except: 63 | print("Conexiunea a fost intrerupta.") 64 | break 65 | 66 | def disconnect(self): 67 | """Deconectarea de la server.""" 68 | if self.client_socket: 69 | self.client_socket.close() 70 | print(f"{self.name} s-a deconectat de la server.") 71 | self.client_socket = None 72 | -------------------------------------------------------------------------------- /src/Server/competitive_companion.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | received_input_data = None 6 | received_output_data = None 7 | 8 | @app.route('/', methods=['POST']) 9 | def parse(): 10 | global received_input_data 11 | global received_output_data 12 | data = request.get_json() 13 | 14 | if 'tests' in data and data['tests']: 15 | received_test_case = data['tests'][0] 16 | input_data = received_test_case['input'] 17 | received_input_data = input_data 18 | output_data = received_test_case['output'] 19 | received_output_data = output_data 20 | return jsonify({"status": "Test case received"}), 200 21 | else: 22 | return jsonify({"error": "No test cases found"}), 400 23 | 24 | def get_received_test_case(): 25 | global received_input_data 26 | global received_output_data 27 | if received_input_data is None or received_output_data is None: 28 | return None 29 | return received_input_data, received_output_data 30 | def run_flask_server(): 31 | app.run(host='localhost', port=10045, debug=False) 32 | -------------------------------------------------------------------------------- /src/Server/server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | 4 | class ServerManager: 5 | def __init__(self,password, host='localhost', port=8080): 6 | self.host = host 7 | self.port = port 8 | self.password = password 9 | self.server_socket = None 10 | self.clients = [] 11 | 12 | def start_server(self): 13 | """Inițializează și pornește serverul.""" 14 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | self.server_socket.bind((self.host, self.port)) 16 | self.server_socket.listen(5) 17 | print(f"Serverul a pornit pe {self.host}:{self.port} cu parola setata.") 18 | self.run() 19 | 20 | def stop_server(self): 21 | """Oprește serverul și eliberează resursele.""" 22 | for client in self.clients: 23 | client['socket'].close() 24 | if self.server_socket: 25 | self.server_socket.close() 26 | print("Serverul a fost oprit.") 27 | 28 | def broadcast(self, message, sender_socket): 29 | """Trimite un mesaj tuturor clienților conectați, cu excepția celui care a trimis mesajul.""" 30 | for client in self.clients: 31 | if client['socket'] != sender_socket: 32 | try: 33 | client['socket'].send(message.encode('utf-8')) 34 | except: 35 | client['socket'].close() 36 | self.clients.remove(client) 37 | 38 | def handle_client(self, client_socket, client_address): 39 | """Gestionarea unui client conectat.""" 40 | try: 41 | # Primirea parolei de la client 42 | received_password = client_socket.recv(1024).decode('utf-8').strip() 43 | print(f"Parola primita de la {client_address}: '{received_password}' (ar trebui sa fie '{self.password}')") 44 | 45 | # Verificarea parolei 46 | if received_password != self.password: 47 | print(f"Conexiune refuzata de la {client_address}. Parola este incorecta.") 48 | client_socket.send("Parola incorecta".encode('utf-8')) 49 | client_socket.close() 50 | return 51 | 52 | # Trimite confirmarea că parola a fost corectă 53 | client_socket.send("OK".encode('utf-8')) 54 | 55 | # Parola este corectă, continuăm cu primirea numelui clientului 56 | client_name = client_socket.recv(1024).decode('utf-8').strip() 57 | print(f"Numele clientului: '{client_name}'") 58 | 59 | client_info = {'socket': client_socket, 'name': client_name} 60 | self.clients.append(client_info) 61 | print(f"{client_name} s-a conectat de la {client_address}") 62 | 63 | while True: 64 | message = client_socket.recv(1024).decode('utf-8') 65 | if not message: 66 | break 67 | print(f"Mesaj de la {client_name}: {message}") 68 | # Transmite mesajul tuturor clienților 69 | self.broadcast(f"{client_name}: {message}", client_socket) 70 | 71 | except ConnectionResetError: 72 | print(f"Conexiunea cu {client_address} a fost întrerupta.") 73 | 74 | print(f"{client_name} s-a deconectat.") 75 | self.clients.remove(client_info) 76 | client_socket.close() 77 | def run(self): 78 | """Rularea serverului pentru a accepta conexiuni noi.""" 79 | try: 80 | while True: 81 | client_socket, client_address = self.server_socket.accept() 82 | threading.Thread(target=self.handle_client, args=(client_socket, client_address)).start() 83 | except KeyboardInterrupt: 84 | self.stop_server() 85 | -------------------------------------------------------------------------------- /src/Tools/kilonova.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import requests 3 | from bs4 import BeautifulSoup 4 | import requests 5 | import sys 6 | import json 7 | import os 8 | from cryptography.fernet import Fernet 9 | import base64 10 | 11 | custom_header = { 12 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0' 13 | } 14 | 15 | def safe_key(key: bytes, filename: str) -> bytes: 16 | with open(filename, 'wb') as file: 17 | file.write(key) 18 | 19 | def load_key(filename: str) -> bytes: 20 | with open(filename, 'rb') as file: 21 | return file.read() 22 | 23 | def contest_info(): 24 | BASE_URL = 'https://kilonova.ro/contests?page=official' 25 | response = requests.get(BASE_URL, headers=custom_header) 26 | soup = BeautifulSoup(response.content, 'html.parser') 27 | first_container = soup.find(class_='c-container mb-2') 28 | contest_name = first_container.find(class_='segment-panel my-1').find('h2').text.strip() 29 | status_paragraph = first_container.find('p', string=lambda t: t and t.startswith('Status:')) 30 | contest_status = ' '.join(status_paragraph.text.split()).replace('Status: ', '') 31 | return contest_name, contest_status 32 | 33 | BASE_URL = 'https://kilonova.ro' 34 | LOGIN_URL = f"{BASE_URL}/api/auth/login" 35 | SUBMIT_URL = f"{BASE_URL}/api/submissions/submit" 36 | 37 | def login_and_submit(win_base, username, password, filepath, problem_id, language="cpp17"): 38 | 39 | with requests.Session() as session: 40 | 41 | login_payload = { 42 | 'username': username, 43 | 'password': password 44 | } 45 | 46 | headers = { 47 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0', 48 | 'Authorization': '' 49 | } 50 | 51 | # Trimite cererea de login 52 | response = session.post(LOGIN_URL, data=login_payload, headers=headers) 53 | 54 | # Verifică dacă autentificarea a fost cu succes și extrage token-ul 55 | if response.status_code == 200: 56 | headers["Authorization"] = response.json()["data"] 57 | else: 58 | win_base.win.status_bar.toggle_inbox_icon(f"Auth error: {response.status_code}, {response.text}") 59 | return 60 | 61 | # Deschide fișierul și construiește payload-ul pentru trimitere 62 | with open(filepath, 'rb') as file: 63 | files = { 64 | 'code': (filepath, file), # Nume și fișierul propriu-zis 65 | } 66 | submit_payload = { 67 | 'problem_id': problem_id, 68 | 'language': language, 69 | } 70 | 71 | submit_response = session.post(SUBMIT_URL, data=submit_payload, files=files, headers=headers) 72 | 73 | try: 74 | key = load_key("app_data_/secret.key") 75 | except FileNotFoundError: 76 | key = Fernet.generate_key() 77 | safe_key(key, "app_data_/secret.key") 78 | fernet = Fernet(key) 79 | if submit_response.status_code == 200: 80 | with open('app_data_/data.json', 'r') as file: 81 | user_login = json.load(file) 82 | username_encrypted = base64.urlsafe_b64encode(fernet.encrypt(username.encode('utf-8'))).decode('utf-8') 83 | password_encrypted = base64.urlsafe_b64encode(fernet.encrypt(password.encode('utf-8'))).decode('utf-8') 84 | user_login['kilonova']['username'] = username_encrypted 85 | user_login['kilonova']['password'] = password_encrypted 86 | with open('app_data_/data.json', 'w') as file: 87 | json.dump(user_login, file, indent=4) 88 | 89 | response_json = submit_response.json() 90 | solution_id = response_json.get("data") 91 | win_base.source_id_label.setText(f"Solution ID: {solution_id}") 92 | sleep(1) 93 | SOLUTION_URL = f"{BASE_URL}/api/submissions/getByID?id={solution_id}" 94 | solution_response = session.get(SOLUTION_URL, headers=headers) 95 | solution_response.raise_for_status() 96 | solution_json = solution_response.json() 97 | score = solution_json.get("data", {}).get("score") 98 | win_base.result_label.setText(f"Score: {score}") 99 | 100 | win_base.win.status_bar.toggle_inbox_icon("Submitted code successfully!") 101 | else: 102 | win_base.win.status_bar.toggle_inbox_icon(f"Error: {submit_response.text}") 103 | print("Status Submit:", submit_response.status_code) 104 | print("Response Submit:", submit_response.text) -------------------------------------------------------------------------------- /src/Tools/pbinfo.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QMainWindow, QLabel, QLineEdit, QPushButton, QTextEdit, QMessageBox, QGridLayout, QWidget 2 | from PySide6.QtGui import QIcon, QFont 3 | from PySide6.QtCore import Qt 4 | import json 5 | import requests 6 | from bs4 import BeautifulSoup 7 | import re 8 | import asyncio 9 | from aiortc import RTCPeerConnection 10 | import time 11 | 12 | class PbinfoInterface(QMainWindow): 13 | BASE_URL = 'https://www.pbinfo.ro' 14 | LOGIN_PAGE_URL = f"{BASE_URL}/login" 15 | LOGIN_URL = f'{BASE_URL}/ajx-module/php-login.php' 16 | PROBLEM_URL = 'https://new.pbinfo.ro/probleme/1/sum' 17 | SUBMIT_URL = 'https://new.pbinfo.ro/probleme/incarcare-solutie/1' 18 | SOLUTION_URL_TEMPLATE = 'https://new.pbinfo.ro/json/solutie/' 19 | 20 | def __init__(self, source_id, result,*args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.source_id_label = source_id 23 | self.result = result 24 | self.login_payload = { 25 | 'user': '', # completare 26 | 'parola': '' # completare 27 | } 28 | self.submit_payload = { 29 | 'csrf': '', 30 | 'sursa': '', # completare 31 | 'limbaj_de_programare': 'cpp', 32 | 'local_ip': '', 33 | 'id': '', # completare 34 | 'id_runda': '0' 35 | } 36 | 37 | def unit(self, username, password, problem_id, source): 38 | 39 | 40 | self.login_payload['user'] = username 41 | self.login_payload['parola'] = password 42 | self.submit_payload['id'] = problem_id 43 | self.submit_payload['sursa'] = source 44 | 45 | self.headers = { 46 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0' 47 | } 48 | self.session = requests.Session() 49 | asyncio.run(self.main()) 50 | 51 | def fetch_login_page(self): 52 | try: 53 | self.response = self.session.get(self.LOGIN_PAGE_URL, headers=self.headers) 54 | self.response.raise_for_status() 55 | return BeautifulSoup(self.response.text, 'html.parser') 56 | except requests.exceptions.RequestException as e: 57 | QMessageBox.critical(self, "Error", f"[Pbinfo - Error]: Error fetching login page: {e}") 58 | return 59 | 60 | def extract_form_token(self, soup): 61 | self.form_token_input = soup.find('input', {'name': 'form_token'}) 62 | if self.form_token_input: 63 | return self.form_token_input.get('value') 64 | else: 65 | QMessageBox.critical(self, "Error", f"[Pbinfo - Error]: Form token not found.") 66 | return 67 | 68 | def get_csrf(self): 69 | problem_response = self.session.get(self.PROBLEM_URL, headers=self.headers) 70 | self.soup = BeautifulSoup(problem_response.text, 'html.parser') 71 | 72 | self.csrf_input = self.soup.find('meta', {'name': 'csrf'}) 73 | if self.csrf_input: 74 | self.csrf_token = self.csrf_input['content'] 75 | return self.csrf_token 76 | else: 77 | QMessageBox.critical(self, "Error", f"[Pbinfo - Error]: CSRF token not found.") 78 | return 79 | 80 | async def get_local_ip(self): 81 | local_ip = None 82 | 83 | def on_ice_candidate(candidate): 84 | nonlocal local_ip 85 | if candidate: 86 | candidate_str = candidate.candidate.split(' ')[4] 87 | if candidate_str: 88 | local_ip = candidate_str 89 | 90 | pc = RTCPeerConnection() 91 | pc.onicecandidate = lambda event: on_ice_candidate(event.candidate) 92 | 93 | # Add a dummy data channel 94 | pc.createDataChannel('dummyChannel') 95 | 96 | await pc.setLocalDescription(await pc.createOffer()) 97 | await asyncio.sleep(2) 98 | 99 | await pc.close() 100 | 101 | return local_ip 102 | 103 | def save_debug_info(self, filename, content): 104 | with open(filename, 'w', encoding='utf-8') as file: 105 | file.write(content) 106 | 107 | def submit_solution(self, local_ip): 108 | csrf_token = self.get_csrf() 109 | 110 | self.submit_payload['csrf'] = csrf_token 111 | self.submit_payload['local_ip'] = local_ip 112 | 113 | files = {key: (None, value) for key, value in self.submit_payload.items()} 114 | 115 | try: 116 | submit_response = self.session.post(self.SUBMIT_URL, files=files, headers=self.headers) 117 | submit_response_converted = json.loads(submit_response.text) 118 | if submit_response_converted.get("raspuns") == "Id problema invalid": 119 | QMessageBox.critical(self, "Error", "Invalid problem ID") 120 | return 121 | submit_response.raise_for_status() 122 | QMessageBox.information(self, "Info", "[Pbinfo]: Solution submitted successfully!") 123 | 124 | match = re.search(r'"id_solutie":(\d+)', submit_response.text) 125 | if match: 126 | solution_id = match.group(1) 127 | return solution_id 128 | else: 129 | print("Solution ID not found in response.") 130 | return 131 | except requests.exceptions.RequestException as e: 132 | QMessageBox.critical(self, "Error", f"[Pbinfo - Error]: Error submitting solution: {e}") 133 | return 134 | 135 | def fetch_solution_score(self, solution_id): 136 | SOLUTION_URL = f"{self.SOLUTION_URL_TEMPLATE}{solution_id}?include_problema" 137 | if solution_id is None: 138 | return 139 | try: 140 | while True: 141 | response = self.session.get(SOLUTION_URL, headers=self.headers) 142 | response.raise_for_status() 143 | 144 | response_json = response.json() 145 | status = response_json['sursa']['status'] 146 | 147 | if status == 'complete': 148 | score = response_json['sursa']['scor'] 149 | self.result.setText(f"Score: {score}") 150 | break 151 | else: 152 | self.result.setText("Score: Evaluating...") 153 | 154 | time.sleep(5) # Așteaptă 5 secunde înainte de a reîncerca 155 | 156 | except requests.exceptions.RequestException as e: 157 | QMessageBox.critical(self, "Error", f"[Pbinfo - Error]: Error fetching solution score: {e}") 158 | except json.JSONDecodeError as je: 159 | print("Error decoding JSON response:", je) 160 | except Exception as ex: 161 | print("An unexpected error occurred:", ex) 162 | 163 | def login(self): 164 | soup = self.fetch_login_page() 165 | form_token = self.extract_form_token(soup) 166 | self.login_payload['form_token'] = form_token 167 | 168 | files = {key: (None, value) for key, value in self.login_payload.items()} 169 | 170 | try: 171 | login_response = self.session.post(self.LOGIN_URL, files=files, headers=self.headers) 172 | login_response.raise_for_status() 173 | return login_response 174 | except requests.exceptions.RequestException as e: 175 | QMessageBox.critical(self, "Error", f"[Pbinfo - Error]: Error during login attempt: {e}") 176 | return 177 | 178 | async def main(self): 179 | login_response = self.login() 180 | login_response_converted = json.loads(login_response.text) 181 | if login_response_converted.get("raspuns") == "Utilizator/parola incorecte!": 182 | QMessageBox.critical(self, "Error", "Login failed: incorrect user/password") 183 | return 184 | local_ip = await self.get_local_ip() 185 | solution_id = self.submit_solution(local_ip) 186 | self.source_id_label.setText(f"Solution ID: {solution_id}") 187 | self.fetch_solution_score(solution_id) 188 | with open('app_data_/data.json', 'r') as config_file: 189 | config_data = json.load(config_file) 190 | config_data["pbinfo"]["username"] = self.login_payload['user'] 191 | config_data["pbinfo"]["password"] = self.login_payload['parola'] 192 | with open('app_data_/data.json', 'w') as config_file: 193 | json.dump(config_data, config_file, indent=4) 194 | -------------------------------------------------------------------------------- /src/Tools/scrap.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def get_latest_version_from_github(owner, repo): 4 | url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" 5 | 6 | try: 7 | response = requests.get(url, timeout=5) # Timeout pentru a evita blocarea 8 | response.raise_for_status() # Ridică o excepție pentru coduri de eroare HTTP 9 | 10 | # Extragem versiunea cea mai recentă 11 | latest_version = response.json().get("tag_name", "Unknown version") 12 | return latest_version 13 | 14 | except requests.ConnectionError: 15 | return "No internet connection" 16 | except requests.Timeout: 17 | return "Request timed out" 18 | except requests.RequestException as e: 19 | # Orice altă eroare 20 | return f"Error: {e}" 21 | -------------------------------------------------------------------------------- /src/Update/internal.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import sys 5 | import requests 6 | 7 | def get_current_version(): 8 | if os.path.exists("app_data_/version.json"): 9 | try: 10 | with open("app_data_/version.json", "r") as f: 11 | config_data = json.load(f) 12 | 13 | version = config_data.get("version") 14 | if version: 15 | return version 16 | except (json.JSONDecodeError, KeyError): 17 | pass 18 | return "2.0" 19 | 20 | 21 | def check_for_updates(status_bar): 22 | current_version = get_current_version() 23 | print(f"Current version: {current_version}") 24 | 25 | try: 26 | url = "https://api.github.com/repos/HojdaAdelin/CodeNimble/releases/latest" 27 | 28 | response = requests.get(url) 29 | data = response.json() 30 | 31 | latest_version = data.get("tag_name") 32 | 33 | if latest_version and latest_version > current_version: 34 | print(f"New version available: {latest_version}") 35 | #update() 36 | else: 37 | status_bar.toggle_inbox_icon(f"Current version: {current_version}\nYou already have the latest version.") 38 | print("You already have the latest version.") 39 | 40 | except Exception as e: 41 | print(f"Failed to check for updates: {e}") 42 | 43 | def update(): 44 | print("Closing application to apply update...") 45 | subprocess.Popen([sys.executable, 'update.py']) 46 | sys.exit() -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QApplication 2 | from GUI import gui 3 | import sys 4 | import threading 5 | from Server.competitive_companion import run_flask_server 6 | 7 | def start_flask_server(): 8 | try: 9 | flask_thread = threading.Thread(target=run_flask_server, daemon=True) 10 | flask_thread.start() 11 | print("Serverul Flask on!") 12 | except Exception as e: 13 | print(f"Error: {e}") 14 | 15 | if __name__ == "__main__": 16 | 17 | start_flask_server() 18 | 19 | app = QApplication(sys.argv) 20 | window = gui.MainView() 21 | window.show() 22 | sys.exit(app.exec()) --------------------------------------------------------------------------------