├── .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 |
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())
--------------------------------------------------------------------------------