Initial commit
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | compile_commands.json | ||||||
|  | build | ||||||
|  | ui/edit-window.glade~ | ||||||
|  | ui/settings-window.glade~ | ||||||
|  | ui/main-window.glade~ | ||||||
|  | resources.c | ||||||
							
								
								
									
										53
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | |||||||
|  | cmake_minimum_required(VERSION 3.16.0) | ||||||
|  |  | ||||||
|  | set(CMAKE_C_STANDARD 99) | ||||||
|  | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) | ||||||
|  |  | ||||||
|  | project(Practice_Timer) | ||||||
|  |  | ||||||
|  | set(G_RESOURCE_DEPENDS ui/settings-window.glade ui/main-window.glade ui/edit-window.glade) | ||||||
|  | set(G_RESOURCE ${CMAKE_CURRENT_SOURCE_DIR}/practicetimer.gresource.xml) | ||||||
|  | set(G_RESOURCE_C ${CMAKE_CURRENT_SOURCE_DIR}/resources.c) | ||||||
|  |  | ||||||
|  | find_package(PkgConfig REQUIRED) | ||||||
|  |  | ||||||
|  | pkg_check_modules(GTK REQUIRED gtk+-3.0 json-glib-1.0) | ||||||
|  |  | ||||||
|  | add_compile_definitions(PLOTUTILS_FALLBACK_FORMAT="png" PLOTUTILS_PREFERRED_FORMAT="svg" FILE_WATCHER_POLL_RATE=60 APPLICATION_VERSION="2.2.1") | ||||||
|  |  | ||||||
|  | # Uncomment to force a specific path for plotutils | ||||||
|  | # set(PLOTUTILS_GRAPH_PATH "/path/to/graph") | ||||||
|  |  | ||||||
|  | if(DEFINED PLOTUTILS_GRAPH_PATH) | ||||||
|  |     add_compile_definitions(PLOTUTILS_GRAPH_PATH="${PLOTUTILS_GRAPH_PATH}") | ||||||
|  | elseif(APPLE) | ||||||
|  |     # Fix issues related to path on macOS | ||||||
|  |     find_program(APPLE_GRAPH_PATH "graph") | ||||||
|  |     if (DEFINED APPLE_GRAPH_PATH) | ||||||
|  |         add_compile_definitions(PLOTUTILS_GRAPH_PATH="${APPLE_GRAPH_PATH}") | ||||||
|  |     else() | ||||||
|  |         add_compile_definitions(PLOTUTILS_GRAPH_PATH="") | ||||||
|  |     endif() | ||||||
|  | else() | ||||||
|  |     add_compile_definitions(PLOTUTILS_GRAPH_PATH="graph") | ||||||
|  | endif() | ||||||
|  |  | ||||||
|  | if(APPLE) | ||||||
|  |     add_compile_definitions(IS_APPLE) | ||||||
|  | endif() | ||||||
|  |  | ||||||
|  | include_directories(${GTK_INCLUDE_DIRS}) | ||||||
|  | link_directories(${GTK_LIBRARY_DIRS}) | ||||||
|  | add_definitions(${GTK_CFLAGS_OTHER} "-Wall") | ||||||
|  |  | ||||||
|  | add_custom_command(OUTPUT ${G_RESOURCE_C} MAIN_DEPENDENCY ${G_RESOURCE} DEPENDS ${G_RESOURCE_DEPENDS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PRE_BUILD COMMAND glib-compile-resources ${G_RESOURCE} --target=${G_RESOURCE_C} --generate-source) | ||||||
|  |  | ||||||
|  | add_custom_target(resource_target DEPENDS ${G_REOSURCE_C}) | ||||||
|  |  | ||||||
|  | add_executable(Practice_Timer main.c TimerApplication.c TimerMainWindow.c TimerEditWindow.c TimerSettingsWindow.c TimerClock.c TimerTaskTree.c TimerGraphWindow.c TimerGraph.c TimerFileWatcher.c ${G_RESOURCE_C}) | ||||||
|  |  | ||||||
|  | set_source_files_properties(${G_RESOURCE_C} PROPERTIES GENERATED TRUE) | ||||||
|  |  | ||||||
|  | add_dependencies(Practice_Timer resource_target) | ||||||
|  |  | ||||||
|  | target_link_libraries(Practice_Timer m ${GTK_LIBRARIES}) | ||||||
							
								
								
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,674 @@ | |||||||
|  |                     GNU GENERAL PUBLIC LICENSE | ||||||
|  |                        Version 3, 29 June 2007 | ||||||
|  |  | ||||||
|  |  Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | ||||||
|  |  Everyone is permitted to copy and distribute verbatim copies | ||||||
|  |  of this license document, but changing it is not allowed. | ||||||
|  |  | ||||||
|  |                             Preamble | ||||||
|  |  | ||||||
|  |   The GNU General Public License is a free, copyleft license for | ||||||
|  | software and other kinds of works. | ||||||
|  |  | ||||||
|  |   The licenses for most software and other practical works are designed | ||||||
|  | to take away your freedom to share and change the works.  By contrast, | ||||||
|  | the GNU General Public License is intended to guarantee your freedom to | ||||||
|  | share and change all versions of a program--to make sure it remains free | ||||||
|  | software for all its users.  We, the Free Software Foundation, use the | ||||||
|  | GNU General Public License for most of our software; it applies also to | ||||||
|  | any other work released this way by its authors.  You can apply it to | ||||||
|  | your programs, too. | ||||||
|  |  | ||||||
|  |   When we speak of free software, we are referring to freedom, not | ||||||
|  | price.  Our General Public Licenses are designed to make sure that you | ||||||
|  | have the freedom to distribute copies of free software (and charge for | ||||||
|  | them if you wish), that you receive source code or can get it if you | ||||||
|  | want it, that you can change the software or use pieces of it in new | ||||||
|  | free programs, and that you know you can do these things. | ||||||
|  |  | ||||||
|  |   To protect your rights, we need to prevent others from denying you | ||||||
|  | these rights or asking you to surrender the rights.  Therefore, you have | ||||||
|  | certain responsibilities if you distribute copies of the software, or if | ||||||
|  | you modify it: responsibilities to respect the freedom of others. | ||||||
|  |  | ||||||
|  |   For example, if you distribute copies of such a program, whether | ||||||
|  | gratis or for a fee, you must pass on to the recipients the same | ||||||
|  | freedoms that you received.  You must make sure that they, too, receive | ||||||
|  | or can get the source code.  And you must show them these terms so they | ||||||
|  | know their rights. | ||||||
|  |  | ||||||
|  |   Developers that use the GNU GPL protect your rights with two steps: | ||||||
|  | (1) assert copyright on the software, and (2) offer you this License | ||||||
|  | giving you legal permission to copy, distribute and/or modify it. | ||||||
|  |  | ||||||
|  |   For the developers' and authors' protection, the GPL clearly explains | ||||||
|  | that there is no warranty for this free software.  For both users' and | ||||||
|  | authors' sake, the GPL requires that modified versions be marked as | ||||||
|  | changed, so that their problems will not be attributed erroneously to | ||||||
|  | authors of previous versions. | ||||||
|  |  | ||||||
|  |   Some devices are designed to deny users access to install or run | ||||||
|  | modified versions of the software inside them, although the manufacturer | ||||||
|  | can do so.  This is fundamentally incompatible with the aim of | ||||||
|  | protecting users' freedom to change the software.  The systematic | ||||||
|  | pattern of such abuse occurs in the area of products for individuals to | ||||||
|  | use, which is precisely where it is most unacceptable.  Therefore, we | ||||||
|  | have designed this version of the GPL to prohibit the practice for those | ||||||
|  | products.  If such problems arise substantially in other domains, we | ||||||
|  | stand ready to extend this provision to those domains in future versions | ||||||
|  | of the GPL, as needed to protect the freedom of users. | ||||||
|  |  | ||||||
|  |   Finally, every program is threatened constantly by software patents. | ||||||
|  | States should not allow patents to restrict development and use of | ||||||
|  | software on general-purpose computers, but in those that do, we wish to | ||||||
|  | avoid the special danger that patents applied to a free program could | ||||||
|  | make it effectively proprietary.  To prevent this, the GPL assures that | ||||||
|  | patents cannot be used to render the program non-free. | ||||||
|  |  | ||||||
|  |   The precise terms and conditions for copying, distribution and | ||||||
|  | modification follow. | ||||||
|  |  | ||||||
|  |                        TERMS AND CONDITIONS | ||||||
|  |  | ||||||
|  |   0. Definitions. | ||||||
|  |  | ||||||
|  |   "This License" refers to version 3 of the GNU General Public License. | ||||||
|  |  | ||||||
|  |   "Copyright" also means copyright-like laws that apply to other kinds of | ||||||
|  | works, such as semiconductor masks. | ||||||
|  |  | ||||||
|  |   "The Program" refers to any copyrightable work licensed under this | ||||||
|  | License.  Each licensee is addressed as "you".  "Licensees" and | ||||||
|  | "recipients" may be individuals or organizations. | ||||||
|  |  | ||||||
|  |   To "modify" a work means to copy from or adapt all or part of the work | ||||||
|  | in a fashion requiring copyright permission, other than the making of an | ||||||
|  | exact copy.  The resulting work is called a "modified version" of the | ||||||
|  | earlier work or a work "based on" the earlier work. | ||||||
|  |  | ||||||
|  |   A "covered work" means either the unmodified Program or a work based | ||||||
|  | on the Program. | ||||||
|  |  | ||||||
|  |   To "propagate" a work means to do anything with it that, without | ||||||
|  | permission, would make you directly or secondarily liable for | ||||||
|  | infringement under applicable copyright law, except executing it on a | ||||||
|  | computer or modifying a private copy.  Propagation includes copying, | ||||||
|  | distribution (with or without modification), making available to the | ||||||
|  | public, and in some countries other activities as well. | ||||||
|  |  | ||||||
|  |   To "convey" a work means any kind of propagation that enables other | ||||||
|  | parties to make or receive copies.  Mere interaction with a user through | ||||||
|  | a computer network, with no transfer of a copy, is not conveying. | ||||||
|  |  | ||||||
|  |   An interactive user interface displays "Appropriate Legal Notices" | ||||||
|  | to the extent that it includes a convenient and prominently visible | ||||||
|  | feature that (1) displays an appropriate copyright notice, and (2) | ||||||
|  | tells the user that there is no warranty for the work (except to the | ||||||
|  | extent that warranties are provided), that licensees may convey the | ||||||
|  | work under this License, and how to view a copy of this License.  If | ||||||
|  | the interface presents a list of user commands or options, such as a | ||||||
|  | menu, a prominent item in the list meets this criterion. | ||||||
|  |  | ||||||
|  |   1. Source Code. | ||||||
|  |  | ||||||
|  |   The "source code" for a work means the preferred form of the work | ||||||
|  | for making modifications to it.  "Object code" means any non-source | ||||||
|  | form of a work. | ||||||
|  |  | ||||||
|  |   A "Standard Interface" means an interface that either is an official | ||||||
|  | standard defined by a recognized standards body, or, in the case of | ||||||
|  | interfaces specified for a particular programming language, one that | ||||||
|  | is widely used among developers working in that language. | ||||||
|  |  | ||||||
|  |   The "System Libraries" of an executable work include anything, other | ||||||
|  | than the work as a whole, that (a) is included in the normal form of | ||||||
|  | packaging a Major Component, but which is not part of that Major | ||||||
|  | Component, and (b) serves only to enable use of the work with that | ||||||
|  | Major Component, or to implement a Standard Interface for which an | ||||||
|  | implementation is available to the public in source code form.  A | ||||||
|  | "Major Component", in this context, means a major essential component | ||||||
|  | (kernel, window system, and so on) of the specific operating system | ||||||
|  | (if any) on which the executable work runs, or a compiler used to | ||||||
|  | produce the work, or an object code interpreter used to run it. | ||||||
|  |  | ||||||
|  |   The "Corresponding Source" for a work in object code form means all | ||||||
|  | the source code needed to generate, install, and (for an executable | ||||||
|  | work) run the object code and to modify the work, including scripts to | ||||||
|  | control those activities.  However, it does not include the work's | ||||||
|  | System Libraries, or general-purpose tools or generally available free | ||||||
|  | programs which are used unmodified in performing those activities but | ||||||
|  | which are not part of the work.  For example, Corresponding Source | ||||||
|  | includes interface definition files associated with source files for | ||||||
|  | the work, and the source code for shared libraries and dynamically | ||||||
|  | linked subprograms that the work is specifically designed to require, | ||||||
|  | such as by intimate data communication or control flow between those | ||||||
|  | subprograms and other parts of the work. | ||||||
|  |  | ||||||
|  |   The Corresponding Source need not include anything that users | ||||||
|  | can regenerate automatically from other parts of the Corresponding | ||||||
|  | Source. | ||||||
|  |  | ||||||
|  |   The Corresponding Source for a work in source code form is that | ||||||
|  | same work. | ||||||
|  |  | ||||||
|  |   2. Basic Permissions. | ||||||
|  |  | ||||||
|  |   All rights granted under this License are granted for the term of | ||||||
|  | copyright on the Program, and are irrevocable provided the stated | ||||||
|  | conditions are met.  This License explicitly affirms your unlimited | ||||||
|  | permission to run the unmodified Program.  The output from running a | ||||||
|  | covered work is covered by this License only if the output, given its | ||||||
|  | content, constitutes a covered work.  This License acknowledges your | ||||||
|  | rights of fair use or other equivalent, as provided by copyright law. | ||||||
|  |  | ||||||
|  |   You may make, run and propagate covered works that you do not | ||||||
|  | convey, without conditions so long as your license otherwise remains | ||||||
|  | in force.  You may convey covered works to others for the sole purpose | ||||||
|  | of having them make modifications exclusively for you, or provide you | ||||||
|  | with facilities for running those works, provided that you comply with | ||||||
|  | the terms of this License in conveying all material for which you do | ||||||
|  | not control copyright.  Those thus making or running the covered works | ||||||
|  | for you must do so exclusively on your behalf, under your direction | ||||||
|  | and control, on terms that prohibit them from making any copies of | ||||||
|  | your copyrighted material outside their relationship with you. | ||||||
|  |  | ||||||
|  |   Conveying under any other circumstances is permitted solely under | ||||||
|  | the conditions stated below.  Sublicensing is not allowed; section 10 | ||||||
|  | makes it unnecessary. | ||||||
|  |  | ||||||
|  |   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||||
|  |  | ||||||
|  |   No covered work shall be deemed part of an effective technological | ||||||
|  | measure under any applicable law fulfilling obligations under article | ||||||
|  | 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||||
|  | similar laws prohibiting or restricting circumvention of such | ||||||
|  | measures. | ||||||
|  |  | ||||||
|  |   When you convey a covered work, you waive any legal power to forbid | ||||||
|  | circumvention of technological measures to the extent such circumvention | ||||||
|  | is effected by exercising rights under this License with respect to | ||||||
|  | the covered work, and you disclaim any intention to limit operation or | ||||||
|  | modification of the work as a means of enforcing, against the work's | ||||||
|  | users, your or third parties' legal rights to forbid circumvention of | ||||||
|  | technological measures. | ||||||
|  |  | ||||||
|  |   4. Conveying Verbatim Copies. | ||||||
|  |  | ||||||
|  |   You may convey verbatim copies of the Program's source code as you | ||||||
|  | receive it, in any medium, provided that you conspicuously and | ||||||
|  | appropriately publish on each copy an appropriate copyright notice; | ||||||
|  | keep intact all notices stating that this License and any | ||||||
|  | non-permissive terms added in accord with section 7 apply to the code; | ||||||
|  | keep intact all notices of the absence of any warranty; and give all | ||||||
|  | recipients a copy of this License along with the Program. | ||||||
|  |  | ||||||
|  |   You may charge any price or no price for each copy that you convey, | ||||||
|  | and you may offer support or warranty protection for a fee. | ||||||
|  |  | ||||||
|  |   5. Conveying Modified Source Versions. | ||||||
|  |  | ||||||
|  |   You may convey a work based on the Program, or the modifications to | ||||||
|  | produce it from the Program, in the form of source code under the | ||||||
|  | terms of section 4, provided that you also meet all of these conditions: | ||||||
|  |  | ||||||
|  |     a) The work must carry prominent notices stating that you modified | ||||||
|  |     it, and giving a relevant date. | ||||||
|  |  | ||||||
|  |     b) The work must carry prominent notices stating that it is | ||||||
|  |     released under this License and any conditions added under section | ||||||
|  |     7.  This requirement modifies the requirement in section 4 to | ||||||
|  |     "keep intact all notices". | ||||||
|  |  | ||||||
|  |     c) You must license the entire work, as a whole, under this | ||||||
|  |     License to anyone who comes into possession of a copy.  This | ||||||
|  |     License will therefore apply, along with any applicable section 7 | ||||||
|  |     additional terms, to the whole of the work, and all its parts, | ||||||
|  |     regardless of how they are packaged.  This License gives no | ||||||
|  |     permission to license the work in any other way, but it does not | ||||||
|  |     invalidate such permission if you have separately received it. | ||||||
|  |  | ||||||
|  |     d) If the work has interactive user interfaces, each must display | ||||||
|  |     Appropriate Legal Notices; however, if the Program has interactive | ||||||
|  |     interfaces that do not display Appropriate Legal Notices, your | ||||||
|  |     work need not make them do so. | ||||||
|  |  | ||||||
|  |   A compilation of a covered work with other separate and independent | ||||||
|  | works, which are not by their nature extensions of the covered work, | ||||||
|  | and which are not combined with it such as to form a larger program, | ||||||
|  | in or on a volume of a storage or distribution medium, is called an | ||||||
|  | "aggregate" if the compilation and its resulting copyright are not | ||||||
|  | used to limit the access or legal rights of the compilation's users | ||||||
|  | beyond what the individual works permit.  Inclusion of a covered work | ||||||
|  | in an aggregate does not cause this License to apply to the other | ||||||
|  | parts of the aggregate. | ||||||
|  |  | ||||||
|  |   6. Conveying Non-Source Forms. | ||||||
|  |  | ||||||
|  |   You may convey a covered work in object code form under the terms | ||||||
|  | of sections 4 and 5, provided that you also convey the | ||||||
|  | machine-readable Corresponding Source under the terms of this License, | ||||||
|  | in one of these ways: | ||||||
|  |  | ||||||
|  |     a) Convey the object code in, or embodied in, a physical product | ||||||
|  |     (including a physical distribution medium), accompanied by the | ||||||
|  |     Corresponding Source fixed on a durable physical medium | ||||||
|  |     customarily used for software interchange. | ||||||
|  |  | ||||||
|  |     b) Convey the object code in, or embodied in, a physical product | ||||||
|  |     (including a physical distribution medium), accompanied by a | ||||||
|  |     written offer, valid for at least three years and valid for as | ||||||
|  |     long as you offer spare parts or customer support for that product | ||||||
|  |     model, to give anyone who possesses the object code either (1) a | ||||||
|  |     copy of the Corresponding Source for all the software in the | ||||||
|  |     product that is covered by this License, on a durable physical | ||||||
|  |     medium customarily used for software interchange, for a price no | ||||||
|  |     more than your reasonable cost of physically performing this | ||||||
|  |     conveying of source, or (2) access to copy the | ||||||
|  |     Corresponding Source from a network server at no charge. | ||||||
|  |  | ||||||
|  |     c) Convey individual copies of the object code with a copy of the | ||||||
|  |     written offer to provide the Corresponding Source.  This | ||||||
|  |     alternative is allowed only occasionally and noncommercially, and | ||||||
|  |     only if you received the object code with such an offer, in accord | ||||||
|  |     with subsection 6b. | ||||||
|  |  | ||||||
|  |     d) Convey the object code by offering access from a designated | ||||||
|  |     place (gratis or for a charge), and offer equivalent access to the | ||||||
|  |     Corresponding Source in the same way through the same place at no | ||||||
|  |     further charge.  You need not require recipients to copy the | ||||||
|  |     Corresponding Source along with the object code.  If the place to | ||||||
|  |     copy the object code is a network server, the Corresponding Source | ||||||
|  |     may be on a different server (operated by you or a third party) | ||||||
|  |     that supports equivalent copying facilities, provided you maintain | ||||||
|  |     clear directions next to the object code saying where to find the | ||||||
|  |     Corresponding Source.  Regardless of what server hosts the | ||||||
|  |     Corresponding Source, you remain obligated to ensure that it is | ||||||
|  |     available for as long as needed to satisfy these requirements. | ||||||
|  |  | ||||||
|  |     e) Convey the object code using peer-to-peer transmission, provided | ||||||
|  |     you inform other peers where the object code and Corresponding | ||||||
|  |     Source of the work are being offered to the general public at no | ||||||
|  |     charge under subsection 6d. | ||||||
|  |  | ||||||
|  |   A separable portion of the object code, whose source code is excluded | ||||||
|  | from the Corresponding Source as a System Library, need not be | ||||||
|  | included in conveying the object code work. | ||||||
|  |  | ||||||
|  |   A "User Product" is either (1) a "consumer product", which means any | ||||||
|  | tangible personal property which is normally used for personal, family, | ||||||
|  | or household purposes, or (2) anything designed or sold for incorporation | ||||||
|  | into a dwelling.  In determining whether a product is a consumer product, | ||||||
|  | doubtful cases shall be resolved in favor of coverage.  For a particular | ||||||
|  | product received by a particular user, "normally used" refers to a | ||||||
|  | typical or common use of that class of product, regardless of the status | ||||||
|  | of the particular user or of the way in which the particular user | ||||||
|  | actually uses, or expects or is expected to use, the product.  A product | ||||||
|  | is a consumer product regardless of whether the product has substantial | ||||||
|  | commercial, industrial or non-consumer uses, unless such uses represent | ||||||
|  | the only significant mode of use of the product. | ||||||
|  |  | ||||||
|  |   "Installation Information" for a User Product means any methods, | ||||||
|  | procedures, authorization keys, or other information required to install | ||||||
|  | and execute modified versions of a covered work in that User Product from | ||||||
|  | a modified version of its Corresponding Source.  The information must | ||||||
|  | suffice to ensure that the continued functioning of the modified object | ||||||
|  | code is in no case prevented or interfered with solely because | ||||||
|  | modification has been made. | ||||||
|  |  | ||||||
|  |   If you convey an object code work under this section in, or with, or | ||||||
|  | specifically for use in, a User Product, and the conveying occurs as | ||||||
|  | part of a transaction in which the right of possession and use of the | ||||||
|  | User Product is transferred to the recipient in perpetuity or for a | ||||||
|  | fixed term (regardless of how the transaction is characterized), the | ||||||
|  | Corresponding Source conveyed under this section must be accompanied | ||||||
|  | by the Installation Information.  But this requirement does not apply | ||||||
|  | if neither you nor any third party retains the ability to install | ||||||
|  | modified object code on the User Product (for example, the work has | ||||||
|  | been installed in ROM). | ||||||
|  |  | ||||||
|  |   The requirement to provide Installation Information does not include a | ||||||
|  | requirement to continue to provide support service, warranty, or updates | ||||||
|  | for a work that has been modified or installed by the recipient, or for | ||||||
|  | the User Product in which it has been modified or installed.  Access to a | ||||||
|  | network may be denied when the modification itself materially and | ||||||
|  | adversely affects the operation of the network or violates the rules and | ||||||
|  | protocols for communication across the network. | ||||||
|  |  | ||||||
|  |   Corresponding Source conveyed, and Installation Information provided, | ||||||
|  | in accord with this section must be in a format that is publicly | ||||||
|  | documented (and with an implementation available to the public in | ||||||
|  | source code form), and must require no special password or key for | ||||||
|  | unpacking, reading or copying. | ||||||
|  |  | ||||||
|  |   7. Additional Terms. | ||||||
|  |  | ||||||
|  |   "Additional permissions" are terms that supplement the terms of this | ||||||
|  | License by making exceptions from one or more of its conditions. | ||||||
|  | Additional permissions that are applicable to the entire Program shall | ||||||
|  | be treated as though they were included in this License, to the extent | ||||||
|  | that they are valid under applicable law.  If additional permissions | ||||||
|  | apply only to part of the Program, that part may be used separately | ||||||
|  | under those permissions, but the entire Program remains governed by | ||||||
|  | this License without regard to the additional permissions. | ||||||
|  |  | ||||||
|  |   When you convey a copy of a covered work, you may at your option | ||||||
|  | remove any additional permissions from that copy, or from any part of | ||||||
|  | it.  (Additional permissions may be written to require their own | ||||||
|  | removal in certain cases when you modify the work.)  You may place | ||||||
|  | additional permissions on material, added by you to a covered work, | ||||||
|  | for which you have or can give appropriate copyright permission. | ||||||
|  |  | ||||||
|  |   Notwithstanding any other provision of this License, for material you | ||||||
|  | add to a covered work, you may (if authorized by the copyright holders of | ||||||
|  | that material) supplement the terms of this License with terms: | ||||||
|  |  | ||||||
|  |     a) Disclaiming warranty or limiting liability differently from the | ||||||
|  |     terms of sections 15 and 16 of this License; or | ||||||
|  |  | ||||||
|  |     b) Requiring preservation of specified reasonable legal notices or | ||||||
|  |     author attributions in that material or in the Appropriate Legal | ||||||
|  |     Notices displayed by works containing it; or | ||||||
|  |  | ||||||
|  |     c) Prohibiting misrepresentation of the origin of that material, or | ||||||
|  |     requiring that modified versions of such material be marked in | ||||||
|  |     reasonable ways as different from the original version; or | ||||||
|  |  | ||||||
|  |     d) Limiting the use for publicity purposes of names of licensors or | ||||||
|  |     authors of the material; or | ||||||
|  |  | ||||||
|  |     e) Declining to grant rights under trademark law for use of some | ||||||
|  |     trade names, trademarks, or service marks; or | ||||||
|  |  | ||||||
|  |     f) Requiring indemnification of licensors and authors of that | ||||||
|  |     material by anyone who conveys the material (or modified versions of | ||||||
|  |     it) with contractual assumptions of liability to the recipient, for | ||||||
|  |     any liability that these contractual assumptions directly impose on | ||||||
|  |     those licensors and authors. | ||||||
|  |  | ||||||
|  |   All other non-permissive additional terms are considered "further | ||||||
|  | restrictions" within the meaning of section 10.  If the Program as you | ||||||
|  | received it, or any part of it, contains a notice stating that it is | ||||||
|  | governed by this License along with a term that is a further | ||||||
|  | restriction, you may remove that term.  If a license document contains | ||||||
|  | a further restriction but permits relicensing or conveying under this | ||||||
|  | License, you may add to a covered work material governed by the terms | ||||||
|  | of that license document, provided that the further restriction does | ||||||
|  | not survive such relicensing or conveying. | ||||||
|  |  | ||||||
|  |   If you add terms to a covered work in accord with this section, you | ||||||
|  | must place, in the relevant source files, a statement of the | ||||||
|  | additional terms that apply to those files, or a notice indicating | ||||||
|  | where to find the applicable terms. | ||||||
|  |  | ||||||
|  |   Additional terms, permissive or non-permissive, may be stated in the | ||||||
|  | form of a separately written license, or stated as exceptions; | ||||||
|  | the above requirements apply either way. | ||||||
|  |  | ||||||
|  |   8. Termination. | ||||||
|  |  | ||||||
|  |   You may not propagate or modify a covered work except as expressly | ||||||
|  | provided under this License.  Any attempt otherwise to propagate or | ||||||
|  | modify it is void, and will automatically terminate your rights under | ||||||
|  | this License (including any patent licenses granted under the third | ||||||
|  | paragraph of section 11). | ||||||
|  |  | ||||||
|  |   However, if you cease all violation of this License, then your | ||||||
|  | license from a particular copyright holder is reinstated (a) | ||||||
|  | provisionally, unless and until the copyright holder explicitly and | ||||||
|  | finally terminates your license, and (b) permanently, if the copyright | ||||||
|  | holder fails to notify you of the violation by some reasonable means | ||||||
|  | prior to 60 days after the cessation. | ||||||
|  |  | ||||||
|  |   Moreover, your license from a particular copyright holder is | ||||||
|  | reinstated permanently if the copyright holder notifies you of the | ||||||
|  | violation by some reasonable means, this is the first time you have | ||||||
|  | received notice of violation of this License (for any work) from that | ||||||
|  | copyright holder, and you cure the violation prior to 30 days after | ||||||
|  | your receipt of the notice. | ||||||
|  |  | ||||||
|  |   Termination of your rights under this section does not terminate the | ||||||
|  | licenses of parties who have received copies or rights from you under | ||||||
|  | this License.  If your rights have been terminated and not permanently | ||||||
|  | reinstated, you do not qualify to receive new licenses for the same | ||||||
|  | material under section 10. | ||||||
|  |  | ||||||
|  |   9. Acceptance Not Required for Having Copies. | ||||||
|  |  | ||||||
|  |   You are not required to accept this License in order to receive or | ||||||
|  | run a copy of the Program.  Ancillary propagation of a covered work | ||||||
|  | occurring solely as a consequence of using peer-to-peer transmission | ||||||
|  | to receive a copy likewise does not require acceptance.  However, | ||||||
|  | nothing other than this License grants you permission to propagate or | ||||||
|  | modify any covered work.  These actions infringe copyright if you do | ||||||
|  | not accept this License.  Therefore, by modifying or propagating a | ||||||
|  | covered work, you indicate your acceptance of this License to do so. | ||||||
|  |  | ||||||
|  |   10. Automatic Licensing of Downstream Recipients. | ||||||
|  |  | ||||||
|  |   Each time you convey a covered work, the recipient automatically | ||||||
|  | receives a license from the original licensors, to run, modify and | ||||||
|  | propagate that work, subject to this License.  You are not responsible | ||||||
|  | for enforcing compliance by third parties with this License. | ||||||
|  |  | ||||||
|  |   An "entity transaction" is a transaction transferring control of an | ||||||
|  | organization, or substantially all assets of one, or subdividing an | ||||||
|  | organization, or merging organizations.  If propagation of a covered | ||||||
|  | work results from an entity transaction, each party to that | ||||||
|  | transaction who receives a copy of the work also receives whatever | ||||||
|  | licenses to the work the party's predecessor in interest had or could | ||||||
|  | give under the previous paragraph, plus a right to possession of the | ||||||
|  | Corresponding Source of the work from the predecessor in interest, if | ||||||
|  | the predecessor has it or can get it with reasonable efforts. | ||||||
|  |  | ||||||
|  |   You may not impose any further restrictions on the exercise of the | ||||||
|  | rights granted or affirmed under this License.  For example, you may | ||||||
|  | not impose a license fee, royalty, or other charge for exercise of | ||||||
|  | rights granted under this License, and you may not initiate litigation | ||||||
|  | (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||||
|  | any patent claim is infringed by making, using, selling, offering for | ||||||
|  | sale, or importing the Program or any portion of it. | ||||||
|  |  | ||||||
|  |   11. Patents. | ||||||
|  |  | ||||||
|  |   A "contributor" is a copyright holder who authorizes use under this | ||||||
|  | License of the Program or a work on which the Program is based.  The | ||||||
|  | work thus licensed is called the contributor's "contributor version". | ||||||
|  |  | ||||||
|  |   A contributor's "essential patent claims" are all patent claims | ||||||
|  | owned or controlled by the contributor, whether already acquired or | ||||||
|  | hereafter acquired, that would be infringed by some manner, permitted | ||||||
|  | by this License, of making, using, or selling its contributor version, | ||||||
|  | but do not include claims that would be infringed only as a | ||||||
|  | consequence of further modification of the contributor version.  For | ||||||
|  | purposes of this definition, "control" includes the right to grant | ||||||
|  | patent sublicenses in a manner consistent with the requirements of | ||||||
|  | this License. | ||||||
|  |  | ||||||
|  |   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||||
|  | patent license under the contributor's essential patent claims, to | ||||||
|  | make, use, sell, offer for sale, import and otherwise run, modify and | ||||||
|  | propagate the contents of its contributor version. | ||||||
|  |  | ||||||
|  |   In the following three paragraphs, a "patent license" is any express | ||||||
|  | agreement or commitment, however denominated, not to enforce a patent | ||||||
|  | (such as an express permission to practice a patent or covenant not to | ||||||
|  | sue for patent infringement).  To "grant" such a patent license to a | ||||||
|  | party means to make such an agreement or commitment not to enforce a | ||||||
|  | patent against the party. | ||||||
|  |  | ||||||
|  |   If you convey a covered work, knowingly relying on a patent license, | ||||||
|  | and the Corresponding Source of the work is not available for anyone | ||||||
|  | to copy, free of charge and under the terms of this License, through a | ||||||
|  | publicly available network server or other readily accessible means, | ||||||
|  | then you must either (1) cause the Corresponding Source to be so | ||||||
|  | available, or (2) arrange to deprive yourself of the benefit of the | ||||||
|  | patent license for this particular work, or (3) arrange, in a manner | ||||||
|  | consistent with the requirements of this License, to extend the patent | ||||||
|  | license to downstream recipients.  "Knowingly relying" means you have | ||||||
|  | actual knowledge that, but for the patent license, your conveying the | ||||||
|  | covered work in a country, or your recipient's use of the covered work | ||||||
|  | in a country, would infringe one or more identifiable patents in that | ||||||
|  | country that you have reason to believe are valid. | ||||||
|  |  | ||||||
|  |   If, pursuant to or in connection with a single transaction or | ||||||
|  | arrangement, you convey, or propagate by procuring conveyance of, a | ||||||
|  | covered work, and grant a patent license to some of the parties | ||||||
|  | receiving the covered work authorizing them to use, propagate, modify | ||||||
|  | or convey a specific copy of the covered work, then the patent license | ||||||
|  | you grant is automatically extended to all recipients of the covered | ||||||
|  | work and works based on it. | ||||||
|  |  | ||||||
|  |   A patent license is "discriminatory" if it does not include within | ||||||
|  | the scope of its coverage, prohibits the exercise of, or is | ||||||
|  | conditioned on the non-exercise of one or more of the rights that are | ||||||
|  | specifically granted under this License.  You may not convey a covered | ||||||
|  | work if you are a party to an arrangement with a third party that is | ||||||
|  | in the business of distributing software, under which you make payment | ||||||
|  | to the third party based on the extent of your activity of conveying | ||||||
|  | the work, and under which the third party grants, to any of the | ||||||
|  | parties who would receive the covered work from you, a discriminatory | ||||||
|  | patent license (a) in connection with copies of the covered work | ||||||
|  | conveyed by you (or copies made from those copies), or (b) primarily | ||||||
|  | for and in connection with specific products or compilations that | ||||||
|  | contain the covered work, unless you entered into that arrangement, | ||||||
|  | or that patent license was granted, prior to 28 March 2007. | ||||||
|  |  | ||||||
|  |   Nothing in this License shall be construed as excluding or limiting | ||||||
|  | any implied license or other defenses to infringement that may | ||||||
|  | otherwise be available to you under applicable patent law. | ||||||
|  |  | ||||||
|  |   12. No Surrender of Others' Freedom. | ||||||
|  |  | ||||||
|  |   If conditions are imposed on you (whether by court order, agreement or | ||||||
|  | otherwise) that contradict the conditions of this License, they do not | ||||||
|  | excuse you from the conditions of this License.  If you cannot convey a | ||||||
|  | covered work so as to satisfy simultaneously your obligations under this | ||||||
|  | License and any other pertinent obligations, then as a consequence you may | ||||||
|  | not convey it at all.  For example, if you agree to terms that obligate you | ||||||
|  | to collect a royalty for further conveying from those to whom you convey | ||||||
|  | the Program, the only way you could satisfy both those terms and this | ||||||
|  | License would be to refrain entirely from conveying the Program. | ||||||
|  |  | ||||||
|  |   13. Use with the GNU Affero General Public License. | ||||||
|  |  | ||||||
|  |   Notwithstanding any other provision of this License, you have | ||||||
|  | permission to link or combine any covered work with a work licensed | ||||||
|  | under version 3 of the GNU Affero General Public License into a single | ||||||
|  | combined work, and to convey the resulting work.  The terms of this | ||||||
|  | License will continue to apply to the part which is the covered work, | ||||||
|  | but the special requirements of the GNU Affero General Public License, | ||||||
|  | section 13, concerning interaction through a network will apply to the | ||||||
|  | combination as such. | ||||||
|  |  | ||||||
|  |   14. Revised Versions of this License. | ||||||
|  |  | ||||||
|  |   The Free Software Foundation may publish revised and/or new versions of | ||||||
|  | the GNU General Public License from time to time.  Such new versions will | ||||||
|  | be similar in spirit to the present version, but may differ in detail to | ||||||
|  | address new problems or concerns. | ||||||
|  |  | ||||||
|  |   Each version is given a distinguishing version number.  If the | ||||||
|  | Program specifies that a certain numbered version of the GNU General | ||||||
|  | Public License "or any later version" applies to it, you have the | ||||||
|  | option of following the terms and conditions either of that numbered | ||||||
|  | version or of any later version published by the Free Software | ||||||
|  | Foundation.  If the Program does not specify a version number of the | ||||||
|  | GNU General Public License, you may choose any version ever published | ||||||
|  | by the Free Software Foundation. | ||||||
|  |  | ||||||
|  |   If the Program specifies that a proxy can decide which future | ||||||
|  | versions of the GNU General Public License can be used, that proxy's | ||||||
|  | public statement of acceptance of a version permanently authorizes you | ||||||
|  | to choose that version for the Program. | ||||||
|  |  | ||||||
|  |   Later license versions may give you additional or different | ||||||
|  | permissions.  However, no additional obligations are imposed on any | ||||||
|  | author or copyright holder as a result of your choosing to follow a | ||||||
|  | later version. | ||||||
|  |  | ||||||
|  |   15. Disclaimer of Warranty. | ||||||
|  |  | ||||||
|  |   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||||
|  | APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||||
|  | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||||
|  | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||||
|  | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||||
|  | PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||||
|  | IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||||
|  | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||||
|  |  | ||||||
|  |   16. Limitation of Liability. | ||||||
|  |  | ||||||
|  |   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||||
|  | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||||
|  | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||||
|  | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||||
|  | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||||
|  | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||||
|  | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||||
|  | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||||
|  | SUCH DAMAGES. | ||||||
|  |  | ||||||
|  |   17. Interpretation of Sections 15 and 16. | ||||||
|  |  | ||||||
|  |   If the disclaimer of warranty and limitation of liability provided | ||||||
|  | above cannot be given local legal effect according to their terms, | ||||||
|  | reviewing courts shall apply local law that most closely approximates | ||||||
|  | an absolute waiver of all civil liability in connection with the | ||||||
|  | Program, unless a warranty or assumption of liability accompanies a | ||||||
|  | copy of the Program in return for a fee. | ||||||
|  |  | ||||||
|  |                      END OF TERMS AND CONDITIONS | ||||||
|  |  | ||||||
|  |             How to Apply These Terms to Your New Programs | ||||||
|  |  | ||||||
|  |   If you develop a new program, and you want it to be of the greatest | ||||||
|  | possible use to the public, the best way to achieve this is to make it | ||||||
|  | free software which everyone can redistribute and change under these terms. | ||||||
|  |  | ||||||
|  |   To do so, attach the following notices to the program.  It is safest | ||||||
|  | to attach them to the start of each source file to most effectively | ||||||
|  | state the exclusion of warranty; and each file should have at least | ||||||
|  | the "copyright" line and a pointer to where the full notice is found. | ||||||
|  |  | ||||||
|  |     <one line to give the program's name and a brief idea of what it does.> | ||||||
|  |     Copyright (C) <year>  <name of author> | ||||||
|  |  | ||||||
|  |     This program is free software: you can redistribute it and/or modify | ||||||
|  |     it under the terms of the GNU General Public License as published by | ||||||
|  |     the Free Software Foundation, either version 3 of the License, or | ||||||
|  |     (at your option) any later version. | ||||||
|  |  | ||||||
|  |     This program is distributed in the hope that it will be useful, | ||||||
|  |     but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |     GNU General Public License for more details. | ||||||
|  |  | ||||||
|  |     You should have received a copy of the GNU General Public License | ||||||
|  |     along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | Also add information on how to contact you by electronic and paper mail. | ||||||
|  |  | ||||||
|  |   If the program does terminal interaction, make it output a short | ||||||
|  | notice like this when it starts in an interactive mode: | ||||||
|  |  | ||||||
|  |     <program>  Copyright (C) <year>  <name of author> | ||||||
|  |     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||||
|  |     This is free software, and you are welcome to redistribute it | ||||||
|  |     under certain conditions; type `show c' for details. | ||||||
|  |  | ||||||
|  | The hypothetical commands `show w' and `show c' should show the appropriate | ||||||
|  | parts of the General Public License.  Of course, your program's commands | ||||||
|  | might be different; for a GUI interface, you would use an "about box". | ||||||
|  |  | ||||||
|  |   You should also get your employer (if you work as a programmer) or school, | ||||||
|  | if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||||
|  | For more information on this, and how to apply and follow the GNU GPL, see | ||||||
|  | <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  |   The GNU General Public License does not permit incorporating your program | ||||||
|  | into proprietary programs.  If your program is a subroutine library, you | ||||||
|  | may consider it more useful to permit linking proprietary applications with | ||||||
|  | the library.  If this is what you want to do, use the GNU Lesser General | ||||||
|  | Public License instead of this License.  But first, please read | ||||||
|  | <https://www.gnu.org/licenses/why-not-lgpl.html>. | ||||||
							
								
								
									
										6
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | # practice-timer | ||||||
|  | A simple program for tracking time spent doing activities. Originally created for my dad to track his music practice. | ||||||
|  |  | ||||||
|  | Build with CMake. Requires GTK+ 3, json-glib, and PkgConfig for CMake. The graphs on the stats page are optional, but they require Plotutils. | ||||||
|  |  | ||||||
|  | Tested on macOS, GNU/Linux, and FreeBSD. | ||||||
							
								
								
									
										93
									
								
								TimerApplication.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								TimerApplication.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | |||||||
|  | #include "TimerApplication.h" | ||||||
|  | #include "TimerMainWindow.h" | ||||||
|  |  | ||||||
|  | #include <sys/stat.h> | ||||||
|  |  | ||||||
|  | #define TIMER_CONFIG_FILE_NAME "settings.conf" | ||||||
|  | #define TIMER_DEFAULT_DATA_FILE_NAME "data.json" | ||||||
|  |  | ||||||
|  | typedef struct { | ||||||
|  |     gboolean versionSwitch; | ||||||
|  | } TimerOptions; | ||||||
|  |  | ||||||
|  | struct _TimerApplication { | ||||||
|  |     GtkApplication parent; | ||||||
|  |  | ||||||
|  |     char *configDir; | ||||||
|  |     char *configFile; | ||||||
|  |     char *defaultDataFile; | ||||||
|  |     TimerOptions options; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerApplication, timer_application, GTK_TYPE_APPLICATION); | ||||||
|  |  | ||||||
|  | static void timer_application_add_options(TimerApplication *app) { | ||||||
|  |     const GOptionEntry entries[] = { | ||||||
|  |         { | ||||||
|  |             .long_name = "version", | ||||||
|  |             .short_name = 'v', | ||||||
|  |             .flags = G_OPTION_FLAG_NONE, | ||||||
|  |             .arg = G_OPTION_ARG_NONE, | ||||||
|  |             .arg_data = &(app->options.versionSwitch), | ||||||
|  |             .description = "Print the version and then exit", | ||||||
|  |             .arg_description = NULL | ||||||
|  |         }, | ||||||
|  |         {NULL} | ||||||
|  |     }; | ||||||
|  |     g_application_add_main_option_entries(G_APPLICATION(app), entries); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_application_check_command_line_options(TimerApplication *self) { | ||||||
|  |     if (self->options.versionSwitch) { | ||||||
|  |         printf("%s\n", APPLICATION_VERSION); | ||||||
|  |         exit(EXIT_SUCCESS); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_application_activate(GApplication *app) { | ||||||
|  |     timer_application_check_command_line_options(TIMER_APPLICATION(app)); | ||||||
|  |     if (g_mkdir_with_parents(TIMER_APPLICATION(app)->configDir, 0700) != 0) { | ||||||
|  |         GtkWidget *msgDiag = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Could not create config dir: %s", strerror(errno)); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |         gtk_widget_destroy(msgDiag); | ||||||
|  |     } | ||||||
|  |     TimerMainWindow *win = timer_main_window_new(TIMER_APPLICATION(app)); | ||||||
|  |     gtk_window_present(GTK_WINDOW(win)); | ||||||
|  | #ifdef IS_APPLE | ||||||
|  |     if (timer_main_window_is_always_on_top(win)) { | ||||||
|  |         gtk_window_set_keep_above(GTK_WINDOW(win), TRUE); | ||||||
|  |     } | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerApplication *timer_application_new() { | ||||||
|  |     return g_object_new(TIMER_TYPE_APPLICATION, "application-id", "org.zander.practicetiemr", "flags", G_APPLICATION_FLAGS_NONE, NULL); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const char *timer_application_get_config_file(TimerApplication *self) { | ||||||
|  |     return self->configFile; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const char *timer_application_get_default_data_file(TimerApplication *self) { | ||||||
|  |     return self->defaultDataFile; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_application_finalize(GObject *self) { | ||||||
|  |     g_free(TIMER_APPLICATION(self)->configFile); | ||||||
|  |     g_free(TIMER_APPLICATION(self)->configDir); | ||||||
|  |     g_free(TIMER_APPLICATION(self)->defaultDataFile); | ||||||
|  |     G_OBJECT_CLASS(timer_application_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_application_class_init(TimerApplicationClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_application_finalize; | ||||||
|  |     G_APPLICATION_CLASS(class)->activate = timer_application_activate; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_application_init(TimerApplication *self) { | ||||||
|  |     self->configFile = g_strdup_printf("%s/practicetimer/%s", g_get_user_config_dir(), TIMER_CONFIG_FILE_NAME); | ||||||
|  |     self->configDir = g_strdup_printf("%s/practicetimer", g_get_user_config_dir()); | ||||||
|  |     self->defaultDataFile = g_strdup_printf("%s/practicetimer/%s", g_get_user_config_dir(), TIMER_DEFAULT_DATA_FILE_NAME); | ||||||
|  |     timer_application_add_options(self); | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								TimerApplication.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								TimerApplication.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_APPLICATION_H | ||||||
|  | #define INCLUDED_TIMER_APPLICATION_H | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_APPLICATION timer_application_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerApplication, timer_application, TIMER, APPLICATION, GtkApplication); | ||||||
|  |  | ||||||
|  | TimerApplication *timer_application_new(void); | ||||||
|  |  | ||||||
|  | const char *timer_application_get_config_file(TimerApplication *self); | ||||||
|  | const char *timer_application_get_default_data_file(TimerApplication *self); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_APPLICATION_H */ | ||||||
							
								
								
									
										69
									
								
								TimerClock.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								TimerClock.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | #include "TimerClock.h" | ||||||
|  |  | ||||||
|  | struct _TimerClock { | ||||||
|  |     GObject parent; | ||||||
|  |  | ||||||
|  |     TimerClockAction action; | ||||||
|  |     int interval; | ||||||
|  |     gpointer data; | ||||||
|  |     GThread *thread; | ||||||
|  |     gboolean running; | ||||||
|  |  | ||||||
|  |     gint64 lastTick; | ||||||
|  |     gint64 currentTime; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerClock, timer_clock, G_TYPE_OBJECT); | ||||||
|  |  | ||||||
|  | static void timer_clock_thread_callback(TimerClock *self) { | ||||||
|  |     self->lastTick = g_get_monotonic_time() / 1000; | ||||||
|  |     while (self->running) { | ||||||
|  |         guint now = g_get_monotonic_time() / 1000; | ||||||
|  |         self->currentTime = now - self->lastTick; | ||||||
|  |         if (self->currentTime >= self->interval) { | ||||||
|  |             self->action(self->data); | ||||||
|  |             self->lastTick = now; | ||||||
|  |             self->currentTime = 0; | ||||||
|  |         } | ||||||
|  |         g_usleep(50); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerClock *timer_clock_new(int interval, TimerClockAction action, gpointer data) { | ||||||
|  |     TimerClock *o = TIMER_CLOCK(g_object_new(TIMER_TYPE_CLOCK, NULL)); | ||||||
|  |     o->action = action; | ||||||
|  |     o->interval = interval; | ||||||
|  |     o->data = data; | ||||||
|  |     return o; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_clock_start(TimerClock *self) { | ||||||
|  |     self->running = TRUE; | ||||||
|  |     self->thread = g_thread_new("timer", (GThreadFunc) timer_clock_thread_callback, self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_clock_stop(TimerClock *self) { | ||||||
|  |     self->running = FALSE; | ||||||
|  |     g_thread_join(self->thread); | ||||||
|  |     self->thread = NULL; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | gboolean timer_clock_is_running(TimerClock *self) { | ||||||
|  |     return self->running; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_clock_finalize(GObject *self) { | ||||||
|  |     TIMER_CLOCK(self)->running = FALSE; | ||||||
|  |     if (TIMER_CLOCK(self)->thread != NULL) { | ||||||
|  |         g_thread_unref(TIMER_CLOCK(self)->thread); | ||||||
|  |     } | ||||||
|  |     G_OBJECT_CLASS(timer_clock_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_clock_class_init(TimerClockClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_clock_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_clock_init(TimerClock *self) { | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								TimerClock.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								TimerClock.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_CLOCK_H | ||||||
|  | #define INCLUDED_TIMER_CLOCK_H | ||||||
|  |  | ||||||
|  | #include <glib-object.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_CLOCK timer_clock_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerClock, timer_clock, TIMER, CLOCK, GObject); | ||||||
|  |  | ||||||
|  | typedef void(*TimerClockAction)(gpointer); | ||||||
|  |  | ||||||
|  | /* All intervals are in miliseconds (unless otherwise stated) */ | ||||||
|  |  | ||||||
|  | TimerClock *timer_clock_new(int interval, TimerClockAction action, gpointer data); | ||||||
|  |  | ||||||
|  | void timer_clock_start(TimerClock *self); | ||||||
|  |  | ||||||
|  | void timer_clock_stop(TimerClock *self); | ||||||
|  |  | ||||||
|  | gboolean timer_clock_is_running(TimerClock *self); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_CLOCK_H */ | ||||||
							
								
								
									
										460
									
								
								TimerEditWindow.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								TimerEditWindow.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,460 @@ | |||||||
|  | #include "TimerEditWindow.h" | ||||||
|  |  | ||||||
|  | #include <math.h> | ||||||
|  |  | ||||||
|  | enum { TIMER_AM = 0, TIMER_PM = 1 }; | ||||||
|  |  | ||||||
|  | enum { TIMER_LAST_START, TIMER_LAST_END }; | ||||||
|  |  | ||||||
|  | struct _TimerEditWindow { | ||||||
|  |     GtkDialog parent; | ||||||
|  |  | ||||||
|  |     GtkWidget *deleteButton; | ||||||
|  |     GtkWidget *cancelButton; | ||||||
|  |     GtkWidget *saveButton; | ||||||
|  |     GtkWidget *nameBox; | ||||||
|  |     GtkWidget *startCalendar; | ||||||
|  |     GtkWidget *endCalendar; | ||||||
|  |     GtkWidget *endCalCheck; | ||||||
|  |     GtkWidget *startHour; | ||||||
|  |     GtkWidget *startMinute; | ||||||
|  |     GtkWidget *startSecond; | ||||||
|  |     GtkWidget *startLean; | ||||||
|  |     GtkWidget *endHour; | ||||||
|  |     GtkWidget *endMinute; | ||||||
|  |     GtkWidget *endSecond; | ||||||
|  |     GtkWidget *endLean; | ||||||
|  |     GtkWidget *lengthHour; | ||||||
|  |     GtkWidget *lengthMinute; | ||||||
|  |     GtkWidget *lengthSecond; | ||||||
|  |     GtkWidget *followBox; | ||||||
|  |     GtkWidget *followButton; | ||||||
|  |  | ||||||
|  |     GDateTime *lastTask; | ||||||
|  |  | ||||||
|  |     int lastEdit; | ||||||
|  |     gboolean invalidTime; | ||||||
|  |     gboolean enableAutoEdit; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerEditWindow, timer_edit_window, GTK_TYPE_DIALOG); | ||||||
|  |  | ||||||
|  | #define TIMER_MAX_LENGTH ((3600 * 99) + (60 * 59) + 59) | ||||||
|  |  | ||||||
|  | static gboolean compare_dates(GDateTime *dt1, GDateTime *dt2) { | ||||||
|  |     int y1, m1, d1, y2, m2, d2; | ||||||
|  |     g_date_time_get_ymd(dt1, &y1, &m1, &d1); | ||||||
|  |     g_date_time_get_ymd(dt2, &y2, &m2, &d2); | ||||||
|  |     return y1 == y2 && m1 == m2 && d1 == d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void to_leaned_time(/* 0-23 */ int t, /* 1-12*/ int *h, int *l) { | ||||||
|  |     if (t == 0) { | ||||||
|  |         *h = 12; | ||||||
|  |         *l = TIMER_AM; | ||||||
|  |     } else if (t == 12) { | ||||||
|  |         *h = 12; | ||||||
|  |         *l = TIMER_PM; | ||||||
|  |     } else if (t > 12) { | ||||||
|  |         *h = t - 12; | ||||||
|  |         *l = TIMER_PM; | ||||||
|  |     } else { | ||||||
|  |         *h = t; | ||||||
|  |         *l = TIMER_AM; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void to_total_time(/* 1-12 */ int h, int l, /* 0-23 */ int *t) { | ||||||
|  |     if (h == 12 && l == TIMER_AM) { | ||||||
|  |         *t = 0; | ||||||
|  |     } else if (h == 12 && l == TIMER_PM) { | ||||||
|  |         *t = 12; | ||||||
|  |     } else if (l == TIMER_PM) { | ||||||
|  |         *t = h + 12; | ||||||
|  |     } else { | ||||||
|  |         *t = h; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_edit_window_calculate_start(TimerEditWindow *self) { | ||||||
|  |     GDateTime *end = timer_edit_window_get_end(self); | ||||||
|  |     gint64 length = timer_edit_window_get_length(self); | ||||||
|  |     GDateTime *start = g_date_time_add_seconds(end, -length); | ||||||
|  |     if (!compare_dates(start, end)) { | ||||||
|  |         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->endCalCheck), | ||||||
|  |                                      FALSE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     int y, month, d; | ||||||
|  |     g_date_time_get_ymd(start, &y, &month, &d); | ||||||
|  |     int s = g_date_time_get_second(start); | ||||||
|  |     int min = g_date_time_get_minute(start); | ||||||
|  |     int h, l; | ||||||
|  |     to_leaned_time(g_date_time_get_hour(start), &h, &l); | ||||||
|  |  | ||||||
|  |     gtk_calendar_select_day(GTK_CALENDAR(self->startCalendar), d); | ||||||
|  |     gtk_calendar_select_month(GTK_CALENDAR(self->startCalendar), month - 1, y); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->startSecond), s); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->startMinute), min); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->startHour), h); | ||||||
|  |     gtk_combo_box_set_active(GTK_COMBO_BOX(self->startLean), l); | ||||||
|  |  | ||||||
|  |     g_date_time_unref(end); | ||||||
|  |     g_date_time_unref(start); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_edit_window_calculate_end(TimerEditWindow *self) { | ||||||
|  |     GDateTime *start = timer_edit_window_get_start(self); | ||||||
|  |     gint64 length = timer_edit_window_get_length(self); | ||||||
|  |     GDateTime *end = g_date_time_add_seconds(start, length); | ||||||
|  |     if (!compare_dates(start, end)) { | ||||||
|  |         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->endCalCheck), | ||||||
|  |                                      FALSE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     int y, month, d; | ||||||
|  |     g_date_time_get_ymd(end, &y, &month, &d); | ||||||
|  |     int s = g_date_time_get_second(end); | ||||||
|  |     int min = g_date_time_get_minute(end); | ||||||
|  |     int h, l; | ||||||
|  |     to_leaned_time(g_date_time_get_hour(end), &h, &l); | ||||||
|  |  | ||||||
|  |     gtk_calendar_select_day(GTK_CALENDAR(self->endCalendar), d); | ||||||
|  |     gtk_calendar_select_month(GTK_CALENDAR(self->endCalendar), month - 1, y); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->endSecond), s); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->endMinute), min); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->endHour), h); | ||||||
|  |     gtk_combo_box_set_active(GTK_COMBO_BOX(self->endLean), l); | ||||||
|  |  | ||||||
|  |     g_date_time_unref(start); | ||||||
|  |     g_date_time_unref(end); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_edit_window_calculate_length(TimerEditWindow *self) { | ||||||
|  |     GDateTime *start = timer_edit_window_get_start(self); | ||||||
|  |     GDateTime *end = timer_edit_window_get_end(self); | ||||||
|  |     GTimeSpan length = g_date_time_difference(end, start) / 1000000; | ||||||
|  |  | ||||||
|  |     if (length < 0 || length > TIMER_MAX_LENGTH) { | ||||||
|  |         self->invalidTime = TRUE; | ||||||
|  |     } else { | ||||||
|  |         self->invalidTime = FALSE; | ||||||
|  |  | ||||||
|  |         int h = floor(length / 3600.0f); | ||||||
|  |         int m = floor(length / 60.0f) - (h * 60); | ||||||
|  |         int s = length - (m * 60) - (h * 3600); | ||||||
|  |  | ||||||
|  |         gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->lengthSecond), s); | ||||||
|  |         gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->lengthMinute), m); | ||||||
|  |         gtk_spin_button_set_value(GTK_SPIN_BUTTON(self->lengthHour), h); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     g_date_time_unref(start); | ||||||
|  |     g_date_time_unref(end); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerEditWindow *timer_edit_window_new(const char *task, GDateTime *start, | ||||||
|  |                                        gint64 length, const char **taskOptions, | ||||||
|  |                                        gsize optionsLen, gboolean isNew, GDateTime *lastTask) { | ||||||
|  |     TimerEditWindow *win = g_object_new(TIMER_TYPE_EDIT_WINDOW, NULL); | ||||||
|  |     gtk_widget_set_visible(win->deleteButton, !isNew); | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < optionsLen; ++i) { | ||||||
|  |         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(win->nameBox), | ||||||
|  |                                        taskOptions[i]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(win->nameBox))), | ||||||
|  |                        task); | ||||||
|  |  | ||||||
|  |     int sy, smonth, sd; | ||||||
|  |     g_date_time_get_ymd(start, &sy, &smonth, &sd); | ||||||
|  |     int ss = g_date_time_get_second(start); | ||||||
|  |     int smin = g_date_time_get_minute(start); | ||||||
|  |     int sh, sl; | ||||||
|  |     to_leaned_time(g_date_time_get_hour(start), &sh, &sl); | ||||||
|  |  | ||||||
|  |     gtk_calendar_select_day(GTK_CALENDAR(win->startCalendar), sd); | ||||||
|  |     gtk_calendar_select_month(GTK_CALENDAR(win->startCalendar), smonth - 1, sy); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->startSecond), ss); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->startMinute), smin); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->startHour), sh); | ||||||
|  |     gtk_combo_box_set_active(GTK_COMBO_BOX(win->startLean), sl); | ||||||
|  |  | ||||||
|  |     int lh = floor(length / 3600.0f); | ||||||
|  |     int lm = floor(length / 60.0f) - (lh * 60); | ||||||
|  |     int ls = length - (lm * 60) - (lh * 3600); | ||||||
|  |  | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->lengthSecond), ls); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->lengthMinute), lm); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->lengthHour), lh); | ||||||
|  |  | ||||||
|  |     timer_edit_window_calculate_end(win); | ||||||
|  |  | ||||||
|  |     if (isNew && lastTask) { | ||||||
|  |         win->lastTask = lastTask; | ||||||
|  |         gtk_widget_show_all(win->followBox); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     win->lastEdit = TIMER_LAST_END; | ||||||
|  |     win->enableAutoEdit = TRUE; | ||||||
|  |     return win; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GDateTime *timer_edit_window_get_start(TimerEditWindow *self) { | ||||||
|  |     unsigned int y, mn, d; | ||||||
|  |     gtk_calendar_get_date(GTK_CALENDAR(self->startCalendar), &y, &mn, &d); | ||||||
|  |     int s = | ||||||
|  |         gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->startSecond)); | ||||||
|  |     int mi = | ||||||
|  |         gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->startMinute)); | ||||||
|  |     int h = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->startHour)); | ||||||
|  |     int l = gtk_combo_box_get_active(GTK_COMBO_BOX(self->startLean)); | ||||||
|  |     int t; | ||||||
|  |     to_total_time(h, l, &t); | ||||||
|  |     return g_date_time_new_local(y, mn + 1, d, t, mi, s); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GDateTime *timer_edit_window_get_end(TimerEditWindow *self) { | ||||||
|  |     unsigned int y, mn, d; | ||||||
|  |     if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(self->endCalCheck))) { | ||||||
|  |         gtk_calendar_get_date(GTK_CALENDAR(self->endCalendar), &y, &mn, &d); | ||||||
|  |     } else { | ||||||
|  |         gtk_calendar_get_date(GTK_CALENDAR(self->startCalendar), &y, &mn, &d); | ||||||
|  |     } | ||||||
|  |     int s = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->endSecond)); | ||||||
|  |     int mi = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->endMinute)); | ||||||
|  |     int h = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->endHour)); | ||||||
|  |     int l = gtk_combo_box_get_active(GTK_COMBO_BOX(self->endLean)); | ||||||
|  |     int t; | ||||||
|  |     to_total_time(h, l, &t); | ||||||
|  |     return g_date_time_new_local(y, mn + 1, d, t, mi, s); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | gint64 timer_edit_window_get_length(TimerEditWindow *self) { | ||||||
|  |     int h = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->lengthHour)); | ||||||
|  |     int m = | ||||||
|  |         gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->lengthMinute)); | ||||||
|  |     int s = | ||||||
|  |         gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->lengthSecond)); | ||||||
|  |     return (h * 3600) + (m * 60) + s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | char *timer_edit_window_get_name(TimerEditWindow *self) { | ||||||
|  |     char *name; | ||||||
|  |     if (gtk_entry_get_text_length( | ||||||
|  |             GTK_ENTRY(gtk_bin_get_child(GTK_BIN(self->nameBox)))) == 0) { | ||||||
|  |         name = g_strdup("Untitled"); | ||||||
|  |     } else { | ||||||
|  |         name = gtk_combo_box_text_get_active_text( | ||||||
|  |             GTK_COMBO_BOX_TEXT(self->nameBox)); | ||||||
|  |     } | ||||||
|  |     return name; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void save_button_callback(GtkButton *btn, TimerEditWindow *win) { | ||||||
|  |     if (win->invalidTime) { | ||||||
|  |         GtkWidget *diag = | ||||||
|  |             gtk_message_dialog_new(GTK_WINDOW(win), GTK_DIALOG_MODAL, | ||||||
|  |                                    GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                                    "Start time must be before end time and " | ||||||
|  |                                    "length must be under 100 hours!"); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |     } else { | ||||||
|  |         gtk_dialog_response(GTK_DIALOG(win), GTK_RESPONSE_APPLY); | ||||||
|  |         gtk_window_close(GTK_WINDOW(win)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void cancel_button_callback(GtkButton *btn, TimerEditWindow *win) { | ||||||
|  |     gtk_dialog_response(GTK_DIALOG(win), GTK_RESPONSE_CANCEL); | ||||||
|  |     gtk_window_close(GTK_WINDOW(win)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void delete_button_callback(GtkButton *btn, TimerEditWindow *win) { | ||||||
|  |     GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |         GTK_WINDOW(win), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, | ||||||
|  |         GTK_BUTTONS_YES_NO, "Are you sure you would like to delete this task?"); | ||||||
|  |     gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |     int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |     gtk_widget_destroy(GTK_WIDGET(diag)); | ||||||
|  |     if (resp == GTK_RESPONSE_YES) { | ||||||
|  |         gtk_dialog_response(GTK_DIALOG(win), GTK_RESPONSE_REJECT); | ||||||
|  |         gtk_window_close(GTK_WINDOW(win)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void start_edit_callback(GtkWidget *obj, TimerEditWindow *win) { | ||||||
|  |     if (win->enableAutoEdit) { | ||||||
|  |         win->enableAutoEdit = FALSE; | ||||||
|  |         timer_edit_window_calculate_length(win); | ||||||
|  |         win->lastEdit = TIMER_LAST_START; | ||||||
|  |         win->enableAutoEdit = TRUE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void end_edit_callback(GtkWidget *btn, TimerEditWindow *win) { | ||||||
|  |     if (win->enableAutoEdit) { | ||||||
|  |         win->enableAutoEdit = FALSE; | ||||||
|  |         timer_edit_window_calculate_length(win); | ||||||
|  |         win->lastEdit = TIMER_LAST_END; | ||||||
|  |         win->enableAutoEdit = TRUE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void length_edit_callback(GtkSpinButton *btn, TimerEditWindow *win) { | ||||||
|  |     if (win->enableAutoEdit) { | ||||||
|  |         win->enableAutoEdit = FALSE; | ||||||
|  |         if (win->lastEdit == TIMER_LAST_START) { | ||||||
|  |             timer_edit_window_calculate_end(win); | ||||||
|  |         } else { | ||||||
|  |             timer_edit_window_calculate_start(win); | ||||||
|  |         } | ||||||
|  |         win->enableAutoEdit = TRUE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void end_calendar_check_callback(GtkToggleButton *tgb, | ||||||
|  |                                         TimerEditWindow *win) { | ||||||
|  |     gtk_widget_set_visible(win->endCalendar, | ||||||
|  |                            !gtk_toggle_button_get_active(tgb)); | ||||||
|  |     if (win->enableAutoEdit) { | ||||||
|  |         win->enableAutoEdit = FALSE; | ||||||
|  |         timer_edit_window_calculate_length(win); | ||||||
|  |         win->lastEdit = TIMER_LAST_END; | ||||||
|  |         win->enableAutoEdit = TRUE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void follow_button_callback(GtkButton *btn, TimerEditWindow *win) { | ||||||
|  |     GDateTime *end = timer_edit_window_get_end(win); | ||||||
|  |     win->enableAutoEdit = FALSE; | ||||||
|  |     int sy, smonth, sd; | ||||||
|  |     g_date_time_get_ymd(win->lastTask, &sy, &smonth, &sd); | ||||||
|  |     int ss = g_date_time_get_second(win->lastTask); | ||||||
|  |     int smin = g_date_time_get_minute(win->lastTask); | ||||||
|  |     int sh, sl; | ||||||
|  |     to_leaned_time(g_date_time_get_hour(win->lastTask), &sh, &sl); | ||||||
|  |  | ||||||
|  |     gtk_calendar_select_day(GTK_CALENDAR(win->startCalendar), sd); | ||||||
|  |     gtk_calendar_select_month(GTK_CALENDAR(win->startCalendar), smonth - 1, sy); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->startSecond), ss); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->startMinute), smin); | ||||||
|  |     gtk_spin_button_set_value(GTK_SPIN_BUTTON(win->startHour), sh); | ||||||
|  |     gtk_combo_box_set_active(GTK_COMBO_BOX(win->startLean), sl); | ||||||
|  |  | ||||||
|  |     GDateTime *start = timer_edit_window_get_start(win); | ||||||
|  |     if (!compare_dates(start, end)) { | ||||||
|  |         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(win->endCalCheck), FALSE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     timer_edit_window_calculate_length(win); | ||||||
|  |     win->lastEdit = TIMER_LAST_START; | ||||||
|  |     win->enableAutoEdit = TRUE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_edit_window_finalize(GObject *obj) { | ||||||
|  |     if (TIMER_EDIT_WINDOW(obj)->lastTask) { | ||||||
|  |         g_date_time_unref(TIMER_EDIT_WINDOW(obj)->lastTask); | ||||||
|  |     } | ||||||
|  |     G_OBJECT_CLASS(timer_edit_window_parent_class)->finalize(obj); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_edit_window_class_init(TimerEditWindowClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_edit_window_finalize; | ||||||
|  |     gtk_widget_class_set_template_from_resource( | ||||||
|  |         GTK_WIDGET_CLASS(class), "/zander/practicetimer/ui/edit-window.glade"); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |         GTK_WIDGET_CLASS(class), TimerEditWindow, cancelButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |         GTK_WIDGET_CLASS(class), TimerEditWindow, deleteButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, saveButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, nameBox); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |         GTK_WIDGET_CLASS(class), TimerEditWindow, startCalendar); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, endCalendar); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, endCalCheck); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, startHour); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, startMinute); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, startSecond); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, startLean); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, endHour); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, endMinute); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, endSecond); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, endLean); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |                                                   TimerEditWindow, lengthHour); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |         GTK_WIDGET_CLASS(class), TimerEditWindow, lengthMinute); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |         GTK_WIDGET_CLASS(class), TimerEditWindow, lengthSecond); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), TimerEditWindow, followButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), TimerEditWindow, followBox); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_edit_window_init(TimerEditWindow *self) { | ||||||
|  |     self->enableAutoEdit = FALSE; | ||||||
|  |     gtk_widget_init_template(GTK_WIDGET(self)); | ||||||
|  |     gtk_window_set_keep_above(GTK_WINDOW(self), TRUE); | ||||||
|  |     g_signal_connect(self->deleteButton, "clicked", | ||||||
|  |                      G_CALLBACK(delete_button_callback), self); | ||||||
|  |     g_signal_connect(self->saveButton, "clicked", | ||||||
|  |                      G_CALLBACK(save_button_callback), self); | ||||||
|  |     g_signal_connect(self->cancelButton, "clicked", | ||||||
|  |                      G_CALLBACK(cancel_button_callback), self); | ||||||
|  |     g_signal_connect(self->startHour, "value-changed", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startMinute, "value-changed", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startSecond, "value-changed", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startLean, "changed", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startCalendar, "day-selected", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startCalendar, "month-changed", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startCalendar, "next-year", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->startCalendar, "prev-year", | ||||||
|  |                      G_CALLBACK(start_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endHour, "value-changed", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endMinute, "value-changed", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endSecond, "value-changed", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endLean, "changed", G_CALLBACK(end_edit_callback), | ||||||
|  |                      self); | ||||||
|  |     g_signal_connect(self->endCalendar, "day-selected", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endCalendar, "month-changed", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endCalendar, "next-year", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endCalendar, "prev-year", | ||||||
|  |                      G_CALLBACK(end_edit_callback), self); | ||||||
|  |     g_signal_connect(self->lengthHour, "value-changed", | ||||||
|  |                      G_CALLBACK(length_edit_callback), self); | ||||||
|  |     g_signal_connect(self->lengthMinute, "value-changed", | ||||||
|  |                      G_CALLBACK(length_edit_callback), self); | ||||||
|  |     g_signal_connect(self->lengthSecond, "value-changed", | ||||||
|  |                      G_CALLBACK(length_edit_callback), self); | ||||||
|  |     g_signal_connect(self->endCalCheck, "toggled", | ||||||
|  |                      G_CALLBACK(end_calendar_check_callback), self); | ||||||
|  |     g_signal_connect(self->followButton, "clicked", G_CALLBACK(follow_button_callback), self); | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								TimerEditWindow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								TimerEditWindow.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_EDIT_WINDOW_H | ||||||
|  | #define INCLUDED_TIMER_EDIT_WINDOW_H | ||||||
|  |  | ||||||
|  | #include "TimerApplication.h" | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_EDIT_WINDOW timer_edit_window_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerEditWindow, timer_edit_window, TIMER, EDIT_WINDOW, GtkDialog) | ||||||
|  |  | ||||||
|  | TimerEditWindow *timer_edit_window_new(const char *task, GDateTime *start, gint64 length, const char **taskOptions, gsize optionsLen, gboolean isNew, GDateTime *lastTask); | ||||||
|  | GDateTime *timer_edit_window_get_start(TimerEditWindow *self); | ||||||
|  | GDateTime *timer_edit_window_get_end(TimerEditWindow *self); | ||||||
|  | gint64 timer_edit_window_get_length(TimerEditWindow *self); | ||||||
|  | char *timer_edit_window_get_name(TimerEditWindow *self); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_EDIT_WINDOW_H */ | ||||||
							
								
								
									
										91
									
								
								TimerFileWatcher.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								TimerFileWatcher.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | |||||||
|  | #include "TimerFileWatcher.h" | ||||||
|  |  | ||||||
|  | #include <glib.h> | ||||||
|  | #include <glib/gstdio.h> | ||||||
|  | #include <sys/stat.h> | ||||||
|  | #include <fcntl.h> | ||||||
|  | #include <time.h> | ||||||
|  |  | ||||||
|  | struct _TimerFileWatcher { | ||||||
|  |     GObject parent; | ||||||
|  |  | ||||||
|  |     char *path; | ||||||
|  |  | ||||||
|  |     GThread *thread; | ||||||
|  |     gboolean running; | ||||||
|  |  | ||||||
|  |     time_t lastTime; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerFileWatcher, timer_file_watcher, G_TYPE_OBJECT); | ||||||
|  |  | ||||||
|  | static void timer_file_watcher_tick(TimerFileWatcher *self) { | ||||||
|  |     gint64 last_tick = 0; | ||||||
|  |     GStatBuf statbuf; | ||||||
|  |     while (self->running) { | ||||||
|  |         gint64 now = g_get_monotonic_time(); | ||||||
|  |         if ((now - last_tick) > (G_USEC_PER_SEC * FILE_WATCHER_POLL_RATE) && g_stat(self->path, &statbuf) == 0) { | ||||||
|  |             if (self->lastTime != statbuf.st_atime) { | ||||||
|  |                 g_signal_emit_by_name(self, "file-changed"); | ||||||
|  |                 self->lastTime = statbuf.st_atime; | ||||||
|  |             } | ||||||
|  |             last_tick = now; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerFileWatcher *timer_file_watcher_new(const char *path) { | ||||||
|  |     TimerFileWatcher *fw = g_object_new(TIMER_TYPE_FILE_WATCHER, NULL); | ||||||
|  |     GStatBuf statbuf; | ||||||
|  |     if (g_stat(path, &statbuf) == 0) { | ||||||
|  |         fw->lastTime = statbuf.st_atime; | ||||||
|  |     } | ||||||
|  |     fw->path = g_strdup(path); | ||||||
|  |     fw->running = TRUE; | ||||||
|  |     fw->thread = g_thread_new(NULL, (GThreadFunc) timer_file_watcher_tick, fw); | ||||||
|  |     return fw; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const char *timer_file_watcher_get_path(TimerFileWatcher* self) { | ||||||
|  |     return self->path; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_file_watcher_pause(TimerFileWatcher *self) { | ||||||
|  |     if (self->running) { | ||||||
|  |         self->running = FALSE; | ||||||
|  |         g_thread_join(self->thread); | ||||||
|  |         g_thread_unref(self->thread); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_file_watcher_resume(TimerFileWatcher *self, gboolean update) { | ||||||
|  |     if (!self->running) { | ||||||
|  |         self->running = TRUE; | ||||||
|  |         if (!update) { | ||||||
|  |             GStatBuf statbuf; | ||||||
|  |             if (g_stat(self->path, &statbuf) == 0) { | ||||||
|  |                 self->lastTime = statbuf.st_atime; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         self->thread = g_thread_new(NULL, (GThreadFunc) timer_file_watcher_tick, self); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | gboolean timer_file_watcher_is_paused(TimerFileWatcher *self) { | ||||||
|  |     return !self->running; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_file_watcher_finalize(GObject *self) { | ||||||
|  |     g_free(TIMER_FILE_WATCHER(self)->path); | ||||||
|  |     timer_file_watcher_pause(TIMER_FILE_WATCHER(self)); | ||||||
|  |     G_OBJECT_CLASS(timer_file_watcher_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_file_watcher_class_init(TimerFileWatcherClass *class) { | ||||||
|  |     g_signal_new("file-changed", TIMER_TYPE_FILE_WATCHER, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, | ||||||
|  |                  0, NULL, NULL, NULL, G_TYPE_NONE, 0); | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_file_watcher_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_file_watcher_init(TimerFileWatcher *self) { | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								TimerFileWatcher.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								TimerFileWatcher.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_FILE_WATCHER_H | ||||||
|  | #define INCLUDED_TIMER_FILE_WATCHER_H | ||||||
|  |  | ||||||
|  | #include <glib-object.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_FILE_WATCHER timer_file_watcher_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerFileWatcher, timer_file_watcher, TIMER, FILE_WATCHER, GObject); | ||||||
|  |  | ||||||
|  | TimerFileWatcher *timer_file_watcher_new(const char *path); | ||||||
|  |  | ||||||
|  | const char *timer_file_watcher_get_path(TimerFileWatcher *self); | ||||||
|  |  | ||||||
|  | void timer_file_watcher_pause(TimerFileWatcher *self); | ||||||
|  |  | ||||||
|  | /* update: if true, call update action if file was changed, else just resume */ | ||||||
|  | void timer_file_watcher_resume(TimerFileWatcher *self, gboolean update); | ||||||
|  |  | ||||||
|  | gboolean timer_file_watcher_is_paused(TimerFileWatcher *self); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_FILE_WATCHER_H */ | ||||||
							
								
								
									
										190
									
								
								TimerGraph.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								TimerGraph.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,190 @@ | |||||||
|  | #include "TimerGraph.h" | ||||||
|  |  | ||||||
|  | #include <unistd.h> | ||||||
|  | #include <sys/wait.h> | ||||||
|  |  | ||||||
|  | struct _TimerGraph { | ||||||
|  |     GtkBin parent; | ||||||
|  |  | ||||||
|  |     GtkWidget *drawingArea; | ||||||
|  |  | ||||||
|  |     GdkPixbuf *originalData; | ||||||
|  |     cairo_surface_t *tex; | ||||||
|  |     double gx; | ||||||
|  |     double gy; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerGraph, timer_graph, GTK_TYPE_BIN); | ||||||
|  |  | ||||||
|  | static char *get_graph_format() { | ||||||
|  |     GSList *list = gdk_pixbuf_get_formats(); | ||||||
|  |     GSList *i; | ||||||
|  |     for (i = list; i != NULL; i = i->next) { | ||||||
|  |         GdkPixbufFormat *f = i->data; | ||||||
|  |         if (!gdk_pixbuf_format_is_disabled(f)) { | ||||||
|  |             gchar **exts = gdk_pixbuf_format_get_extensions(f); | ||||||
|  |             while (*exts != NULL) { | ||||||
|  |                 if (strcmp(*exts, PLOTUTILS_PREFERRED_FORMAT) == 0){ | ||||||
|  |                     g_slist_free(list); | ||||||
|  |                     return PLOTUTILS_PREFERRED_FORMAT; | ||||||
|  |                 } | ||||||
|  |                 ++exts; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     g_warning("fallback image forat ('" PLOTUTILS_FALLBACK_FORMAT "') used"); | ||||||
|  |     g_slist_free(list); | ||||||
|  |     return PLOTUTILS_FALLBACK_FORMAT; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static char *create_graph_bytes(const TimerGraphPoint *verts, gsize length, const char *format, const char *xLabel, const char *yLabel, const char *title, gsize *len) { | ||||||
|  |     GString *dataString = g_string_new(NULL); | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < length; ++i) { | ||||||
|  |         g_string_append_printf(dataString, "%d.0 %d.0", verts[i].x, verts[i].y); | ||||||
|  |         if (i < length - 1) { | ||||||
|  |             g_string_append_c(dataString, '\n'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     char *pointsString = g_string_free(dataString, FALSE); | ||||||
|  |     int ptoc[2]; | ||||||
|  |     if (pipe(ptoc) < 0) { | ||||||
|  |         g_free(pointsString); | ||||||
|  |         GtkWidget *diag = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "An internal error occured. Please try again."); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     int ctop[2]; | ||||||
|  |     if (pipe(ctop) < 0) { | ||||||
|  |         g_free(pointsString); | ||||||
|  |         close(ptoc[0]); | ||||||
|  |         close(ptoc[1]); | ||||||
|  |         GtkWidget *diag = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "An internal error occured. Please try again."); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     pid_t p = fork(); | ||||||
|  |     if (p == 0) { | ||||||
|  |         /* child */ | ||||||
|  |         g_free(pointsString); | ||||||
|  |         dup2(ptoc[0], fileno(stdin)); | ||||||
|  |         close(ptoc[1]); | ||||||
|  |         dup2(ctop[1], fileno(stdout)); | ||||||
|  |         close(ctop[0]); | ||||||
|  |         freopen("/dev/null", "w", stderr); | ||||||
|  |         execlp(PLOTUTILS_GRAPH_PATH, PLOTUTILS_GRAPH_PATH, "-T", format, "-X", xLabel, "-Y", yLabel, "-L", title, NULL); | ||||||
|  |         /* If graph(1) could not be executed */ | ||||||
|  |         exit(0); | ||||||
|  |     } else { | ||||||
|  |         /* parent */ | ||||||
|  |         int out = ptoc[1]; | ||||||
|  |         close(ptoc[0]); | ||||||
|  |         int in = ctop[0]; | ||||||
|  |         close(ctop[1]); | ||||||
|  |         write(out, pointsString, strlen(pointsString)); | ||||||
|  |         close(out); | ||||||
|  |         g_free(pointsString); | ||||||
|  |         int status; | ||||||
|  |         if (waitpid(p, &status, 0) < 0) { | ||||||
|  |             close(in); | ||||||
|  |             g_warning("graph(1) failed"); | ||||||
|  |             return NULL; | ||||||
|  |         } else if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { | ||||||
|  |             close(in); | ||||||
|  |             g_warning("graph(1) failed"); | ||||||
|  |             return NULL; | ||||||
|  |         } | ||||||
|  |         GString *buff = g_string_new(NULL); | ||||||
|  |         char c; | ||||||
|  |         *len = 0; | ||||||
|  |         while (read(in, &c, 1) > 0) { | ||||||
|  |             g_string_append_c(buff, c); | ||||||
|  |             ++(*len); | ||||||
|  |         } | ||||||
|  |         close(in); | ||||||
|  |         char *resp = g_string_free(buff, FALSE); | ||||||
|  |         return resp; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static GdkPixbuf *pixbuf_from_data(const char *data, gsize len) { | ||||||
|  |     GdkPixbufLoader *loader = gdk_pixbuf_loader_new(); | ||||||
|  |     GError *err = NULL; | ||||||
|  |     gdk_pixbuf_loader_write(loader, (const guchar *) data, len, &err); | ||||||
|  |     gdk_pixbuf_loader_close(loader, NULL); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         GtkWidget *diag = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "An internal error occured. Please try again."); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |         g_object_unref(loader); | ||||||
|  |         g_error_free(err); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader); | ||||||
|  |     g_object_ref(pixbuf); | ||||||
|  |     g_object_unref(loader); | ||||||
|  |     return pixbuf; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void da_size_callback(GtkDrawingArea *self, GtkAllocation *allocation, TimerGraph *graph) { | ||||||
|  |     if (graph->originalData) { | ||||||
|  |         int size = MIN(allocation->width, allocation->height); | ||||||
|  |         if (graph->tex) { | ||||||
|  |             cairo_surface_destroy(graph->tex); | ||||||
|  |         } | ||||||
|  |         graph->gx = ((double) allocation->width / 2) - ((double) size / 2); | ||||||
|  |         graph->gy = ((double) allocation->height / 2) - ((double) size / 2); | ||||||
|  |         GdkPixbuf *scaled = gdk_pixbuf_scale_simple(graph->originalData, size, size, GDK_INTERP_BILINEAR); | ||||||
|  |         graph->tex = gdk_cairo_surface_create_from_pixbuf(scaled, 1, gtk_widget_get_window(GTK_WIDGET(self))); | ||||||
|  |         g_object_unref(scaled); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean da_draw_callback(GtkDrawingArea *self, cairo_t *cr, TimerGraph *graph) { | ||||||
|  |     GtkStyleContext *cxt = gtk_widget_get_style_context(GTK_WIDGET(self)); | ||||||
|  |     guint width = gtk_widget_get_allocated_width(GTK_WIDGET(self)); | ||||||
|  |     guint height = gtk_widget_get_allocated_height(GTK_WIDGET(self)); | ||||||
|  |     gtk_render_background(cxt, cr, 0, 0, width, height); | ||||||
|  |     if (graph->tex) { | ||||||
|  |         cairo_set_source_surface(cr, graph->tex, graph->gx, graph->gy); | ||||||
|  |     } | ||||||
|  |     cairo_paint(cr); | ||||||
|  |     return TRUE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GtkWidget *timer_graph_new(const TimerGraphPoint *verts, gsize length, const char *xLabel, const char *yLabel, const char *title) { | ||||||
|  |     TimerGraph *g = g_object_new(TIMER_TYPE_GRAPH, NULL); | ||||||
|  |     gsize len = 0; | ||||||
|  |     char *data = create_graph_bytes(verts, length, get_graph_format(), xLabel, yLabel, title, &len); | ||||||
|  |     g->originalData = pixbuf_from_data(data, len); | ||||||
|  |     g_free(data); | ||||||
|  |     return GTK_WIDGET(g); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_finalize(GObject *obj) { | ||||||
|  |     TimerGraph *self = TIMER_GRAPH(obj); | ||||||
|  |     if (self->originalData) { | ||||||
|  |         g_object_unref(self->originalData); | ||||||
|  |     } | ||||||
|  |     if (self->tex) { | ||||||
|  |         cairo_surface_destroy(self->tex); | ||||||
|  |     } | ||||||
|  |     G_OBJECT_CLASS(timer_graph_parent_class)->finalize(obj); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_class_init(TimerGraphClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_graph_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_init(TimerGraph* self) { | ||||||
|  |     self->drawingArea = gtk_drawing_area_new(); | ||||||
|  |     gtk_container_add(GTK_CONTAINER(self), self->drawingArea); | ||||||
|  |     gtk_widget_set_size_request(GTK_WIDGET(self), 100, 100); | ||||||
|  |     g_signal_connect(self->drawingArea, "size-allocate", G_CALLBACK(da_size_callback), self); | ||||||
|  |     g_signal_connect(self->drawingArea, "draw", G_CALLBACK(da_draw_callback), self); | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								TimerGraph.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								TimerGraph.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | #ifndef TIMER_GRAPH_H | ||||||
|  | #define TIMER_GRAPH_H | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | typedef struct { | ||||||
|  |     int x; | ||||||
|  |     int y; | ||||||
|  | } TimerGraphPoint; | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_GRAPH timer_graph_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerGraph, timer_graph, TIMER, GRAPH, GtkBin); | ||||||
|  |  | ||||||
|  | GtkWidget *timer_graph_new(const TimerGraphPoint *verts, gsize length, const char *xLabel, const char *yLabel, const char *title); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* TIMER_GRAPH_H */ | ||||||
							
								
								
									
										349
									
								
								TimerGraphWindow.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								TimerGraphWindow.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,349 @@ | |||||||
|  | #include "TimerGraphWindow.h" | ||||||
|  | #include "TimerTaskTree.h" | ||||||
|  | #include "TimerGraph.h" | ||||||
|  |  | ||||||
|  | #include <sys/types.h> | ||||||
|  | #include <sys/wait.h> | ||||||
|  | #include <math.h> | ||||||
|  |  | ||||||
|  | enum { | ||||||
|  |     DATA_COLUMN = 0, | ||||||
|  |     SEVEN_DAY_COLUMN, | ||||||
|  |     THIRTY_DAY_COLUMN, | ||||||
|  |     SIX_MONTH_COLUMN, | ||||||
|  |     YEAR_COLUMN, | ||||||
|  |     ALL_TIME_COLUMN, | ||||||
|  |     N_COLUMNS | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | typedef struct { | ||||||
|  |     gint64 max; | ||||||
|  |     gint64 min; | ||||||
|  |     gint64 avg; | ||||||
|  | } TimerStatSet; | ||||||
|  |  | ||||||
|  | struct _TimerGraphWindow { | ||||||
|  |     GtkApplicationWindow parent; | ||||||
|  |  | ||||||
|  |     GtkWidget *closeButton; | ||||||
|  |     GtkWidget *chartBox; | ||||||
|  |     GtkWidget *dayChart; | ||||||
|  |     GtkWidget *monthChart; | ||||||
|  |  | ||||||
|  |     GtkWidget *table; | ||||||
|  |     GtkTreeStore *tableStore; | ||||||
|  |  | ||||||
|  |     TimerDataPoint *dayData; | ||||||
|  |     gsize dayDataLen; | ||||||
|  |  | ||||||
|  |     TimerDataPoint *taskData; | ||||||
|  |     gsize taskDataLen; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerGraphWindow, timer_graph_window, GTK_TYPE_DIALOG); | ||||||
|  |  | ||||||
|  | static gboolean came_after_or_same_day(GDateTime *start, GDateTime *date) { | ||||||
|  |     int y1, m1, d1, y2, m2, d2; | ||||||
|  |     g_date_time_get_ymd(start, &y1, &m1, &d1); | ||||||
|  |     g_date_time_get_ymd(date, &y2, &m2, &d2); | ||||||
|  |     return (y1 < y2) || (y1 == y2 && m1 < m2) || | ||||||
|  |         (y1 == y2 && m1 == m2 && d1 <= d2); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gint day_offset(GDateTime *start, GDateTime *end) { | ||||||
|  |     gint64 su = g_date_time_to_unix(start); | ||||||
|  |     gint64 eu = g_date_time_to_unix(end); | ||||||
|  |     return round((float) (eu - su) / (60 * 60 * 24)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static TimerStatSet timer_graph_window_get_stats_for_days(TimerGraphWindow *self, gsize days) { | ||||||
|  |     GDateTime *ref = NULL; | ||||||
|  |     if (days != 0) { | ||||||
|  |         GDateTime *now = g_date_time_new_now_local(); | ||||||
|  |         ref = g_date_time_add_days(now, -days); | ||||||
|  |         g_date_time_unref(now); | ||||||
|  |     } else { | ||||||
|  |         days = self->dayDataLen; | ||||||
|  |     } | ||||||
|  |     gint64 total = 0, max = 0, min = G_MAXINT64; | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < self->dayDataLen; ++i) { | ||||||
|  |         if (ref == NULL || came_after_or_same_day(ref, self->dayData[i].date)) { | ||||||
|  |             total += self->dayData[i].lenght; | ||||||
|  |             if (self->dayData[i].lenght > max) { | ||||||
|  |                 max = self->dayData[i].lenght; | ||||||
|  |             } | ||||||
|  |             if (self->dayData[i].lenght < min) { | ||||||
|  |                 min = self->dayData[i].lenght; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if (ref != NULL) { | ||||||
|  |         g_date_time_unref(ref); | ||||||
|  |     } | ||||||
|  |     if (min == G_MAXINT64) { | ||||||
|  |         min = 0; | ||||||
|  |     } | ||||||
|  |     int avg = 0; | ||||||
|  |     if (days != 0) { | ||||||
|  |         avg = floor((float)total / (float)days); | ||||||
|  |     } | ||||||
|  |     return (TimerStatSet){max, min, avg}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | static char *format_hours_and_min(gint64 time) { | ||||||
|  |     int hour = floor(time / 3600.0f); | ||||||
|  |     int min = floor(time / 60.0f) - (hour * 60); | ||||||
|  |     return g_strdup_printf("%02d:%02d", hour, min); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_add_table_data(TimerGraphWindow *self) { | ||||||
|  |     TimerStatSet d7 = timer_graph_window_get_stats_for_days(self, 7); | ||||||
|  |     TimerStatSet d30 = timer_graph_window_get_stats_for_days(self, 30); | ||||||
|  |     TimerStatSet d183 = timer_graph_window_get_stats_for_days(self, 183); | ||||||
|  |     TimerStatSet d365 = timer_graph_window_get_stats_for_days(self, 365); | ||||||
|  |     TimerStatSet da = timer_graph_window_get_stats_for_days(self, 0); | ||||||
|  |  | ||||||
|  |     GtkTreeIter highIter; | ||||||
|  |     gtk_tree_store_append(self->tableStore, &highIter, NULL); | ||||||
|  |     char *d7ms = format_hours_and_min(d7.max); | ||||||
|  |     char *d30ms = format_hours_and_min(d30.max); | ||||||
|  |     char *d183ms = format_hours_and_min(d183.max); | ||||||
|  |     char *d365ms = format_hours_and_min(d365.max); | ||||||
|  |     char *dams = format_hours_and_min(da.max); | ||||||
|  |     gtk_tree_store_set(self->tableStore, &highIter, DATA_COLUMN, "High", | ||||||
|  |             SEVEN_DAY_COLUMN, d7ms, THIRTY_DAY_COLUMN, d30ms, | ||||||
|  |             SIX_MONTH_COLUMN, d183ms, YEAR_COLUMN, d365ms, | ||||||
|  |             ALL_TIME_COLUMN, dams, -1); | ||||||
|  |     g_free(d7ms); | ||||||
|  |     g_free(d30ms); | ||||||
|  |     g_free(d183ms); | ||||||
|  |     g_free(d365ms); | ||||||
|  |     g_free(dams); | ||||||
|  |  | ||||||
|  |     /* Dad requested I remove this | ||||||
|  |     GtkTreeIter lowIter; | ||||||
|  |     gtk_tree_store_append(self->tableStore, &lowIter, NULL); | ||||||
|  |     char *d7ls = format_hours_and_min(d7.min); | ||||||
|  |     char *d30ls = format_hours_and_min(d30.min); | ||||||
|  |     char *d183ls = format_hours_and_min(d183.min); | ||||||
|  |     char *d365ls = format_hours_and_min(d365.min); | ||||||
|  |     char *dals = format_hours_and_min(da.min); | ||||||
|  |     gtk_tree_store_set(self->tableStore, &lowIter, DATA_COLUMN, "Low", | ||||||
|  |             SEVEN_DAY_COLUMN, d7ls, THIRTY_DAY_COLUMN, d30ls, | ||||||
|  |             SIX_MONTH_COLUMN, d183ls, YEAR_COLUMN, d365ls, | ||||||
|  |             ALL_TIME_COLUMN, dals, -1); | ||||||
|  |     g_free(d7ls); | ||||||
|  |     g_free(d30ls); | ||||||
|  |     g_free(d183ls); | ||||||
|  |     g_free(d365ls); | ||||||
|  |     g_free(dals); */ | ||||||
|  |  | ||||||
|  |     GtkTreeIter avgIter; | ||||||
|  |     gtk_tree_store_append(self->tableStore, &avgIter, NULL); | ||||||
|  |     char *d7as = format_hours_and_min(d7.avg); | ||||||
|  |     char *d30as = format_hours_and_min(d30.avg); | ||||||
|  |     char *d183as = format_hours_and_min(d183.avg); | ||||||
|  |     char *d365as = format_hours_and_min(d365.avg); | ||||||
|  |     char *daas = format_hours_and_min(da.avg); | ||||||
|  |     gtk_tree_store_set(self->tableStore, &avgIter, DATA_COLUMN, "Average", | ||||||
|  |             SEVEN_DAY_COLUMN, d7as, THIRTY_DAY_COLUMN, d30as, | ||||||
|  |             SIX_MONTH_COLUMN, d183as, YEAR_COLUMN, d365as, | ||||||
|  |             ALL_TIME_COLUMN, daas, -1); | ||||||
|  |     g_free(d7as); | ||||||
|  |     g_free(d30as); | ||||||
|  |     g_free(d183as); | ||||||
|  |     g_free(d365as); | ||||||
|  |     g_free(daas); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static TimerGraphPoint *timer_graph_window_get_day_graph_data(TimerGraphWindow *self) { | ||||||
|  |     TimerGraphPoint *pts = g_malloc_n(14, sizeof(TimerGraphPoint)); | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < 14; ++i) { | ||||||
|  |         pts[i].x = i + 1; | ||||||
|  |         pts[i].y = 0; | ||||||
|  |     } | ||||||
|  |     GDateTime *now = g_date_time_new_now_local(); | ||||||
|  |     for (i = 0; i < self->dayDataLen; ++i) { | ||||||
|  |         int off = day_offset(self->dayData[i].date, now); | ||||||
|  |         if (off <= 14) { | ||||||
|  |             pts[off - 1].y += (int) (self->dayData[i].lenght / 60); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     g_date_time_unref(now); | ||||||
|  |     return pts; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_make_day_graph(TimerGraphWindow *self) { | ||||||
|  |     TimerGraphPoint *pts = timer_graph_window_get_day_graph_data(self); | ||||||
|  |     self->dayChart = timer_graph_new(pts, 14, "Day", "Time (Total Minutes)", "Last 14 Days"); | ||||||
|  |     gtk_widget_set_vexpand(self->dayChart, TRUE); | ||||||
|  |     gtk_widget_set_hexpand(self->dayChart, TRUE); | ||||||
|  |     g_free(pts); | ||||||
|  |     gtk_box_pack_start(GTK_BOX(self->chartBox), self->dayChart, TRUE, TRUE, 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean same_month(GDateTime *d1, GDateTime *d2) { | ||||||
|  |     int y1, m1, y2, m2; | ||||||
|  |     g_date_time_get_ymd(d1, &y1, &m1, NULL); | ||||||
|  |     g_date_time_get_ymd(d2, &y2, &m2, NULL); | ||||||
|  |     return y1 == y2 && m1 == m2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static int get_days_for_month(int month, int year) { | ||||||
|  |     switch (month) { | ||||||
|  |         case 1: | ||||||
|  |         case 3: | ||||||
|  |         case 5: | ||||||
|  |         case 7: | ||||||
|  |         case 8: | ||||||
|  |         case 10: | ||||||
|  |         case 12: | ||||||
|  |             return 31; | ||||||
|  |         case 4: | ||||||
|  |         case 6: | ||||||
|  |         case 9: | ||||||
|  |         case 11: | ||||||
|  |             return 30; | ||||||
|  |         case 2: | ||||||
|  |             if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) { | ||||||
|  |                 return 29; | ||||||
|  |             } else { | ||||||
|  |                 return 28; | ||||||
|  |             } | ||||||
|  |         default: | ||||||
|  |             return 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static TimerGraphPoint *timer_graph_window_get_month_graph_data(TimerGraphWindow *self) { | ||||||
|  |     TimerGraphPoint *pts = g_malloc_n(12, sizeof(TimerGraphPoint)); | ||||||
|  |     GDateTime *now = g_date_time_new_now_local(); | ||||||
|  |     GDateTime *ref = g_date_time_add_months(now, -1); | ||||||
|  |     g_date_time_unref(now); | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < 12; ++i) { | ||||||
|  |         pts[i].x = i + 1; | ||||||
|  |         int total = 0; | ||||||
|  |         gsize j; | ||||||
|  |         for (j = 0; j < self->dayDataLen; ++j) { | ||||||
|  |             if (same_month(self->dayData[j].date, ref)) { | ||||||
|  |                 total += (int) (self->dayData[j].lenght / 60); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         int y, m; | ||||||
|  |         g_date_time_get_ymd(ref, &y, &m, NULL); | ||||||
|  |         pts[i].y = total / get_days_for_month(m, y); | ||||||
|  |         GDateTime *new = g_date_time_add_months(ref, -1); | ||||||
|  |         g_date_time_unref(ref); | ||||||
|  |         ref = new; | ||||||
|  |     } | ||||||
|  |     g_date_time_unref(ref); | ||||||
|  |     return pts; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_make_month_graph(TimerGraphWindow *self) { | ||||||
|  |     TimerGraphPoint *pts = timer_graph_window_get_month_graph_data(self); | ||||||
|  |     self->monthChart = timer_graph_new(pts, 12, "Month", "Time (Average Minutes)", "Last Year"); | ||||||
|  |     gtk_widget_set_vexpand(self->monthChart, TRUE); | ||||||
|  |     gtk_widget_set_hexpand(self->monthChart, TRUE); | ||||||
|  |     g_free(pts); | ||||||
|  |     gtk_box_pack_start(GTK_BOX(self->chartBox), self->monthChart, TRUE, TRUE, 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerGraphWindow *timer_graph_window_new(TimerApplication *app, | ||||||
|  |         TimerDataPoint *dayData, gsize dayDataLen, TimerDataPoint *taskData, gsize taskDataLen) { | ||||||
|  |     TimerGraphWindow *w = | ||||||
|  |         g_object_new(TIMER_TYPE_GRAPH_WINDOW, "application", app, "title", "Statistics", NULL); | ||||||
|  |     w->dayData = dayData; | ||||||
|  |     w->taskData = taskData; | ||||||
|  |     w->dayDataLen = dayDataLen; | ||||||
|  |     w->taskDataLen = taskDataLen; | ||||||
|  |     timer_graph_window_add_table_data(w); | ||||||
|  |     timer_graph_window_make_day_graph(w); | ||||||
|  |     timer_graph_window_make_month_graph(w); | ||||||
|  |     GdkScreen *screen = gtk_window_get_screen(GTK_WINDOW(w)); | ||||||
|  |     GdkDisplay *display = gdk_screen_get_display(screen); | ||||||
|  |     GdkMonitor *monitor = gdk_display_get_primary_monitor(display); | ||||||
|  |     if (monitor) { | ||||||
|  |         GdkRectangle monSize; | ||||||
|  |         gdk_monitor_get_geometry(monitor, &monSize); | ||||||
|  |         gtk_window_set_default_size(GTK_WINDOW(w), monSize.width * 0.75f, monSize.height * 0.75f); | ||||||
|  |     } | ||||||
|  |     gtk_widget_show_all(GTK_WIDGET(w)); | ||||||
|  |     return w; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void close_button_callback(GtkButton *btn, TimerGraphWindow *win) { | ||||||
|  |     gtk_window_close(GTK_WINDOW(win)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_build_ui(TimerGraphWindow *self) { | ||||||
|  |     self->closeButton = gtk_dialog_add_button(GTK_DIALOG(self), "Close", 0); | ||||||
|  |     GtkWidget *topBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); | ||||||
|  |     self->table = gtk_tree_view_new(); | ||||||
|  |     gtk_widget_set_halign(self->table, GTK_ALIGN_CENTER); | ||||||
|  |     gtk_box_pack_start(GTK_BOX(topBox), self->table, FALSE, FALSE, 0); | ||||||
|  |     self->chartBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); | ||||||
|  |     gtk_container_add(GTK_CONTAINER(topBox), self->chartBox); | ||||||
|  |     gtk_container_add( | ||||||
|  |             GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(self))), topBox); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_add_tree_columns(TimerGraphWindow *self) { | ||||||
|  |     gtk_tree_view_append_column( | ||||||
|  |             GTK_TREE_VIEW(self->table), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "", gtk_cell_renderer_text_new(), "text", DATA_COLUMN, NULL)); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self->table), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "Last 7 Days", gtk_cell_renderer_text_new(), | ||||||
|  |                 "text", SEVEN_DAY_COLUMN, NULL)); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self->table), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "Last 30 Days", | ||||||
|  |                 gtk_cell_renderer_text_new(), "text", | ||||||
|  |                 THIRTY_DAY_COLUMN, NULL)); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self->table), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "Last 6 Months", | ||||||
|  |                 gtk_cell_renderer_text_new(), "text", | ||||||
|  |                 SIX_MONTH_COLUMN, NULL)); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self->table), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "Last Year", gtk_cell_renderer_text_new(), | ||||||
|  |                 "text", YEAR_COLUMN, NULL)); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self->table), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "All Time", gtk_cell_renderer_text_new(), | ||||||
|  |                 "text", ALL_TIME_COLUMN, NULL)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_finalize(GObject *self) { | ||||||
|  |     timer_free_task_data(TIMER_GRAPH_WINDOW(self)->dayData, TIMER_GRAPH_WINDOW(self)->dayDataLen); | ||||||
|  |     timer_free_task_data(TIMER_GRAPH_WINDOW(self)->taskData, TIMER_GRAPH_WINDOW(self)->taskDataLen); | ||||||
|  |     G_OBJECT_CLASS(timer_graph_window_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_class_init(TimerGraphWindowClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_graph_window_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_graph_window_init(TimerGraphWindow *self) { | ||||||
|  |     gtk_window_set_keep_above(GTK_WINDOW(self), TRUE); | ||||||
|  |     gtk_window_set_position(GTK_WINDOW(self), GTK_WIN_POS_MOUSE); | ||||||
|  |     timer_graph_window_build_ui(self); | ||||||
|  |     g_signal_connect(self->closeButton, "clicked", | ||||||
|  |             G_CALLBACK(close_button_callback), self); | ||||||
|  |     gtk_tree_selection_set_mode( | ||||||
|  |             gtk_tree_view_get_selection(GTK_TREE_VIEW(self->table)), | ||||||
|  |             GTK_SELECTION_NONE); | ||||||
|  |     self->tableStore = gtk_tree_store_new( | ||||||
|  |             N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, | ||||||
|  |             G_TYPE_STRING, G_TYPE_STRING); | ||||||
|  |     gtk_tree_view_set_model(GTK_TREE_VIEW(self->table), | ||||||
|  |             GTK_TREE_MODEL(self->tableStore)); | ||||||
|  |     timer_graph_window_add_tree_columns(self); | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								TimerGraphWindow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								TimerGraphWindow.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | #ifndef TIMER_GRAPH_WINDOW_H | ||||||
|  | #define TIMER_GRAPH_WINDOW_H | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | #include "TimerApplication.h" | ||||||
|  | #include "TimerTaskTree.h" | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_GRAPH_WINDOW timer_graph_window_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerGraphWindow, timer_graph_window, TIMER, GRAPH_WINDOW, GtkDialog); | ||||||
|  |  | ||||||
|  | TimerGraphWindow *timer_graph_window_new(TimerApplication *app, TimerDataPoint *dayData, gsize dayDataLen, TimerDataPoint *taskData, gsize taskDataLen); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* TIMER_GRAPH_WINDOW_H */ | ||||||
							
								
								
									
										447
									
								
								TimerMainWindow.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										447
									
								
								TimerMainWindow.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,447 @@ | |||||||
|  | #include "TimerMainWindow.h" | ||||||
|  | #include "TimerClock.h" | ||||||
|  | #include "TimerEditWindow.h" | ||||||
|  | #include "TimerSettingsWindow.h" | ||||||
|  |  | ||||||
|  | #include <math.h> | ||||||
|  |  | ||||||
|  | struct _TimerMainWindow { | ||||||
|  |     GtkApplicationWindow parent; | ||||||
|  |  | ||||||
|  |     GtkWidget *timerButton; | ||||||
|  |     GtkWidget *timerLabel; | ||||||
|  |     GtkWidget *nameBox; | ||||||
|  |     GtkWidget *startStopButton; | ||||||
|  |     GtkWidget *resetButton; | ||||||
|  |     GtkWidget *saveButton; | ||||||
|  |     GtkWidget *optionsButton; | ||||||
|  |     GtkWidget *taskTreeBox; | ||||||
|  |     GtkWidget *taskTree; | ||||||
|  |  | ||||||
|  |     TimerClock *timerClock; | ||||||
|  |     TimerClock *updateClock; | ||||||
|  |     GKeyFile *keyFile; | ||||||
|  |  | ||||||
|  |     char *labelColorString; | ||||||
|  |  | ||||||
|  |     GDateTime *lastUpdateTime; | ||||||
|  |  | ||||||
|  |     /* time in seconds */ | ||||||
|  |     gint64 currentTime; | ||||||
|  |     GDateTime *startTime; | ||||||
|  |  | ||||||
|  |     int widthBuff; | ||||||
|  |     int heightBuff; | ||||||
|  |     int xBuff; | ||||||
|  |     int yBuff; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerMainWindow, timer_main_window, GTK_TYPE_APPLICATION_WINDOW); | ||||||
|  |  | ||||||
|  | static gboolean timer_main_window_get_boolean_with_default( | ||||||
|  |         TimerMainWindow *self, const char *group, const char *key, gboolean def) { | ||||||
|  |     GError *err = NULL; | ||||||
|  |     gboolean b = g_key_file_get_boolean(self->keyFile, group, key, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         g_error_free(err); | ||||||
|  |         return def; | ||||||
|  |     } | ||||||
|  |     return b; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static char *timer_main_window_get_string_with_default(TimerMainWindow *self, | ||||||
|  |         const char *group, | ||||||
|  |         const char *key, | ||||||
|  |         const char *def) { | ||||||
|  |     GError *err = NULL; | ||||||
|  |     char *s = g_key_file_get_string(self->keyFile, group, key, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         g_error_free(err); | ||||||
|  |         return g_strdup(def); | ||||||
|  |     } | ||||||
|  |     return s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_main_window_make_data_file(TimerMainWindow *self) { | ||||||
|  |     char *path = timer_main_window_get_string_with_default(self, "Settings", | ||||||
|  |             "Data Path", timer_application_get_default_data_file(TIMER_APPLICATION(gtk_window_get_application(GTK_WINDOW(self))))); | ||||||
|  |     GFile *file = g_file_new_for_path(path); | ||||||
|  |     GFile *parent = g_file_get_parent(file); | ||||||
|  |     GError *err = NULL; | ||||||
|  |     g_file_make_directory_with_parents(parent, NULL, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         if (err->code != G_IO_ERROR_EXISTS) { | ||||||
|  |             GtkWidget *msgDiag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "Could not create data file: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |             gtk_widget_destroy(msgDiag); | ||||||
|  |         } | ||||||
|  |         g_clear_error(&err); | ||||||
|  |     } | ||||||
|  |     GFileOutputStream *stream = | ||||||
|  |         g_file_create(file, G_FILE_CREATE_NONE, NULL, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         if (err->code != G_IO_ERROR_EXISTS) { | ||||||
|  |             GtkWidget *msgDiag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "Could not create data file: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |             gtk_widget_destroy(msgDiag); | ||||||
|  |         } | ||||||
|  |         g_error_free(err); | ||||||
|  |     } else { | ||||||
|  |         g_output_stream_write(G_OUTPUT_STREAM(stream), "[]", 2, NULL, &err); | ||||||
|  |         if (err != NULL) { | ||||||
|  |             GtkWidget *msgDiag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "An internal error occured: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |             gtk_widget_destroy(msgDiag); | ||||||
|  |             g_error_free(err); | ||||||
|  |         } | ||||||
|  |         g_object_unref(stream); | ||||||
|  |     } | ||||||
|  |     timer_task_tree_set_data_path(TIMER_TASK_TREE(self->taskTree), path); | ||||||
|  |     g_free(path); | ||||||
|  |     g_object_unref(parent); | ||||||
|  |     g_object_unref(file); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | gboolean timer_main_window_is_always_on_top(TimerMainWindow *self) { | ||||||
|  |     return timer_main_window_get_boolean_with_default(self, "Settings", "Always on Top", FALSE); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GDateTime *timer_main_window_get_last_task_end(TimerMainWindow *self) { | ||||||
|  |     return timer_task_tree_get_last_task_end(TIMER_TASK_TREE(self->taskTree)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_main_window_interpret_settings(TimerMainWindow *self) { | ||||||
|  |     gtk_window_set_keep_above(GTK_WINDOW(self), | ||||||
|  |             timer_main_window_get_boolean_with_default( | ||||||
|  |                 self, "Settings", "Always on Top", FALSE)); | ||||||
|  |  | ||||||
|  |     gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(self->nameBox)); | ||||||
|  |     gsize len; | ||||||
|  |     GError *err = NULL; | ||||||
|  |     char **data = g_key_file_get_string_list(self->keyFile, "Settings", "Tasks", | ||||||
|  |             &len, &err); | ||||||
|  |     if (err == NULL) { | ||||||
|  |         timer_task_tree_set_task_names(TIMER_TASK_TREE(self->taskTree), | ||||||
|  |                 (const char **)data, len); | ||||||
|  |         gsize i; | ||||||
|  |         for (i = 0; i < len; ++i) { | ||||||
|  |             gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(self->nameBox), | ||||||
|  |                     data[i]); | ||||||
|  |             g_free(data[i]); | ||||||
|  |         } | ||||||
|  |         g_free(data); | ||||||
|  |     } else { | ||||||
|  |         timer_task_tree_set_task_names(TIMER_TASK_TREE(self->taskTree), NULL, | ||||||
|  |                 0); | ||||||
|  |         g_error_free(err); | ||||||
|  |     } | ||||||
|  |     char *taskName = timer_main_window_get_string_with_default( | ||||||
|  |             self, "Cache", "Current Name", ""); | ||||||
|  |     gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(self->nameBox))), | ||||||
|  |             taskName); | ||||||
|  |     g_free(taskName); | ||||||
|  |     int x = g_key_file_get_integer(self->keyFile, "Cache", "x", NULL); | ||||||
|  |     int y = g_key_file_get_integer(self->keyFile, "Cache", "y", NULL); | ||||||
|  |     int w = g_key_file_get_integer(self->keyFile, "Cache", "width", NULL); | ||||||
|  |     int h = g_key_file_get_integer(self->keyFile, "Cache", "height", NULL); | ||||||
|  |     gtk_window_move(GTK_WINDOW(self), x, y); | ||||||
|  |     gtk_window_resize(GTK_WINDOW(self), w, h); | ||||||
|  |     timer_main_window_make_data_file(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerMainWindow *timer_main_window_new(TimerApplication *app) { | ||||||
|  |     TimerMainWindow *win = TIMER_MAIN_WINDOW( | ||||||
|  |             g_object_new(TIMER_TYPE_MAIN_WINDOW, "application", app, NULL)); | ||||||
|  |     g_key_file_load_from_file(win->keyFile, | ||||||
|  |             timer_application_get_config_file(app), | ||||||
|  |             G_KEY_FILE_NONE, NULL); | ||||||
|  |     timer_main_window_interpret_settings(win); | ||||||
|  |     timer_task_tree_expand_today(TIMER_TASK_TREE(win->taskTree)); | ||||||
|  |     win->lastUpdateTime = g_date_time_new_now_local(); | ||||||
|  |     timer_clock_start(win->updateClock); | ||||||
|  |     return win; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | char *timer_main_window_get_task_csv(TimerMainWindow *self) { | ||||||
|  |     return timer_task_tree_get_csv(TIMER_TASK_TREE(self->taskTree)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerDataPoint *timer_main_window_get_day_data(TimerMainWindow *self, | ||||||
|  |         gsize *len) { | ||||||
|  |     return timer_task_tree_get_day_data(TIMER_TASK_TREE(self->taskTree), len); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerDataPoint *timer_main_window_get_task_data(TimerMainWindow *self, | ||||||
|  |         gsize *len) { | ||||||
|  |     return timer_task_tree_get_task_data(TIMER_TASK_TREE(self->taskTree), len); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean timer_main_window_update_time(TimerMainWindow *self) { | ||||||
|  |     int hour = floor(self->currentTime / 3600.0f); | ||||||
|  |     int minute = floor(self->currentTime / 60.0f) - (hour * 60); | ||||||
|  |     int second = self->currentTime - (hour * 3600) - (minute * 60); | ||||||
|  |     char *time = g_strdup_printf( | ||||||
|  |             "<span foreground='%s'>%02d:%02d:%02d</span>", | ||||||
|  |             timer_clock_is_running(self->timerClock) ? "red" : self->labelColorString, | ||||||
|  |             hour, minute, second); | ||||||
|  |     gtk_label_set_markup(GTK_LABEL(self->timerLabel), time); | ||||||
|  |     g_free(time); | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_clock_callback(TimerMainWindow *win) { | ||||||
|  |     ++win->currentTime; | ||||||
|  |     g_main_context_invoke(NULL, G_SOURCE_FUNC(timer_main_window_update_time), win); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void options_button_callback(GtkWidget *btn, TimerMainWindow *win) { | ||||||
|  |     TimerSettingsWindow *diag = timer_settings_window_new( | ||||||
|  |             TIMER_APPLICATION(gtk_window_get_application(GTK_WINDOW(win))), | ||||||
|  |             win->keyFile, GTK_WINDOW(win)); | ||||||
|  |     int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |     if (resp == GTK_RESPONSE_APPLY) { | ||||||
|  |         GKeyFile *sKeyFile = timer_settings_window_get_key_file(diag); | ||||||
|  |         gsize len; | ||||||
|  |         char *data = g_key_file_to_data(sKeyFile, &len, NULL); | ||||||
|  |         g_key_file_load_from_data(win->keyFile, data, len, G_KEY_FILE_NONE, | ||||||
|  |                 NULL); | ||||||
|  |         g_free(data); | ||||||
|  |         GError *err = NULL; | ||||||
|  |         g_key_file_save_to_file( | ||||||
|  |                 win->keyFile, | ||||||
|  |                 timer_application_get_config_file( | ||||||
|  |                     TIMER_APPLICATION(gtk_window_get_application(GTK_WINDOW(win)))), | ||||||
|  |                 &err); | ||||||
|  |         if (err != NULL) { | ||||||
|  |             GtkWidget *msgDiag = gtk_message_dialog_new( | ||||||
|  |                     GTK_WINDOW(win), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, | ||||||
|  |                     GTK_BUTTONS_OK, "Could not save settings: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |             g_error_free(err); | ||||||
|  |             gtk_widget_destroy(msgDiag); | ||||||
|  |         } | ||||||
|  |         timer_main_window_interpret_settings(win); | ||||||
|  |     } | ||||||
|  |     gtk_widget_destroy(GTK_WIDGET(diag)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void start_stop_button_callback(GtkButton *btn, TimerMainWindow *win) { | ||||||
|  |     if (timer_clock_is_running(win->timerClock)) { | ||||||
|  |         timer_clock_stop(win->timerClock); | ||||||
|  |         gtk_button_set_label(GTK_BUTTON(win->startStopButton), "Start"); | ||||||
|  |         timer_main_window_update_time(win); | ||||||
|  |     } else { | ||||||
|  |         timer_clock_start(win->timerClock); | ||||||
|  |         gtk_button_set_label(GTK_BUTTON(win->startStopButton), "Stop"); | ||||||
|  |         gtk_widget_set_sensitive(win->resetButton, TRUE); | ||||||
|  |         gtk_widget_set_sensitive(win->saveButton, TRUE); | ||||||
|  |         win->startTime = g_date_time_new_now_local(); | ||||||
|  |         timer_main_window_update_time(win); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void reset_button_callback(GtkButton *btn, TimerMainWindow *win) { | ||||||
|  |     if (timer_clock_is_running(win->timerClock)) { | ||||||
|  |         timer_clock_stop(win->timerClock); | ||||||
|  |     } | ||||||
|  |     win->currentTime = 0; | ||||||
|  |     timer_main_window_update_time(win); | ||||||
|  |     gtk_button_set_label(GTK_BUTTON(win->startStopButton), "Start"); | ||||||
|  |     gtk_widget_set_sensitive(win->resetButton, FALSE); | ||||||
|  |     gtk_widget_set_sensitive(win->saveButton, FALSE); | ||||||
|  |     g_date_time_unref(win->startTime); | ||||||
|  |     win->startTime = NULL; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void save_button_callback(GtkButton *btn, TimerMainWindow *win) { | ||||||
|  |     if (timer_clock_is_running(win->timerClock)) { | ||||||
|  |         timer_clock_stop(win->timerClock); | ||||||
|  |     } | ||||||
|  |     const char *text = | ||||||
|  |         gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(win->nameBox)); | ||||||
|  |     timer_task_tree_add_task(TIMER_TASK_TREE(win->taskTree), win->startTime, | ||||||
|  |             strcmp(text, "") == 0 ? "Untitled" : text, | ||||||
|  |             win->currentTime); | ||||||
|  |     win->currentTime = 0; | ||||||
|  |     g_date_time_unref(win->startTime); | ||||||
|  |     win->startTime = NULL; | ||||||
|  |     timer_main_window_update_time(win); | ||||||
|  |     gtk_button_set_label(GTK_BUTTON(win->startStopButton), "Start"); | ||||||
|  |     gtk_widget_set_sensitive(win->resetButton, FALSE); | ||||||
|  |     gtk_widget_set_sensitive(win->saveButton, FALSE); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_button_callback(GtkButton *btn, TimerMainWindow *win) { | ||||||
|  |     if (timer_clock_is_running(win->timerClock)) { | ||||||
|  |         timer_clock_stop(win->timerClock); | ||||||
|  |         timer_main_window_update_time(win); | ||||||
|  |         gtk_button_set_label(GTK_BUTTON(win->startStopButton), "Start"); | ||||||
|  |     } | ||||||
|  |     gsize optLen; | ||||||
|  |     const char **names = | ||||||
|  |         timer_task_tree_get_task_names(TIMER_TASK_TREE(win->taskTree), &optLen); | ||||||
|  |     GDateTime *start = win->startTime == NULL | ||||||
|  |         ? g_date_time_new_now_local() | ||||||
|  |         : g_date_time_to_local(win->startTime); | ||||||
|  |     TimerEditWindow *diag = timer_edit_window_new( | ||||||
|  |             gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(win->nameBox)), | ||||||
|  |             start, win->currentTime, names, optLen, TRUE, timer_main_window_get_last_task_end(win)); | ||||||
|  |     g_date_time_unref(start); | ||||||
|  |     int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |     if (resp == GTK_RESPONSE_APPLY) { | ||||||
|  |         if (win->startTime) { | ||||||
|  |             g_date_time_unref(win->startTime); | ||||||
|  |         } | ||||||
|  |         win->startTime = timer_edit_window_get_start(diag); | ||||||
|  |         win->currentTime = timer_edit_window_get_length(diag); | ||||||
|  |         char *name = timer_edit_window_get_name(diag); | ||||||
|  |         gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(win->nameBox))), | ||||||
|  |                 name); | ||||||
|  |         g_free(name); | ||||||
|  |         timer_main_window_update_time(win); | ||||||
|  |         gtk_widget_set_sensitive(win->resetButton, TRUE); | ||||||
|  |         gtk_widget_set_sensitive(win->saveButton, TRUE); | ||||||
|  |     } | ||||||
|  |     gtk_widget_destroy(GTK_WIDGET(diag)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean do_update_header_dates(TimerMainWindow *win) { | ||||||
|  |     timer_task_tree_update_header_dates(TIMER_TASK_TREE(win->taskTree)); | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean compare_date(GDateTime *dt1, GDateTime *dt2) { | ||||||
|  |     int y1, m1, d1, y2, m2, d2; | ||||||
|  |     g_date_time_get_ymd(dt1, &y1, &m1, &d1); | ||||||
|  |     g_date_time_get_ymd(dt2, &y2, &m2, &d2); | ||||||
|  |     return y1 == y2 && m1 == m2 && d1 == d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void update_clock_callback(TimerMainWindow *win) { | ||||||
|  |     GDateTime *now = g_date_time_new_now_local(); | ||||||
|  |     if (!compare_date(win->lastUpdateTime, now)) { | ||||||
|  |         g_main_context_invoke(NULL, G_SOURCE_FUNC(do_update_header_dates), win); | ||||||
|  |         g_date_time_unref(win->lastUpdateTime); | ||||||
|  |         win->lastUpdateTime = now; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_main_window_get_defualt_label_color(TimerMainWindow *self) { | ||||||
|  |     GtkStyleContext *ctx = gtk_widget_get_style_context(self->timerLabel); | ||||||
|  |     GdkRGBA color; | ||||||
|  |     gtk_style_context_get_color(ctx, GTK_STATE_FLAG_NORMAL, &color); | ||||||
|  |     self->labelColorString = g_strdup_printf( | ||||||
|  |             "#%x%x%x", (int)round(color.red * 255), (int)round(color.green * 255), | ||||||
|  |             (int)round(color.blue * 255)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean window_configure_callback(TimerMainWindow *win) { | ||||||
|  |     int x, y, w, h; | ||||||
|  |     gtk_window_get_size(GTK_WINDOW(win), &w, &h); | ||||||
|  |     gtk_window_get_position(GTK_WINDOW(win), &x, &y); | ||||||
|  |     g_key_file_set_integer(win->keyFile, "Cache", "x", x); | ||||||
|  |     g_key_file_set_integer(win->keyFile, "Cache", "y", y); | ||||||
|  |     g_key_file_set_integer(win->keyFile, "Cache", "width", w); | ||||||
|  |     g_key_file_set_integer(win->keyFile, "Cache", "height", h); | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void window_destroy_callback(TimerMainWindow *win) { | ||||||
|  |     g_key_file_set_string(win->keyFile, "Cache", "Current Name", | ||||||
|  |             gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(win->nameBox))))); | ||||||
|  |     GError *err = NULL; | ||||||
|  |     g_key_file_save_to_file(win->keyFile, | ||||||
|  |             timer_application_get_config_file(TIMER_APPLICATION( | ||||||
|  |                     gtk_window_get_application(GTK_WINDOW(win)))), | ||||||
|  |             &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                 NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                 "Could not save window state: %s", err->message); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |         g_error_free(err); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean window_delete_event(TimerMainWindow *win, GdkEvent *evt) { | ||||||
|  |     if (win->startTime != NULL) { | ||||||
|  |         GtkWidget *diag = gtk_message_dialog_new(GTK_WINDOW(win), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "Are you sure you would like to exit?"); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |         return resp == GTK_RESPONSE_NO; | ||||||
|  |     } | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_main_window_finalize(GObject *self) { | ||||||
|  |     g_free(TIMER_MAIN_WINDOW(self)->labelColorString); | ||||||
|  |     g_object_unref(TIMER_MAIN_WINDOW(self)->timerClock); | ||||||
|  |     g_object_unref(TIMER_MAIN_WINDOW(self)->updateClock); | ||||||
|  |     if (TIMER_MAIN_WINDOW(self)->lastUpdateTime != NULL) { | ||||||
|  |         g_date_time_unref(TIMER_MAIN_WINDOW(self)->lastUpdateTime); | ||||||
|  |     } | ||||||
|  |     if (TIMER_MAIN_WINDOW(self)->startTime != NULL) { | ||||||
|  |         g_date_time_unref(TIMER_MAIN_WINDOW(self)->startTime); | ||||||
|  |     } | ||||||
|  |     G_OBJECT_CLASS(timer_main_window_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_main_window_class_init(TimerMainWindowClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_main_window_finalize; | ||||||
|  |     gtk_widget_class_set_template_from_resource( | ||||||
|  |             GTK_WIDGET_CLASS(class), "/zander/practicetimer/ui/main-window.glade"); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerMainWindow, timerButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerMainWindow, timerLabel); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerMainWindow, nameBox); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerMainWindow, startStopButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerMainWindow, resetButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerMainWindow, saveButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerMainWindow, optionsButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerMainWindow, taskTreeBox); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_main_window_init(TimerMainWindow *self) { | ||||||
|  |     gtk_widget_init_template(GTK_WIDGET(self)); | ||||||
|  |     self->taskTree = timer_task_tree_new(); | ||||||
|  |     gtk_container_add(GTK_CONTAINER(self->taskTreeBox), self->taskTree); | ||||||
|  |     self->keyFile = g_key_file_new(); | ||||||
|  |     g_signal_connect(self->optionsButton, "clicked", | ||||||
|  |             G_CALLBACK(options_button_callback), self); | ||||||
|  |     self->timerClock = | ||||||
|  |         timer_clock_new(1000, (TimerClockAction)timer_clock_callback, self); | ||||||
|  |     self->updateClock = | ||||||
|  |         timer_clock_new(60000 /* one minute */, (TimerClockAction) update_clock_callback, self); | ||||||
|  |     g_signal_connect(self->startStopButton, "clicked", | ||||||
|  |             G_CALLBACK(start_stop_button_callback), self); | ||||||
|  |     g_signal_connect(self->resetButton, "clicked", | ||||||
|  |             G_CALLBACK(reset_button_callback), self); | ||||||
|  |     g_signal_connect(self->saveButton, "clicked", | ||||||
|  |             G_CALLBACK(save_button_callback), self); | ||||||
|  |     g_signal_connect(self->timerButton, "clicked", | ||||||
|  |             G_CALLBACK(timer_button_callback), self); | ||||||
|  |     g_signal_connect(self, "destroy", G_CALLBACK(window_destroy_callback), NULL); | ||||||
|  |     g_signal_connect(self, "configure-event", G_CALLBACK(window_configure_callback), NULL); | ||||||
|  |     g_signal_connect(self, "delete-event", G_CALLBACK(window_delete_event), NULL); | ||||||
|  |     timer_main_window_get_defualt_label_color(self); | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								TimerMainWindow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								TimerMainWindow.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_MAIN_WINDOW_H | ||||||
|  | #define INCLUDED_TIMER_MAIN_WINDOW_H | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | #include "TimerApplication.h" | ||||||
|  | #include "TimerTaskTree.h" | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_MAIN_WINDOW timer_main_window_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerMainWindow, timer_main_window, TIMER, MAIN_WINDOW, GtkApplicationWindow) | ||||||
|  |  | ||||||
|  | TimerMainWindow *timer_main_window_new(TimerApplication *app); | ||||||
|  | char *timer_main_window_get_task_csv(TimerMainWindow *self); | ||||||
|  | TimerDataPoint *timer_main_window_get_task_data(TimerMainWindow *self, gsize *len); | ||||||
|  | TimerDataPoint *timer_main_window_get_day_data(TimerMainWindow *self, gsize *len); | ||||||
|  | gboolean timer_main_window_is_always_on_top(TimerMainWindow *self); | ||||||
|  | GDateTime *timer_main_window_get_last_task_end(TimerMainWindow *self); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_MAIN_WINDOW_H */ | ||||||
							
								
								
									
										291
									
								
								TimerSettingsWindow.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								TimerSettingsWindow.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,291 @@ | |||||||
|  | #include "TimerSettingsWindow.h" | ||||||
|  | #include "TimerGraphWindow.h" | ||||||
|  | #include "TimerMainWindow.h" | ||||||
|  |  | ||||||
|  | #define DEFAULT_DATA_FILE_NAME "practicetimer/data.json" | ||||||
|  |  | ||||||
|  | struct _TimerSettingsWindow { | ||||||
|  |     GtkDialog parent; | ||||||
|  |  | ||||||
|  |     GtkWidget *cancelButton; | ||||||
|  |     GtkWidget *saveButton; | ||||||
|  |     GtkWidget *dataPathButton; | ||||||
|  |     GtkWidget *taskBox; | ||||||
|  |     GtkWidget *removeTaskButton; | ||||||
|  |     GtkWidget *taskNameEntry; | ||||||
|  |     GtkWidget *addTaskButton; | ||||||
|  |     GtkWidget *graphButton; | ||||||
|  |     GtkWidget *exportButton; | ||||||
|  |     GtkWidget *alwaysOnTopCheck; | ||||||
|  |     GtkWidget *versionLabel; | ||||||
|  |  | ||||||
|  |     GKeyFile *keyFile; | ||||||
|  |  | ||||||
|  |     TimerMainWindow *parentWindow; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerSettingsWindow, timer_settings_window, GTK_TYPE_DIALOG) | ||||||
|  |  | ||||||
|  |     static void timer_settings_window_copy_key_file(TimerSettingsWindow *self, | ||||||
|  |             GKeyFile *keyFile) { | ||||||
|  |         self->keyFile = g_key_file_new(); | ||||||
|  |         gsize len; | ||||||
|  |         char *data = g_key_file_to_data(keyFile, &len, NULL); | ||||||
|  |         g_key_file_load_from_data(self->keyFile, data, len, G_KEY_FILE_NONE, NULL); | ||||||
|  |         g_free(data); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | static gboolean | ||||||
|  | timer_settings_window_get_boolean_with_default(TimerSettingsWindow *self, | ||||||
|  |         const char *group, | ||||||
|  |         const char *key, gboolean def) { | ||||||
|  |     GError *err = NULL; | ||||||
|  |     gboolean b = g_key_file_get_boolean(self->keyFile, group, key, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         g_error_free(err); | ||||||
|  |         return def; | ||||||
|  |     } | ||||||
|  |     return b; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static char *timer_settings_window_get_string_with_default( | ||||||
|  |         TimerSettingsWindow *self, const char *group, const char *key, | ||||||
|  |         const char *def) { | ||||||
|  |     GError *err = NULL; | ||||||
|  |     char *s = g_key_file_get_string(self->keyFile, group, key, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         g_error_free(err); | ||||||
|  |         return g_strdup(def); | ||||||
|  |     } | ||||||
|  |     return s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void | ||||||
|  | timer_settings_window_interpret_settings(TimerSettingsWindow *self) { | ||||||
|  |     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->alwaysOnTopCheck), | ||||||
|  |             timer_settings_window_get_boolean_with_default( | ||||||
|  |                 self, "Settings", "Always on Top", FALSE)); | ||||||
|  |  | ||||||
|  |     /* Data path stuff */ | ||||||
|  |     char *defaultDataPath = g_strdup_printf("%s/%s", g_get_user_config_dir(), | ||||||
|  |             DEFAULT_DATA_FILE_NAME); | ||||||
|  |     char *dataPath = timer_settings_window_get_string_with_default( | ||||||
|  |             self, "Settings", "Data Path", defaultDataPath); | ||||||
|  |     gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(self->dataPathButton), | ||||||
|  |             dataPath); | ||||||
|  |     g_free(defaultDataPath); | ||||||
|  |     g_free(dataPath); | ||||||
|  |  | ||||||
|  |     /* Task list stuff */ | ||||||
|  |     GError *err = NULL; | ||||||
|  |     gsize len; | ||||||
|  |     char **tasks = g_key_file_get_string_list(self->keyFile, "Settings", | ||||||
|  |             "Tasks", &len, &err); | ||||||
|  |     if (err == NULL) { | ||||||
|  |         gsize i; | ||||||
|  |         for (i = 0; i < len; ++i) { | ||||||
|  |             gtk_list_box_insert(GTK_LIST_BOX(self->taskBox), | ||||||
|  |                     gtk_label_new(tasks[i]), -1); | ||||||
|  |             g_free(tasks[i]); | ||||||
|  |         } | ||||||
|  |         g_free(tasks); | ||||||
|  |     } else { | ||||||
|  |         g_error_free(err); | ||||||
|  |     } | ||||||
|  |     gtk_widget_show_all(self->taskBox); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerSettingsWindow *timer_settings_window_new(TimerApplication *app, | ||||||
|  |         GKeyFile *keyFile, | ||||||
|  |         GtkWindow *parentWindow) { | ||||||
|  |     TimerSettingsWindow *win = TIMER_SETTINGS_WINDOW( | ||||||
|  |             g_object_new(TIMER_TYPE_SETTINGS_WINDOW, "application", app, NULL)); | ||||||
|  |     timer_settings_window_copy_key_file(win, keyFile); | ||||||
|  |     timer_settings_window_interpret_settings(win); | ||||||
|  |     if (TIMER_IS_MAIN_WINDOW(parentWindow)) { | ||||||
|  |         win->parentWindow = TIMER_MAIN_WINDOW(parentWindow); | ||||||
|  |     } | ||||||
|  |     return win; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GKeyFile *timer_settings_window_get_key_file(TimerSettingsWindow *self) { | ||||||
|  |     return self->keyFile; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean timer_settings_window_has_task(TimerSettingsWindow *self, | ||||||
|  |         const char *task) { | ||||||
|  |     GList *tasks = gtk_container_get_children(GTK_CONTAINER(self->taskBox)); | ||||||
|  |     GList *i; | ||||||
|  |     for (i = tasks; i != NULL; i = i->next) { | ||||||
|  |         if (strcmp(task, gtk_label_get_text(GTK_LABEL( | ||||||
|  |                             gtk_bin_get_child(GTK_BIN(i->data))))) == 0) { | ||||||
|  |             return TRUE; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     g_list_free(tasks); | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void add_button_callback(GtkButton *btn, TimerSettingsWindow *win) { | ||||||
|  |     const char *task = gtk_entry_get_text(GTK_ENTRY(win->taskNameEntry)); | ||||||
|  |     if (strlen(task) != 0) { | ||||||
|  |         if (!timer_settings_window_has_task(win, task)) { | ||||||
|  |             gtk_list_box_insert(GTK_LIST_BOX(win->taskBox), gtk_label_new(task), | ||||||
|  |                     -1); | ||||||
|  |             gtk_widget_show_all(win->taskBox); | ||||||
|  |         } | ||||||
|  |         gtk_entry_set_text(GTK_ENTRY(win->taskNameEntry), ""); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void remove_button_callback(GtkButton *btn, TimerSettingsWindow *win) { | ||||||
|  |     GList *rows = gtk_list_box_get_selected_rows(GTK_LIST_BOX(win->taskBox)); | ||||||
|  |     GList *i; | ||||||
|  |     for (i = rows; i != NULL; i = i->next) { | ||||||
|  |         gtk_container_remove(GTK_CONTAINER(win->taskBox), GTK_WIDGET(i->data)); | ||||||
|  |     } | ||||||
|  |     g_list_free(rows); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void cancel_button_callback(GtkButton *btn, TimerSettingsWindow *win) { | ||||||
|  |     gtk_dialog_response(GTK_DIALOG(win), GTK_RESPONSE_CANCEL); | ||||||
|  |     gtk_window_close(GTK_WINDOW(win)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static const char *const * | ||||||
|  | timer_settings_window_get_task_list_string(TimerSettingsWindow *self, | ||||||
|  |         gsize *len) { | ||||||
|  |     GList *tasks = gtk_container_get_children(GTK_CONTAINER(self->taskBox)); | ||||||
|  |     gsize taskCount = g_list_length(tasks); | ||||||
|  |     char **taskStrings = g_malloc(sizeof(char *) * taskCount); | ||||||
|  |     GList *it; | ||||||
|  |     int in; | ||||||
|  |     for (in = 0, it = tasks; it != NULL; ++in, it = it->next) { | ||||||
|  |         taskStrings[in] = (char *)gtk_label_get_text( | ||||||
|  |                 GTK_LABEL(gtk_bin_get_child(GTK_BIN(it->data)))); | ||||||
|  |     } | ||||||
|  |     g_list_free(tasks); | ||||||
|  |     if (len != NULL) { | ||||||
|  |         *len = taskCount; | ||||||
|  |     } | ||||||
|  |     return (const char *const *)taskStrings; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void save_button_callback(GtkButton *btn, TimerSettingsWindow *win) { | ||||||
|  |     g_key_file_set_boolean( | ||||||
|  |             win->keyFile, "Settings", "Always on Top", | ||||||
|  |             gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(win->alwaysOnTopCheck))); | ||||||
|  |     g_key_file_set_string( | ||||||
|  |             win->keyFile, "Settings", "Data Path", | ||||||
|  |             gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(win->dataPathButton))); | ||||||
|  |     gsize taskCount; | ||||||
|  |     const char *const *taskList = | ||||||
|  |         timer_settings_window_get_task_list_string(win, &taskCount); | ||||||
|  |     g_key_file_set_string_list(win->keyFile, "Settings", "Tasks", taskList, | ||||||
|  |             taskCount); | ||||||
|  |     g_free((gpointer)taskList); | ||||||
|  |     gtk_dialog_response(GTK_DIALOG(win), GTK_RESPONSE_APPLY); | ||||||
|  |     gtk_window_close(GTK_WINDOW(win)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void export_button_callback(GtkButton *btn, TimerSettingsWindow *win) { | ||||||
|  |     GtkWidget *diag = gtk_file_chooser_dialog_new( | ||||||
|  |             "Export Data", GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", | ||||||
|  |             GTK_RESPONSE_CANCEL, "_Export", GTK_RESPONSE_ACCEPT, NULL); | ||||||
|  |     int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |     if (resp == GTK_RESPONSE_ACCEPT) { | ||||||
|  |         char *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(diag)); | ||||||
|  |         char *csvData = timer_main_window_get_task_csv(win->parentWindow); | ||||||
|  |         GFile *file = g_file_new_for_path(path); | ||||||
|  |         GError *err = NULL; | ||||||
|  |         GFileOutputStream *out = | ||||||
|  |             g_file_create(file, G_FILE_CREATE_NONE, NULL, &err); | ||||||
|  |         if (err != NULL) { | ||||||
|  |             GtkWidget *msgDiag = gtk_message_dialog_new( | ||||||
|  |                     GTK_WINDOW(win), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, | ||||||
|  |                     GTK_BUTTONS_OK, "Could not export data: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |             gtk_widget_destroy(msgDiag); | ||||||
|  |             g_clear_error(&err); | ||||||
|  |         } | ||||||
|  |         g_output_stream_write(G_OUTPUT_STREAM(out), csvData, strlen(csvData), | ||||||
|  |                 NULL, &err); | ||||||
|  |         if (err != NULL) { | ||||||
|  |             GtkWidget *msgDiag = gtk_message_dialog_new( | ||||||
|  |                     GTK_WINDOW(win), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, | ||||||
|  |                     GTK_BUTTONS_OK, "Could not export data: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(msgDiag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(msgDiag)); | ||||||
|  |             gtk_widget_destroy(msgDiag); | ||||||
|  |             g_error_free(err); | ||||||
|  |         } | ||||||
|  |         g_object_unref(out); | ||||||
|  |         g_object_unref(file); | ||||||
|  |         g_free(csvData); | ||||||
|  |         g_free(path); | ||||||
|  |     } | ||||||
|  |     gtk_widget_destroy(diag); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void graph_button_callback(GtkButton *btn, TimerSettingsWindow *win) { | ||||||
|  |     gsize taskLen; | ||||||
|  |     TimerDataPoint *taskData = timer_main_window_get_task_data(win->parentWindow, &taskLen); | ||||||
|  |     gsize dayLen; | ||||||
|  |     TimerDataPoint *dayData = timer_main_window_get_day_data(win->parentWindow, &dayLen); | ||||||
|  |     TimerGraphWindow *diag = timer_graph_window_new(TIMER_APPLICATION(gtk_window_get_application(GTK_WINDOW(win))), dayData, dayLen, taskData, taskLen); | ||||||
|  |     gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |     gtk_widget_destroy(GTK_WIDGET(diag)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_settings_window_finalize(GObject *self) { | ||||||
|  |     g_key_file_free(TIMER_SETTINGS_WINDOW(self)->keyFile); | ||||||
|  |     G_OBJECT_CLASS(timer_settings_window_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_settings_window_class_init(TimerSettingsWindowClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_settings_window_finalize; | ||||||
|  |     gtk_widget_class_set_template_from_resource( | ||||||
|  |             GTK_WIDGET_CLASS(class), | ||||||
|  |             "/zander/practicetimer/ui/settings-window.glade"); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, cancelButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, saveButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, dataPathButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal(GTK_WIDGET_CLASS(class), | ||||||
|  |             TimerSettingsWindow, taskBox); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, removeTaskButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, taskNameEntry); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, addTaskButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, graphButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, exportButton); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, alwaysOnTopCheck); | ||||||
|  |     gtk_widget_class_bind_template_child_internal( | ||||||
|  |             GTK_WIDGET_CLASS(class), TimerSettingsWindow, versionLabel); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_settings_window_init(TimerSettingsWindow *self) { | ||||||
|  |     gtk_widget_init_template(GTK_WIDGET(self)); | ||||||
|  |     gtk_window_set_keep_above(GTK_WINDOW(self), TRUE); | ||||||
|  |     g_signal_connect(self->cancelButton, "clicked", | ||||||
|  |             G_CALLBACK(cancel_button_callback), self); | ||||||
|  |     g_signal_connect(self->saveButton, "clicked", | ||||||
|  |             G_CALLBACK(save_button_callback), self); | ||||||
|  |     g_signal_connect(self->addTaskButton, "clicked", | ||||||
|  |             G_CALLBACK(add_button_callback), self); | ||||||
|  |     g_signal_connect(self->removeTaskButton, "clicked", | ||||||
|  |             G_CALLBACK(remove_button_callback), self); | ||||||
|  |     g_signal_connect(self->exportButton, "clicked", | ||||||
|  |             G_CALLBACK(export_button_callback), self); | ||||||
|  |     g_signal_connect(self->graphButton, "clicked", | ||||||
|  |             G_CALLBACK(graph_button_callback), self); | ||||||
|  |     gtk_label_set_text(GTK_LABEL(self->versionLabel), "Version " APPLICATION_VERSION); | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								TimerSettingsWindow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								TimerSettingsWindow.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_SETTINGS_WINDOW_H | ||||||
|  | #define INCLUDED_TIMER_SETTINGS_WINDOW_H | ||||||
|  |  | ||||||
|  | #include "TimerApplication.h" | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_SETTINGS_WINDOW timer_settings_window_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerSettingsWindow, timer_settings_window, TIMER, SETTINGS_WINDOW, GtkDialog) | ||||||
|  |  | ||||||
|  | TimerSettingsWindow *timer_settings_window_new(TimerApplication *app, GKeyFile *keyFile, GtkWindow *parentWindow); | ||||||
|  |  | ||||||
|  | GKeyFile *timer_settings_window_get_key_file(TimerSettingsWindow *self); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_SETTINGS_WINDOW_H */ | ||||||
							
								
								
									
										832
									
								
								TimerTaskTree.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										832
									
								
								TimerTaskTree.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,832 @@ | |||||||
|  | #include "TimerTaskTree.h" | ||||||
|  | #include "TimerEditWindow.h" | ||||||
|  | #include "TimerFileWatcher.h" | ||||||
|  |  | ||||||
|  | #include <json-glib/json-glib.h> | ||||||
|  | #include <math.h> | ||||||
|  |  | ||||||
|  | static gint hash_date(GDateTime *dt) { | ||||||
|  |     int y, m, d; | ||||||
|  |     g_date_time_get_ymd(dt, &y, &m, &d); | ||||||
|  |     GDate *da = g_date_new_dmy(d, m, y); | ||||||
|  |     return g_date_get_julian(da); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean compare_date(GDateTime *dt1, GDateTime *dt2) { | ||||||
|  |     int y1, m1, d1, y2, m2, d2; | ||||||
|  |     g_date_time_get_ymd(dt1, &y1, &m1, &d1); | ||||||
|  |     g_date_time_get_ymd(dt2, &y2, &m2, &d2); | ||||||
|  |     return y1 == y2 && m1 == m2 && d1 == d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gint hash_time(GDateTime *t) { | ||||||
|  |     int h = g_date_time_get_hour(t); | ||||||
|  |     int m = g_date_time_get_minute(t); | ||||||
|  |     int s = g_date_time_get_second(t); | ||||||
|  |     return (h * 3600) + (m * 60) + s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum { DATE_COLUMN, TOTAL_TIME_COLUMN, SORT_COLUMN, DATA_COLUMN, N_COLUMNS }; | ||||||
|  |  | ||||||
|  | /* TimerHeader declarations */ | ||||||
|  | #define TIMER_TYPE_HEADER timer_header_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerHeader, timer_header, TIMER, HEADER, GObject); | ||||||
|  |  | ||||||
|  | static TimerHeader *timer_header_new(GDateTime *date, TimerTaskTree *tree); | ||||||
|  | static void timer_header_add_task(TimerHeader *self, const char *name, | ||||||
|  |         GDateTime *start, gint64 length); | ||||||
|  |  | ||||||
|  | /* TimerTask declarations */ | ||||||
|  | #define TIMER_TYPE_TASK timer_task_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerTask, timer_task, TIMER, TASK, GObject); | ||||||
|  |  | ||||||
|  | static TimerTask *timer_task_new(const char *name, GDateTime *start, | ||||||
|  |         gint64 length, TimerHeader *header); | ||||||
|  | static gint64 timer_task_get_length(TimerTask *self); | ||||||
|  | static JsonObject *timer_task_serialize(TimerTask *self); | ||||||
|  |  | ||||||
|  | /* TimerTaskTree variables */ | ||||||
|  | struct _TimerTaskTree { | ||||||
|  |     GtkTreeView parent; | ||||||
|  |  | ||||||
|  |     GtkTreeStore *store; | ||||||
|  |     GHashTable *headers; | ||||||
|  |  | ||||||
|  |     GtkWidget *popup; | ||||||
|  |     GtkWidget *deleteButton; | ||||||
|  |     GtkWidget *editButton; | ||||||
|  |  | ||||||
|  |     char **taskNames; | ||||||
|  |     gsize taskNamesLen; | ||||||
|  |  | ||||||
|  |     TimerFileWatcher *fileWatcher; | ||||||
|  |  | ||||||
|  |     char *dataPath; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerTaskTree, timer_task_tree, GTK_TYPE_TREE_VIEW); | ||||||
|  |  | ||||||
|  | /* TimerTask variables */ | ||||||
|  | struct _TimerTask { | ||||||
|  |     GObject parent; | ||||||
|  |  | ||||||
|  |     TimerTaskTree *tree; | ||||||
|  |     GtkTreeStore *store; | ||||||
|  |  | ||||||
|  |     char *name; | ||||||
|  |     GDateTime *start; | ||||||
|  |     gint64 length; | ||||||
|  |     GtkTreeIter iter; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerTask, timer_task, G_TYPE_OBJECT); | ||||||
|  |  | ||||||
|  | /* TimerHeader class */ | ||||||
|  | struct _TimerHeader { | ||||||
|  |     GObject parent; | ||||||
|  |  | ||||||
|  |     TimerTaskTree *tree; | ||||||
|  |     GtkTreeStore *store; | ||||||
|  |  | ||||||
|  |     GDateTime *date; | ||||||
|  |     GtkTreeIter iter; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | G_DEFINE_TYPE(TimerHeader, timer_header, G_TYPE_OBJECT); | ||||||
|  |  | ||||||
|  | static char *timer_header_get_date_string(TimerHeader *self) { | ||||||
|  |     GDateTime *now = g_date_time_new_now_local(); | ||||||
|  |     if (compare_date(self->date, now)) { | ||||||
|  |         g_date_time_unref(now); | ||||||
|  |         return g_strdup("Today"); | ||||||
|  |     } | ||||||
|  |     GDateTime *yesterday = g_date_time_add_days(now, -1); | ||||||
|  |     g_date_time_unref(now); | ||||||
|  |     if (compare_date(self->date, yesterday)) { | ||||||
|  |         g_date_time_unref(yesterday); | ||||||
|  |         return g_strdup("Yesterday"); | ||||||
|  |     } | ||||||
|  |     g_date_time_unref(yesterday); | ||||||
|  |     int y, m, d; | ||||||
|  |     g_date_time_get_ymd(self->date, &y, &m, &d); | ||||||
|  |     return g_strdup_printf("%02d/%02d/%04d", m, d, y); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static JsonObject *timer_header_serialize(TimerHeader *self) { | ||||||
|  |     JsonObject *root = json_object_new(); | ||||||
|  |     json_object_set_int_member(root, "date", g_date_time_to_unix(self->date)); | ||||||
|  |     JsonArray *tasks = json_array_new(); | ||||||
|  |     GtkTreeIter child; | ||||||
|  |     if (gtk_tree_model_iter_children(GTK_TREE_MODEL(self->store), &child, | ||||||
|  |                 &self->iter)) { | ||||||
|  |         TimerTask *t; | ||||||
|  |         do { | ||||||
|  |             gtk_tree_model_get(GTK_TREE_MODEL(self->store), &child, DATA_COLUMN, | ||||||
|  |                     &t, -1); | ||||||
|  |             g_object_unref(t); | ||||||
|  |             json_array_add_object_element(tasks, timer_task_serialize(t)); | ||||||
|  |         } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(self->store), &child)); | ||||||
|  |     } | ||||||
|  |     json_object_set_array_member(root, "tasks", tasks); | ||||||
|  |     return root; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static TimerHeader *timer_header_new(GDateTime *date, TimerTaskTree *tree) { | ||||||
|  |     TimerHeader *h = g_object_new(TIMER_TYPE_HEADER, NULL); | ||||||
|  |     h->date = g_date_time_to_local(date); | ||||||
|  |     h->tree = tree; | ||||||
|  |     h->store = GTK_TREE_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(tree))); | ||||||
|  |     gtk_tree_store_append(h->store, &h->iter, NULL); | ||||||
|  |     char *ds = timer_header_get_date_string(h); | ||||||
|  |     gtk_tree_store_set(h->store, &h->iter, DATE_COLUMN, ds, TOTAL_TIME_COLUMN, | ||||||
|  |             "00:00:00", SORT_COLUMN, hash_date(h->date), DATA_COLUMN, | ||||||
|  |             h, -1); | ||||||
|  |     g_object_unref(h); | ||||||
|  |     g_free(ds); | ||||||
|  |     gtk_widget_show_all(GTK_WIDGET(tree)); | ||||||
|  |     return h; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static guint64 timer_header_get_total_time(TimerHeader *self) { | ||||||
|  |     guint64 time = 0; | ||||||
|  |     GtkTreeIter task; | ||||||
|  |     if (gtk_tree_model_iter_children(GTK_TREE_MODEL(self->store), &task, | ||||||
|  |                 &self->iter)) { | ||||||
|  |         TimerTask *t; | ||||||
|  |         do { | ||||||
|  |             gtk_tree_model_get(GTK_TREE_MODEL(self->store), &task, DATA_COLUMN, | ||||||
|  |                     &t, -1); | ||||||
|  |             g_object_unref(t); | ||||||
|  |             time += timer_task_get_length(t); | ||||||
|  |         } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(self->store), &task)); | ||||||
|  |     } | ||||||
|  |     return time; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_set_time(TimerHeader *self, guint64 time) { | ||||||
|  |     int h = floor(time / 3600.0f); | ||||||
|  |     int m = floor(time / 60.0f) - (h * 60); | ||||||
|  |     int s = time - (m * 60) - (h * 3600); | ||||||
|  |     char *total = g_strdup_printf("%02d:%02d:%02d", h, m, s); | ||||||
|  |     gtk_tree_store_set(self->store, &self->iter, TOTAL_TIME_COLUMN, total, -1); | ||||||
|  |     g_free(total); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_update_time(TimerHeader *self) { | ||||||
|  |     timer_header_set_time(self, timer_header_get_total_time(self)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_add_task(TimerHeader *self, const char *name, | ||||||
|  |         GDateTime *start, gint64 length) { | ||||||
|  |     timer_task_new(name, start, length, self); | ||||||
|  |     timer_header_update_time(self); | ||||||
|  |     gtk_widget_show_all(GTK_WIDGET(self->tree)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_remove(TimerHeader *self) { | ||||||
|  |     gtk_tree_store_remove(self->store, &self->iter); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_remove_task(TimerHeader *self, TimerTask *task) { | ||||||
|  |     gtk_tree_store_remove(self->store, &task->iter); | ||||||
|  |     if (!gtk_tree_model_iter_has_child(GTK_TREE_MODEL(self->store), | ||||||
|  |                 &self->iter)) { | ||||||
|  |         timer_header_remove(self); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_finalize(GObject *self) { | ||||||
|  |     g_hash_table_remove(TIMER_HEADER(self)->tree->headers, | ||||||
|  |             TIMER_HEADER(self)->date); | ||||||
|  |     g_date_time_unref(TIMER_HEADER(self)->date); | ||||||
|  |     G_OBJECT_CLASS(timer_header_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_class_init(TimerHeaderClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_header_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_header_init(TimerHeader *self) { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* TimerTask class */ | ||||||
|  | static TimerHeader *timer_task_get_header(TimerTask *self) { | ||||||
|  |     TimerHeader *h; | ||||||
|  |     GtkTreeIter parent; | ||||||
|  |     gtk_tree_model_iter_parent(GTK_TREE_MODEL(self->store), &parent, | ||||||
|  |             &self->iter); | ||||||
|  |     gtk_tree_model_get(GTK_TREE_MODEL(self->store), &parent, DATA_COLUMN, &h, | ||||||
|  |             -1); | ||||||
|  |     g_object_unref(h); | ||||||
|  |     return h; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_update_time(TimerTask *self) { | ||||||
|  |     int h = floor(self->length / 3600.0f); | ||||||
|  |     int m = floor(self->length / 60.0f) - (h * 60); | ||||||
|  |     int s = self->length - (m * 60) - (h * 3600); | ||||||
|  |     char *total = g_strdup_printf("%02d:%02d:%02d", h, m, s); | ||||||
|  |     gtk_tree_store_set(self->store, &self->iter, TOTAL_TIME_COLUMN, total, -1); | ||||||
|  |     TimerHeader *th = timer_task_get_header(self); | ||||||
|  |     timer_header_update_time(th); | ||||||
|  |     g_free(total); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static JsonObject *timer_task_serialize(TimerTask *self) { | ||||||
|  |     JsonObject *root = json_object_new(); | ||||||
|  |     json_object_set_string_member(root, "name", self->name); | ||||||
|  |     json_object_set_int_member(root, "length", self->length); | ||||||
|  |     json_object_set_int_member(root, "start", g_date_time_to_unix(self->start)); | ||||||
|  |     return root; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static TimerTask *timer_task_new(const char *name, GDateTime *start, | ||||||
|  |         gint64 length, TimerHeader *header) { | ||||||
|  |     TimerTask *t = g_object_new(TIMER_TYPE_TASK, NULL); | ||||||
|  |     t->name = g_strdup(name); | ||||||
|  |     t->start = g_date_time_to_local(start); | ||||||
|  |     t->length = length; | ||||||
|  |     t->tree = header->tree; | ||||||
|  |     t->store = header->store; | ||||||
|  |     gtk_tree_store_append(t->store, &t->iter, &header->iter); | ||||||
|  |     gtk_tree_store_set(t->store, &t->iter, DATE_COLUMN, t->name, SORT_COLUMN, | ||||||
|  |             hash_time(t->start), DATA_COLUMN, t, -1); | ||||||
|  |     g_object_unref(t); | ||||||
|  |     timer_task_update_time(t); | ||||||
|  |     gtk_widget_show_all(GTK_WIDGET(t->tree)); | ||||||
|  |     return t; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_edit(TimerTask *self) { | ||||||
|  |     TimerEditWindow *diag = timer_edit_window_new( | ||||||
|  |             self->name, self->start, self->length, | ||||||
|  |             (const char **)self->tree->taskNames, self->tree->taskNamesLen, FALSE, NULL); | ||||||
|  |     int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |     if (resp == GTK_RESPONSE_APPLY) { | ||||||
|  |         GDateTime *newTime = timer_edit_window_get_start(diag); | ||||||
|  |         if (compare_date(self->start, newTime)) { | ||||||
|  |             g_free(self->name); | ||||||
|  |             self->name = timer_edit_window_get_name(diag); | ||||||
|  |             g_date_time_unref(self->start); | ||||||
|  |             self->start = newTime; | ||||||
|  |             gtk_tree_store_set(self->store, &self->iter, DATE_COLUMN, | ||||||
|  |                     self->name, SORT_COLUMN, hash_time(newTime), -1); | ||||||
|  |             self->length = timer_edit_window_get_length(diag); | ||||||
|  |             timer_task_update_time(self); | ||||||
|  |             timer_task_tree_save(self->tree); | ||||||
|  |         } else { | ||||||
|  |             TimerHeader *h = timer_task_get_header(self); | ||||||
|  |             timer_header_remove_task(h, self); | ||||||
|  |             char *name = timer_edit_window_get_name(diag); | ||||||
|  |             timer_task_tree_add_task(h->tree, newTime, name, | ||||||
|  |                     timer_edit_window_get_length(diag)); | ||||||
|  |             g_free(name); | ||||||
|  |             g_date_time_unref(newTime); | ||||||
|  |         } | ||||||
|  |     } else if (resp == GTK_RESPONSE_REJECT) { | ||||||
|  |         TimerHeader *h = timer_task_get_header(self); | ||||||
|  |         timer_header_remove_task(h, self); | ||||||
|  |     } | ||||||
|  |     gtk_widget_destroy(GTK_WIDGET(diag)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gint64 timer_task_get_length(TimerTask *self) { | ||||||
|  |     return self->length; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_finalize(GObject *self) { | ||||||
|  |     g_free(TIMER_TASK(self)->name); | ||||||
|  |     g_date_time_unref(TIMER_TASK(self)->start); | ||||||
|  |     G_OBJECT_CLASS(timer_task_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_class_init(TimerTaskClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_task_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_init(TimerTask *self) { | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* TimerTaskTree class */ | ||||||
|  | GtkWidget *timer_task_tree_new() { | ||||||
|  |     TimerTaskTree *t = g_object_new(TIMER_TYPE_TASK_TREE, NULL); | ||||||
|  |     t->dataPath = NULL; | ||||||
|  |     return GTK_WIDGET(t); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_tree_add_task_no_save(TimerTaskTree *self, | ||||||
|  |         GDateTime *date, const char *task, | ||||||
|  |         gint64 time) { | ||||||
|  |     TimerHeader *h; | ||||||
|  |     if (g_hash_table_contains(self->headers, date)) { | ||||||
|  |         h = g_hash_table_lookup(self->headers, date); | ||||||
|  |     } else { | ||||||
|  |         h = timer_header_new(date, self); | ||||||
|  |         g_hash_table_insert(self->headers, g_date_time_to_local(date), h); | ||||||
|  |     } | ||||||
|  |     timer_header_add_task(h, task, date, time); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_add_task(TimerTaskTree *self, GDateTime *date, | ||||||
|  |         const char *task, gint64 time) { | ||||||
|  |     timer_task_tree_add_task_no_save(self, date, task, time); | ||||||
|  |     timer_task_tree_save(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_set_task_names(TimerTaskTree *self, const char **names, | ||||||
|  |         gsize len) { | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < self->taskNamesLen; ++i) { | ||||||
|  |         g_free(self->taskNames[i]); | ||||||
|  |     } | ||||||
|  |     g_free(self->taskNames); | ||||||
|  |     self->taskNames = g_malloc_n(len, sizeof(char *)); | ||||||
|  |     for (i = 0; i < len; ++i) { | ||||||
|  |         self->taskNames[i] = g_strdup(names[i]); | ||||||
|  |     } | ||||||
|  |     self->taskNamesLen = len; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const char **timer_task_tree_get_task_names(TimerTaskTree *self, gsize *len) { | ||||||
|  |     *len = self->taskNamesLen; | ||||||
|  |     return (const char **)self->taskNames; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_update_header_dates(TimerTaskTree *self) { | ||||||
|  |     GHashTableIter iter; | ||||||
|  |     g_hash_table_iter_init(&iter, self->headers); | ||||||
|  |     TimerHeader *h = NULL; | ||||||
|  |     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &h)) { | ||||||
|  |         char *ds = timer_header_get_date_string(h); | ||||||
|  |         gtk_tree_store_set(self->store, &h->iter, DATE_COLUMN, ds, -1); | ||||||
|  |         free(ds); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_tree_update_data(TimerTaskTree *self) { | ||||||
|  |     GHashTableIter iter; | ||||||
|  |     g_hash_table_iter_init(&iter, self->headers); | ||||||
|  |     GList *headers = NULL; | ||||||
|  |     TimerHeader *h; | ||||||
|  |     while (g_hash_table_iter_next(&iter, NULL, (gpointer *)&h)) { | ||||||
|  |         headers = g_list_append(headers, h); | ||||||
|  |     } | ||||||
|  |     GList *i; | ||||||
|  |     for (i = headers; i != NULL; i = i->next) { | ||||||
|  |         timer_header_remove(TIMER_HEADER(i->data)); | ||||||
|  |     } | ||||||
|  |     g_list_free(headers); | ||||||
|  |     timer_task_tree_add_from_file(self, self->dataPath); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean do_async_update_data_file(TimerTaskTree *tree) { | ||||||
|  |     timer_task_tree_update_data(tree); | ||||||
|  |     timer_task_tree_expand_today(tree); | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void data_file_updated(TimerFileWatcher *fw, TimerTaskTree *tree) { | ||||||
|  |     g_main_context_invoke(NULL, G_SOURCE_FUNC(do_async_update_data_file), tree); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_set_data_path(TimerTaskTree *self, const char *path) { | ||||||
|  |     g_free(self->dataPath); | ||||||
|  |     self->dataPath = g_strdup(path); | ||||||
|  |     timer_task_tree_update_data(self); | ||||||
|  |     if (self->fileWatcher) { | ||||||
|  |         g_object_unref(self->fileWatcher); | ||||||
|  |     } | ||||||
|  |     self->fileWatcher = timer_file_watcher_new(path); | ||||||
|  |     g_signal_connect(self->fileWatcher, "file-changed", G_CALLBACK(data_file_updated), self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_expand_today(TimerTaskTree *self) { | ||||||
|  |     GDateTime *today = g_date_time_new_now_local(); | ||||||
|  |     TimerHeader *h = g_hash_table_lookup(self->headers, today); | ||||||
|  |     g_date_time_unref(today); | ||||||
|  |     if (h != NULL) {  | ||||||
|  |         GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(self->store), &h->iter); | ||||||
|  |         gtk_tree_view_expand_row(GTK_TREE_VIEW(self), path, TRUE); | ||||||
|  |         gtk_tree_path_free(path); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_save(TimerTaskTree *self) { | ||||||
|  |     if (self->fileWatcher) { | ||||||
|  |         timer_file_watcher_pause(self->fileWatcher); | ||||||
|  |     } | ||||||
|  |     GHashTableIter iter; | ||||||
|  |     g_hash_table_iter_init(&iter, self->headers); | ||||||
|  |     TimerHeader *h; | ||||||
|  |     JsonArray *headers = json_array_new(); | ||||||
|  |     while (g_hash_table_iter_next(&iter, NULL, (gpointer *)&h)) { | ||||||
|  |         json_array_add_object_element(headers, timer_header_serialize(h)); | ||||||
|  |     } | ||||||
|  |     JsonNode *root = json_node_new(JSON_NODE_ARRAY); | ||||||
|  |     json_node_set_array(root, headers); | ||||||
|  |     JsonGenerator *out = json_generator_new(); | ||||||
|  |     json_generator_set_root(out, root); | ||||||
|  |     json_node_unref(root); | ||||||
|  |     if (self->dataPath != NULL) { | ||||||
|  |         GError *err = NULL; | ||||||
|  |         json_generator_to_file(out, self->dataPath, &err); | ||||||
|  |         if (err != NULL) { | ||||||
|  |             GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "Could not save tasks: %s", err->message); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |             gtk_widget_destroy(diag); | ||||||
|  |             g_error_free(err); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     g_object_unref(out); | ||||||
|  |     if (self->fileWatcher) { | ||||||
|  |         timer_file_watcher_resume(self->fileWatcher, FALSE); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean timer_task_tree_add_task_from_object(TimerTaskTree *self, | ||||||
|  |         JsonObject *obj, | ||||||
|  |         gboolean status) { | ||||||
|  |     JsonNode *nameNode = json_object_get_member(obj, "name"); | ||||||
|  |     JsonNode *lengthNode = json_object_get_member(obj, "length"); | ||||||
|  |     JsonNode *startNode = json_object_get_member(obj, "start"); | ||||||
|  |     if (nameNode == NULL || lengthNode == NULL || startNode == NULL || | ||||||
|  |             json_node_get_value_type(nameNode) != G_TYPE_STRING || | ||||||
|  |             json_node_get_value_type(lengthNode) != G_TYPE_INT64 || | ||||||
|  |             json_node_get_value_type(startNode) != G_TYPE_INT64) { | ||||||
|  |         if (status) { | ||||||
|  |             GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "Corrupt task found! It and all future corrupt objects will be " | ||||||
|  |                     "skipped. No furtur errors will be emmited for the remailder " | ||||||
|  |                     "of this load."); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |             gtk_widget_destroy(diag); | ||||||
|  |         } | ||||||
|  |         return FALSE; | ||||||
|  |     } | ||||||
|  |     GDateTime *date = | ||||||
|  |         g_date_time_new_from_unix_local(json_node_get_int(startNode)); | ||||||
|  |     timer_task_tree_add_task_no_save(self, date, json_node_get_string(nameNode), | ||||||
|  |             json_node_get_int(lengthNode)); | ||||||
|  |     g_date_time_unref(date); | ||||||
|  |     return TRUE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean timer_task_tree_add_header_from_object(TimerTaskTree *self, | ||||||
|  |         JsonObject *obj, | ||||||
|  |         gboolean status) { | ||||||
|  |     JsonNode *tasksNode = json_object_get_member(obj, "tasks"); | ||||||
|  |     if (tasksNode == NULL || JSON_NODE_TYPE(tasksNode) != JSON_NODE_ARRAY) { | ||||||
|  |         if (status) { | ||||||
|  |             GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "Corrupt header found! It and all future corrupt objects will " | ||||||
|  |                     "be skipped. No furtur errors will be emmited for the " | ||||||
|  |                     "remailder of this load."); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |             gtk_widget_destroy(diag); | ||||||
|  |         } | ||||||
|  |         return FALSE; | ||||||
|  |     } | ||||||
|  |     JsonArray *arr = json_node_get_array(tasksNode); | ||||||
|  |     GList *tasks = json_array_get_elements(arr); | ||||||
|  |     GList *i; | ||||||
|  |     for (i = tasks; i != NULL; i = i->next) { | ||||||
|  |         JsonNode *taskNode = (JsonNode *)i->data; | ||||||
|  |         if (JSON_NODE_TYPE(taskNode) != JSON_NODE_OBJECT) { | ||||||
|  |             if (status) { | ||||||
|  |                 GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                         NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                         "Corrupt header found! It and all future corrupt objects " | ||||||
|  |                         "will be skipped. No furtur errors will be emmited for the " | ||||||
|  |                         "remailder of this load."); | ||||||
|  |                 gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |                 gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |                 gtk_widget_destroy(diag); | ||||||
|  |             } | ||||||
|  |             status = FALSE; | ||||||
|  |         } else { | ||||||
|  |             if (!timer_task_tree_add_task_from_object( | ||||||
|  |                         self, json_node_get_object(taskNode), status)) { | ||||||
|  |                 status = FALSE; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     g_list_free(tasks); | ||||||
|  |     return status; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_task_tree_add_from_file(TimerTaskTree *self, const char *path) { | ||||||
|  |     JsonParser *in = json_parser_new_immutable(); | ||||||
|  |     GError *err = NULL; | ||||||
|  |     json_parser_load_from_file(in, path, &err); | ||||||
|  |     if (err != NULL) { | ||||||
|  |         GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                 NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                 "Could not loads tasks: %s", err->message); | ||||||
|  |         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |         gtk_widget_destroy(diag); | ||||||
|  |         g_error_free(err); | ||||||
|  |     } else { | ||||||
|  |         JsonNode *root = json_parser_get_root(in); | ||||||
|  |         if (JSON_NODE_TYPE(root) != JSON_NODE_ARRAY) { | ||||||
|  |             GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                     NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, | ||||||
|  |                     "Task file corrupt! Root was not an array!"); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |             gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |             gtk_widget_destroy(diag); | ||||||
|  |         } else { | ||||||
|  |             gboolean status = TRUE; | ||||||
|  |             GList *headers = | ||||||
|  |                 json_array_get_elements(json_node_get_array((root))); | ||||||
|  |             GList *i; | ||||||
|  |             for (i = headers; i != NULL; i = i->next) { | ||||||
|  |                 JsonNode *hNode = (JsonNode *)i->data; | ||||||
|  |                 if (JSON_NODE_TYPE(hNode) != JSON_NODE_OBJECT) { | ||||||
|  |                     if (status) { | ||||||
|  |                         GtkWidget *diag = gtk_message_dialog_new( | ||||||
|  |                                 NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, | ||||||
|  |                                 GTK_BUTTONS_OK, | ||||||
|  |                                 "Corrupt header found! It and all future corrupt " | ||||||
|  |                                 "objects will be skipped. No furtur errors will be " | ||||||
|  |                                 "emmited for the remailder of this load."); | ||||||
|  |                         gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |                         gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |                         gtk_widget_destroy(diag); | ||||||
|  |                     } | ||||||
|  |                     status = FALSE; | ||||||
|  |                 } else { | ||||||
|  |                     if (!timer_task_tree_add_header_from_object( | ||||||
|  |                                 self, json_node_get_object(hNode), status)) { | ||||||
|  |                         status = FALSE; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             g_list_free(headers); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     g_object_unref(in); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | char *timer_task_tree_get_csv(TimerTaskTree *self) { | ||||||
|  |     GString *string = g_string_new("name,start,length\n"); | ||||||
|  |     GHashTableIter hIter; | ||||||
|  |     g_hash_table_iter_init(&hIter, self->headers); | ||||||
|  |     TimerHeader *header; | ||||||
|  |     while (g_hash_table_iter_next(&hIter, NULL, (gpointer *)&header)) { | ||||||
|  |         GtkTreeIter child; | ||||||
|  |         if (gtk_tree_model_iter_children(GTK_TREE_MODEL(self->store), &child, | ||||||
|  |                     &header->iter)) { | ||||||
|  |             TimerTask *task; | ||||||
|  |             do { | ||||||
|  |                 gtk_tree_model_get(GTK_TREE_MODEL(self->store), &child, | ||||||
|  |                         DATA_COLUMN, &task, -1); | ||||||
|  |                 g_object_unref(task); | ||||||
|  |                 g_string_append_printf(string, "%s,%ld,%ld\n", task->name, | ||||||
|  |                         g_date_time_to_unix(task->start), | ||||||
|  |                         task->length); | ||||||
|  |             } while ( | ||||||
|  |                     gtk_tree_model_iter_next(GTK_TREE_MODEL(self->store), &child)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return g_string_free(string, FALSE); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerDataPoint *timer_task_tree_get_day_data(TimerTaskTree *self, gsize *length) { | ||||||
|  |     *length = 0; | ||||||
|  |     TimerDataPoint *arr = g_malloc(1); | ||||||
|  |     GHashTableIter hIter; | ||||||
|  |     g_hash_table_iter_init(&hIter, self->headers); | ||||||
|  |     TimerHeader *header; | ||||||
|  |     while (g_hash_table_iter_next(&hIter, NULL, (gpointer *)&header)) { | ||||||
|  |         arr = g_realloc(arr, sizeof(TimerDataPoint) * ++(*length)); | ||||||
|  |         arr[*length - 1] = (TimerDataPoint){ | ||||||
|  |             g_date_time_to_local(header->date), timer_header_get_total_time(header)}; | ||||||
|  |     } | ||||||
|  |     return arr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | TimerDataPoint *timer_task_tree_get_task_data(TimerTaskTree *self, gsize *length) { | ||||||
|  |     *length = 0; | ||||||
|  |     TimerDataPoint *arr = g_malloc(1); | ||||||
|  |     GHashTableIter hIter; | ||||||
|  |     g_hash_table_iter_init(&hIter, self->headers); | ||||||
|  |     TimerHeader *header; | ||||||
|  |     while (g_hash_table_iter_next(&hIter, NULL, (gpointer *)&header)) { | ||||||
|  |         GtkTreeIter child; | ||||||
|  |         if (gtk_tree_model_iter_children(GTK_TREE_MODEL(self->store), &child, | ||||||
|  |                     &header->iter)) { | ||||||
|  |             TimerTask *task; | ||||||
|  |             do { | ||||||
|  |                 gtk_tree_model_get(GTK_TREE_MODEL(self->store), &child, | ||||||
|  |                         DATA_COLUMN, &task, -1); | ||||||
|  |                 g_object_unref(task); | ||||||
|  |                 arr = g_realloc(arr, sizeof(TimerDataPoint) * ++(*length)); | ||||||
|  |                 arr[*length - 1] = (TimerDataPoint){ | ||||||
|  |                     g_date_time_to_local(task->start), task->length}; | ||||||
|  |             } while ( | ||||||
|  |                     gtk_tree_model_iter_next(GTK_TREE_MODEL(self->store), &child)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return arr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void timer_free_task_data(TimerDataPoint *data, gsize length) { | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < length; ++i) { | ||||||
|  |         g_date_time_unref(data[i].date); | ||||||
|  |     } | ||||||
|  |     g_free(data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GDateTime *timer_task_tree_get_last_task_end(TimerTaskTree *self) { | ||||||
|  |     GtkTreeIter headerIter; | ||||||
|  |     if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(self->store), &headerIter)) { | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     GtkTreeIter child; | ||||||
|  |     if (!gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(self->store), &child, &headerIter, 0)) { | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  |     TimerTask *task; | ||||||
|  |     gtk_tree_model_get(GTK_TREE_MODEL(self->store), &child, DATA_COLUMN, &task, -1); | ||||||
|  |     g_object_unref(task); | ||||||
|  |     GDateTime *dt = g_date_time_add_seconds(task->start, task->length); | ||||||
|  |     return dt; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gint tree_sort_compare_func(GtkTreeModel *model, GtkTreeIter *i1, | ||||||
|  |         GtkTreeIter *i2) { | ||||||
|  |     GValue v1 = G_VALUE_INIT, v2 = G_VALUE_INIT; | ||||||
|  |     gtk_tree_model_get_value(model, i1, SORT_COLUMN, &v1); | ||||||
|  |     gtk_tree_model_get_value(model, i2, SORT_COLUMN, &v2); | ||||||
|  |     return g_value_get_uint64(&v1) - g_value_get_uint64(&v2); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static gboolean mouse_click_callback(TimerTaskTree *tree, | ||||||
|  |         GdkEventButton *event) { | ||||||
|  |     if (event->type == GDK_BUTTON_PRESS && event->button == 3) { | ||||||
|  |         GtkTreeSelection *selection = | ||||||
|  |             gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); | ||||||
|  |         GtkTreePath *path; | ||||||
|  |         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(tree), event->x, | ||||||
|  |                     event->y, &path, NULL, NULL, NULL)) { | ||||||
|  |             gtk_tree_selection_unselect_all(selection); | ||||||
|  |             gtk_tree_selection_select_path(selection, path); | ||||||
|  |             gtk_tree_path_free(path); | ||||||
|  |         } | ||||||
|  |         GtkTreeIter currentIter, parentIter; | ||||||
|  |         if (gtk_tree_selection_get_selected(selection, NULL, ¤tIter)) { | ||||||
|  |             if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), | ||||||
|  |                         &parentIter, ¤tIter)) { | ||||||
|  |                 gtk_widget_set_visible(tree->editButton, TRUE); | ||||||
|  |             } else { | ||||||
|  |                 gtk_widget_set_visible(tree->editButton, FALSE); | ||||||
|  |             } | ||||||
|  |             gtk_menu_popup_at_pointer(GTK_MENU(tree->popup), (GdkEvent *)event); | ||||||
|  |         } | ||||||
|  |         return TRUE; | ||||||
|  |     } else if (event->type == GDK_2BUTTON_PRESS && event->button == 1) { | ||||||
|  |         GtkTreeSelection *selection = | ||||||
|  |             gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); | ||||||
|  |         GtkTreePath *path; | ||||||
|  |         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(tree), event->x, | ||||||
|  |                     event->y, &path, NULL, NULL, NULL)) { | ||||||
|  |             gtk_tree_selection_unselect_all(selection); | ||||||
|  |             gtk_tree_selection_select_path(selection, path); | ||||||
|  |             gtk_tree_path_free(path); | ||||||
|  |         } | ||||||
|  |         GtkTreeIter select; | ||||||
|  |         if (gtk_tree_selection_get_selected(selection, NULL, &select)) { | ||||||
|  |             GObject *obj; | ||||||
|  |             gtk_tree_model_get(GTK_TREE_MODEL(tree->store), &select, | ||||||
|  |                     DATA_COLUMN, &obj, -1); | ||||||
|  |             g_object_unref(obj); | ||||||
|  |             if (TIMER_IS_TASK(obj)) { | ||||||
|  |                 timer_task_edit(TIMER_TASK(obj)); | ||||||
|  |                 timer_task_tree_save(tree); | ||||||
|  |             } else if (TIMER_IS_HEADER(obj)) { | ||||||
|  |                 GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &TIMER_HEADER(obj)->iter); | ||||||
|  |                 if (gtk_tree_view_row_expanded(GTK_TREE_VIEW(tree), path)) { | ||||||
|  |                     gtk_tree_view_collapse_row(GTK_TREE_VIEW(tree), path); | ||||||
|  |                 } else { | ||||||
|  |                     gtk_tree_view_expand_row(GTK_TREE_VIEW(tree), path, TRUE); | ||||||
|  |                 } | ||||||
|  |                 gtk_tree_path_free(path); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return TRUE; | ||||||
|  |     } | ||||||
|  |     return FALSE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void popup_edit_button_callback(GtkMenuItem *btn, TimerTaskTree *tree) { | ||||||
|  |     GtkTreeSelection *selection = | ||||||
|  |         gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); | ||||||
|  |     GtkTreeIter select; | ||||||
|  |     if (gtk_tree_selection_get_selected(selection, NULL, &select)) { | ||||||
|  |         TimerTask *task; | ||||||
|  |         gtk_tree_model_get(GTK_TREE_MODEL(tree->store), &select, DATA_COLUMN, | ||||||
|  |                 &task, -1); | ||||||
|  |         g_object_unref(task); | ||||||
|  |         timer_task_edit(task); | ||||||
|  |         timer_task_tree_save(tree); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void popup_delete_button_callback(GtkMenuItem *btn, | ||||||
|  |         TimerTaskTree *tree) { | ||||||
|  |     GtkTreeSelection *selection = | ||||||
|  |         gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); | ||||||
|  |     GtkTreeIter select; | ||||||
|  |     if (gtk_tree_selection_get_selected(selection, NULL, &select)) { | ||||||
|  |         GObject *obj; | ||||||
|  |         gtk_tree_model_get(GTK_TREE_MODEL(tree->store), &select, DATA_COLUMN, | ||||||
|  |                 &obj, -1); | ||||||
|  |         g_object_unref(obj); | ||||||
|  |         if (TIMER_IS_TASK(obj)) { | ||||||
|  |             timer_header_remove_task(timer_task_get_header(TIMER_TASK(obj)), | ||||||
|  |                     TIMER_TASK(obj)); | ||||||
|  |         } else { | ||||||
|  |             GtkWidget *diag = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "Are you sure you would like to delete ALL tasks on this day?"); | ||||||
|  |             gtk_window_set_position(GTK_WINDOW(diag), GTK_WIN_POS_MOUSE); | ||||||
|  |             int resp = gtk_dialog_run(GTK_DIALOG(diag)); | ||||||
|  |             if (resp == GTK_RESPONSE_YES) { | ||||||
|  |                 timer_header_remove(TIMER_HEADER(obj)); | ||||||
|  |             } | ||||||
|  |             gtk_widget_destroy(diag); | ||||||
|  |         } | ||||||
|  |         timer_task_tree_save(tree); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_tree_finalize(GObject *self) { | ||||||
|  |     gsize i; | ||||||
|  |     for (i = 0; i < TIMER_TASK_TREE(self)->taskNamesLen; ++i) { | ||||||
|  |         g_free(TIMER_TASK_TREE(self)->taskNames[i]); | ||||||
|  |     } | ||||||
|  |     g_free(TIMER_TASK_TREE(self)->dataPath); | ||||||
|  |     g_free(TIMER_TASK_TREE(self)->taskNames); | ||||||
|  |     gtk_widget_destroy(TIMER_TASK_TREE(self)->popup); | ||||||
|  |     g_object_unref(TIMER_TASK_TREE(self)->store); | ||||||
|  |     g_hash_table_destroy(TIMER_TASK_TREE(self)->headers); | ||||||
|  |     G_OBJECT_CLASS(timer_task_tree_parent_class)->finalize(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_tree_class_init(TimerTaskTreeClass *class) { | ||||||
|  |     G_OBJECT_CLASS(class)->finalize = timer_task_tree_finalize; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void timer_task_tree_init(TimerTaskTree *self) { | ||||||
|  |     self->store = gtk_tree_store_new(N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, | ||||||
|  |             G_TYPE_UINT64, G_TYPE_OBJECT); | ||||||
|  |     gtk_tree_view_set_model(GTK_TREE_VIEW(self), GTK_TREE_MODEL(self->store)); | ||||||
|  |     self->headers = | ||||||
|  |         g_hash_table_new_full((GHashFunc)hash_date, (GCompareFunc)compare_date, | ||||||
|  |                 (GDestroyNotify)g_date_time_unref, NULL); | ||||||
|  |     GtkCellRenderer *render = gtk_cell_renderer_text_new(); | ||||||
|  |     GValue font = G_VALUE_INIT; | ||||||
|  |     g_value_init(&font, G_TYPE_STRING); | ||||||
|  |     g_value_set_static_string(&font, "16"); | ||||||
|  |     g_object_set_property(G_OBJECT(render), "font", &font); | ||||||
|  |     g_value_unset(&font); | ||||||
|  |     gtk_tree_view_append_column( | ||||||
|  |             GTK_TREE_VIEW(self), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "Date", render, "text", DATE_COLUMN, NULL)); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self), | ||||||
|  |             gtk_tree_view_column_new_with_attributes( | ||||||
|  |                 "Time", render, | ||||||
|  |                 "text", TOTAL_TIME_COLUMN, NULL)); | ||||||
|  |     GtkTreeViewColumn *sort = gtk_tree_view_column_new_with_attributes( | ||||||
|  |             "Sort", render, NULL); | ||||||
|  |  | ||||||
|  |     gtk_tree_view_column_set_visible(sort, FALSE); | ||||||
|  |     gtk_tree_view_append_column(GTK_TREE_VIEW(self), sort); | ||||||
|  |     gtk_tree_sortable_set_default_sort_func( | ||||||
|  |             GTK_TREE_SORTABLE(self->store), | ||||||
|  |             (GtkTreeIterCompareFunc)tree_sort_compare_func, NULL, NULL); | ||||||
|  |     gtk_tree_sortable_set_sort_column_id( | ||||||
|  |             GTK_TREE_SORTABLE(self->store), | ||||||
|  |             GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID, GTK_SORT_DESCENDING); | ||||||
|  |     gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE); | ||||||
|  |     self->popup = gtk_menu_new(); | ||||||
|  |     self->deleteButton = gtk_menu_item_new_with_label("Delete"); | ||||||
|  |     self->editButton = gtk_menu_item_new_with_label("Edit"); | ||||||
|  |     gtk_menu_shell_append(GTK_MENU_SHELL(self->popup), self->deleteButton); | ||||||
|  |     gtk_menu_shell_append(GTK_MENU_SHELL(self->popup), self->editButton); | ||||||
|  |     gtk_widget_show_all(self->popup); | ||||||
|  |     g_signal_connect(self, "button_press_event", | ||||||
|  |             G_CALLBACK(mouse_click_callback), NULL); | ||||||
|  |     g_signal_connect(self->deleteButton, "activate", | ||||||
|  |             G_CALLBACK(popup_delete_button_callback), self); | ||||||
|  |     g_signal_connect(self->editButton, "activate", | ||||||
|  |             G_CALLBACK(popup_edit_button_callback), self); | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								TimerTaskTree.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								TimerTaskTree.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | #ifndef INCLUDED_TIMER_TASK_TREE_H | ||||||
|  | #define INCLUDED_TIMER_TASK_TREE_H | ||||||
|  |  | ||||||
|  | #include <gtk/gtk.h> | ||||||
|  |  | ||||||
|  | G_BEGIN_DECLS | ||||||
|  |  | ||||||
|  | #define TIMER_TYPE_TASK_TREE timer_task_tree_get_type() | ||||||
|  | G_DECLARE_FINAL_TYPE(TimerTaskTree, timer_task_tree, TIMER, TASK_TREE, GtkTreeView) | ||||||
|  |  | ||||||
|  | GtkWidget *timer_task_tree_new(); | ||||||
|  |  | ||||||
|  | /* time in seconds */ | ||||||
|  | void timer_task_tree_add_task(TimerTaskTree *self, GDateTime *date, const char *task, gint64 time); | ||||||
|  | void timer_task_tree_set_task_names(TimerTaskTree *self, const char **names, gsize len); | ||||||
|  | const char** timer_task_tree_get_task_names(TimerTaskTree *self, gsize *len); | ||||||
|  | void timer_task_tree_update_header_dates(TimerTaskTree *self); | ||||||
|  | void timer_task_tree_set_data_path(TimerTaskTree *self, const char *path); | ||||||
|  | void timer_task_tree_save(TimerTaskTree *self); | ||||||
|  | void timer_task_tree_add_from_file(TimerTaskTree *self, const char *path); | ||||||
|  | char *timer_task_tree_get_csv(TimerTaskTree *self); | ||||||
|  | void timer_task_tree_expand_today(TimerTaskTree *self); | ||||||
|  |  | ||||||
|  | typedef struct { | ||||||
|  |     GDateTime *date; | ||||||
|  |     gint64 lenght; | ||||||
|  | } TimerDataPoint; | ||||||
|  |  | ||||||
|  | TimerDataPoint *timer_task_tree_get_day_data(TimerTaskTree *self, gsize *length); | ||||||
|  | TimerDataPoint *timer_task_tree_get_task_data(TimerTaskTree *self, gsize *length); | ||||||
|  | void timer_free_task_data(TimerDataPoint *data, gsize length); | ||||||
|  | GDateTime *timer_task_tree_get_last_task_end(TimerTaskTree *self); | ||||||
|  |  | ||||||
|  | G_END_DECLS | ||||||
|  |  | ||||||
|  | #endif /* INCLUDED_TIMER_TASK_TREE_H */ | ||||||
							
								
								
									
										8
									
								
								main.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								main.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | #include "TimerApplication.h" | ||||||
|  |  | ||||||
|  | int main(int argc, char **argv) { | ||||||
|  |     TimerApplication *app = timer_application_new(); | ||||||
|  |     int status = g_application_run(G_APPLICATION(app), argc, argv); | ||||||
|  |     g_object_unref(app); | ||||||
|  |     return status; | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								practicetimer.gresource.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								practicetimer.gresource.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <gresources> | ||||||
|  |     <gresource prefix="/zander/practicetimer"> | ||||||
|  |         <file preprocess="xml-stripblanks">ui/main-window.glade</file> | ||||||
|  |         <file preprocess="xml-stripblanks">ui/edit-window.glade</file> | ||||||
|  |         <file preprocess="xml-stripblanks">ui/settings-window.glade</file> | ||||||
|  |     </gresource> | ||||||
|  | </gresources> | ||||||
							
								
								
									
										47
									
								
								tools/Practice Timer.app/Contents/Info.plist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tools/Practice Timer.app/Contents/Info.plist
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | <?xml version="1.0" ?> | ||||||
|  | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||||
|  | <plist version="1.0"> | ||||||
|  |     <dict> | ||||||
|  |     <key>CFBundleDevelopmentRegion</key> | ||||||
|  |     <string>English</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleExecutable</key> | ||||||
|  |     <string>Practice_Timer</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleIconFile</key> | ||||||
|  |     <string>icon.icns</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleIdentifier</key> | ||||||
|  |     <string>com.zander.practice_timer</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleDisplayName</key> | ||||||
|  |     <string>Practice Timer</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleInfoDictionaryVersion</key> | ||||||
|  |     <string>6.0</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleName</key> | ||||||
|  |     <string>Practice Timer</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundlePackageType</key> | ||||||
|  |     <string>APPL</string> | ||||||
|  |  | ||||||
|  |     <key>NSPrincipalClass</key> | ||||||
|  |     <string>NSApplication</string> | ||||||
|  |     <key>NSHighResolutionCapable</key> | ||||||
|  |     <string>True</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleShortVersionString</key> | ||||||
|  |     <string>2.2.0</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleSignature</key> | ||||||
|  |     <string>????</string> | ||||||
|  |  | ||||||
|  |     <key>CFBundleVersion</key> | ||||||
|  |     <string>2.2.0</string> | ||||||
|  |  | ||||||
|  |     <key>NSHumanReadableCopyright</key> | ||||||
|  |     <string>(c) 2021 Alexander Rosenberg</string> | ||||||
|  |  | ||||||
|  |     </dict> | ||||||
|  | </plist> | ||||||
							
								
								
									
										
											BIN
										
									
								
								tools/Practice Timer.app/Contents/Resources/icon.icns
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tools/Practice Timer.app/Contents/Resources/icon.icns
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										39
									
								
								tools/convert.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										39
									
								
								tools/convert.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | #!/usr/bin/python | ||||||
|  |  | ||||||
|  | import xml.etree.ElementTree as ET | ||||||
|  | import json | ||||||
|  | import fileinput | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | if len(sys.argv) == 2 and sys.argv[1] == '-h': | ||||||
|  |     print(sys.argv[0], ': Convert Practice Timer 1 data into Praceice Timer 2 data') | ||||||
|  |     print(sys.argv[0], ': usage: ', sys.argv[0], ' <input>') | ||||||
|  |     print(sys.argv[0], ': output will be on stdout. Input can also be read from stdin') | ||||||
|  |     exit(0) | ||||||
|  |  | ||||||
|  | xmlData = "" | ||||||
|  |  | ||||||
|  | for f in fileinput.input(): | ||||||
|  |     xmlData += f | ||||||
|  |  | ||||||
|  | root = ET.fromstring(xmlData) | ||||||
|  |  | ||||||
|  | headers = root[1] | ||||||
|  |  | ||||||
|  | jo = [] | ||||||
|  |  | ||||||
|  | for header in headers: | ||||||
|  |     ho = { | ||||||
|  |         "date": int(round(int(header.get('date')) / 1000)), | ||||||
|  |         "tasks": [] | ||||||
|  |     } | ||||||
|  |     for task in header: | ||||||
|  |         to = { | ||||||
|  |             "name": task.get('name'), | ||||||
|  |             "length": int(task.get('length')), | ||||||
|  |             "start": int(round(int(task.get('start')) / 1000)) | ||||||
|  |         } | ||||||
|  |         ho['tasks'].append(to) | ||||||
|  |     jo.append(ho) | ||||||
|  |  | ||||||
|  | print(json.dumps(jo)) | ||||||
							
								
								
									
										36
									
								
								tools/linux/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								tools/linux/PKGBUILD
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | # Maintainer: Alexander Rosenberg <zanderpkg at pm dot me> | ||||||
|  |  | ||||||
|  | pkgname=practice-timer | ||||||
|  | pkgver=1 | ||||||
|  | pkgrel=1 | ||||||
|  | pkgdesc="Track and analyze statistics about how long you do various activities" | ||||||
|  | url="https://gitlab.com/zander671/practice-timer" | ||||||
|  | arch=('x86_64') | ||||||
|  | license=('GPL3') | ||||||
|  | depends=('gtk3') | ||||||
|  | makedepends=('cmake' 'make' 'git') | ||||||
|  | optdepends=('plotutils: graphs in the statistics window') | ||||||
|  | source=("git+https://gitlab.com/zander671/practice-timer.git") | ||||||
|  | sha256sums=('SKIP') | ||||||
|  |  | ||||||
|  | pkgver(){ | ||||||
|  |   cd "$pkgname" | ||||||
|  |   git rev-list --count HEAD | ||||||
|  | } | ||||||
|  |  | ||||||
|  | build() { | ||||||
|  |   cd "$pkgname" | ||||||
|  |   cmake -B build | ||||||
|  |   make -C build | ||||||
|  | } | ||||||
|  |  | ||||||
|  | package() { | ||||||
|  |   cd "$pkgname" | ||||||
|  |   install -m755 -D build/Practice_Timer "$pkgdir/usr/bin/practice-timer" | ||||||
|  |   install -m644 -D LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" | ||||||
|  |   install -m644 -D README.md "$pkgdir/usr/share/doc/$pkgname/README" | ||||||
|  |   install -m644 -D tools/linux/practice-timer.xpm "$pkgdir/usr/share/pixmaps/practice-timer.xpm" | ||||||
|  |   install -m644 -D tools/linux/practice-timer.png "$pkgdir/usr/share/icons/hicolor/48x48/apps/practice-timer.png" | ||||||
|  |   install -m644 -D tools/linux/practice-timer.svg "$pkgdir/usr/share/icons/hicolor/scalable/apps/practice-timer.svg" | ||||||
|  |   install -m644 -D tools/linux/practice-timer.desktop "$pkgdir/usr/share/applications/practice-timer.desktop" | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								tools/linux/practice-timer.desktop
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tools/linux/practice-timer.desktop
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | [Desktop Entry] | ||||||
|  | Name=Practice Timer | ||||||
|  | GenericName=Activity Log | ||||||
|  | Comment=Track and analyze statistics about how long you do various activities | ||||||
|  | Exec=practice-timer | ||||||
|  | Icon=practice-timer | ||||||
|  | Type=Application | ||||||
|  | Terminal=false | ||||||
|  | Categories=Graphics;Utility; | ||||||
|  | StartupWMClass=PracticeTimer | ||||||
|  | Keywords=Practice Timer;Timer;Log; | ||||||
							
								
								
									
										
											BIN
										
									
								
								tools/linux/practice-timer.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tools/linux/practice-timer.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										103
									
								
								tools/linux/practice-timer.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								tools/linux/practice-timer.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,103 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||||
|  |  | ||||||
|  | <svg | ||||||
|  |    width="500" | ||||||
|  |    height="500" | ||||||
|  |    viewBox="0 0 132.29166 132.29167" | ||||||
|  |    version="1.1" | ||||||
|  |    id="svg5" | ||||||
|  |    inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)" | ||||||
|  |    sodipodi:docname="practice-timer.svg" | ||||||
|  |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|  |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg"> | ||||||
|  |   <sodipodi:namedview | ||||||
|  |      id="namedview7" | ||||||
|  |      pagecolor="#ffffff" | ||||||
|  |      bordercolor="#999999" | ||||||
|  |      borderopacity="1" | ||||||
|  |      inkscape:pageshadow="0" | ||||||
|  |      inkscape:pageopacity="0" | ||||||
|  |      inkscape:pagecheckerboard="0" | ||||||
|  |      inkscape:document-units="mm" | ||||||
|  |      showgrid="false" | ||||||
|  |      units="px" | ||||||
|  |      width="500px" | ||||||
|  |      inkscape:snap-smooth-nodes="false" | ||||||
|  |      inkscape:zoom="0.94964927" | ||||||
|  |      inkscape:cx="265.88764" | ||||||
|  |      inkscape:cy="255.88394" | ||||||
|  |      inkscape:window-width="1280" | ||||||
|  |      inkscape:window-height="739" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="0" | ||||||
|  |      inkscape:window-maximized="1" | ||||||
|  |      inkscape:current-layer="layer3" /> | ||||||
|  |   <defs | ||||||
|  |      id="defs2" /> | ||||||
|  |   <g | ||||||
|  |      id="layer2" | ||||||
|  |      inkscape:label="Background" | ||||||
|  |      sodipodi:insensitive="true" | ||||||
|  |      inkscape:groupmode="layer" | ||||||
|  |      style="display:inline"> | ||||||
|  |     <circle | ||||||
|  |        style="opacity:0.996191;fill:#484848;fill-opacity:1;stroke-width:24.375;stroke-linejoin:round" | ||||||
|  |        id="path980" | ||||||
|  |        cx="66.145836" | ||||||
|  |        cy="66.145836" | ||||||
|  |        r="66.145836" | ||||||
|  |        inkscape:label="outer-border" /> | ||||||
|  |     <circle | ||||||
|  |        style="opacity:0.996191;fill:#808080;fill-opacity:1;stroke-width:19.9876;stroke-linejoin:round" | ||||||
|  |        id="path980-3" | ||||||
|  |        cx="66.145836" | ||||||
|  |        cy="66.145836" | ||||||
|  |        r="54.239582" | ||||||
|  |        inkscape:label="inner-border" /> | ||||||
|  |   </g> | ||||||
|  |   <g | ||||||
|  |      inkscape:groupmode="layer" | ||||||
|  |      id="layer3" | ||||||
|  |      inkscape:label="Hands" | ||||||
|  |      style="display:inline"> | ||||||
|  |     <rect | ||||||
|  |        style="opacity:0.996191;fill:#000000;fill-opacity:1;stroke-width:9.01714;stroke-linejoin:round" | ||||||
|  |        id="rect3422-6" | ||||||
|  |        width="5.3008456" | ||||||
|  |        height="26.504229" | ||||||
|  |        x="22.885847" | ||||||
|  |        y="-119.15646" | ||||||
|  |        transform="rotate(120)" | ||||||
|  |        inkscape:label="small-hand" /> | ||||||
|  |     <rect | ||||||
|  |        style="opacity:0.996191;fill:#000000;fill-opacity:1;stroke-width:14.0031;stroke-linejoin:round" | ||||||
|  |        id="rect3422" | ||||||
|  |        width="3.96875" | ||||||
|  |        height="39.6875" | ||||||
|  |        x="64.161461" | ||||||
|  |        y="26.458334" | ||||||
|  |        inkscape:label="large-hand" /> | ||||||
|  |     <circle | ||||||
|  |        style="opacity:0.996191;fill:#000000;fill-opacity:1;stroke-width:11.1485;stroke-linejoin:round" | ||||||
|  |        id="path3120" | ||||||
|  |        cx="66.145836" | ||||||
|  |        cy="66.145836" | ||||||
|  |        r="6.6145835" | ||||||
|  |        inkscape:label="center" /> | ||||||
|  |   </g> | ||||||
|  |   <g | ||||||
|  |      inkscape:label="Time Marks" | ||||||
|  |      inkscape:groupmode="layer" | ||||||
|  |      id="layer1" | ||||||
|  |      sodipodi:insensitive="true"> | ||||||
|  |     <path | ||||||
|  |        id="rect5894-7-3-6-0-6-2" | ||||||
|  |        style="opacity:0.996191;stroke-width:50.0001;stroke-linejoin:round" | ||||||
|  |        d="m 245,65 v 45.01758 c 3.33361,-0.0323 6.66832,0.056 10,0.25586 V 65 Z m -83.16992,22.285156 -8.66016,5 22.13477,38.337894 c 2.80559,-1.843 5.66192,-3.56452 8.57226,-5.15039 z m 176.33984,0 -22.89648,39.658204 c 2.96389,1.53286 5.87971,3.15331 8.73047,4.87891 l 22.82617,-39.537114 z m -245.884764,65.884764 -5,8.66016 37.929684,21.89844 c 1.52688,-2.95808 3.17761,-5.85665 4.93946,-8.69532 z m 315.429684,0 -38.66796,22.32617 c 1.76932,2.81381 3.41493,5.71528 4.96093,8.68164 l 38.70703,-22.34765 z M 65,245 v 10 h 45.16602 c -0.14901,-3.33279 -0.1868,-6.66745 -0.11524,-10 z m 324.79102,0 c 0.0551,1.59738 0.0942,3.19459 0.0937,4.79297 0.0762,1.74427 0.10446,3.47897 0.10546,5.20703 H 435 v -10 z m -16.0254,70.68164 c -1.63934,2.90452 -3.38331,5.74875 -5.23437,8.52539 l 39.18359,22.62305 5,-8.66016 z m -247.78515,0.14844 -38.695314,22.33984 5,8.66016 38.550784,-22.25781 c -1.71971,-2.85556 -3.34481,-5.76802 -4.85547,-8.74219 z m 197.5957,51.60742 c -2.82988,1.76315 -5.72182,3.42626 -8.66015,5.00195 l 23.2539,40.27539 8.66016,-5 z m -148.04492,1.54492 -22.36133,38.73242 8.66016,5 22.55859,-39.07226 c -3.04457,-1.4412 -5.99191,-3.00194 -8.85742,-4.66016 z M 255,389.74414 c -3.32798,0.19999 -6.66259,0.28465 -10,0.23242 V 435 h 10 z" | ||||||
|  |        transform="scale(0.26458333)" | ||||||
|  |        inkscape:label="time-marks" /> | ||||||
|  |   </g> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 4.2 KiB | 
							
								
								
									
										84
									
								
								tools/linux/practice-timer.xpm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								tools/linux/practice-timer.xpm
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | /* XPM */ | ||||||
|  | static char *practice_timer[] = { | ||||||
|  | /* columns rows colors chars-per-pixel */ | ||||||
|  | "32 32 46 1 ", | ||||||
|  | "  c None", | ||||||
|  | ". c black", | ||||||
|  | "X c #030303", | ||||||
|  | "o c #090909", | ||||||
|  | "O c #0B0B0B", | ||||||
|  | "+ c #0F0F0F", | ||||||
|  | "@ c #1E1E1E", | ||||||
|  | "# c #242424", | ||||||
|  | "$ c #262626", | ||||||
|  | "% c #272727", | ||||||
|  | "& c #383838", | ||||||
|  | "* c #393939", | ||||||
|  | "= c #424242", | ||||||
|  | "- c #434343", | ||||||
|  | "; c #454545", | ||||||
|  | ": c #484848", | ||||||
|  | "> c #494949", | ||||||
|  | ", c #4A4A4A", | ||||||
|  | "< c #4B4B4B", | ||||||
|  | "1 c #4E4E4E", | ||||||
|  | "2 c #505050", | ||||||
|  | "3 c #535353", | ||||||
|  | "4 c #575757", | ||||||
|  | "5 c #5A5A5A", | ||||||
|  | "6 c #5C5C5C", | ||||||
|  | "7 c #5D5D5D", | ||||||
|  | "8 c #5E5E5E", | ||||||
|  | "9 c #626262", | ||||||
|  | "0 c #666666", | ||||||
|  | "q c #696969", | ||||||
|  | "w c #6A6A6A", | ||||||
|  | "e c #6B6B6B", | ||||||
|  | "r c #6C6C6C", | ||||||
|  | "t c #6E6E6E", | ||||||
|  | "y c #707070", | ||||||
|  | "u c #717171", | ||||||
|  | "i c #727272", | ||||||
|  | "p c #787878", | ||||||
|  | "a c #797979", | ||||||
|  | "s c #7A7A7A", | ||||||
|  | "d c #7B7B7B", | ||||||
|  | "f c #7C7C7C", | ||||||
|  | "g c #7D7D7D", | ||||||
|  | "h c #7E7E7E", | ||||||
|  | "j c #7F7F7F", | ||||||
|  | "k c #808080", | ||||||
|  | /* pixels */ | ||||||
|  | "            ::::::::            ", | ||||||
|  | "         ::::::::::::::         ", | ||||||
|  | "       ::::::::11::::::::       ", | ||||||
|  | "      :::::5rskkkksr5:::::      ", | ||||||
|  | "     ::::8skkkk88kkkks8::::     ", | ||||||
|  | "    :::1igrkkkk44kkkkrgi1:::    ", | ||||||
|  | "   :::2skg%gkkk::kkkg%gks2:::   ", | ||||||
|  | "  :::1skkk8;kkk--kkk;8kkks1:::  ", | ||||||
|  | "  :::ikkkkkrkkk--kkk0kkkkki:::  ", | ||||||
|  | " :::8ggkkkkkkkk--kkkkkkkkgg8::: ", | ||||||
|  | " :::sr%8kkkkkkk--kkkkkkk8%ws::: ", | ||||||
|  | " ::5kkg;ikkkkkk--kkkkkkw;gkk5:: ", | ||||||
|  | ":::rkkkkkkkkkkk--kkkkkkkkkkkr:::", | ||||||
|  | ":::skkkkkkkkkkk--kkkkkkkkkkks:::", | ||||||
|  | ":::kkkkkkkkkkkg%%gkkkkkkkkkkk:::", | ||||||
|  | "::1k844gkkkkkk-..-kkkkkkg448k1::", | ||||||
|  | "::1k844gkkkkkk-..%skkkkkg448k1::", | ||||||
|  | ":::kkkkkkkkkkkg-&O+2gkkkkkkkk:::", | ||||||
|  | ":::skkkkkkkkkkkkks&.@0kkkkkks:::", | ||||||
|  | ":::rkkkkkkkkkkkkkkkr%Oskkkkkr:::", | ||||||
|  | " ::5kkg;rkkkkkkkkkkkkikw;gkk5:: ", | ||||||
|  | " :::sr%8kkkkkkkkkkkkkkkk8%ws::: ", | ||||||
|  | " :::8ggkkkkkkkkkkkkkkkkkkgg8::: ", | ||||||
|  | "  :::ikkkkkwkkkkkkkk9kkkkki:::  ", | ||||||
|  | "  :::1skkk8;kkkggkkk;8kkks1:::  ", | ||||||
|  | "   :::2skg%gkkk44kkkg%gks2:::   ", | ||||||
|  | "    :::1igrkkkk44kkkkwgi1:::    ", | ||||||
|  | "     ::::8skkkk88kkkks8::::     ", | ||||||
|  | "      :::::5rskkkksr5:::::      ", | ||||||
|  | "       ::::::::11::::::::       ", | ||||||
|  | "         ::::::::::::::         ", | ||||||
|  | "            ::::::::            " | ||||||
|  | }; | ||||||
							
								
								
									
										634
									
								
								ui/edit-window.glade
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										634
									
								
								ui/edit-window.glade
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,634 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- Generated with glade 3.38.2 --> | ||||||
|  | <interface> | ||||||
|  |   <requires lib="gtk+" version="3.24"/> | ||||||
|  |   <object class="GtkAdjustment" id="endHourAdjustment"> | ||||||
|  |     <property name="lower">1</property> | ||||||
|  |     <property name="upper">12</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="endMinuteAdjustment"> | ||||||
|  |     <property name="upper">59</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="endSecondAdjustment"> | ||||||
|  |     <property name="upper">59</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="lenHourAdjustment"> | ||||||
|  |     <property name="upper">99</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="lenMinuteAdjustment"> | ||||||
|  |     <property name="upper">59</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="lenSecondAdjustment"> | ||||||
|  |     <property name="upper">59</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="startHourAdjustment"> | ||||||
|  |     <property name="lower">1</property> | ||||||
|  |     <property name="upper">12</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="startMinuteAdjustment"> | ||||||
|  |     <property name="upper">59</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <object class="GtkAdjustment" id="startSecondAdjustment"> | ||||||
|  |     <property name="upper">59</property> | ||||||
|  |     <property name="step-increment">1</property> | ||||||
|  |     <property name="page-increment">10</property> | ||||||
|  |   </object> | ||||||
|  |   <template class="TimerEditWindow" parent="GtkDialog"> | ||||||
|  |     <property name="can-focus">False</property> | ||||||
|  |     <property name="title" translatable="yes">Edit Task</property> | ||||||
|  |     <property name="modal">True</property> | ||||||
|  |     <property name="window-position">mouse</property> | ||||||
|  |     <property name="type-hint">dialog</property> | ||||||
|  |     <child internal-child="vbox"> | ||||||
|  |       <object class="GtkBox"> | ||||||
|  |         <property name="can-focus">False</property> | ||||||
|  |         <property name="orientation">vertical</property> | ||||||
|  |         <property name="spacing">2</property> | ||||||
|  |         <child internal-child="action_area"> | ||||||
|  |           <object class="GtkButtonBox"> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="layout-style">end</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="deleteButton"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |                 <property name="always-show-image">True</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkImage"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="icon-name">edit-delete</property> | ||||||
|  |                   </object> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |                 <property name="non-homogeneous">True</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="cancelButton"> | ||||||
|  |                 <property name="label" translatable="yes">Cancel</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="saveButton"> | ||||||
|  |                 <property name="label" translatable="yes">Save</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">2</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">False</property> | ||||||
|  |             <property name="position">0</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkBox"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="orientation">vertical</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkComboBoxText" id="nameBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="has-entry">True</property> | ||||||
|  |                 <child internal-child="entry"> | ||||||
|  |                   <object class="GtkEntry"> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="placeholder-text" translatable="yes">What  are you up to?</property> | ||||||
|  |                   </object> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkBox"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="orientation">vertical</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkCalendar" id="startCalendar"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="year">2021</property> | ||||||
|  |                         <property name="day">16</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">0</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkCheckButton" id="endCalCheck"> | ||||||
|  |                         <property name="label" translatable="yes">Same start and end dates</property> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="receives-default">False</property> | ||||||
|  |                         <property name="active">True</property> | ||||||
|  |                         <property name="draw-indicator">True</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">1</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkCalendar" id="endCalendar"> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="year">2021</property> | ||||||
|  |                         <property name="day">16</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">2</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkBox"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="orientation">vertical</property> | ||||||
|  |                     <property name="homogeneous">True</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkLabel"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <property name="label" translatable="yes">Start:</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">0</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkLabel"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <property name="label" translatable="yes">End:</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">1</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkLabel"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <property name="label" translatable="yes">Length:</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">2</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkBox"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="orientation">vertical</property> | ||||||
|  |                     <property name="homogeneous">True</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkBox"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="startHour"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="input-purpose">digits</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">startHourAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">0</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkLabel"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="label" translatable="yes">:</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">False</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">1</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="startMinute"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">startMinuteAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">2</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkLabel"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="label" translatable="yes">:</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">False</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">3</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="startSecond"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">startSecondAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">4</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">0</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkBox"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="endHour"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">endHourAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">0</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkLabel"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="label" translatable="yes">:</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">False</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">1</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="endMinute"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">endMinuteAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">2</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkLabel"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="label" translatable="yes">:</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">False</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">3</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="endSecond"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">endSecondAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">4</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">1</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkBox"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="lengthHour"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">lenHourAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">0</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkLabel"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="label" translatable="yes">:</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">False</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">1</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="lengthMinute"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">lenMinuteAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">2</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkLabel"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="label" translatable="yes">:</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">False</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">3</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkSpinButton" id="lengthSecond"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">True</property> | ||||||
|  |                             <property name="text" translatable="yes">0</property> | ||||||
|  |                             <property name="orientation">vertical</property> | ||||||
|  |                             <property name="adjustment">lenSecondAdjustment</property> | ||||||
|  |                             <property name="climb-rate">1</property> | ||||||
|  |                             <property name="numeric">True</property> | ||||||
|  |                           </object> | ||||||
|  |                           <packing> | ||||||
|  |                             <property name="expand">True</property> | ||||||
|  |                             <property name="fill">True</property> | ||||||
|  |                             <property name="position">4</property> | ||||||
|  |                           </packing> | ||||||
|  |                         </child> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">2</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">True</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">2</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkBox"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="orientation">vertical</property> | ||||||
|  |                     <property name="homogeneous">True</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkComboBoxText" id="startLean"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <property name="active">0</property> | ||||||
|  |                         <property name="active-id">0</property> | ||||||
|  |                         <items> | ||||||
|  |                           <item id="0" translatable="yes">AM</item> | ||||||
|  |                           <item id="1" translatable="yes">PM</item> | ||||||
|  |                         </items> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">0</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkComboBoxText" id="endLean"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <property name="active">0</property> | ||||||
|  |                         <property name="active-id">0</property> | ||||||
|  |                         <items> | ||||||
|  |                           <item id="0" translatable="yes">AM</item> | ||||||
|  |                           <item id="1" translatable="yes">PM</item> | ||||||
|  |                         </items> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">1</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkLabel"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">2</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">3</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkBox" id="followBox"> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="orientation">vertical</property> | ||||||
|  |                     <property name="homogeneous">True</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkButton" id="followButton"> | ||||||
|  |                         <property name="label" translatable="yes">Follow Last Task</property> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="receives-default">True</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">0</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkLabel"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">1</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkLabel"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">2</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">True</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">4</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">True</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |       </object> | ||||||
|  |     </child> | ||||||
|  |   </template> | ||||||
|  | </interface> | ||||||
							
								
								
									
										148
									
								
								ui/main-window.glade
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								ui/main-window.glade
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,148 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- Generated with glade 3.38.2 --> | ||||||
|  | <interface> | ||||||
|  |   <requires lib="gtk+" version="3.24"/> | ||||||
|  |   <template class="TimerMainWindow" parent="GtkApplicationWindow"> | ||||||
|  |     <property name="can-focus">False</property> | ||||||
|  |     <property name="title" translatable="yes">Practice Timer</property> | ||||||
|  |     <child> | ||||||
|  |       <object class="GtkBox"> | ||||||
|  |         <property name="visible">True</property> | ||||||
|  |         <property name="can-focus">False</property> | ||||||
|  |         <property name="orientation">vertical</property> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkButton" id="timerButton"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">True</property> | ||||||
|  |             <property name="receives-default">True</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkLabel" id="timerLabel"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="label" translatable="yes">00:00:00</property> | ||||||
|  |                 <attributes> | ||||||
|  |                   <attribute name="scale" value="6"/> | ||||||
|  |                 </attributes> | ||||||
|  |               </object> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">0</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkComboBoxText" id="nameBox"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="has-entry">True</property> | ||||||
|  |             <child internal-child="entry"> | ||||||
|  |               <object class="GtkEntry"> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="placeholder-text" translatable="yes">What are you up to?</property> | ||||||
|  |               </object> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkBox"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="startStopButton"> | ||||||
|  |                 <property name="label" translatable="yes">Start</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="resetButton"> | ||||||
|  |                 <property name="label" translatable="yes">Reset</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="sensitive">False</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="saveButton"> | ||||||
|  |                 <property name="label" translatable="yes">Save</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="sensitive">False</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">2</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="optionsButton"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkImage"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="icon-name">document-properties</property> | ||||||
|  |                   </object> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">3</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">2</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkScrolledWindow"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">True</property> | ||||||
|  |             <property name="shadow-type">in</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkViewport" id="taskTreeBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <child> | ||||||
|  |                   <placeholder/> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">True</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">3</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |       </object> | ||||||
|  |     </child> | ||||||
|  |   </template> | ||||||
|  | </interface> | ||||||
							
								
								
									
										265
									
								
								ui/settings-window.glade
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								ui/settings-window.glade
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,265 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <!-- Generated with glade 3.38.2 --> | ||||||
|  | <interface> | ||||||
|  |   <requires lib="gtk+" version="3.24"/> | ||||||
|  |   <template class="TimerSettingsWindow" parent="GtkDialog"> | ||||||
|  |     <property name="can-focus">False</property> | ||||||
|  |     <property name="title" translatable="yes">Settings</property> | ||||||
|  |     <property name="modal">True</property> | ||||||
|  |     <property name="window-position">mouse</property> | ||||||
|  |     <property name="type-hint">dialog</property> | ||||||
|  |     <child internal-child="vbox"> | ||||||
|  |       <object class="GtkBox"> | ||||||
|  |         <property name="can-focus">False</property> | ||||||
|  |         <property name="orientation">vertical</property> | ||||||
|  |         <property name="spacing">2</property> | ||||||
|  |         <child internal-child="action_area"> | ||||||
|  |           <object class="GtkButtonBox"> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="layout-style">end</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="cancelButton"> | ||||||
|  |                 <property name="label" translatable="yes">Cancel</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkButton" id="saveButton"> | ||||||
|  |                 <property name="label" translatable="yes">Save</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">False</property> | ||||||
|  |             <property name="fill">False</property> | ||||||
|  |             <property name="position">0</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |         <child> | ||||||
|  |           <object class="GtkBox"> | ||||||
|  |             <property name="visible">True</property> | ||||||
|  |             <property name="can-focus">False</property> | ||||||
|  |             <property name="orientation">vertical</property> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkLabel"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="label" translatable="yes">Data Path:</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkFileChooserButton" id="dataPathButton"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="do-overwrite-confirmation">True</property> | ||||||
|  |                     <property name="title" translatable="yes"/> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">True</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">0</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <property name="orientation">vertical</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkLabel"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <property name="label" translatable="yes">Tasks:</property> | ||||||
|  |                     <property name="xalign">2.2351741291171123e-10</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkScrolledWindow"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="shadow-type">in</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkViewport"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">False</property> | ||||||
|  |                         <child> | ||||||
|  |                           <object class="GtkListBox" id="taskBox"> | ||||||
|  |                             <property name="visible">True</property> | ||||||
|  |                             <property name="can-focus">False</property> | ||||||
|  |                             <property name="selection-mode">multiple</property> | ||||||
|  |                           </object> | ||||||
|  |                         </child> | ||||||
|  |                       </object> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">True</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkBox"> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">False</property> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkButton" id="removeTaskButton"> | ||||||
|  |                         <property name="label" translatable="yes">-</property> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="receives-default">True</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">0</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkEntry" id="taskNameEntry"> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="placeholder-text" translatable="yes">Task Name</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">True</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">1</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                     <child> | ||||||
|  |                       <object class="GtkButton" id="addTaskButton"> | ||||||
|  |                         <property name="label" translatable="yes">+</property> | ||||||
|  |                         <property name="visible">True</property> | ||||||
|  |                         <property name="can-focus">True</property> | ||||||
|  |                         <property name="receives-default">True</property> | ||||||
|  |                       </object> | ||||||
|  |                       <packing> | ||||||
|  |                         <property name="expand">False</property> | ||||||
|  |                         <property name="fill">True</property> | ||||||
|  |                         <property name="position">2</property> | ||||||
|  |                       </packing> | ||||||
|  |                     </child> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">False</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">2</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">True</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">1</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkBox"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkButton" id="graphButton"> | ||||||
|  |                     <property name="label" translatable="yes">Statistics</property> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="receives-default">True</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">True</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">0</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |                 <child> | ||||||
|  |                   <object class="GtkButton" id="exportButton"> | ||||||
|  |                     <property name="label" translatable="yes">Export</property> | ||||||
|  |                     <property name="visible">True</property> | ||||||
|  |                     <property name="can-focus">True</property> | ||||||
|  |                     <property name="receives-default">True</property> | ||||||
|  |                   </object> | ||||||
|  |                   <packing> | ||||||
|  |                     <property name="expand">True</property> | ||||||
|  |                     <property name="fill">True</property> | ||||||
|  |                     <property name="position">1</property> | ||||||
|  |                   </packing> | ||||||
|  |                 </child> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">2</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkCheckButton" id="alwaysOnTopCheck"> | ||||||
|  |                 <property name="label" translatable="yes">Always on Top</property> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">True</property> | ||||||
|  |                 <property name="receives-default">False</property> | ||||||
|  |                 <property name="draw-indicator">True</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">3</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |             <child> | ||||||
|  |               <object class="GtkLabel" id="versionLabel"> | ||||||
|  |                 <property name="visible">True</property> | ||||||
|  |                 <property name="can-focus">False</property> | ||||||
|  |               </object> | ||||||
|  |               <packing> | ||||||
|  |                 <property name="expand">False</property> | ||||||
|  |                 <property name="fill">True</property> | ||||||
|  |                 <property name="position">4</property> | ||||||
|  |               </packing> | ||||||
|  |             </child> | ||||||
|  |           </object> | ||||||
|  |           <packing> | ||||||
|  |             <property name="expand">True</property> | ||||||
|  |             <property name="fill">True</property> | ||||||
|  |             <property name="position">1</property> | ||||||
|  |           </packing> | ||||||
|  |         </child> | ||||||
|  |       </object> | ||||||
|  |     </child> | ||||||
|  |   </template> | ||||||
|  | </interface> | ||||||
		Reference in New Issue
	
	Block a user